4. 串口通信与调试终端

在 MCX 边缘智能终端上,串口往往是最先被点亮、也最常被依赖的外设:它既是启动日志的输出通道,也是调试阶段的交互入口,还可能在产品形态中承担与模组、传感器或上位机的数据链路。MCX 系列使用 LPUART(Low Power Universal Asynchronous Receiver/Transmitter)作为低功耗异步串口控制器,理解它的时钟来源、引脚复用、FIFO 与中断/DMA 触发机制,是后续所有调试手段的基础。

LPUART 的配置要点

LPUART 的波特率由模块时钟与分频寄存器共同决定。以常见的 12 MHz LPUART 功能时钟、目标 115200 bps 为例,分频系数 OSR 与 SBR 需满足 baud = f_clk / (OSR × (SBR + 1))。取 OSR=16 时,SBR ≈ 12 000 000 / (16 × 115200) − 1 ≈ 5.51,实际取整后存在约 0.2% 的误差,通常可接受;若时钟为 8 MHz,则误差会明显增大,此时应优先选择 16 MHz 或 24 MHz 等更易整除的时钟源。配置顺序建议为:先使能 LPUART 时钟与对应端口时钟,再配置引脚复用为 LPUART_TX/RX,随后设置 CTRL 中的 OSR、SBR、数据位、停止位与校验位,最后使能 TE/RE 与 FIFO。若使用硬件流控,还需把 RTS/CTS 引脚一并复用。

LPUART 内部带有发送与接收 FIFO,FIFO 水位是中断与 DMA 触发的关键参数。接收水位设得过高会增大溢出风险,设得过低则中断频繁;在 115200 bps 下,若主循环可能被其他任务阻塞数毫秒,建议把 RX 水位设为 1/4 或 1/2,并配合 DMA 搬运,避免逐字节中断带来的 CPU 开销。

LPUART 收发链路与调试终端数据流 PC 串口终端 115200 8N1 命令行输入 / 日志显示 USB-TTL 转换 MCX 微控制器 LPUART OSR / SBR 分频 TX FIFO / RX FIFO 水位触发标志 TE / RE 使能 RDRF / TDRE / IDLE NVIC 中断 RX 水位 / IDLE eDMA 环形缓冲搬运 环形接收缓冲区(Ring Buffer) head 写入指针 / tail 读取指针 / 溢出计数 命令行解析器 按行分割 → 匹配命令表 → 执行回调 → 回显结果 printf 重定向 _write / fputc 阻塞发送或 DMA 发送 日志格式化输出 TX RX 回显/日志 中断适合低频命令交互;DMA + 环形缓冲适合连续数据流;printf 重定向统一日志出口;命令行解析器负责把字节流变成可执行命令

中断收发与 DMA 收发的取舍

中断方式实现简单:在 LPUART 的 RX 中断服务函数中读取 DATA 寄存器,把字节写入软件环形缓冲区,并更新写指针;主循环从环形缓冲区取字节。这种方式在命令交互场景下完全够用,因为人输入命令的速率很低。但若对端以 1 Mbps 连续发送,逐字节中断会带来可观的 CPU 占用,此时应改用 DMA。

DMA 接收的典型做法是:把 DMA 源地址设为 LPUART 的 DATA 寄存器,目的地址设为环形缓冲区,传输长度设为缓冲区大小,并开启循环模式。当 DMA 完成一轮搬运或 LPUART 产生 IDLE 标志时,在中断中计算本次新增数据量并更新写指针。这里有一个容易出错的细节:DMA 的剩余计数寄存器记录的是“还剩多少字节未搬”,因此新增数据量等于“缓冲区大小 − 剩余计数”,而不是直接读剩余计数。若缓冲区大小为 256,剩余计数为 200,则本次新增 56 字节。

发送方向若使用 DMA,需要保证在上一笔发送完成前不覆盖发送缓冲区。常见做法是维护一个发送队列,DMA 完成中断触发后再启动下一笔;若只是打印日志,也可以采用阻塞发送,但要注意不要在中断上下文中调用阻塞发送,否则会拖长中断响应时间。

printf 重定向

在嵌入式工程中,printf 默认依赖半主机或系统调用,需要重定向到 LPUART。以 GCC/newlib 为例,通常实现 _write 或 fputc:

int _write(int fd, char *buf, int len)
{
    (void)fd;
    for (int i = 0; i < len; i++) {
        while (!(LPUART0->STAT & LPUART_STAT_TDRE_MASK)) {
            /* 等待发送数据寄存器空 */
        }
        LPUART0->DATA = (uint8_t)buf[i];
    }
    return len;
}

若希望日志不阻塞主循环,可把 _write 改为把数据放入发送环形缓冲区,由 DMA 或发送中断异步送出。需要注意:异步发送时,若缓冲区满,应选择丢弃、覆盖还是阻塞等待,这取决于日志的重要程度。调试阶段通常选择阻塞等待,产品阶段则倾向于丢弃低优先级日志并累加丢弃计数。

串口命令行交互终端实现

命令行终端的基本结构是“接收 → 组帧 → 解析 → 执行 → 回显”。接收层由中断或 DMA 把字节写入环形缓冲区;组帧层按行结束符(如 \r\n 或 \n)切分出一条命令,同时处理退格键(0x08)和删除键(0x7F),实现简单的行编辑;解析层按空格切分参数,查命令表;执行层调用对应回调;回显层输出结果或提示。

命令表可以用结构体数组描述,每个条目包含命令名、帮助字符串和函数指针。例如:

typedef struct {
    const char *name;
    const char *help;
    void (*handler)(int argc, char **argv);
} cmd_t;

static const cmd_t cmd_table[] = {
    { "help",  "列出所有命令",       cmd_help  },
    { "led",   "led <on|off>",      cmd_led   },
    { "adc",   "读取 ADC 通道值",    cmd_adc   },
    { "reset", "复位系统",           cmd_reset },
};

主循环中不断从环形缓冲区取字符,遇到行结束符就调用解析函数。解析时先跳过前导空格,取出命令名,再逐个提取参数,最后在命令表中查找。若未找到,输出 unknown command 并提示输入 help。为了便于使用,终端还应支持提示符(如 mcx>)、历史命令(可用环形数组保存最近若干条)以及参数个数校验。

调试建议:先用阻塞发送点亮“Hello LPUART”,确认波特率与引脚无误;再加入中断接收,用回环测试验证收发一致;最后接入 DMA 与命令行解析。每一步都保留可观察的输出,排障时才能快速定位是时钟、引脚、FIFO 水位还是缓冲区指针的问题。