4. STM32 USART模块详解:USART/LIN模式硬件特性、波特率计算、中断与DMA机制
好,咱们进入第四讲。这一章我打算把STM32的USART模块掰开了揉碎了讲清楚。尤其是它跟LIN总线相关的那些硬件特性。说实话,USART这个外设,在STM32里算是“老演员”了,但它的LIN模式,很多人用起来还是容易踩坑。
我个人习惯,在开始写代码之前,先把硬件的“脾气”摸透。你想想看,如果连波特率怎么算、中断什么时候触发都不清楚,那调试起来得多痛苦?
4.1 USART的LIN模式硬件特性
STM32的USART,说白了就是一个万能串口。它不仅能做标准的异步串口通信,还能切换成LIN总线模式。这个切换,是通过配置控制寄存器USART_CR2的LINEN位来实现的。
进入LIN模式后,USART的硬件行为会发生几个关键变化:
- 帧格式固定:LIN总线只支持8位数据位,加上1位起始位、1位停止位,没有奇偶校验。这个跟标准UART的8N1格式是一样的。
- 波特率限制:LIN总线的波特率最高20kbps,最低1kbps。虽然USART理论上能跑更高,但在LIN模式下,我建议你严格遵守这个范围。
- 同步间隔场检测:这是LIN总线最核心的特性。USART硬件能自动检测13位以上的显性电平(也就是低电平),作为帧起始的同步间隔场。
- 同步场自动处理:从机收到同步间隔场后,会等待同步场(0x55)。USART硬件能自动测量这个字节的位时间,用来校准波特率。
重点来了:STM32的USART在LIN模式下,不能自动发送同步间隔场。发送间隔场需要你手动控制TX引脚拉低一段时间。这个坑,我当年第一次做LIN项目时就踩过。
我记得有一次,从机怎么都收不到主机的帧头。查了半天,发现是主机发送的间隔场长度不够。LIN协议要求至少13位显性电平,而我当时只拉了11位。嗯,从那以后,我每次都会用逻辑分析仪确认间隔场的长度。
4.2 波特率计算:从理论到实践
波特率计算,是USART开发的基础。STM32的USART波特率公式很简单:
波特率 = fCK / (16 * USARTDIV)
其中fCK是USART的时钟频率,USARTDIV是一个16位的分频系数,存放在USART_BRR寄存器中。USARTDIV的高4位是整数部分,低12位是小数部分。
举个例子,假设fCK = 72MHz,目标波特率是19200bps:
USARTDIV = 72000000 / (16 * 19200) = 234.375
整数部分 = 234 = 0xEA
小数部分 = 0.375 * 16 = 6 = 0x6
USART_BRR = (0xEA << 4) | 0x6 = 0xEA6
实际波特率 = 72000000 / (16 * 234.375) = 19200bps,完美匹配。
小技巧:我一般会写一个宏定义来计算USART_BRR的值,避免手动计算出错。比如:
#define USART_BRR_VALUE(CLK, BAUD) ((CLK + (BAUD * 8)) / (16 * BAUD))
这个宏做了四舍五入,精度更高。
对于LIN总线,波特率通常比较低,比如9600bps或19200bps。这时候分频系数会比较大,误差也相对容易控制。但如果你用1kbps的超低波特率,分频系数会非常大,这时候要注意USART_BRR寄存器的溢出问题。
警告:USART_BRR寄存器只有16位,最大分频系数是65535。如果fCK = 72MHz,目标波特率 = 1kbps,那么USARTDIV = 4500,还在范围内。但如果fCK = 168MHz(比如STM32F4),USARTDIV = 10500,也还行。但再低就不行了。
4.3 中断机制:什么时候该用中断?
USART的中断,说白了就是硬件告诉你“有事发生了”。STM32的USART中断源很多,但常用的就这几个:
| 中断标志位 | 触发条件 | 典型用途 |
|---|---|---|
| RXNE | 接收数据寄存器非空 | 收到一个字节,赶紧去读 |
| TXE | 发送数据寄存器空 | 可以往DR寄存器写下一个字节 |
| TC | 发送完成 | 最后一个字节已经发出去了 |
| IDLE | 总线空闲 | 一帧数据接收完毕 |
| LBD | LIN间隔场检测 | 检测到同步间隔场 |
我个人习惯,在LIN从机开发中,必须开启RXNE中断和LBD中断。RXNE用来接收数据,LBD用来检测帧起始。至于TXE和TC,看情况。如果数据量不大,用轮询发送也行。
中断优先级怎么设?我一般把LBD中断设成最高优先级,因为它标志着新一帧的开始。RXNE中断次之,发送中断最低。这样能保证不会漏掉帧头。
你可能会问:“为什么不用IDLE中断?” IDLE中断确实可以用来判断一帧结束,但它的触发时机有点微妙。在LIN总线中,帧结束后的总线空闲时间很短,IDLE中断可能来不及响应。我建议用接收超时或者帧长度计数来判断帧结束,更可靠。
4.4 DMA机制:解放CPU的利器
DMA,全称是Direct Memory Access,直接存储器访问。它的作用,说白了就是让数据在USART和内存之间自动搬运,不需要CPU插手。
在LIN从机开发中,DMA特别适合处理两种情况:
- 接收大量数据:比如接收一帧8字节的LIN报文。用DMA的话,你只需要设置好接收缓冲区和数据长度,然后等着DMA传输完成中断就行。
- 发送固定数据:比如从机响应主机的请求,需要发送固定的几个字节。用DMA发送,CPU可以去做别的事情。
配置DMA的步骤,我总结了一下:
- 使能DMA时钟:RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
- 配置DMA通道:设置源地址、目标地址、数据长度、传输方向等。
- 关联USART:将DMA通道与USART的发送或接收请求连接起来。
- 使能DMA:启动DMA传输。
- 使能USART的DMA请求:在USART_CR3寄存器中设置DMAT或DMAR位。
注意:DMA传输完成后,会触发DMA的中断。你需要在DMA中断服务函数里处理数据。比如,接收完成后,把数据从缓冲区拷贝到协议栈里。
我记得有一次,用DMA接收LIN报文,结果发现数据总是错位。查了半天,原来是DMA的缓冲区地址没有对齐。STM32的DMA要求缓冲区地址按字对齐(4字节对齐),否则会出错。从那以后,我定义DMA缓冲区时都会加上__attribute__((aligned(4)))。
4.5 实战建议:中断+DMA的配合
在实际项目中,我通常这样搭配使用:
- LBD中断:检测到同步间隔场,立即复位接收状态机,准备接收新帧。
- DMA接收:自动将后续的同步场、标识符、数据、校验和存入缓冲区。
- DMA传输完成中断:一帧数据接收完毕,通知协议栈处理。
- DMA发送:需要发送响应时,用DMA自动发送数据。
这种方案的好处是,CPU几乎不用参与数据收发,可以专心处理协议逻辑。对于波特率不高的LIN总线来说,绰绰有余。
嗯,这一章的内容就到这里。下一章,我会带你手把手配置USART的LIN模式,从寄存器到代码,一步步来。到时候你会看到,理论跟实践结合起来,其实没那么难。