STM32(十七)——串口通信

一、库函数

USART_ClockInit()

把配置好的时钟参数写入硬件寄存器

USART_ClockStructInit()

给时钟配置结构体赋默认值

这两个函数只在 USART 同步模式下有用(比如模拟 SPI 通信),异步模式(普通串口)无需调用,调用了也无效;

USART_SendData()

把数据写入 USART 发送寄存器(TDR)

USART_ReceiveData()

从 USART 接收寄存器(RDR)读取已接收的数据

USART_GetFlagStatus()

读取 USART 状态寄存器(SR)的某一位,返回 "置 1 / 置 0"阻塞式收发、轮询判断硬件

USART_ClearFlag()

手动清除状态寄存器中的指定标志位清除非中断触发的标志(如空闲帧、溢出错误)

USART_GetITStatus()

检查 "某中断是否触发"(需同时满足:标志位置 1 + 中断使能)中断服务函数中判断中断来源

USART_ClearITPendingBit()

清除中断挂起位,结束本次中断

二、串口发送

接线图

选用串口1,串口1TX对应的是PA9

打开时钟
复制代码
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
配置GPIO口

PA9由外设串口控制,配置为复用推挽输出。

复制代码
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&GPIO_InitStructure);
配置串口
复制代码
    USART_InitTypeDef USART_InitStructure;
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_Init(USART1,&USART_InitStructure);

波特率:9600 流控:关闭 USART模式:仅配置为发送模式 校验位:无

停止位:1位 字长:8位

打开串口模块
复制代码
 USART_Cmd(USART1,ENABLE);
传送单个数据

发送数据,等待数据发送完成。

复制代码
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

Serial_SendByte(0x41);
发送字节数组
复制代码
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for(i = 0; i < Length; i ++)
	{
		Serial_SendByte(Array[i]);
	}
}

	uint8_t MyArray[] = {0x42, 0x43, 0x44, 0x45};
	Serial_SendArray(MyArray,4);
发送字符串
复制代码
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)
	{
		Serial_SendByte(String[i]);
	}
}

Serial_SendString("\r\nNum1=");
发送数字
复制代码
uint32_t Setial_Pow(uint32_t X,uint32_t Y)
{
	uint32_t Result = 1;
	while(Y --)
	{
		Result *= X;
	}
	return Result;
}
void Serial_SendNumber(uint32_t Number,uint8_t Length)
{
	uint8_t i;
	for(i = 0; i < Length; i++)
	{
		Serial_SendByte(Number / Setial_Pow(10, Length - i - 1) % 10 + '0');
	}
}

Serial_SendNumber(111,3);

三、串口接收

串口1RX映射的是PA10

复制代码
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
配置串口模式
复制代码
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
打开串口中断
复制代码
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
配置串口中断
复制代码
    NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	
	NVIC_Init(&NVIC_InitStructure);
中断函数

串口触发中断 --> 检查标志位 --> 将串口接收的数据放在RxData --> 将串口接收标志位置1 --> 手动清除中断标志位。

复制代码
uint8_t Serial_RxData;
uint8_t Serial_RxFlag;

void USART1_IRQHandler(void)
{
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
		Serial_RxData = USART_ReceiveData(USART1);
		Serial_RxFlag = 1;
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}
读取接收标志位
复制代码
uint8_t Serial_GetRxFlag(void)
{
	if(Serial_RxFlag == 1)
	{
		Serial_RxFlag = 0;
		return 1;
	}
	return 0;
}
读取接收的数据
复制代码
uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;
}
调用
复制代码
while(1)
	{
		if(Serial_GetRxFlag() == 1)
		{
			RxData = Serial_GetRxData();
			Serial_SendByte(RxData);
			OLED_ShowHexNum(1,8,RxData,2);
		
		}
	}	

四、printf重定向

C 标准库的printf并非直接输出数据,而是通过fputc(file put character)这个 "中间接口" 逐字符输出:

  • PC 端默认逻辑printf → 解析格式化字符串 → 调用fputc → 写入控制台
  • STM32 重定向逻辑printf → 解析格式化字符串 → 调用我们重写的fputc → 写入串口

简单说:fputc是 printf 的 "输出管道",我们把管道的出口从 "控制台" 换成 "串口",就实现了 printf 的串口重定向。

