1. STM32串口通信基础与HAL库实现
串口通信(USART/UART)是嵌入式开发中最基础也最常用的通信方式之一。作为STM32开发者,掌握串口通信是必备技能。相比直接操作寄存器,使用ST官方提供的HAL库可以大幅降低开发难度,让我们更专注于业务逻辑的实现。
在实际项目中,串口通常用于:
- 设备调试信息输出
- 与其他微控制器或传感器的数据交互
- 通过USB转串口与PC通信
- 固件升级(如IAP功能)
HAL库对串口通信进行了高度封装,提供了简洁易用的API接口。但要想用好这些接口,我们需要先理解串口通信的基本原理。
2. 串口通信的分层模型
2.1 物理层:电平标准与硬件连接
串口通信的物理层主要定义电气特性和机械特性。常见的电平标准有两种:
| 特性 | TTL电平 | RS-232电平 |
|---|---|---|
| 逻辑1电压 | +3.3V ~ +5V | -3V ~ -15V |
| 逻辑0电压 | 0V | +3V ~ +15V |
| 传输距离 | 短(通常<1米) | 较长(可达15米) |
| 典型应用 | 板级设备间通信 | 工业设备、老式外设 |
在STM32开发中,我们通常使用TTL电平。当需要连接RS-232设备时,需要使用电平转换芯片如MAX3232。这款芯片的特点包括:
- 工作电压范围宽(3V-5.5V)
- 低功耗(典型值0.3μA关断电流)
- 集成电荷泵,无需外部电源即可产生±10V RS-232电平
硬件连接方面,串口通信最少只需要两根线:
- TX(发送端):连接到对方设备的RX
- RX(接收端):连接到对方设备的TX
注意:TTL电平的串口不能直接与RS-232设备连接,必须经过电平转换,否则可能损坏芯片。
2.2 协议层:数据帧格式与通信参数
串口通信是异步通信,收发双方需要约定相同的通信参数才能正常通信。一个完整的数据帧包含以下几个部分:
- 起始位:1位低电平,标志数据帧的开始
- 数据位:5-9位,通常使用8位(1字节)
- 校验位:可选,用于错误检测(奇校验/偶校验)
- 停止位:1-2位高电平,标志数据帧结束
常见的参数配置组合:
- 115200波特率,8数据位,无校验,1停止位(最常用)
- 9600波特率,8数据位,偶校验,1停止位
- 57600波特率,9数据位,无校验,2停止位
波特率计算是串口配置的关键。STM32的波特率计算公式为:
code复制波特率 = fPCLK / (16 * USARTDIV)
其中:
- fPCLK是USART的时钟频率
- USARTDIV是一个无符号定点数,整数部分存放在USART_BRR寄存器的DIV_Mantissa[11:0],小数部分在DIV_Fraction[3:0]
例如,当fPCLK=72MHz,目标波特率=115200时:
code复制USARTDIV = 72000000/(16*115200) = 39.0625
因此:
- DIV_Mantissa = 39 (0x27)
- DIV_Fraction = 0.0625*16 = 1 (0x1)
- USART_BRR = 0x271
3. STM32CubeMX配置USART
3.1 基本参数配置
使用STM32CubeMX配置串口的步骤如下:
- 在Pinout视图中找到USART1
- 模式选择"Asynchronous"(异步模式)
- 配置基本参数:
- Baud Rate: 115200
- Word Length: 8 Bits
- Parity: None
- Stop Bits: 1
- Over Sampling: 16 Samples
提示:在高速通信(>1Mbps)时,可以考虑使用8倍过采样(Over Sampling: 8 Samples)以提高通信速率。
3.2 生成代码分析
生成的初始化代码主要包含两部分:
- USART外设初始化:
c复制huart1.Instance = USART1;
huart1.Init.BaudRate = 115200;
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
huart1.Init.Mode = UART_MODE_TX_RX;
huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart1.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart1) != HAL_OK)
{
Error_Handler();
}
- GPIO初始化(以USART1为例):
c复制__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9; // TX
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10; // RX
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
注意:USART的TX引脚应配置为复用推挽输出(AF_PP),RX引脚配置为浮空输入(INPUT)。
4. HAL库串口发送实现
4.1 单字节发送
使用HAL_UART_Transmit函数发送单个字节:
c复制uint8_t ch = 'A';
HAL_UART_Transmit(&huart1, &ch, 1, HAL_MAX_DELAY);
参数说明:
- &huart1:USART句柄指针
- &ch:待发送数据缓冲区
- 1:发送数据长度(字节数)
- HAL_MAX_DELAY:超时时间(0xFFFFFFFF表示无限等待)
底层实现原理:
- 检查UART状态是否就绪
- 等待TXE标志(发送数据寄存器空)
- 将数据写入DR寄存器
- 等待TC标志(发送完成)
- 恢复UART状态
调试技巧:如果发送失败,可以检查huart1.gState的值,常见状态有:
- HAL_UART_STATE_READY(就绪)
- HAL_UART_STATE_BUSY_TX(正在发送)
- HAL_UART_STATE_BUSY_RX(正在接收)
4.2 字符串发送
发送字符串与发送单字节类似,只是数据长度不同:
c复制char msg[] = "Hello, USART!\r\n";
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), HAL_MAX_DELAY);
注意事项:
- 字符串结尾应添加"\r\n"实现换行
- 需要包含string.h头文件以使用strlen函数
- 强制类型转换(uint8_t*)是必要的,因为HAL库接口要求uint8_t指针
4.3 printf重定向
为了方便调试,我们通常会将printf重定向到串口输出。实现方法如下:
- 在代码中包含stdio.h头文件
- 重写fputc函数:
c复制#include <stdio.h>
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, HAL_MAX_DELAY);
return ch;
}
- 在工程设置中勾选"Use MicroLIB"(Keil MDK)
使用示例:
c复制printf("System Clock: %d Hz\r\n", HAL_RCC_GetSysClockFreq());
常见问题:如果printf无法正常工作,请检查:
- 是否勾选了Use MicroLIB
- 是否正确定义了fputc函数
- 串口初始化是否正确
- 是否包含了stdio.h头文件
5. 实际应用中的经验技巧
5.1 波特率误差与稳定性
波特率误差会影响通信质量,建议:
- 选择常见的标准波特率(如9600, 115200等)
- 确保时钟源稳定(使用外部晶振优于内部RC振荡器)
- 误差控制在3%以内(STM32通常可以做到0.1%以内)
计算实际波特率误差的公式:
code复制误差(%) = |(实际波特率 - 理论波特率)| / 理论波特率 * 100%
5.2 抗干扰设计
工业环境中串口通信易受干扰,可采取以下措施:
- 使用双绞线传输
- 添加终端电阻(通常120Ω)
- 在RX/TX线上串联33Ω电阻
- 对地并联TVS二极管防止浪涌
5.3 调试技巧
-
使用逻辑分析仪抓取波形,检查:
- 实际波特率
- 数据帧格式
- 信号质量
-
当通信异常时,按以下顺序排查:
- 检查硬件连接(TX/RX是否交叉连接)
- 确认双方波特率一致
- 检查数据格式(数据位、停止位、校验位)
- 用示波器观察信号波形
-
在代码中添加错误处理:
c复制HAL_StatusTypeDef status = HAL_UART_Transmit(...);
if(status != HAL_OK) {
printf("UART发送失败,错误码: %d\r\n", status);
}
6. 性能优化与高级功能
6.1 中断与DMA传输
对于高速或实时性要求高的场景,建议使用中断或DMA方式:
- 中断方式:
c复制HAL_UART_Transmit_IT(&huart1, pData, Size);
- DMA方式:
c复制HAL_UART_Transmit_DMA(&huart1, pData, Size);
对比:
| 方式 | CPU占用 | 实时性 | 适用场景 |
|---|---|---|---|
| 轮询 | 高 | 低 | 简单应用,低波特率 |
| 中断 | 中 | 中 | 中等波特率,多任务 |
| DMA | 低 | 高 | 高速传输,大数据量 |
6.2 自定义协议设计
在实际项目中,通常需要设计简单的通信协议。一个典型的帧结构可以是:
code复制[帧头][长度][数据][校验][帧尾]
示例实现:
c复制typedef struct {
uint8_t header; // 0xAA
uint8_t len; // 数据长度
uint8_t cmd; // 命令字
uint8_t data[32]; // 数据
uint8_t checksum; // 校验和
uint8_t footer; // 0x55
} UART_Frame;
void Send_Frame(UART_HandleTypeDef *huart, UART_Frame *frame)
{
// 计算校验和
frame->checksum = frame->cmd;
for(int i=0; i<frame->len; i++) {
frame->checksum += frame->data[i];
}
HAL_UART_Transmit(huart, (uint8_t*)frame, sizeof(UART_Frame), HAL_MAX_DELAY);
}
6.3 流控制(硬件流控)
在高波特率或不可靠环境中,可以使用硬件流控(RTS/CTS):
- 在CubeMX中使能硬件流控
- 连接对应的RTS/CTS引脚
- 代码中无需特殊处理,HAL库会自动管理
配置示例:
c复制huart1.Init.HwFlowCtl = UART_HWCONTROL_RTS_CTS;
7. 常见问题与解决方案
7.1 数据丢失或错乱
可能原因:
- 波特率不匹配
- 缓冲区溢出
- 中断优先级配置不当
解决方案:
- 精确测量实际波特率
- 增加接收缓冲区大小
- 调整中断优先级(USART中断应高于其他非关键中断)
7.2 只能发送不能接收
排查步骤:
- 检查RX引脚配置是否正确
- 确认对方设备确实发送了数据
- 检查是否启用了接收中断或轮询接收
7.3 printf输出浮点数异常
原因:MicroLIB默认不支持浮点数格式化
解决方案:
- 使用以下格式指定浮点支持:
c复制#pragma import(__use_no_semihosting_swi)
#pragma import(_printf_float)
- 或者将浮点数转换为整数输出:
c复制float temp = 25.6;
printf("Temperature: %d.%d\r\n", (int)temp, (int)(temp*10)%10);
8. 进阶应用实例
8.1 串口命令解析器
实现一个简单的命令行接口:
c复制void UART_Command_Parser(uint8_t *data, uint16_t len)
{
if(strncmp((char*)data, "LED_ON", 6) == 0) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
printf("LED is ON\r\n");
}
else if(strncmp((char*)data, "LED_OFF", 7) == 0) {
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
printf("LED is OFF\r\n");
}
else {
printf("Unknown command\r\n");
}
}
8.2 数据包接收与处理
使用状态机实现数据包接收:
c复制typedef enum {
STATE_HEADER,
STATE_LENGTH,
STATE_DATA,
STATE_CHECKSUM,
STATE_FOOTER
} UART_State;
void UART_Receive_Handler(uint8_t byte)
{
static UART_State state = STATE_HEADER;
static uint8_t buffer[64];
static uint8_t index = 0;
static uint8_t length = 0;
static uint8_t checksum = 0;
switch(state) {
case STATE_HEADER:
if(byte == 0xAA) {
state = STATE_LENGTH;
checksum = 0;
}
break;
case STATE_LENGTH:
length = byte;
checksum += byte;
index = 0;
state = STATE_DATA;
break;
case STATE_DATA:
buffer[index++] = byte;
checksum += byte;
if(index >= length) {
state = STATE_CHECKSUM;
}
break;
case STATE_CHECKSUM:
if(byte == checksum) {
state = STATE_FOOTER;
} else {
state = STATE_HEADER; // 校验失败
}
break;
case STATE_FOOTER:
if(byte == 0x55) {
// 完整数据包接收完成
Process_Packet(buffer, length);
}
state = STATE_HEADER;
break;
}
}
在实际项目中,串口通信的稳定性和可靠性至关重要。通过合理设计通信协议、添加错误检测机制、优化硬件设计,可以构建出适应各种工业环境的串口通信系统。HAL库提供的API虽然简单易用,但理解其底层原理对于解决复杂问题仍然非常必要。