第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 避坑指南

最后,分享几个我踩过的坑:

  1. 引脚复用配置错误:S32K的引脚功能很多,一定要确认你用的引脚确实支持LPUART功能。我曾经在S32K144上把PTA2配成了LPUART0,结果它其实是LPUART1的TX。查了两天数据手册才发现。
  2. 时钟没使能:LPUART的时钟门控在PCC模块里。忘了使能时钟,寄存器写进去全是0。这个坑我至少踩过三次。
  3. 波特率计算误差:前面说了,误差要控制在2%以内。如果误差太大,考虑用外部时钟或者换一个波特率。
  4. 中断优先级:如果用了中断,记得在NVIC里设置好优先级。我见过有人把UART中断优先级设得比系统滴答定时器还高,结果系统调度直接崩了。

好了,这一章的内容就到这里。串口打印虽然基础,但它是我们调试的"眼睛"。把这一步走稳了,后面的CAN、SPI、I2C才能玩得转。下一章我们讲GPIO中断,到时候见。