复制代码
int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);
	return ch;
}

标准printf依赖微库,且代码体积稍大;自定义Serial_Printf更轻量、可控性更高,核心是借助va_list处理可变参数,vsprintf格式化字符串。

  • 定义缓冲区string[100]存储格式化后的字符串。

  • 使用va_listva_startva_end处理可变参数列表。

  • vsprintf将格式化结果写入缓冲区,Serial_SendString发送完整字符串。

    void Serial_Printf(char *format, ...)
    {
    // 静态缓冲区:避免栈溢出,大小可按需调整(如200/512)
    static char string[200];
    va_list arg; // 可变参数列表

    复制代码
      // 1. 初始化可变参数列表
      va_start(arg, format);
      // 2. 安全格式化字符串(vsnprintf防止缓冲区溢出)
      vsnprintf(string, sizeof(string), format, arg);
      // 3. 结束可变参数列表
      va_end(arg);
      // 4. 发送格式化后的字符串
      Serial_SendString(string);

    }

重定向后直接调用printf会卡死?因为标准库需要 "微库(MicroLIB)" 支持:

  1. 打开 MDK 工程,点击魔法棒(Target);
  2. 切换到Tar get选项卡,勾选Use MicroLIB(使用精简版 C 库);
  3. 切换到c/c++选项卡,在MiscControls填写--no-multibyte-chars
  4. 保存并重新编译。

头文件不能少

  • #include <stdio.h>:支持FILE/fputc/printf
  • #include <stdarg.h>:支持va_list/va_start/vsnprintf
  • 缺少会报 "未定义标识符" 错误。

五、串口收发HEX数据包

接线图
发送数据包

定义发送数组

复制代码
uint8_t Serial_Txpacket[4];

在.h中声明

复制代码
extern uint8_t Serial_Txpacket[];

发送

复制代码
void Serial_sendpacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_Txpacket,4);
	Serial_SendByte(0xFE);
}
接收数据包

定义接收数据包的缓存区和标志位

复制代码
uint8_t Serial_Rxpacket[4];

在.h中声明

复制代码
extern uint8_t Serial_Rxpacket[];

用状态机执行接收逻辑

用 3 个状态解析数据包,避免数据错乱:

  • 状态 0:空闲状态,等待接收包头(0xFF);
  • 状态 1:接收数据状态,连续接收 3 字节有效数据;
  • 状态 2:校验包尾状态,等待接收包尾(0xFE),校验通过则标记 "数据包接收完成"。

流程图

复制代码
void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;
	static uint8_t pRxpacket = 0;
	if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		if(RxState == 0)
		{
			if(RxData == 0xFF)
			{
				RxState = 1;
				pRxpacket = 0;
			}
		}
		else if(RxState == 1)
		{
			Serial_Rxpacket[pRxpacket] = RxData;
			pRxpacket++;
			
			if(pRxpacket >= 4)
			{
				RxState = 2;
			}
		}
		else if(RxState == 2)
		{
			if(RxData == 0xFE)
			{
				RxState = 0;
				Serial_RxFlag = 1;
			}
		}
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);
	}
}
调用
复制代码
int main(void)
{
	OLED_Init();
	key_Init();
	Serial_Init();
	OLED_ShowString(1,1,"Txpacket");
	OLED_ShowString(3,1,"Rxpacket");
	Serial_Txpacket[0] = 0x01;
	Serial_Txpacket[1] = 0x02;
	Serial_Txpacket[2] = 0x03;
	Serial_Txpacket[3] = 0x04;
	while(1)
	{
		keyNum = key_GetNum();
		if(keyNum == 1)
		{
			Serial_Txpacket[0] ++;
			Serial_Txpacket[1] ++;
			Serial_Txpacket[2] ++;
			Serial_Txpacket[3] ++;
			
			Serial_sendpacket();
			
			OLED_ShowHexNum(2,1,Serial_Txpacket[0],2);
			OLED_ShowHexNum(2,4,Serial_Txpacket[1],2);
			OLED_ShowHexNum(2,7,Serial_Txpacket[2],2);
			OLED_ShowHexNum(2,10,Serial_Txpacket[3],2);
				
		}
		if(Serial_GetRxFlag() == 1)
		{
			OLED_ShowHexNum(4,1,Serial_Rxpacket[0],2);
			OLED_ShowHexNum(4,4,Serial_Rxpacket[1],2);
			OLED_ShowHexNum(4,7,Serial_Rxpacket[2],2);
			OLED_ShowHexNum(4,10,Serial_Rxpacket[3],2);
		}
	}	
}

