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;
		}
	}
}
相关推荐
悠哉悠哉愿意7 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
Lester_11017 天前
STM32霍尔传感器输入口设置为复用功能输入口时,还能用GPIO函数直接读取IO的状态吗
stm32·单片机·嵌入式硬件·电机控制
LCG元7 天前
低功耗显示方案:STM32L0驱动OLED,动态波形绘制与优化
stm32·嵌入式硬件·信息可视化
三佛科技-187366133977 天前
120W小体积碳化硅电源方案(LP8841SC极简方案12V10A/24V5A输出)
单片机·嵌入式硬件
z20348315207 天前
STM32F103系列单片机定时器介绍(二)
stm32·单片机·嵌入式硬件
古译汉书8 天前
【IoT死磕系列】Day 7:只传8字节怎么控机械臂?学习工业控制 CANopen 的“对象字典”(附企业级源码)
数据结构·stm32·物联网·http
Alaso_shuang8 天前
STM32 核心输入、输出模式
stm32·单片机·嵌入式硬件
脚后跟8 天前
AI助力嵌入式物联网项目全栈开发
嵌入式硬件·物联网·ai编程
2501_918126918 天前
stm32死锁是怎么实现的
stm32·单片机·嵌入式硬件·学习·个人开发
z20348315208 天前
STM32F103系列单片机定时器介绍(一)
stm32·单片机