1. UART通信基础认知
第一次接触UART是在调试树莓派和Arduino通信的时候,这个看似简单的双线通信协议背后藏着不少门道。UART(Universal Asynchronous Receiver/Transmitter)本质上是用时间约定代替时钟线的艺术——发送方和接收方约定好相同的波特率,就能在两根线上实现全双工通信。
最让我印象深刻的是它的异步特性:不需要像SPI那样额外拉一根时钟线,仅靠TX(发送)和RX(接收)两根数据线就能工作。但这也带来了同步难题——如果两端波特率存在误差,连续传输多个字节时就可能出现"滑码"。去年调试GPS模块时就遇到过这个问题,模块默认9600bps而我的程序设成了19200bps,结果收到的NMEA语句全是乱码。
关键细节:UART帧格式包含起始位(必为低电平)、5-9位数据位、可选校验位和1-2位停止位(必为高电平)。常见的8N1配置即8数据位、无校验、1停止位
2. 硬件层实现剖析
2.1 电平标准演变史
早期UART使用±12V的RS-232标准,现在的3.3V/5V TTL电平更省电但抗干扰差。我收藏的2003年款PLC模块还保留着DB9接口,用MAX232芯片做电平转换。现在嵌入式设备多用CH340这类USB转TTL芯片,成本不到5元。
实测中发现TTL电平的电压容限:
- 输入高电平≥2V(3.3V系统)或≥2.4V(5V系统)
- 输入低电平≤0.8V
- 输出高电平≥2.4V(带负载时)
- 输出低电平≤0.4V
2.2 波特率精度控制
STM32的USART波特率计算公式为:
code复制波特率 = fCK / (16 * USARTDIV)
其中USARTDIV是16位定点数(整数部分+小数部分/16)。曾用72MHz主频配置115200bps时,实际计算:
code复制USARTDIV = 72000000/(16*115200) ≈ 39.0625
对应寄存器值:整数39(0x27),小数1(0x1),实际波特率115107bps,误差0.08%在允许范围内。
3. 软件实现关键点
3.1 环形缓冲区设计
串口中断服务程序(ISR)必须足够快,我的解决方案是双缓冲策略:
c复制#define BUF_SIZE 128
typedef struct {
uint8_t head;
uint8_t tail;
uint8_t data[BUF_SIZE];
} ring_buf;
volatile ring_buf rx_buf; // 中断修改需加volatile
void USART1_IRQHandler(void) {
if(USART_GetITStatus(USART1, USART_IT_RXNE)) {
uint8_t ch = USART_ReceiveData(USART1);
uint8_t next_head = (rx_buf.head + 1) % BUF_SIZE;
if(next_head != rx_buf.tail) { // 非满
rx_buf.data[rx_buf.head] = ch;
rx_buf.head = next_head;
}
}
}
3.2 协议解析状态机
处理Modbus RTU协议时,需要超时检测帧结束。我的状态机实现:
c复制typedef enum {
STATE_IDLE,
STATE_RECEIVING,
STATE_COMPLETE
} uart_state;
void parse_uart(uint8_t ch) {
static uart_state state = STATE_IDLE;
static uint32_t last_rx_time = 0;
switch(state) {
case STATE_IDLE:
if(ch == ':') { // 帧起始符
reset_buffer();
state = STATE_RECEIVING;
}
break;
case STATE_RECEIVING:
if(get_tick() - last_rx_time > 3.5 * char_time) {
// 3.5字符超时判定帧结束
state = STATE_COMPLETE;
} else {
append_to_buffer(ch);
}
break;
}
last_rx_time = get_tick();
}
4. 实战调试经验
4.1 示波器抓包技巧
当通信异常时,我的诊断步骤:
- 先确认物理连接:TX/RX是否交叉连接(设备A的TX接设备B的RX)
- 用示波器测量:
- 检查起始位下降沿是否清晰
- 测量比特宽度(1/波特率)
- 验证停止位电平
- 逻辑分析仪解码:Saleae可以自动解析UART数据
4.2 常见故障处理表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 收到0x00或0xFF | 线路接触不良 | 检查连接器氧化情况 |
| 首字节正确后续乱码 | 波特率误差累积 | 改用更精确的时钟源 |
| 偶发数据错误 | 电磁干扰 | 增加100Ω终端电阻 |
| 无法接收任何数据 | 电平不匹配 | 确认双方是TTL或RS232 |
5. 性能优化进阶
5.1 DMA加速传输
STM32的USART配合DMA可大幅降低CPU占用。配置要点:
c复制// 发送DMA配置
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = tx_len;
DMA_Init(DMA1_Channel4, &DMA_InitStructure);
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
5.2 自适应波特率
有些Bootloader需要自动检测波特率,我的实现方法:
- 发送字符'U'(0x55,01010101b)
- 接收端测量两个下降沿之间的时间Δt
- 计算波特率 = 1 / (Δt * 8)
在STM32上通过输入捕获模式实现:
c复制TIM_ICInitStructure.TIM_Channel = TIM_Channel_2;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
6. 特殊应用场景
6.1 单线半双工模式
某些传感器使用单总线UART(如DHT12),需要切换方向:
c复制void set_tx_mode() {
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(UART_GPIO, &GPIO_InitStructure);
}
void set_rx_mode() {
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(UART_GPIO, &GPIO_InitStructure);
}
6.2 红外遥控编码
UART经38kHz载波调制后可传输红外信号,用PWM实现:
c复制TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_Pulse = 13; // 38kHz @ 72MHz
TIM_OC2Init(TIM4, &TIM_OCInitStructure);
最后分享一个冷知识:早期电传打字机使用的5位Baudot码,正是UART 5-8位数据位长度的历史渊源。现在虽然常用8N1格式,但在某些工业设备中仍能见到7E1(7数据位+偶校验)的配置
