1. STM32F407串口通信实验详解
串口通信作为嵌入式系统中最基础也最常用的通信方式之一,掌握其原理和实现方法对嵌入式开发者至关重要。本实验基于STM32F407开发板,详细讲解串口通信的实现过程,从基础概念到实际代码实现,带你全面理解串口通信的方方面面。
1.1 通信速率基础概念
在数字通信系统中,通信速率(传输速率)指数据在信道中传输的速度,它分为两种:传信率和传码率。
传信率(比特率):每秒钟传输的信息量,即每秒钟传输的二进制位数,单位为bit/s(比特每秒)。这是我们最常接触到的概念,比如我们常说的115200bps就是指比特率为115200bit/s。
传码率(波特率):每秒钟传输的码元个数,单位为Baud(波特每秒)。波特率被传输的是码元,码元是信号被调制后的概念,每个码元都可以表示一定bit的数据信息量。
注意:比特率和波特率这两个概念常常被混淆。它们之间的关系可以用以下公式表示:
比特率 = 波特率 × log₂M
其中M表示码元承载的信息量,也可以理解为码元的进制数。
举例说明:如果波特率为100 Baud,即每秒传输100个码元:
- 当码元采用十六进制编码(M=16)时,比特率=100×log₂16=400bit/s
- 当码元采用二进制编码(M=2)时,比特率=100×log₂2=100bit/s
可以看到,采用二进制编码时,波特率和比特率在数值上相等,但它们的意义仍然不同。由于我们所用的数字系统都是二进制的,所以久而久之很多人就直接把波特率和比特率混淆了。
1.2 串口通信协议解析
串口通信协议数据包组成可以分为两部分:波特率和数据帧格式。
1.2.1 波特率设置
本章主要讲解的是串口异步通信,异步通信不需要时钟信号,但需要通信双方约定好相同的波特率。波特率决定了数据帧中每一个位的时间长度。常见的波特率有4800、9600、115200等。在STM32F407中,波特率可以通过寄存器配置,HAL库已经为我们封装好了相关函数。
1.2.2 数据帧格式
串口通信的数据帧需要通信双方提前约定好格式,主要包括以下几部分:
-
起始位和停止位:
- 起始位:由一个逻辑0的数据位表示
- 停止位:可以是0.5、1、1.5或2个逻辑1的数据位表示,只要双方约定一致即可
-
有效数据位:
- 起始位之后就是数据位,也称有效数据位
- 通常约定为5、6、7或8个位长
- 传输顺序是低位(LSB)在前,高位(MSB)在后
-
校验位:
- 校验位是一个特殊的数据位,用于判断接收的数据位有无错误
- 校验方法有:奇校验、偶校验、0检验、1检验以及无检验
在实际应用中,我们一般使用无校验的情况,这样可以减少通信开销,提高传输效率。
2. STM32F407串口硬件资源
2.1 STM32F407串口简介
STM32F407的串口资源相当丰富,功能也相当强大。STM32F407ZGT6最多可提供6路串口,具有以下特性:
- 分数波特率发生器
- 支持同步单线通信和半双工单线通讯
- 支持LIN协议
- 支持调制解调器操作
- 支持智能卡协议和IrDA SIR ENDEC规范
- 具有DMA功能
STM32F4的串口分为两种:
- USART(通用同步异步收发器)
- UART(通用异步收发器)
UART是在USART基础上裁剪掉了同步通信功能,只剩下异步通信功能。简单区分同步和异步就是看通信时需不需要对外提供时钟输出,我们平时用串口通信基本都是异步通信。
2.2 串口时钟源
STM32F4有4个USART和2个UART:
- USART1和USART6的时钟源来自APB2时钟,最大频率为84MHz
- 其他4个串口的时钟源可以来自APB1时钟,最大频率为42MHz
时钟源的配置会影响波特率的计算精度,因此需要根据实际需求选择合适的时钟源。
2.3 电平转换
STM32的串口输出的是TTL电平信号,如果需要RS-232标准的信号,可以使用MAX3232芯片进行转换。本实验我们是通过USB转串口芯片CH340C来与电脑的上位机进行通信,CH340C已经内置了电平转换电路,因此我们不需要额外添加电平转换芯片。
3. HAL库串口驱动解析
3.1 HAL_UART_Init函数
要使用串口,首先需要对其进行初始化。HAL库提供了HAL_UART_Init函数用于初始化异步模式的收发器。
c复制HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
函数参数是一个UART_HandleTypeDef结构体指针,这个结构体包含了串口的所有配置信息:
c复制typedef struct {
USART_TypeDef *Instance; /* UART寄存器基地址 */
UART_InitTypeDef Init; /* UART通信参数 */
UART_AdvFeatureInitTypeDef AdvancedInit; /* UART高级功能配置结构体 */
uint8_t *pTxBuffPtr; /* 指向UART发送缓冲区 */
uint16_t TxXferSize; /* UART发送数据的大小 */
__IO uint16_t TxXferCount; /* UART发送数据的个数 */
uint8_t *pRxBuffPtr; /* 指向UART接收缓冲区 */
uint16_t RxXferSize; /* UART接收数据大小 */
__IO uint16_t RxXferCount; /* UART接收数据的个数 */
uint16_t Mask; /* UART数据接收寄存器掩码 */
DMA_HandleTypeDef *hdmatx; /* UART发送参数设置(DMA) */
DMA_HandleTypeDef *hdmarx; /* UART接收参数设置(DMA) */
HAL_LockTypeDef Lock; /* 锁定对象 */
__IO HAL_UART_StateTypeDef gState; /* UART发送状态结构体 */
__IO HAL_UART_StateTypeDef RxState; /* UART接收状态结构体 */
__IO uint32_t ErrorCode; /* UART操作错误信息 */
} UART_HandleTypeDef;
其中,UART_InitTypeDef结构体用于配置UART的通信参数:
c复制typedef struct {
uint32_t BaudRate; /* 波特率 */
uint32_t WordLength; /* 字长 */
uint32_t StopBits; /* 停止位 */
uint32_t Parity; /* 校验位 */
uint32_t Mode; /* UART模式 */
uint32_t HwFlowCtl; /* 硬件流设置 */
uint32_t OverSampling; /* 过采样设置 */
} UART_InitTypeDef;
3.2 HAL_UART_Receive_IT函数
HAL_UART_Receive_IT函数用于开启串口接收中断:
c复制HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
这个函数会开启接收中断(标志位UART_IT_RXNE),并设置接收缓冲区以及接收缓冲区的最大数据量。
3.3 HAL_UART_IRQHandler函数
HAL_UART_IRQHandler是HAL库提供的中断处理公共函数,在串口中断服务函数中被调用:
c复制void HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
这个函数会根据中断类型调用相应的回调函数,如:
- HAL_UART_TxCpltCallback:发送完成回调
- HAL_UART_RxCpltCallback:接收完成回调
- HAL_UART_ErrorCallback:错误回调
这些回调函数都是弱定义的,我们可以直接在用户代码中重定义它们来实现自己的逻辑。
4. 串口通信配置步骤
4.1 串口初始化流程
-
串口参数初始化:
- 配置波特率、字长、奇偶校验等参数
- 调用HAL_UART_Init函数完成初始化
-
使能串口和GPIO时钟:
- 使能USART1时钟
- 使能GPIOA时钟(PA9和PA10)
-
GPIO模式设置:
- 配置PA9为复用推挽输出(USART1_TX)
- 配置PA10为浮空输入(USART1_RX)
-
开启串口相关中断:
- 配置串口中断优先级
- 使能串口中断
-
编写中断服务函数:
- 在USART1_IRQHandler中调用HAL_UART_IRQHandler
- 在HAL_UART_RxCpltCallback中实现数据接收处理
-
串口数据收发:
- 使用HAL_UART_Transmit发送数据
- 使用HAL_UART_Receive接收数据
4.2 关键代码解析
4.2.1 串口引脚定义
c复制/* 串口1的GPIO */
#define USART_TX_GPIO_PORT GPIOA
#define USART_TX_GPIO_PIN GPIO_PIN_9
#define USART_TX_GPIO_AF GPIO_AF7_USART1
#define USART_TX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_RX_GPIO_PORT GPIOA
#define USART_RX_GPIO_PIN GPIO_PIN_10
#define USART_RX_GPIO_AF GPIO_AF7_USART1
#define USART_RX_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
#define USART_UX USART1
#define USART_UX_IRQn USART1_IRQn
#define USART_UX_IRQHandler USART1_IRQHandler
#define USART_UX_CLK_ENABLE() do{ __HAL_RCC_USART1_CLK_ENABLE(); }while(0)
4.2.2 串口初始化函数
c复制void usart_init(uint32_t baudrate) {
uartx_handle.Instance = USART_UX; /* USART1 */
uartx_handle.Init.BaudRate = baudrate; /* 波特率 */
uartx_handle.Init.WordLength = UART_WORDLENGTH_8B; /* 字长为8位数据格式 */
uartx_handle.Init.StopBits = UART_STOPBITS_1; /* 一个停止位 */
uartx_handle.Init.Parity = UART_PARITY_NONE; /* 无奇偶校验位 */
uartx_handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; /* 无硬件流控 */
uartx_handle.Init.Mode = UART_MODE_TX_RX; /* 收发模式 */
HAL_UART_Init(&uartx_handle); /* HAL_UART_Init()会使能UART1 */
/* 开启接收中断 */
HAL_UART_Receive_IT(&uartx_handle, (uint8_t *)aRxbuffer, RXBUFFERSIZE);
}
4.2.3 MSP初始化函数
c复制void HAL_UART_MspInit(UART_HandleTypeDef *huart) {
GPIO_InitTypeDef gpio_init_struct;
if(huart->Instance == USART_UX) {
USART_UX_CLK_ENABLE(); /* USART1时钟使能 */
USART_TX_GPIO_CLK_ENABLE(); /* 发送引脚时钟使能 */
USART_RX_GPIO_CLK_ENABLE(); /* 接收引脚时钟使能 */
gpio_init_struct.Pin = USART_TX_GPIO_PIN; /* TX引脚 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = USART_TX_GPIO_AF; /* 复用为USART1 */
HAL_GPIO_Init(USART_TX_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = USART_RX_GPIO_PIN; /* RX引脚 */
gpio_init_struct.Alternate = USART_RX_GPIO_AF; /* 复用为USART1 */
HAL_GPIO_Init(USART_RX_GPIO_PORT, &gpio_init_struct);
#if USART_EN_RX
HAL_NVIC_EnableIRQ(USART_UX_IRQn); /* 使能USART1中断通道 */
HAL_NVIC_SetPriority(USART_UX_IRQn, 3, 3); /* 抢占优先级3,子优先级3 */
#endif
}
}
4.2.4 中断服务函数
c复制void USART_UX_IRQHandler(void) {
#if SYS_SUPPORT_OS
OSIntEnter();
#endif
HAL_UART_IRQHandler(&g_uart1_handle);
#if SYS_SUPPORT_OS
OSIntExit();
#endif
}
4.2.5 接收完成回调函数
c复制void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if(huart->Instance == USART_UX) {
if((g_usart_rx_sta & 0x8000) == 0) {
if(g_usart_rx_sta & 0x4000) {
if(aRxBuffer[0] != 0x0A) {
g_usart_rx_sta = 0;
} else {
g_usart_rx_sta |= 0x8000;
}
} else {
if (aRxBuffer[0] == 0x0D)
g_usart_rx_sta |= 0x4000;
else {
g_usart_rx_buf[g_usart_rx_sta & 0x3FFF] = aRxBuffer[0];
g_usart_rx_sta++;
if (g_usart_rx_sta > (USART_REC_LEN - 1)) {
g_usart_rx_sta = 0;
}
}
}
}
}
}
5. 主程序实现
5.1 主函数代码
c复制int main(void) {
uint8_t len;
uint16_t times = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
while(1) {
if (g_usart_rx_sta & 0x8000) {
len = g_usart_rx_sta & 0x3fff;
printf("\r\n您发送的消息为:\r\n");
HAL_UART_Transmit(&g_uart1_handle, (uint8_t*)g_usart_rx_buf, len, 1000);
while(__HAL_UART_GET_FLAG(&g_uart1_handle,UART_FLAG_TC) != SET);
printf("\r\n\r\n");
g_usart_rx_sta = 0;
} else {
times++;
if (times % 5000 == 0) {
printf("\r\n正点原子STM32开发板 串口实验\r\n");
printf("正点原子@ALIENTEK\r\n\r\n\r\n");
}
if (times % 200 == 0) printf("请输入数据,以回车键结束\r\n");
if (times % 30 == 0) LED0_TOGGLE();
delay_ms(10);
}
}
}
5.2 程序逻辑解析
-
初始化阶段:
- 初始化HAL库
- 配置系统时钟为168MHz
- 初始化延时函数
- 初始化串口(波特率115200)
- 初始化LED
-
主循环:
- 检查接收状态寄存器g_usart_rx_sta的最高位
- 如果最高位为1,表示接收到完整数据,将数据回显到串口
- 如果最高位为0,定期发送提示信息并闪烁LED
-
数据接收处理:
- 使用自定义的接收协议,通过回车(0x0D)和换行(0x0A)判断数据接收完成
- 接收状态寄存器g_usart_rx_sta的低14位记录接收到的数据长度
- 接收到回车后等待换行,如果收到则标记接收完成,否则视为接收错误
6. 常见问题与解决方案
6.1 波特率不匹配
现象:接收到的数据乱码
原因:通信双方的波特率设置不一致
解决方案:
- 检查STM32和上位机的波特率设置是否相同
- 确保系统时钟配置正确,波特率计算依赖于系统时钟
6.2 数据接收不完整
现象:只能接收到部分数据
原因:
- 接收缓冲区太小
- 中断优先级设置不当导致数据丢失
解决方案: - 增大接收缓冲区大小(修改USART_REC_LEN)
- 提高串口中断优先级
6.3 发送数据丢失
现象:发送的数据偶尔丢失
原因:未等待发送完成就进行下一次发送
解决方案:
- 使用HAL_UART_Transmit的timeout参数
- 或者检查USART_SR寄存器的TC位是否置位
c复制while(__HAL_UART_GET_FLAG(&huart, UART_FLAG_TC) == RESET);
6.4 中断响应不及时
现象:高速通信时数据丢失
原因:中断处理函数执行时间过长
解决方案:
- 优化中断处理函数,减少不必要的操作
- 使用DMA方式进行数据传输
- 提高系统时钟频率
7. 性能优化建议
7.1 使用DMA传输
对于高速串口通信,建议使用DMA来减轻CPU负担:
- 配置串口DMA发送和接收
- 使用DMA中断处理大量数据传输
7.2 环形缓冲区
实现环形缓冲区可以提高数据接收的可靠性:
- 定义环形缓冲区结构
- 在中断中只负责将数据存入缓冲区
- 在主循环中处理缓冲区数据
7.3 中断优先级优化
合理设置中断优先级可以避免数据丢失:
- 给串口中断设置较高的优先级
- 避免在串口中断中执行耗时操作
7.4 错误处理机制
完善的错误处理可以提高系统稳定性:
- 检查串口各种错误标志(溢出错误、噪声错误等)
- 实现自动重传机制
- 添加超时处理
8. 实验扩展
8.1 多串口通信
STM32F407有多个串口,可以尝试:
- 同时使用多个串口进行通信
- 实现串口之间的数据转发
8.2 自定义通信协议
在基础串口通信上实现更复杂的协议:
- 添加数据帧头帧尾
- 实现数据校验(CRC校验)
- 添加重传机制
8.3 与上位机通信
开发简单的上位机程序实现:
- 数据采集与显示
- 参数配置
- 固件升级
通过本实验,我们全面了解了STM32F407的串口通信实现方法,从基础概念到实际代码实现,掌握了串口初始化的各个步骤以及数据收发的处理方式。在实际项目中,可以根据具体需求选择合适的优化方案,提高通信的可靠性和效率。
