利用MC68HC908定时器双通道实现全双工软件SCI串口通信

利用MC68HC908定时器双通道实现全双工软件SCI串口通信 1. 项目概述为什么需要软件SCI在嵌入式项目里串口通信UART/SCI就像开发者的“瑞士军刀”调试、打印日志、设备间数据交换都离不开它。但很多低成本、小封装的微控制器比如经典的MC68HC908系列其硬件UART外设数量有限甚至根本没有。当你的项目需要多个串口或者硬件资源被其他功能占用时问题就来了是换一颗更贵的芯片还是外挂一颗UART芯片这两种方案都会增加BOM成本和PCB面积。这时候软件SCISoftware Serial Communication Interface的价值就凸显出来了。它的核心思想是“用时间换资源”利用微控制器里几乎都有的定时器模块TIM通过精准的定时中断来模拟UART通信的时序——包括起始位、数据位、停止位甚至奇偶校验位。这相当于在软件层面“虚拟”出了一个串口。我最早接触这个技术是在一个电池供电的无线传感器节点项目上主控用的就是MC68HC908QY4只有一个硬件串口要用于无线模块而调试信息又必须输出软件SCI就成了救命稻草。但早期的软件串口大多是“半双工”的要么发要么收不能同时进行。这在需要双向实时交互的场景下就很别扭。而本文要深入探讨的是一种更高级的玩法利用HC08 TIM模块的两个独立通道实现真正的全双工软件SCI。这意味着你可以像使用硬件UART一样同时进行发送和接收通信效率直接翻倍。这不仅仅是多用一个定时器通道那么简单它涉及到发送和接收两个独立状态机的并行调度、中断冲突的避免、以及数据缓冲区的精细管理是软件模拟串口技术中颇具挑战性也极具实用价值的一环。2. 核心原理TIM如何化身全双工串口要理解软件SCI首先得拆解异步串行通信的本质。它没有时钟线通信双方依靠预先约定好的波特率如9600bps来同步。每个字节的数据被封装成一个“帧”先是1个低电平的起始位然后是5-9个数据位通常8位接着是可选的奇偶校验位最后是1个或更多个高电平的停止位。发送端在固定的时间间隔由波特率决定切换TX引脚电平接收端则在检测到起始位后在每位时间的中间点对RX引脚采样以获取最稳定的数据。硬件UART内部有专用的波特率发生器、移位寄存器和缓冲区这些工作都是硬件自动完成的。而软件SCI则需要我们用定时器来“手动”实现这一切。HC08的定时器模块TIM通常有多个输入捕捉/输出比较通道我们可以将其配置为输出比较模式并使其在每次比较匹配时产生中断。2.1 单通道半双工的实现局限最简单的软件SCI使用一个定时器通道。以发送为例在发送起始位时设置TX引脚为低电平并启动定时器设定一个位时间例如9600bps时约为104us后中断。在中断服务程序ISR中根据当前要发送的数据位设置TX引脚电平然后重新装载定时器等待下一位时间。如此循环直到发送完停止位。接收过程类似但更复杂需要在检测到起始位下降沿通常用输入捕捉或外部中断后启动定时器并在半个位时间后为了在数据位中心采样和后续的每个整位时间后产生中断在中断中读取RX引脚电平并拼装成数据。这种单通道方案的致命缺陷在于发送和接收共享同一个定时器和中断服务程序。这意味着当你正在发送数据时如果外部有数据进来你无法及时响应起始位反之亦然。即使通过复杂的状态标志位来切换模式也无法实现真正的并发本质上还是分时复用是“半双工”的。2.2 双通道全双工的架构优势HC08的TIM模块通常提供两个或更多独立的通道如Channel 0和Channel 1。每个通道都有自己的比较寄存器TIMx_C0, TIMx_C1和独立的中断标志位。这正是实现全双工的关键。核心思路是职责分离通道A例如TIM通道0专职负责发送时序。它被配置为输出比较模式中断服务程序只处理发送状态机从发送缓冲区取出数据按位设置TX引脚并更新下一次中断的时间点。通道B例如TIM通道1专职负责接收时序。它同样配置为输出比较模式但其中断服务程序只处理接收状态机在精确的采样点读取RX引脚电平拼装数据并处理帧错误如停止位不对。两个通道的定时器基准时钟总线时钟分频而来是同一个因此它们的时间基准是同步的能保证发送和接收的波特率严格一致。但由于中断源独立两个状态机可以完全并行运行互不干扰。发送中断发生时不会打断接收中断的计时接收中断在采样时也无需关心发送进行到哪一位。这才是真正的“全双工”软件实现。注意这里有一个关键细节输入文档中提到的“使用TIM的两个通道”在接收侧通常并非使用TIM的“输入捕捉”功能来检测起始位。因为输入捕捉需要外部信号边沿触发在软件模拟中更常见的做法是将接收引脚RX配置为普通IO口并开启其外部中断功能。起始位的下降沿触发外部中断在外部中断服务程序中再启动负责接收的TIM通道通道B并首次设定为半个位时间后中断以实现中心采样。TIM通道B在接收过程中是工作在输出比较模式下的用于产生精确的位采样定时。3. 方案设计两种协议模式的权衡与选型根据输入文档AN2502/D的结论部分它提到了两种编码实现的软件SCI模式。这在实际开发中对应着不同的应用场景和可靠性需求我们需要深入理解其区别。3.1 简易异步通信协议模式这种模式追求极致的简洁和最小的资源开销。它通常具有以下特征固定数据格式通常为1位起始位、8位数据位、1位停止位8N1不可配置。无错误检测没有奇偶校验位也不验证停止位是否正确。它假设通信环境良好或者应用层有校验机制。功能单一核心就是完成数据的搬移把字节从发送缓冲区搬出去把从引脚采样到的位拼成字节存进接收缓冲区。适用场景与优缺点优点代码体积小中断服务程序执行时间短对CPU占用率低。非常适合用于单向的、非关键的调试信息输出或者在对功耗极其敏感、且通信误码率极低的场合如短距离板内通信。缺点可靠性差。任何线路上的噪声、时序的微小偏差都可能导致数据错误且接收方无法感知。我曾在一个电机控制板上用这种简易模式打印实时转速偶尔会出现乱码就是因为电机启停时电源噪声干扰了接收引脚。资源占用通常只需要两个字节的全局变量作为发送/接收移位寄存器以及小型的环形缓冲区。3.2 支持错误检测的多格式异步协议模式这是更健壮、更接近工业标准的实现。它在简易模式的基础上增加了可配置数据格式数据位长度5-9位、停止位数量1或2位、奇偶校验类型奇校验、偶校验或无校验可以通过宏定义或运行时配置来选择。硬件无关的错误检测帧错误Framing Error在停止位的时间点采样如果引脚不是预期的停止位高电平则置位帧错误标志。这通常意味着波特率不匹配或数据被打断。奇偶校验错误Parity Error如果使能了奇偶校验发送方会计算并附加校验位接收方重新计算并与接收到的校验位比较不一致则置位校验错误标志。溢出错误Overrun Error当接收方尚未取走前一个数据新的一帧数据已经接收完成时发生。这提示主循环处理数据的速度跟不上接收速度。适用场景与实现考量优点可靠性高能发现大部分物理层和数据链路层的错误。适合用于关键的数据通信如传感器数据上传、配置命令下发等。缺点代码更复杂中断服务程序需要判断当前是数据位还是校验位、停止位并执行校验计算增加了单次中断的执行时间中断延迟。对定时器中断响应的及时性要求更高。资源占用需要额外的变量来存储当前帧的配置状态、错误状态位。奇偶校验计算会消耗少量CPU周期。如何选择我的经验是对于产品开发优先考虑支持错误检测的模式。即使你初期觉得环境很好但产品可能会用在各种复杂电磁环境下。一个校验位带来的代码体积增加是微不足道的但它提供的错误诊断能力是无价的。简易模式可以保留作为“调试专用串口”在最终发布版本中通过宏定义关闭。在资源紧张到必须使用简易模式时务必在应用层协议如数据包末尾加CRC校验上弥补物理层的可靠性不足。4. 实操详解双通道全双工软件SCI的构建步骤下面我将基于MC68HC908QY4其TIM模块有2个通道和错误检测模式拆解构建全双工软件SCI的关键步骤。这里假设系统总线频率为2MHz目标波特率为9600bps。4.1 硬件与引脚配置首先你需要分配两个普通的I/O引脚分别作为软件SCI的TX和RX。假设我们使用PTB0作为TXPTB1作为RX。// 引脚定义 #define SOFT_UART_TX_DIR DDRB_Bit0 // PTB0方向寄存器位 #define SOFT_UART_TX_DATA PTB_Bit0 // PTB0数据寄存器位 #define SOFT_UART_RX_DIR DDRB_Bit1 // PTB1方向寄存器位应配置为输入 #define SOFT_UART_RX_PIN PTB_Bit1 // PTB1引脚输入位 // 初始化函数片段 void SoftUART_Init(void) { // 1. 配置TX引脚为输出并初始化为高电平空闲状态 SOFT_UART_TX_DIR 1; // 输出模式 SOFT_UART_TX_DATA 1; // 输出高电平 // 2. 配置RX引脚为输入上拉可选取决于硬件连接 SOFT_UART_RX_DIR 0; // 输入模式 // OPTIONAL: 使能内部上拉电阻如果MCU支持且需要 // PERB_Bit1 1; // 3. 配置RX引脚的外部中断用于检测起始位下降沿 // 这里需要查阅具体型号的数据手册配置相应的中断控制寄存器 // 例如可能涉及IRQSC等寄存器使能下降沿触发 // IRQSC_IRQIE 1; IRQSC_IRQEDG 0; // 假设配置 // 4. 初始化发送和接收状态机、缓冲区 txState TX_IDLE; rxState RX_IDLE; txBufferHead txBufferTail 0; rxBufferHead rxBufferTail 0; lastRxError 0; }4.2 定时器TIM模块初始化这是核心需要配置两个通道独立工作。void TIM_InitForSoftUART(void) { // 假设总线时钟BusClock 2MHz // 目标波特率 Baud 9600 // 位时间 T_bit 1/9600 ≈ 104.167 us // 定时器时钟源使用BusClock不分频。定时器计数频率 2MHz周期 0.5us。 // 每个位时间需要的计数值T_bit / 0.5us 104.167 / 0.5 ≈ 208.33 // 取整为208个计数。实际波特率 2MHz / 208 ≈ 9615 bps误差约0.16%在可接受范围。 TSC0 0x00; // 停止定时器清零计数器 TMODH 0x00; // 定时器模数寄存器高字节若需要 TMODL 0x00; // 定时器模数寄存器低字节自由运行模式可忽略模数 // 配置通道0用于发送 TSC0 0x04; // 通道0输出比较模式 TCH0H (208 8) 0xFF; // 位时间计数值高字节首次装载值发送起始位时间 TCH0L 208 0xFF; // 低字节 TSC0_TCH0F 0; // 清除通道0标志 // 先不使能通道0中断等待有数据发送时启动 // 配置通道1用于接收 TSC1 0x04; // 通道1输出比较模式 // 通道1的初始比较值无关紧要因为会在起始位中断中设置 TSC1_TCH1F 0; // 清除通道1标志 // 先不使能通道1中断等待起始位触发 // 启动定时器开始计数 TSC_TEN 1; }关键点计算波特率对应的定时器计数值时务必考虑定时器的时钟分频。这里假设TSC[PS2:PS0]000即不分频。如果系统时钟不同需要重新计算。误差应控制在±2%以内根据UART标准通常±3%以内也能工作但余量越小越好。4.3 发送状态机与中断服务程序实现发送是一个主动过程由应用层调用发送函数启动。volatile enum {TX_IDLE, TX_START_BIT, TX_DATA_BITS, TX_PARITY_BIT, TX_STOP_BIT} txState; volatile uint8_t txShiftReg; // 发送移位寄存器 volatile uint8_t txBitCount; // 已发送数据位数 volatile uint8_t txParity; // 奇偶校验计算值 volatile uint8_t txBuffer[16]; // 发送环形缓冲区 volatile uint8_t txBufferHead, txBufferTail; // 应用层调用将数据放入发送缓冲区并尝试启动发送 uint8_t SoftUART_Transmit(uint8_t data) { uint8_t nextHead (txBufferHead 1) % sizeof(txBuffer); if(nextHead txBufferTail) { return 0; // 缓冲区满发送失败 } txBuffer[txBufferHead] data; txBufferHead nextHead; // 如果发送器空闲则启动它 if(txState TX_IDLE) { SoftUART_StartTransmission(); } return 1; // 成功入队 } // 启动一次发送从缓冲区取出一个字节 void SoftUART_StartTransmission(void) { if(txBufferHead txBufferTail) { return; // 缓冲区空无事可做 } DisableInterrupts; // 进入临界区 txShiftReg txBuffer[txBufferTail]; txBufferTail (txBufferTail 1) % sizeof(txBuffer); // 初始化发送状态机 txState TX_START_BIT; txBitCount 0; txParity 0; // 如果使能奇偶校验这里初始化 // 发送起始位拉低TX引脚 SOFT_UART_TX_DATA 0; // 设置通道0比较寄存器一个位时间后中断 // 注意需要计算当前定时器计数器的值加上位时间偏移 uint16_t currentTime TCNTH; currentTime (currentTime 8) | TCNTL; uint16_t nextCompare currentTime BIT_TIME_TICKS; // BIT_TIME_TICKS208 TCH0H (nextCompare 8) 0xFF; TCH0L nextCompare 0xFF; TSC0_TCH0F 0; // 清除旧标志 TSC0_TCH0IE 1; // 使能通道0中断 EnableInterrupts; } // TIM通道0中断服务程序发送 #pragma interrupt_handler TIM_CH0_ISR void TIM_CH0_ISR(void) { TSC0_TCH0F 0; // 必须手动清除中断标志 switch(txState) { case TX_START_BIT: txState TX_DATA_BITS; // 发送数据位LSB first if(txShiftReg 0x01) { SOFT_UART_TX_DATA 1; if(PARITY_ENABLED PARITY_EVEN) txParity ^ 1; // 计算偶校验 } else { SOFT_UART_TX_DATA 0; if(PARITY_ENABLED PARITY_ODD) txParity ^ 1; // 计算奇校验 } txShiftReg 1; txBitCount; break; case TX_DATA_BITS: if(txBitCount DATA_BITS) { // DATA_BITS通常为8 // 发送下一个数据位 if(txShiftReg 0x01) { SOFT_UART_TX_DATA 1; if(PARITY_ENABLED PARITY_EVEN) txParity ^ 1; } else { SOFT_UART_TX_DATA 0; if(PARITY_ENABLED PARITY_ODD) txParity ^ 1; } txShiftReg 1; txBitCount; } else { // 数据位发送完毕 if(PARITY_ENABLED) { txState TX_PARITY_BIT; // 发送校验位 SOFT_UART_TX_DATA txParity ? 1 : 0; // 根据校验类型调整 } else { txState TX_STOP_BIT; // 发送停止位 SOFT_UART_TX_DATA 1; } } break; case TX_PARITY_BIT: txState TX_STOP_BIT; SOFT_UART_TX_DATA 1; // 发送停止位 break; case TX_STOP_BIT: // 停止位发送完成 SOFT_UART_TX_DATA 1; // 确保引脚保持高电平空闲 txState TX_IDLE; TSC0_TCH0IE 0; // 关闭发送中断 // 检查缓冲区是否还有数据待发送有则启动下一次发送 if(txBufferHead ! txBufferTail) { SoftUART_StartTransmission(); } break; default: txState TX_IDLE; TSC0_TCH0IE 0; break; } // 为下一次中断设置比较值固定增加一个位时间 uint16_t nextCompare TCH0H; nextCompare (nextCompare 8) | TCH0L; nextCompare BIT_TIME_TICKS; TCH0H (nextCompare 8) 0xFF; TCH0L nextCompare 0xFF; }4.4 接收状态机与中断服务程序实现接收是一个被动过程由起始位下降沿触发的外部中断启动。volatile enum {RX_IDLE, RX_START_DELAY, RX_DATA_BITS, RX_PARITY_BIT, RX_STOP_BIT} rxState; volatile uint8_t rxShiftReg; volatile uint8_t rxBitCount; volatile uint8_t rxParityCalc, rxParityReceived; volatile uint8_t rxBuffer[16]; volatile uint8_t rxBufferHead, rxBufferTail; volatile uint8_t lastRxError; // 可以定义成位域记录FE, PE, OE // RX引脚外部中断服务程序检测起始位 #pragma interrupt_handler RX_EXT_ISR void RX_EXT_ISR(void) { // 清除外部中断标志具体寄存器依型号而定 // IRQSC_IRQF 0; // 只有在空闲状态检测到的下降沿才被认为是起始位 if(rxState RX_IDLE) { // 验证是否为起始位低电平。可在此处增加去抖动逻辑如短暂延时再采样。 if(SOFT_UART_RX_PIN 0) { rxState RX_START_DELAY; rxBitCount 0; rxParityCalc 0; rxShiftReg 0; // 禁用RX外部中断防止在接收过程中被干扰 // IRQSC_IRQIE 0; // 启动接收定时器TIM通道1设定在半个位时间后中断以采样第一个数据位的中心 uint16_t currentTime TCNTH; currentTime (currentTime 8) | TCNTL; // 半个位时间用于对准数据位中心采样 uint16_t nextCompare currentTime (BIT_TIME_TICKS / 2); TCH1H (nextCompare 8) 0xFF; TCH1L nextCompare 0xFF; TSC1_TCH1F 0; TSC1_TCH1IE 1; // 使能通道1中断 } } // 如果rxState不是IDLE说明上一帧还没收完这次下降沿可能是噪声或新帧提前到来可视为帧错误或忽略。 } // TIM通道1中断服务程序接收 #pragma interrupt_handler TIM_CH1_ISR void TIM_CH1_ISR(void) { TSC1_TCH1F 0; switch(rxState) { case RX_START_DELAY: // 此时应位于第一个数据位的中心采样数据位 rxState RX_DATA_BITS; // 注意这里没有break继续执行RX_DATA_BITS的逻辑采样第一位 case RX_DATA_BITS: { uint8_t bitValue SOFT_UART_RX_PIN; rxShiftReg 1; // 注意接收通常是LSB first所以先右移 if(bitValue) { rxShiftReg | (1 (DATA_BITS - 1)); // 将收到的位放到最高位最后再调整 if(PARITY_ENABLED) rxParityCalc ^ 1; } rxBitCount; if(rxBitCount DATA_BITS) { // 数据位接收完毕 // 调整字节方向如果LSB first接收此时rxShiftReg是反的需要调整 rxShiftReg ReverseBits(rxShiftReg, DATA_BITS); // 需要一个位反转函数 if(PARITY_ENABLED) { rxState RX_PARITY_BIT; } else { rxState RX_STOP_BIT; } } } break; case RX_PARITY_BIT: { uint8_t bitValue SOFT_UART_RX_PIN; rxParityReceived bitValue; // 可以在此处比较rxParityCalc和rxParityReceived记录奇偶校验错误 if(PARITY_ENABLED) { uint8_t expectedParity (PARITY_EVEN) ? 0 : 1; // 根据配置调整 if((rxParityCalc ^ expectedParity) ! rxParityReceived) { lastRxError | PARITY_ERROR_MASK; } } rxState RX_STOP_BIT; } break; case RX_STOP_BIT: { uint8_t bitValue SOFT_UART_RX_PIN; if(!bitValue) { // 停止位应该是高电平如果是低电平则是帧错误 lastRxError | FRAMING_ERROR_MASK; } // 一帧接收完成将数据存入缓冲区如果没有溢出 uint8_t nextHead (rxBufferHead 1) % sizeof(rxBuffer); if(nextHead ! rxBufferTail) { rxBuffer[rxBufferHead] rxShiftReg; rxBufferHead nextHead; } else { lastRxError | OVERRUN_ERROR_MASK; } // 恢复状态准备接收下一帧 rxState RX_IDLE; TSC1_TCH1IE 0; // 关闭接收定时器中断 // 重新使能RX引脚外部中断以检测下一个起始位 // IRQSC_IRQIE 1; } break; default: // 异常状态复位接收机 rxState RX_IDLE; TSC1_TCH1IE 0; // IRQSC_IRQIE 1; break; } // 设置下一次中断一个完整的位时间后 uint16_t nextCompare TCH1H; nextCompare (nextCompare 8) | TCH1L; nextCompare BIT_TIME_TICKS; TCH1H (nextCompare 8) 0xFF; TCH1L nextCompare 0xFF; }4.5 主循环与缓冲区管理发送和接收都在后台由中断服务程序完成主循环或应用任务只需与缓冲区交互。// 检查是否收到数据 uint8_t SoftUART_Available(void) { return (rxBufferHead ! rxBufferTail); } // 读取一个接收到的字节非阻塞 uint8_t SoftUART_Read(uint8_t *error) { if(error) *error lastRxError; lastRxError 0; // 读取时清除错误标志 if(rxBufferHead rxBufferTail) { return 0; // 缓冲区空 } uint8_t data rxBuffer[rxBufferTail]; rxBufferTail (rxBufferTail 1) % sizeof(rxBuffer); return data; } // 主函数示例 void main(void) { System_Init(); // 初始化时钟等 SoftUART_Init(); TIM_InitForSoftUART(); EnableInterrupts; SoftUART_Transmit(H); SoftUART_Transmit(i); SoftUART_Transmit(\r); SoftUART_Transmit(\n); while(1) { if(SoftUART_Available()) { uint8_t err; uint8_t data SoftUART_Read(err); if(err) { // 处理错误例如重发请求或记录日志 } else { // 处理有效数据例如回显 SoftUART_Transmit(data); } } // 其他应用任务 __RESET_WATCHDOG(); // 喂狗 } }5. 调试与优化从理论到稳定运行将代码烧录进去只是第一步让软件SCI稳定可靠地工作需要细致的调试和优化。以下是我在实际项目中积累的几个关键点。5.1 波特率精度与时钟校准软件SCI的波特率完全依赖于定时器的时钟源精度。如果系统使用内部RC振荡器其频率可能随温度和电压漂移典型误差±1%到±2%。这可能导致与标准设备如电脑串口通信时累积误差最终引发帧错误。校准建议使用外部晶振如果对通信可靠性要求高优先选用外部晶振作为主时钟源其精度通常远高于内部RC。软件自动波特率检测可选在通信起始阶段可以通过测量起始位低电平的持续时间来反推对方的波特率并动态调整自己的定时器重载值。但这会显著增加代码复杂度。误差计算与测试按照前面所述公式计算理论计数值和实际波特率误差。在代码中定义ACTUAL_BAUD和BAUD_ERROR_PERCENT宏编译时给出警告。实测时可以用逻辑分析仪或示波器测量位时间验证其是否在允许范围内。5.2 中断响应时间与临界区保护中断服务程序的执行时间直接影响位定时的准确性。如果ISR执行时间过长可能导致下一次比较匹配时ISR还未退出从而错过精确的引脚操作时机。优化与保护策略保持ISR极度精简只做最必要的操作读写引脚、访问状态变量、更新比较寄存器。复杂的计算如奇偶校验可以优化为查表或使用条件编译简化。使用静态变量和寄存器变量在ISR内部使用的局部变量可声明为register类型或使用全局/静态变量减少栈操作。临界区保护在SoftUART_StartTransmission等函数中修改txState、操作缓冲区指针时必须禁用全局中断DisableInterrupts操作完成后立即开启EnableInterrupts。防止主循环和中断服务程序同时访问这些共享资源导致数据错乱。中断嵌套HC08默认可能不支持中断嵌套或者需要谨慎配置。确保TIM通道0和通道1的中断优先级相同如果可配避免一个中断服务程序被另一个打断除非你非常清楚时序影响。最安全的做法是关闭中断嵌套。5.3 抗干扰与错误恢复软件SCI没有硬件过滤更容易受到噪声干扰。起始位验证在RX外部中断中不要立即认定下降沿就是起始位。可以加入简单的去抖动在中断中短暂延时几个指令周期后再次采样如果仍然是低电平才确认是起始位。这能滤除窄脉冲噪声。帧错误处理一旦检测到帧错误停止位为低应立即复位接收状态机到RX_IDLE并重新使能外部中断。可以增加一个错误计数器连续多次错误后可以尝试短暂关闭接收或触发应用层告警。缓冲区溢出预防确保接收缓冲区大小足够。如果应用层处理数据较慢应考虑增大缓冲区或者在SoftUART_Read函数被调用频率较低时在ISR中检测到缓冲区将满时主动丢弃旧数据或设置标志位通知主循环。5.4 资源消耗评估实现一个全双工、带错误检测的软件SCI需要消耗的资源ROM代码空间约1-2KB取决于代码优化程度和功能完整性。RAM几个状态变量约10字节 发送/接收缓冲区如32字节 x 2 64字节。总共约100字节以内。CPU占用率这是最需要关注的。以9600bps8N1格式计算每发送/接收一个字节需要产生10次中断1起始8数据1停止。全双工同时工作时每秒最大可处理960字节即每秒9600次中断。每次中断服务程序执行时间假设为20个指令周期经过优化后可能达到在2MHz总线频率下每次中断约10us。则CPU占用率约为9600 * 10us / 1s 9.6%。这在许多应用中是可接受的但如果波特率提高到115200中断频率将提高12倍CPU占用率将超过100%导致系统瘫痪。因此软件SCI通常适用于低波特率通信一般不超过38400bps。6. 常见问题排查与实战心得即使代码逻辑正确在实际硬件上调试时也可能遇到各种问题。下面是一个快速排查指南和我踩过的一些坑。6.1 问题速查表现象可能原因排查步骤完全无收发1. 引脚配置错误输入/输出方向反。2. 定时器未启动或时钟源错误。3. 全局中断未开启。1. 用万用表或IO控制LED测试引脚输出是否正常。2. 检查定时器控制寄存器TSC的TEN位检查预分频设置。3. 检查CCR寄存器中的I位是否清零。能发不能收1. RX引脚外部中断未正确配置或未使能。2. 接收状态机在非IDLE状态卡死。3. 起始位检测逻辑过于严格如去抖动误判。1. 用示波器或逻辑分析仪看RX引脚是否有数据波形同时监控外部中断标志位是否置位。2. 在接收ISR的不同状态设置调试IO翻转用逻辑分析仪看状态流转。3. 暂时去掉起始位验证逻辑看是否能收到数据可能是噪声导致。能收不能发1. 发送缓冲区管理逻辑错误导致StartTransmission未被调用。2. TX引脚被其他功能复用。3. 发送中断未使能。1. 单步调试检查调用SoftUART_Transmit后txState是否从TX_IDLE变为TX_START_BIT。2. 检查端口控制寄存器确保TX引脚是普通GPIO输出模式。3. 在发送ISR入口点翻转一个调试引脚用示波器看是否有脉冲。数据错乱乱码1. 波特率计算错误误差过大。2. 中断服务程序执行时间过长导致时序偏移。3. 发送和接收的位顺序LSB/MSB first不匹配。4. 奇偶校验配置不一致。1. 用逻辑分析仪测量实际位时间与理论值对比。2. 优化ISR代码或降低波特率测试。3. 确认发送txShiftReg右移和接收rxShiftReg左/右移逻辑对应。4. 检查通信双方的数据格式数据位、停止位、校验位是否完全一致。通信一段时间后死机1. 中断标志未清除导致连续进入中断。2. 缓冲区溢出状态机紊乱。3. 堆栈溢出如果ISR使用了大量局部变量或调用函数。1.务必在ISR开头或操作完寄存器后立即清除对应的中断标志位TSCx_TCHxF 0。2. 增加缓冲区溢出检测和恢复机制。3. 减少ISR的调用深度和局部变量使用。6.2 实战心得与技巧调试利器——逻辑分析仪软件SCI调试一个几十块钱的逻辑分析仪比示波器还好用。同时抓取TX、RX引脚以及一两个用于标记状态的调试IO例如在发送ISR入口和退出时翻转可以清晰地看到每一位的时序、状态转换是否准确中断响应延迟有多少。这是定位时序问题的终极手段。先实现半双工再扩展为全双工如果你对状态机编程不熟可以先实现一个只用单通道的、查询式的简单发送函数确保TX波形正确。然后再实现接收部分。最后再将两者合并用两个通道独立运行。分步验证能极大降低调试复杂度。缓冲区大小的权衡缓冲区并非越大越好。在HC08这类RAM很小的MCU上大的缓冲区意味着宝贵的RAM被占用。我的经验是发送缓冲区可以小一些如16字节因为发送节奏由本机控制接收缓冲区可以稍大如32字节以应对对方可能的数据突发。如果应用层是逐字节处理甚至可以用“字节就绪”标志代替缓冲区但那样要求主循环响应必须非常及时。考虑使用RTOS或协作式调度器如果你的项目已经使用了简单的任务调度器可以将软件SCI的缓冲区管理和协议解析放在一个低优先级的任务中而ISR只负责最底层的位搬运和放入/取出缓冲区。这样能更好地解耦提高系统可维护性。功耗考量定时器中断会频繁唤醒MCU不利于低功耗模式。如果设备需要休眠在进入休眠前必须关闭定时器和相关中断。唤醒后需要重新初始化。对于间歇性通信的设备可以考虑仅在需要通信的窗口期开启软件SCI功能。实现一个稳定的全双工软件SCI就像在MCU内部用代码搭建了一个精密的数字时序电路。它对代码质量、中断理解和硬件定时器的运用提出了较高要求。但一旦调试成功这种不增加一分钱硬件成本就能“变”出一个串口的能力会让你在资源受限的嵌入式设计中游刃有余。这份对底层硬件的掌控感也正是嵌入式开发的乐趣所在。