
1. 项目概述与核心价值在嵌入式开发领域串行通信接口SCI或通用异步收发器UART是连接传感器、显示屏、无线模块或进行系统调试的“生命线”。然而硬件资源总是有限的尤其是当你手头的微控制器MCU只有一个硬件UART而项目需求却需要两个甚至更多独立的串口通道时问题就来了。直接更换芯片或增加外部UART扩展芯片会增加成本和PCB面积这时候软件UARTSoftware UART的价值就凸显出来了。简单来说软件UART就是“用代码模拟硬件”。它不依赖MCU内置的专用串口硬件而是利用MCU上现有的通用外设比如通用定时器或PWM模块通过精确的定时和GPIO引脚电平控制来“模仿”出UART通信所需的时序波形。这次我们要拆解的就是基于Freescale现NXPMMC2001这款M•CORE架构的32位MCU利用其PWM模块和中断系统实现一个最高支持19200波特率、8N1格式的半双工软件UART。这个方案的精妙之处在于其“中断驱动”的架构。它不像一些简单的轮询式软件串口那样会长时间霸占CPU而是让PWM定时器在每一位数据发送或接收的精确时刻产生中断CPU只在中断服务程序ISR中执行极短的操作如翻转一个IO口电平或读取一个引脚状态随后立即返回处理其他任务。这使得CPU利用率极高特别适合在需要同时处理多任务的实时系统中作为补充通信通道。对于从事工业控制、物联网终端、或者任何需要低成本扩展串口数量的开发者而言掌握这套从硬件原理到软件实现的全流程是一项非常扎实且实用的技能。2. 方案核心为何选择PWM与中断在动手写代码之前我们必须先想清楚为什么是PWM为什么用中断这背后是嵌入式系统设计中对资源利用率和实时性的权衡。2.1 PWM模块作为精确定时器的优势MMC2001拥有6个独立的PWM通道。虽然名为“脉冲宽度调制”但其核心是一个可编程的、带预分频器的递减计数器。我们可以完全忽略其“调制”功能而将其配置为一个简单的周期性定时器。具体来说可编程周期通过设置周期寄存器PWMPR我们可以精确控制定时器溢出的时间间隔这个间隔正好对应UART通信中一个比特位的持续时间位周期。中断触发PWM模块可以在计数器达到周期值时产生中断请求这为我们提供了位级别的精确时间基准。独立运行每个PWM通道独立工作互不干扰。这允许我们用一个PWM如PWM5专门负责发送时序另一个PWM如PWM4专门负责接收采样实现了逻辑上的分离。相比于使用系统滴答定时器SysTick或普通通用定时器PWM模块的配置更直接中断源独立不会与其他系统定时任务冲突是实现精准位定时的理想选择。2.2 中断驱动 vs. 轮询查询这是本方案性能优劣的关键。UART通信是慢速的相对于CPU主频。以9600波特率为例发送一个字节10位包括起始位、8数据位、停止位需要约1.04毫秒。轮询方式CPU需要不断查询定时器标志位或进行软件延时在1.04毫秒内几乎被完全占用无法执行其他任务效率极低。中断驱动方式CPU仅在每个位周期104微秒的边界处被中断一次执行几十条指令设置或读取GPIO后立即返回。CPU在绝大部分时间约99.9%是空闲的可以处理其他应用程序。因此中断驱动方案将CPU从繁重的时序等待中解放出来实现了“异步”通信这是构建高效多任务嵌入式系统的基石。当然中断引入的“抖动”Jitter和中断响应延迟是需要仔细评估和优化的点但对于19200波特率及以下的低速通信MMC2001的M•CORE内核完全能够胜任。2.3 半双工通信的设计考量原文实现的是一个半双工软件UART。这意味着在同一时刻它只能发送或接收不能同时进行。这是基于简化设计和资源复用的考虑引脚复用发送和接收可以共用同一个GPIO引脚通过方向寄存器切换或者使用两个独立引脚但逻辑上互斥。原文中使用了独立的TX和RX引脚。状态管理简化无需处理同时收发可能带来的缓冲区竞争和中断优先级嵌套等复杂问题。满足多数场景很多主从式通信协议如Modbus RTU本身就是半双工的一问一答。这种设计完全够用。如果需要全双工理论上可以扩展为使用两个独立的PWM定时器和两个外部中断引脚分别独立管理发送和接收时序但代码复杂度和中断负载会翻倍。3. 硬件与软件架构深度解析理解了“为什么”之后我们来看“是什么”。整个软件UART系统可以看作由几个紧密协作的模块构成。3.1 硬件资源映射在MMC2001上具体资源分配如下发送端TX定时器PWM5通道。负责产生精确的位周期中断。数据引脚PWM5对应的GPIO引脚配置为输出。直接通过写PWM控制寄存器的DATA位来控制该引脚输出高逻辑1或低逻辑0。接收端RX定时器PWM4通道。负责在检测到起始位后产生位于每个数据位中点时刻的中断用于采样。数据引脚外部中断引脚INT6属于EDGE端口。配置为下降沿触发用于检测起始位的开始。这里有一个关键技巧接收端使用了两个中断源。第一个是INT6的边沿中断用于“唤醒”接收流程第二个是PWM4的周期中断用于在位中间进行采样。这种设计比单纯用定时器轮询检测起始位要可靠和节能得多。3.2 软件状态机与数据流软件的核心是三个中断服务程序ISR和一组全局变量构成的状态机。发送状态机sci_txISR状态0发送起始位将TX引脚拉低进入状态1。状态1发送8个数据位根据全局变量mask从tx_buffer中取出当前要发送的位LSB优先设置TX引脚电平。mask左移一位。当mask移出最高位后进入状态2。状态2发送停止位将TX引脚拉高。重置mask和状态为0准备发送下一个字节。循环与结束每发送完一个字节字节索引j加1。当j等于发送数组长度tx_length时停止PWM5定时器结束本次发送并清除“正在发送”标志。接收状态机第一级sci_rxISRINT6中断当INT6引脚检测到下降沿可能是起始位如果当前不在发送状态则立即启动PWM4定时器。注意此时PWM4的周期被预设为半个位周期例如对于9600波特率周期设为416个时钟目的是为了在半个位周期后即起始位的正中间产生第一次中断去验证是否为真正的起始位低电平。第二级sci_receiveISRPWM4中断第一次中断起始位中点采样检查INT6引脚是否仍为低电平。如果是则确认是有效的起始位立即将PWM4的周期改为一个完整的位周期如833并将采样状态索引k加1跳过起始位。后续第2-9次中断数据位中点采样在数据位中点采样INT6引脚电平并将该位移入接收缓冲区rx_buffer的当前字节中。采用右移方式实现LSB优先接收。第10次中断停止位采样检查INT6引脚是否为高电平停止位。如果是则停止PWM4定时器完成一个字节的接收递增接收缓冲区索引并重新将PWM4周期设为半个位周期以准备检测下一个字节的起始位。这种“起始位边沿触发位中点定时采样”的双重机制极大地提高了抗干扰能力能有效滤除短时脉冲干扰。3.3 全局变量与共享资源管理由于中断服务程序不能有复杂的参数传递因此通过一组全局变量在ISR和主程序间通信tx_buffer,rx_buffer发送/接收数据缓冲区指针。tx_length,rx_length发送/接收数据长度。tx_state,mask,j,k发送和接收状态机的状态标识、位掩码和索引。transmit一个布尔标志。为TRUE时表示正在发送此时禁止接收中断启动新的接收流程实现半双工互斥。注意在多任务或主循环频繁操作这些变量的场景下需要考虑临界区保护。虽然本例中主循环除了启动发送外基本是空闲的但在更复杂的系统中当主程序读取rx_buffer或修改tx_buffer时可能需要暂时关闭相关中断以防止数据错乱。4. 代码实现与关键寄存器配置详解让我们深入到代码层面看看如何配置MMC2001的PWM和中断控制器以及三个核心ISR是如何实现的。4.1 初始化函数sci_init()初始化是搭建舞台的关键任何错误都会导致通信失败。void sci_init(void) { void *VBA __PWS_OnChipRamBase; // 中断向量表基地址设为内部RAM起始地址 // 1. 配置发送PWM5 PWM_A_SetRegister( tx_pwmptr, PWM_A_PWMCR_SWITCH, tx_pwmptr-PWMCR PWM_A_IRQEN_MASK | PWM_A_DATA_MASK | PWM_A_DIR_MASK | PWM_A_DIV_4 ); PWM_A_UpdateOutput(tx_pwmptr, FALSE, 833, 416); SCI_TX_ON; // 将TX引脚置高空闲状态 // 2. 配置接收PWM4 PWM_A_SetRegister( rx_pwmptr, PWM_A_PWMCR_SWITCH, rx_pwmptr-PWMCR | PWM_A_IRQEN_MASK // 仅使能中断引脚默认为输入 ); PWM_A_UpdateOutput(rx_pwmptr, FALSE, 416, 208); // 初始周期为半位周期用于起始位验证 // 3. 初始化中断控制器并设置向量表 INTC_A_Init(intctlr, VBA, funcs); // 4. 注册三个中断服务函数 INTC_A_SetISF(intctlr, INTSRC_PWM5_BITNO, INTSRC_PWM5_MASK, (ddErr_t(*)(void *, void *))sci_tx, NULL, NULL); INTC_A_SetISF(intctlr, INTSRC_INT6_BITNO, INTSRC_INT6_MASK, (ddErr_t(*)(void *, void *))sci_rx, NULL, NULL); INTC_A_SetISF(intctlr, INTSRC_PWM4_BITNO, INTSRC_PWM4_MASK, (ddErr_t(*)(void *, void *))sci_receive, NULL, NULL); // 5. 使能中断 INTC_A_IntEnable(intctlr, INTSRC_PWM4_MASK | INTSRC_PWM5_MASK | INTSRC_INT6_MASK, TRUE, TRUE); // 6. 配置INT6引脚为下降沿敏感 edgeportptr-EPPAR | EPPAR_EPPA6_FALLING_EDGE_MASK; }关键点解析时钟分频PWM_A_DIV_4将系统时钟假设32MHz4分频得到8MHz的PWM参考时钟。这是计算位周期计数值的基础。周期计算位周期计数值 PWM参考时钟频率 / 目标波特率。对于9600波特率8,000,000 / 9600 ≈ 833。这就是发送PWM5的周期值。接收PWM4初始化为半周期416用于起始位中点采样。中断向量INTC_A_Init将中断向量表重定位到内部RAM0x30000000。这样做的好处是允许动态修改中断服务程序地址比固化在Flash中更灵活。中断使能层级需要三层使能外设级PWMCR中的IRQEN、中断控制器级FIER/NIER、以及处理器核心级PSR中的EE/FE。INTC_A_IntEnable函数一次性完成了后两层的配置。4.2 发送函数sci_send()与中断服务程序sci_tx()发送由应用层触发通过中断逐位完成。void sci_send(u1 *buffer_ptr, s2 buffer_length) { transmit TRUE; // 设置发送标志阻塞接收 tx_length buffer_length; tx_buffer buffer_ptr; PWM_A_Start(tx_pwmptr); // 启动PWM5定时器开始产生位周期中断 }SCI_TX_OFF和SCI_TX_ON是两个宏直接操作PWM5控制寄存器的DATA位来控制引脚电平。这种方式比通过通用GPIO模块操作更快代码更简洁。#define SCI_TX_OFF tx_pwmptr-PWMCR ~PWM_A_DATA_MASK // DATA位清0引脚输出低电平 #define SCI_TX_ON tx_pwmptr-PWMCR | PWM_A_DATA_MASK // DATA位置1引脚输出高电平SCI_TXISR是一个清晰的状态机void sci_tx(void) { tx_pwmptr-PWMCR ~PWM_A_IRQ_MASK; // 清除PWM中断标志位重要 switch (tx_state) { case 0: // 发送起始位 SCI_TX_OFF; // 起始位为低电平 tx_state 1; break; case 1: // 发送数据位 if (tx_buffer[j] mask) // 检查当前位是1还是0 SCI_TX_ON; else SCI_TX_OFF; mask 1; // 准备下一个数据位 if(!mask) // 如果mask移出最高位说明8个数据位已发完 tx_state 2; break; case 2: // 发送停止位 SCI_TX_ON; // 停止位为高电平 mask MASK_INIT; // 重置掩码指向LSB tx_state 0; j; // 指向下一个待发送字节 } if (j tx_length) { // 所有字节发送完毕 PWM_A_Stop(tx_pwmptr); j 0; transmit FALSE; // 清除发送标志允许接收 } }4.3 接收中断服务程序sci_rx()与sci_receive()接收是完全由中断驱动的被动过程。void sci_rx(void) { edgeportptr-EPFR | EPFR_EPF6_MASK; // 清除INT6边沿中断标志必须 if (!transmit) // 半双工检查如果当前不在发送 PWM_A_Start(rx_pwmptr); // 启动接收定时器PWM4准备半周期后采样 else PWM_A_Stop(rx_pwmptr); // 如果正在发送则忽略此次接收 }SCI_RECEIVEISR是接收状态机的核心void sci_receive(void) { intctlr-FIER ~INTSRC_PWM5_MASK; // 临时禁用发送中断防止干扰 rx_pwmptr-PWMCR ~PWM_A_IRQ_MASK; // 清除PWM4中断标志 if ((edgeportptr-EPDR EPDR_EPD6_MASK) 0) { // 在起始位中点采样 // 确认是有效的起始位低电平 rx_pwmptr-PWMPR 833; // 将PWM4周期改为一个完整的位周期 if (k 0) k; // k0表示是第一次进入起始位跳过k1开始是数据位 } else if (k 9) { // 接收第1到第8个数据位 (k1~8) rx_buffer[rx_length] 1; // 将已接收的位右移 // 如果当前采样为高电平则设置到最高位因为我们是右移进来的 if (edgeportptr-EPDR EPDR_EPD6_MASK) rx_buffer[rx_length] | 0x80; k; } else if (edgeportptr-EPDR EPDR_EPD6_MASK) { // 第9次中断检查停止位高电平 PWM_A_Stop(rx_pwmptr); // 停止接收定时器 k 0; // 重置位索引 rx_length; // 递增接收缓冲区索引 intctlr-FIER | INTSRC_INT6_MASK; // 重新使能INT6中断准备接收下一个字节 rx_pwmptr-PWMPR 416; // 将PWM4周期重置为半位周期用于下一次起始位检测 } }关键技巧接收数据时rx_buffer[rx_length] 1;这行代码实现了LSB优先的移位接收。首次右移时缓冲区是0所以没关系。采样到的高电平被| 0x80设置到最高位然后通过后续的右移逐渐移动到正确的位置。这是一种非常高效的位组装方法。5. 波特率计算与时钟配置实战波特率配置的准确性直接决定了通信的成败。这里详细推导一下计算过程。已知条件MMC2001系统主时钟SYSCLK假设为32 MHz具体需查阅你的芯片手册和时钟配置。PWM时钟预分频器CLKSEL我们选择4分频PWM_A_DIV_4。目标波特率9600 bps。计算步骤计算PWM定时器时钟PWM_CLKPWM_CLK SYSCLK / DIV_RATIO 32 MHz / 4 8 MHz这意味着PWM计数器每递增1需要的时间为1 / 8 MHz 0.125 µs。计算一个位周期所需的定时器计数值PERIOD_VALUE 位周期T_bit 1 / 波特率 1 / 9600 ≈ 104.1667 µs。PERIOD_VALUE T_bit / (1 / PWM_CLK) 104.1667 µs / 0.125 µs ≈ 833.33由于定时器周期寄存器是整数我们取整为833。验证实际波特率误差 实际位周期T_bit_actual 833 * 0.125 µs 104.125 µs。 实际波特率Baud_actual 1 / 104.125 µs ≈ 9603.84 bps。 误差率 (9603.84 - 9600) / 9600 * 100% ≈ 0.04%。这个误差远小于RS-232标准允许的误差通常3%因此完全可行。接收起始位采样点的周期值 为了在起始位的正中间采样第一次中断应在起始位开始后的半个位周期触发。HALF_PERIOD_VALUE PERIOD_VALUE / 2 ≈ 416.5取整为416。配置到代码中发送PWM5周期寄存器PWMPR设置为833。接收PWM4周期寄存器初始化为416在确认起始位后改为833。重要提示如果系统时钟不是32MHz或者你需要其他波特率如19200 4800请按照上述公式重新计算。提高波特率时需注意中断服务程序的执行时间必须远小于位周期否则CPU可能无法及时响应。6. 调试技巧、常见问题与优化建议在实际焊接电路和编写代码时你几乎一定会遇到通信失败的情况。别慌按照以下步骤排查和思考优化。6.1 硬件连接与信号测量电平匹配MMC2001的GPIO引脚通常是3.3V CMOS电平。如果你的通信对象如PC是RS-232电平±12V必须使用电平转换芯片如MAX3232否则会损坏MCU且无法通信。共地确保发送端和接收端的GND地线可靠连接这是信号参考的基础。示波器是你的眼睛这是最直接的调试工具。发送端将探头接到TX引脚。触发发送一个字节如0x55二进制01010101。你应该能看到一个清晰的、周期为104µs的方波起始位为低停止位为高数据位符合0x55的 patternLSB优先低-高-低-高-低-高-低-高。接收端让PC或其他设备发送数据用示波器看INT6引脚。应该能看到同样的波形。重点检查起始位的下降沿是否清晰以及每个位周期的宽度是否稳定。6.2 软件调试与逻辑分析中断是否进入在SCI_TX、SCI_RX、SCI_RECEIVE三个ISR的入口处设置一个GPIO引脚翻转或点亮不同的LED。通过观察引脚波形或LED闪烁可以判断中断是否被正确触发以及触发顺序是否符合预期。变量监视如果使用调试器可以实时观察全局变量tx_state,mask,j,k,rx_buffer等的变化。这能帮你确认状态机是否在正确运转。起始位误触发INT6是下降沿触发任何干扰毛刺都可能误判为起始位。可以在SCI_RXISR中加入简单的消抖逻辑例如启动定时器后在极短时间如2-5µs后再次采样如果仍是低电平才确认。但要注意这个时间必须远小于半个位周期。6.3 常见问题排查表现象可能原因排查步骤完全无发送信号1. PWM5未启动或配置错误。2. TX引脚未配置为输出。3. 中断未使能。1. 检查sci_init中PWM5的配置特别是PWM_A_Start是否被调用。2. 确认PWMCR5的DIR位和MODE位已正确设置。3. 检查中断控制器INTC和PSR的中断使能位。发送波形畸变或位宽度不对1. 波特率计算错误。2. 系统时钟频率与预期不符。3. 中断服务程序执行时间过长。1. 用示波器测量位周期反推实际波特率核对计算。2. 确认系统时钟配置检查是否有分频设置被忽略。3. 优化ISR代码确保其执行时间小于位周期的1/10。能发送不能接收1. INT6引脚配置错误非输入、边沿错误。2. 接收PWM4未启动。3.transmit标志一直为TRUE锁死了接收。1. 检查EPPAR寄存器对INT6的配置。2. 在SCI_RXISR中设置断点或IO翻转看是否被触发。3. 检查发送完成后transmit是否被正确置为FALSE。接收数据错位或乱码1. 起始位采样点不准半周期计算错误。2. 数据位采样点不在位中间。3. LSB/MSB顺序弄反。1. 用示波器对准起始位下降沿测量第一个采样中断发生的时间应为52µs左右半位周期。2. 确认在确认起始位后PWM4周期是否从416正确切换到了833。3. 核对发送mask左移和接收数据右移的逻辑确保都是LSB优先。高波特率下通信不稳定1. 中断响应延迟和ISR执行时间总和接近或超过位周期。2. 系统中断优先级冲突导致UART中断被阻塞。1. 尝试降低波特率测试。使用编译器优化选项如-O2并精简ISR代码。2. 检查系统中其他高优先级中断如系统滴答定时器确保它们不会长时间关闭全局中断。6.4 性能优化与扩展思路使用DMA如果MCU支持对于更高波特率或更频繁的数据传输可以考虑用DMA来搬运发送/接收缓冲区的数据进一步减轻CPU负担。但本方案中的MMC2001可能不具备此功能这是一个更高级的优化方向。双缓冲与环形队列目前的实现使用简单的全局数组。在生产环境中应为发送和接收缓冲区实现环形队列Ring Buffer。发送时主程序将数据填入发送队列发送ISR从队列中取出数据发送接收ISR将数据放入接收队列主程序从队列中读取。这能实现流控防止数据丢失。支持更灵活的协议当前固化了8N1格式。可以通过增加配置参数如数据位长度5-8停止位1/2奇偶校验使能来增强灵活性。这需要在状态机和ISR中增加更多的判断逻辑。全双工改造如前所述使用两套独立的PWM和GPIO引脚并精心设计中断优先级理论上可以实现全双工。关键在于确保发送和接收中断不会相互长时间阻塞。实现一个稳定可靠的软件UART是对开发者嵌入式系统理解深度的一次很好检验。它涉及到底层硬件寄存器、中断系统、定时器精确定时以及状态机编程等多个核心知识点。把这个项目吃透不仅能解决串口不够用的问题更能让你对异步串行通信的本质和嵌入式实时编程有更深刻的把握。当你看到自己用代码“无中生有”创造出的串口稳定地与外界设备交换数据时那种成就感绝对是直接用硬件UART无法比拟的。