六、串口收发文本数据包

复制代码
void USART1_IRQHandler(void)
{
	// static修饰:中断多次调用时保留状态和指针值
	static uint8_t RxState = 0;
	static uint8_t pRxPacket = 0;
	
	// 检测USART1的接收非空中断(RXNE)
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		// 读取接收到的1字节数据
		uint8_t RxData = USART_ReceiveData(USART1);
		
		// 状态机解析:@开头 + 内容 + \r\n结尾
		if (RxState == 0)
		{
			// 初始状态:收到@且上一包已处理(Serial_RxFlag=0),进入接收状态
			if (RxData == '@' && Serial_RxFlag == 0)
			{
				RxState = 1;
				pRxPacket = 0; // 重置数据包指针
			}
		}
		else if (RxState == 1)
		{
			// 接收内容状态:收到\r则进入校验换行状态,否则存储数据
			if (RxData == '\r')
			{
				RxState = 2;
			}
			else
			{
				Serial_RxPacket[pRxPacket] = RxData;
				pRxPacket ++;
			}
		}
		else if (RxState == 2)
		{
			// 校验换行状态:收到\n则完成接收,置位标志
			if (RxData == '\n')
			{
				RxState = 0;
				Serial_RxPacket[pRxPacket] = '\0'; // 字符串结束符
				Serial_RxFlag = 1; // 接收完成标志置1
			}
		}
		
		// 清除中断挂起位,避免中断反复触发
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
	}
}
复制代码
int main(void)
{
	OLED_Init();
	LED_Init();
	Serial_Init();
	
	OLED_ShowString(1, 1, "TxPacket");
	OLED_ShowString(3, 1, "RxPacket");
	
	while (1)
	{
		if (Serial_RxFlag == 1)
		{
			OLED_ShowString(4, 1, "                ");
			OLED_ShowString(4, 1, Serial_RxPacket);
			
			if (strcmp(Serial_RxPacket, "LED_ON") == 0)
			{
				LED1_ON();
				Serial_SendString("LED_ON_OK\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_ON_OK");
			}
			else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)
			{
				LED1_OFF();
				Serial_SendString("LED_OFF_OK\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_OFF_OK");
			}
			else
			{
				Serial_SendString("ERROR_COMMAND\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "ERROR_COMMAND");
			}
			
			Serial_RxFlag = 0;
		}
	}
}
相关推荐
不做无法实现的梦~11 小时前
ros2实现路径规划---nav2部分
linux·stm32·嵌入式硬件·机器人·自动驾驶
熊猫_豆豆15 小时前
同步整流 Buck 降压变换器
单片机·嵌入式硬件·matlab
chenchen0000000020 小时前
49元能否买到四核性能?HZ-RK3506G2_MiniEVM开发板评测:MCU+三核CPU带来的超高性价比
单片机·嵌入式硬件
孤芳剑影20 小时前
反馈环路设计总结
嵌入式硬件·学习
dump linux21 小时前
设备树子系统与驱动开发入门
linux·驱动开发·嵌入式硬件
专注VB编程开发20年21 小时前
简易虚拟 PLC 服务器-流水线自动化,上位机程序维护升级,西门子PLC仿真
服务器·单片机·自动化·上位机·plc·流水线·工控
LeoZY_1 天前
CH347/339W开源项目:集SPI、I2C、JTAG、SWD、UART、GPIO多功能为一体(3)
stm32·单片机·嵌入式硬件·mcu·开源
chenchen000000001 天前
国产显示芯势力新篇章:内置DDR+四核A35!MY-SSD2351-MINI开发板深度评测
驱动开发·嵌入式硬件
BackCatK Chen1 天前
第13篇:TMC2240 StallGuard4失速检测|寄存器配置+状态读取(保姆级)
单片机·嵌入式硬件·tmc2240·stm32实战·stallguard4·失速检测·电机故障识别
Hello_Embed1 天前
libmodbus STM32 板载串口实验(双串口主从通信)
笔记·stm32·单片机·学习·modbus