1. 项目概述与核心价值
在嵌入式开发领域,串行通信接口(SCI)或通用异步收发器(UART)是连接传感器、显示屏、无线模块或进行系统调试的“生命线”。然而,硬件资源总是有限的,尤其是当你手头的微控制器(MCU)只有一个硬件UART,而项目需求却需要两个甚至更多独立的串口通道时,问题就来了。直接更换芯片或增加外部UART扩展芯片会增加成本和PCB面积,这时候,软件UART(Software UART)的价值就凸显出来了。
简单来说,软件UART就是“用代码模拟硬件”。它不依赖MCU内置的专用串口硬件,而是利用MCU上现有的通用外设,比如通用定时器或PWM模块,通过精确的定时和GPIO引脚电平控制,来“模仿”出UART通信所需的时序波形。这次我们要拆解的,就是基于Freescale(现NXP)MMC2001这款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_rxISR(INT6中断):当INT6引脚检测到下降沿(可能是起始位),如果当前不在发送状态,则立即启动PWM4定时器。注意,此时PWM4的周期被预设为半个位周期(例如对于9600波特率,周期设为416个时钟),目的是为了在半个位周期后(即起始位的正中间)产生第一次中断,去验证是否为真正的起始位(低电平)。 - 第二级:
sci_receiveISR(PWM4中断):- 第一次中断(起始位中点采样):检查INT6引脚是否仍为低电平。如果是,则确认是有效的起始位,立即将PWM4的周期改为一个完整的位周期(如833),并将采样状态索引
k加1(跳过起始位)。 - 后续第2-9次中断(数据位中点采样):在数据位中点采样INT6引脚电平,并将该位移入接收缓冲区
rx_buffer的当前字节中。采用右移方式实现LSB优先接收。 - 第10次中断(停止位采样):检查INT6引脚是否为高电平(停止位)。如果是,则停止PWM4定时器,完成一个字节的接收,递增接收缓冲区索引,并重新将PWM4周期设为半个位周期,以准备检测下一个字节的起始位。
- 第一次中断(起始位中点采样):检查INT6引脚是否仍为低电平。如果是,则确认是有效的起始位,立即将PWM4的周期改为一个完整的位周期(如833),并将采样状态索引
这种“起始位边沿触发+位中点定时采样”的双重机制,极大地提高了抗干扰能力,能有效滤除短时脉冲干扰。
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将系统时钟(假设32MHz)4分频,得到8MHz的PWM参考时钟。这是计算位周期计数值的基础。 - 周期计算:位周期计数值 = PWM参考时钟频率 / 目标波特率。对于9600波特率:8,000,000 / 9600 ≈ 833。这就是发送PWM5的周期值。接收PWM4初始化为半周期(416),用于起始位中点采样。
- 中断向量:
INTC_A_Init将中断向量表重定位到内部RAM(0x30000000)。这样做的好处是允许动态修改中断服务程序地址,比固化在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++; // k=0表示是第一次进入(起始位),跳过,k=1开始是数据位 } else if (k < 9) { // 接收第1到第8个数据位 (k=1~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_CLK):
PWM_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的 pattern(LSB优先:低-高-低-高-低-高-低-高)。
- 接收端:让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无法比拟的。