4. UART串口通信驱动开发(上):协议基础与CMS32寄存器配置
各位同学,咱们今天正式进入UART串口通信的实战环节。UART这个东西,说简单也简单,说复杂也复杂。我做了这么多年嵌入式,可以说90%的调试工作都离不开它——打印日志、与传感器通信、甚至做Bootloader升级,全得靠这条串口线。
这一章我们先打好基础,把UART协议本身搞明白,再把CMS32芯片上的UART模块寄存器摸清楚。波特率怎么配、数据位/停止位/校验位怎么设,这些看似基础的东西,其实藏着不少坑。我当年刚入行时就在波特率偏差上吃过亏,后面会跟大家细说。
4.1 UART协议基础:串口通信的"老规矩"
UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,它就是一根发送线(TX)、一根接收线(RX),再加上地线(GND),就能实现两个设备之间的通信。
为什么叫"异步"?因为没有独立的时钟线。收发双方得事先约定好通信速率——也就是波特率,然后各自用自己的时钟去采样数据。这就好比两个人约好"每秒说一个字",然后各自掐表,谁也别依赖谁。
UART数据帧结构(标准格式):
- 起始位:1位,逻辑低电平,表示"我要开始发数据了"
- 数据位:5~9位,常见为8位(一字节)
- 校验位:0或1位,可选奇校验/偶校验/无校验
- 停止位:1位、1.5位或2位,逻辑高电平,表示"这一帧结束"
我给大家画个图,这样更直观:
这里有个细节我想强调一下:数据位是LSB first,也就是最低位先发。比如你要发0x55(二进制01010101),先发出去的是最低位的1,而不是最高位的0。这个顺序跟SPI、I2C不一样,别搞混了。
个人经验:我调试串口时,最常用的套路就是发0x55和0xAA这两个数。0x55是01010101,0xAA是10101010,用示波器一看波形,数据位顺序对不对、波特率准不准,一目了然。
4.2 CMS32 UART模块寄存器全景
好,协议层面搞清楚了,咱们来看看CMS32芯片上的UART模块长什么样。CMS32的UART模块,我习惯叫它"串口外设",它内部有一组寄存器来控制通信行为。
咱们不需要把所有寄存器都背下来,但核心的几个必须烂熟于心。我列个表,大家对照着看:
| 寄存器名称 | 偏移地址 | 功能描述 | 我常用的配置 |
|---|---|---|---|
| UART_BR | 0x00 | 波特率配置寄存器 | 根据时钟频率计算分频值 |
| UART_CR | 0x04 | 控制寄存器(使能、中断、模式) | 使能TX/RX,开启接收中断 |
| UART_LCR | 0x08 | 线路控制寄存器(数据位/停止位/校验位) | 8N1(8数据位、无校验、1停止位) |
| UART_SR | 0x0C | 状态寄存器(发送完成、接收就绪、溢出等) | 轮询或中断中读取判断 |
| UART_TDR | 0x10 | 发送数据寄存器 | 写入要发送的字节 |
| UART_RDR | 0x14 | 接收数据寄存器 | 读取接收到的字节 |
这6个寄存器,就是咱们操作UART的全部家当。你想想看,一个串口收发,本质上就是:配波特率 → 设格式 → 写TDR发数据 → 读RDR收数据。就这么简单。
注意:CMS32的UART模块在配置寄存器时,建议先关闭UART使能(CR寄存器中的EN位清0),等所有参数配好后再打开。我曾经在项目里图省事,没关使能就直接改波特率,结果串口直接"哑"了,排查了半天才发现是配置时序的问题。
4.3 波特率配置:算不对就等着收乱码
波特率是UART通信的"节拍器"。收发双方必须用同一个速率,否则收到的数据全是乱码。
CMS32的波特率生成原理是这样的:系统时钟(比如48MHz)经过一个分频器,得到目标波特率。分频值计算公式为:
分频值 = 系统时钟频率 / (目标波特率 × 16) - 1
为什么要除以16?因为UART模块内部会用16倍波特率的时钟去采样每一位数据,这样能提高抗干扰能力。这个16是硬件固定的,咱们不用纠结。
举个例子,假设系统时钟是48MHz,想要115200bps的波特率:
分频值 = 48000000 / (115200 × 16) - 1
= 48000000 / 1843200 - 1
= 26.04 - 1
≈ 25(取整)
取整后实际波特率 = 48000000 / (16 × (25 + 1)) = 48000000 / 416 = 115384.6 bps
误差 = (115384.6 - 115200) / 115200 ≈ 0.16%
波特率误差容忍度:一般要求误差在±2%以内。0.16%完全没问题。但如果系统时钟是16MHz,想要921600bps,分频值算出来可能只有0点几,取整后误差就大了。这时候就得考虑换更高精度的时钟源,或者降低波特率。
代码实现也很直接:
/* 波特率配置函数 */
void UART_SetBaudRate(uint32_t sys_clk_hz, uint32_t target_baud) {
uint32_t divisor;
/* 计算分频值 */
divisor = sys_clk_hz / (target_baud * 16) - 1;
/* 写入波特率寄存器 */
UART_BR = divisor & 0xFFFF;
/* 我习惯在这里加个断言,检查误差是否在允许范围内 */
/* 实际项目中可以加个调试打印 */
}
避坑指南:我曾经在一个项目里,系统时钟是24MHz,想要配个9600波特率。算出来分频值是155.5,取整156。结果实际波特率是9600.6,误差0.006%,看起来没问题对吧?但那个项目用的是外部晶振,精度只有±50ppm,加上温度漂移,实际误差就超了。所以,波特率配置一定要考虑时钟源的精度,不能只看理论计算。
4.4 数据位/停止位/校验位设置
这三个参数都在线路控制寄存器(UART_LCR)里配。咱们一个个说。
4.4.1 数据位长度
CMS32支持5、6、7、8位数据长度。现在绝大多数场景都用8位,因为一字节正好8位。但有些老设备或者特殊协议会用7位甚至5位,比如某些工业仪表用7位ASCII码传输。
LCR寄存器中,数据位由两个位控制(假设为LCR[1:0]):
| LCR[1:0] | 数据位长度 | 适用场景 |
|---|---|---|
| 00 | 5位 | 老式电传打字机(现在基本不用) |
| 01 | 6位 | 极少见 |
| 10 | 7位 | ASCII码传输(某些工业设备) |
| 11 | 8位 | 最常用,默认选这个 |
4.4.2 停止位长度
停止位是帧结束的标志,高电平。CMS32支持1位、1.5位和2位停止位。
- 1位停止位:最常用,效率最高
- 1.5位:很少见,某些老设备会用
- 2位停止位:给接收端更多处理时间,适合低速设备或长距离传输
我个人习惯,只要不是特殊要求,一律用1位停止位。2位停止位相当于每帧多浪费1位时间,在高速传输时影响挺明显的。
4.4.3 校验位
校验位用于简单的错误检测。有三种模式:
- 无校验(None):不发送校验位,帧长度最短
- 奇校验(Odd):数据位+校验位中1的总数为奇数
- 偶校验(Even):数据位+校验位中1的总数为偶数
举个例子,数据是0x55(二进制01010101,有4个1):
- 奇校验:校验位=1(让总1数变成5,奇数)
- 偶校验:校验位=0(总1数保持4,偶数)
配置代码也很简单:
/* 配置UART为8N1模式:8数据位、无校验、1停止位 */
void UART_SetFormat_8N1(void) {
uint32_t lcr_val = 0;
/* 数据位:8位 -> LCR[1:0] = 11 */
lcr_val |= (0x03 << 0);
/* 停止位:1位 -> LCR[2] = 0 */
lcr_val &= ~(1 << 2);
/* 校验位:无校验 -> LCR[4:3] = 00 */
lcr_val &= ~((1 << 3) | (1 << 4));
/* 写入寄存器 */
UART_LCR = lcr_val;
}
重要提醒:配置LCR寄存器时,一定要一次性写完所有位,不要分多次写。因为LCR的某些位变化会触发硬件重新初始化,分步写可能导致中间状态不稳定。我见过有同事先配数据位,再配停止位,结果中间那几微秒里UART发出了错误的帧,把对端设备搞"懵"了。
4.5 完整初始化流程总结
好了,知识点都讲完了。咱们把UART初始化的完整流程串一下:
- 使能UART模块时钟(在时钟管理寄存器里打开UART外设时钟门控)
- 配置GPIO引脚复用(把TX、RX引脚设为UART功能)
- 关闭UART使能(CR寄存器EN位清0)
- 配置波特率(写UART_BR寄存器)
- 配置数据格式(写UART_LCR寄存器,一次性完成)
- 配置中断(可选)(使能接收中断、发送完成中断等)
- 打开UART使能(CR寄存器EN位置1)
这个顺序是我多年调试总结出来的,每一步都有它的道理。尤其是第3步和第7步,先关再开,能保证所有配置在UART使能前就绪,避免出现不可预期的行为。
核心要点回顾:
- UART是异步通信,收发双方靠约定好的波特率同步
- 数据帧结构:起始位(1) + 数据位(5~8) + 校验位(0/1) + 停止位(1/1.5/2)
- 波特率分频值 = 系统时钟 / (目标波特率 × 16) - 1,误差需控制在±2%以内
- LCR寄存器必须一次性配置,避免中间态
- 初始化流程:时钟→GPIO→关使能→配参数→开使能