第3章 UART驱动移植:串口打印实战
各位同学,今天我们来聊聊UART。说实话,串口通信在嵌入式开发里就像人的呼吸一样自然。你调试代码、打印日志、跟外设通信,哪样离得开它?我做了这么多年汽车电子,几乎每个项目都会用到UART。今天我们就把它彻底搞明白。
3.1 UART通信协议回顾
UART,全称是Universal Asynchronous Receiver/Transmitter。说白了,就是一种异步串行通信协议。它只需要两根线:TX(发送)和RX(接收)。
为什么叫异步?因为没有时钟线。收发双方靠约定的波特率来同步。就像两个人约定好每分钟说多少个字,这样即使没有节拍器,也能听懂对方在说什么。
UART的数据帧结构是这样的:
| 起始位 | 数据位(5-8位) | 校验位(可选) | 停止位(1/1.5/2位) |
|---|---|---|---|
| 1位(低电平) | LSB先发 | 奇/偶校验 | 高电平 |
我个人习惯用8位数据位、无校验、1位停止位,也就是常说的8N1。这个配置在绝大多数场景下都够用了。
关键参数:波特率、数据位、停止位、校验位。这四个参数必须收发双方一致,否则就是乱码。
我在项目中遇到过一个问题:两个设备波特率都设成了115200,但就是通信不上。查了半天,发现一个用的是16MHz晶振,另一个是12MHz。波特率误差超过了2%,导致误码。所以啊,波特率精度很重要。
3.2 S32K的LPUART模块
S32K系列用的是LPUART,全称Low Power UART。名字里带个"Low Power",说明它针对低功耗做了优化。但别被名字骗了,它的功能一点都不少。
LPUART的主要特性:
- 支持最高115200bps(实际可以更高,但汽车电子常用这个)
- 内置FIFO,发送和接收各4字节
- 支持中断和DMA
- 支持硬件流控(RTS/CTS)
- 支持单线半双工模式
嗯,这里要注意:LPUART的时钟源可以选择总线时钟或者外部时钟。我建议用总线时钟,因为这样波特率计算更简单。
波特率计算公式:
波特率 = 时钟频率 / (16 × (SBR + 1))
其中SBR是波特率分频寄存器(Baud Rate Divider)的值。举个例子:
假设总线时钟 = 80MHz,目标波特率 = 115200
SBR = 80000000 / (16 × 115200) - 1 ≈ 42.4
取整后 SBR = 42
实际波特率 = 80000000 / (16 × 43) ≈ 116279
误差 = (116279 - 115200) / 115200 ≈ 0.94%
小技巧:波特率误差最好控制在2%以内。如果误差太大,可以考虑调整时钟源或者换一个更接近的波特率值。
3.3 中断模式 vs 轮询模式
这两种模式,说白了就是两种干活的方式。
轮询模式:CPU一直在那里问"数据来了吗?数据来了吗?"
- 优点:实现简单,代码量少
- 缺点:占用CPU,不能做其他事
- 适用场景:初始化打印、调试信息输出
中断模式:CPU该干嘛干嘛,数据来了它会通知你
- 优点:CPU利用率高,响应及时
- 缺点:代码复杂,需要处理中断嵌套
- 适用场景:实时通信、大数据量传输
我曾经在项目里犯过一个低级错误:用轮询模式接收数据,结果主循环里有个延时函数,直接把数据给丢了。从那以后,凡是接收数据我都用中断。
3.4 实战:串口打印
好了,理论说完了,我们来动手。今天的目标:用S32K的LPUART实现串口打印"Hello, S32K!"。
先看初始化代码:
void LPUART_Init(void)
{
/* 1. 使能LPUART时钟 */
PCC->PCCn[PCC_LPUART1_INDEX] |= PCC_PCCn_CGC_MASK;
/* 2. 配置引脚复用 */
/* 假设使用PTA2作为TX,PTA3作为RX */
PORTA->PCR[2] = PORT_PCR_MUX(2); /* LPUART1_TX */
PORTA->PCR[3] = PORT_PCR_MUX(2); /* LPUART1_RX */
/* 3. 复位LPUART */
LPUART1->GLOBAL |= LPUART_GLOBAL_RST_MASK;
LPUART1->GLOBAL &= ~LPUART_GLOBAL_RST_MASK;
/* 4. 配置波特率 */
/* 总线时钟80MHz,目标115200 */
LPUART1->BAUD = LPUART_BAUD_SBR(42); /* SBR = 42 */
/* 5. 配置数据格式:8N1 */
LPUART1->CTRL = LPUART_CTRL_M(0) | /* 8位数据 */
LPUART_CTRL_PE(0) | /* 无校验 */
LPUART_CTRL_ILT(1); /* 空闲线检测 */
/* 6. 使能发送和接收 */
LPUART1->CTRL |= LPUART_CTRL_TE_MASK | LPUART_CTRL_RE_MASK;
}
然后是发送一个字节的函数:
void LPUART_SendByte(uint8_t data)
{
/* 等待发送缓冲区空 */
while(!(LPUART1->STAT & LPUART_STAT_TDRE_MASK));
/* 发送数据 */
LPUART1->DATA = data;
}
最后是打印字符串:
void LPUART_SendString(const char *str)
{
while(*str != '\0')
{
LPUART_SendByte(*str++);
}
}
/* 主函数中调用 */
int main(void)
{
LPUART_Init();
LPUART_SendString("Hello, S32K!\r\n");
while(1)
{
/* 主循环 */
}
}
注意:发送字符串时别忘了加回车换行符"\r\n"。我刚开始做的时候经常忘记,结果所有打印都挤在一行,看着特别难受。
如果你用的是中断模式接收,中断服务函数大概长这样:
void LPUART1_IRQHandler(void)
{
uint8_t status = LPUART1->STAT;
uint8_t data;
if(status & LPUART_STAT_RDRF_MASK) /* 接收数据就绪 */
{
data = LPUART1->DATA;
/* 处理接收到的数据 */
/* 比如存入环形缓冲区 */
}
if(status & LPUART_STAT_OR_MASK) /* 溢出错误 */
{
LPUART1->STAT |= LPUART_STAT_OR_MASK; /* 清除标志 */
}
}
我个人建议:刚开始学的时候先用轮询模式,把流程跑通。等熟悉了再切换到中断模式。一口吃不成胖子,对吧?
3.5 避坑指南
最后,分享几个我踩过的坑:
- 引脚复用配置错误:S32K的引脚功能很多,一定要确认你用的引脚确实支持LPUART功能。我曾经在S32K144上把PTA2配成了LPUART0,结果它其实是LPUART1的TX。查了两天数据手册才发现。
- 时钟没使能:LPUART的时钟门控在PCC模块里。忘了使能时钟,寄存器写进去全是0。这个坑我至少踩过三次。
- 波特率计算误差:前面说了,误差要控制在2%以内。如果误差太大,考虑用外部时钟或者换一个波特率。
- 中断优先级:如果用了中断,记得在NVIC里设置好优先级。我见过有人把UART中断优先级设得比系统滴答定时器还高,结果系统调度直接崩了。
好了,这一章的内容就到这里。串口打印虽然基础,但它是我们调试的"眼睛"。把这一步走稳了,后面的CAN、SPI、I2C才能玩得转。下一章我们讲GPIO中断,到时候见。