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 开销。
中断收发与 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>)、历史命令(可用环形数组保存最近若干条)以及参数个数校验。