1. USART轮询通信基础与工程搭建
第一次接触STM32的USART通信时,我被各种术语搞得晕头转向。后来在实际项目中才发现,轮询方式是最容易上手的串口通信方法。所谓轮询,就是CPU不断查询串口状态来决定是否进行数据收发,就像你不停地查看手机有没有新消息一样。
我用STM32CubeIDE配置USART1时,PA9(TX)和PA10(RX)这两个引脚会自动高亮显示。配置界面有几个关键参数需要注意:
- 波特率:常用115200,这个值要和串口调试工具保持一致
- 数据位:默认8位
- 校验位:None(简单通信可以不用校验)
- 停止位:1位
生成代码后,在main.c里会自动出现MX_USART1_UART_Init()初始化函数。我习惯在初始化完成后加个测试发送,验证串口是否正常工作:
c复制uint8_t test_msg[] = "USART Init OK!\r\n";
HAL_UART_Transmit(&huart1, test_msg, sizeof(test_msg)-1, 100);
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据发送的格式化技巧
2.1 数值转字符串的三种方法
在物联网项目中,经常需要把传感器数值通过串口发送。最开始我傻傻地用原始二进制发送,结果调试起来特别痛苦。后来总结了三种实用的格式化方法:
- sprintf大法:最灵活的格式化工具
c复制float temp = 25.6;
char msg[32];
sprintf(msg, "Temperature: %.1fC\r\n", temp);
HAL_UART_Transmit(&huart1, (uint8_t*)msg, strlen(msg), 100);
- itoa转换:适合整数快速转换
c复制int voltage = 3300;
char volt_str[10];
itoa(voltage, volt_str, 10); // 十进制转换
- 自定义格式函数:当需要特定格式时
c复制void sendHex(uint8_t data) {
char hex[3];
hex[0] = (data >> 4) & 0x0F;
hex[0] += hex[0] < 10 ? '0' : 'A'-10;
hex[1] = data & 0x0F;
hex[1] += hex[1] < 10 ? '0' : 'A'-10;
hex[2] = '\0';
HAL_UART_Transmit(&huart1, (uint8_t*)hex, 2, 100);
}
2.2 高效发送缓冲区设计
在连续发送大量数据时,我踩过一个坑:直接用小缓冲区循环发送会导致数据碎片化。后来改用环形缓冲区后稳定多了:
c复制#define BUF_SIZE 256
typedef struct {
uint8_t data[BUF_SIZE];
uint16_t head;
uint16_t tail;
} RingBuffer;
void UART_SendBuffer(RingBuffer *buf) {
if(buf->head != buf->tail) {
uint16_t len = (buf->head > buf->tail) ?
(buf->head - buf->tail) :
(BUF_SIZE - buf->tail);
HAL_UART_Transmit(&huart1, &buf->data[buf->tail], len, 100);
buf->tail = (buf->tail + len) % BUF_SIZE;
}
}
3. 数据接收与协议解析
3.1 可靠接收的三种策略
轮询接收数据时,超时处理特别重要。经过多次测试,我总结出三种实用方案:
- 定长接收:适合固定格式协议
c复制uint8_t rx_data[10];
if(HAL_UART_Receive(&huart1, rx_data, 10, 50) == HAL_OK) {
// 处理10字节数据
}
- 超时拼接:处理变长数据
c复制uint8_t rx_buf[64];
uint16_t index = 0;
while(1) {
uint8_t ch;
if(HAL_UART_Receive(&huart1, &ch, 1, 10) == HAL_OK) {
rx_buf[index++] = ch;
if(ch == '\n' || index >= sizeof(rx_buf)) break;
} else {
if(index > 0) break; // 超时但收到部分数据
}
}
- 双缓冲机制:避免数据覆盖
c复制uint8_t buf1[128], buf2[128];
uint8_t *active_buf = buf1;
uint8_t *process_buf = buf2;
// 接收时交替使用两个缓冲区
3.2 指令解析实战案例
在智能家居项目中,我设计了一套简单指令格式:"CMD:PARAM\n"。比如"LED:ON\n"、"TEMP:?\n"。解析代码如下:
c复制void parseCommand(uint8_t *data) {
char *cmd = strtok((char*)data, ":");
char *param = strtok(NULL, "\r\n");
if(strcmp(cmd, "LED") == 0) {
if(strcmp(param, "ON") == 0) {
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, GPIO_PIN_RESET);
}
}
else if(strcmp(cmd, "GET") == 0) {
if(strcmp(param, "TEMP") == 0) {
float temp = readTemperature();
char reply[32];
sprintf(reply, "TEMP:%.1f\n", temp);
HAL_UART_Transmit(&huart1, (uint8_t*)reply, strlen(reply), 100);
}
}
}
4. 调试技巧与性能优化
4.1 串口调试三板斧
- 十六进制视图:当字符显示异常时,切换到HEX模式能看清真实数据
- 时间戳功能:在串口助手中开启时间戳,分析通信间隔
- 数据回显:在代码中添加接收回显,验证数据完整性
c复制HAL_UART_Transmit(&huart1, rx_buf, received_len, 100); // 回显接收到的数据
4.2 性能优化实践
- 减少轮询频率:在main循环中加入适当延时
c复制while(1) {
UART_Process(); // 串口处理函数
HAL_Delay(10); // 10ms轮询间隔
}
- DMA+轮询混合模式:大数据量接收时先用DMA,再用轮询处理尾包
- 发送缓冲优化:积累到一定数据量再统一发送,减少碎片化传输
记得在项目中期,我们遇到串口通信偶尔丢包的问题。后来发现是发送和接收处理在同一个while循环里,导致接收响应不及时。通过将接收处理移到中断优先级更高的定时器回调中,问题才彻底解决。这也让我明白,轮询方式虽然简单,但在复杂场景下还是需要考虑实时性要求。
