HC08 MCU软件SCI实现:定时器模拟全双工串口通信
2026/6/26 12:43:28 网站建设 项目流程

1. 项目概述与核心价值

在嵌入式开发领域,串行通信(SCI/UART)几乎是每个项目都绕不开的基础功能。无论是调试信息输出、连接传感器模块,还是与其他主控进行数据交换,一个稳定可靠的串行通信接口都至关重要。然而,现实情况往往很骨感:许多低成本微控制器(MCU)要么只提供一个硬件SCI,要么干脆没有。当你的项目需要同时与两个串口设备通信时,硬件资源的捉襟见肘就成了拦路虎。

这时候,软件SCI(Software SCI)的价值就凸显出来了。它不依赖专用的硬件收发器,而是利用MCU的通用定时器和GPIO,通过精密的软件时序控制,在代码层面“模拟”出一个完整的串口。这听起来像是“螺蛳壳里做道场”,但对成本敏感、引脚资源有限或者需要多路串口的应用来说,这几乎是唯一的出路。我最早接触软件串口是在8051时代,用定时器中断去“bit-bang”(位敲打),代码复杂且CPU占用率高。后来在HC08这类更现代的MCU上,发现其定时器模块(TIM)的功能相当强大,特别是输入捕获和输出比较功能,能让软件SCI的实现变得优雅和高效。

本文要探讨的,正是基于Freescale(现NXP)HC08系列MCU,利用其定时器接口模块(TIM)的两个独立通道,实现一个全双工、中断驱动的软件SCI方案。全双工意味着可以同时收发数据,中断驱动则能最大限度解放CPU,让它在通信间隙去处理其他任务。这个方案来自一份经典的Freescale应用笔记(AN2502),我将其核心思想、实现细节以及我个人的调试心得揉碎了讲给你听。无论你是正在为项目寻找多串口解决方案,还是想深入理解定时器在通信协议模拟中的应用,这篇文章都能提供一条清晰的路径和一堆可以“抄作业”的代码思路。

2. 方案设计思路与硬件基础

2.1 为什么是TIM?为什么是两个通道?

在动手写代码之前,我们必须先理解“武器”的特性。HC08的TIM模块核心是一个16位的自由运行计数器(Free-Running Counter),它就像一块永不停止的电子表,按照系统时钟(或分频后的时钟)匀速累加。围绕这个计数器,TIM提供了几个关键功能,其中对我们最重要的两个是:

  1. 输入捕获(Input Capture):可以配置在输入引脚发生特定边沿(如上升沿或下降沿)时,瞬间“冻结”并记录下当前计数器的值。这就像用高速相机拍下事件发生的精确时刻,对于检测异步串行通信的起始位(一个由高到低的跳变)至关重要。
  2. 输出比较(Output Compare):可以预先设置一个目标计数值。当自由运行计数器的值等于这个目标值时,硬件会自动改变指定输出引脚的电平(置高、置低或翻转),并产生中断。这就像设了一个闹钟,到点就自动执行操作,完美用于在精确的时间点发送数据位停止位

那么,为什么需要两个通道?这是实现全双工的关键。串行通信的发送(TX)和接收(RX)在时序上是完全独立、可以同时进行的两个过程。

  • 接收通道:我们需要一个专用的TIM通道(比如通道0)配置为输入捕获,专门“监听”RX引脚上的起始位下降沿。一旦捕获到,就以此为时间基准,切换到输出比较模式,在后续每个数据位的中间时刻去采样RX引脚的电平。
  • 发送通道:我们需要另一个独立的TIM通道(比如通道1)配置为输出比较,专门负责在精确计算好的时间点,控制TX引脚输出起始位(低电平)、各个数据位和停止位(高电平)。

如果只有一个通道,你只能分时复用,无法实现真正的同步收发,也就是半双工。两个通道的独立性,为全双工通信打下了硬件基础。

2.2 核心挑战与设计哲学

用软件模拟硬件,最大的挑战在于时序精度CPU开销的平衡。

  • “Bit-Banging”的弊端:最原始的软件串口是让CPU在一个循环里死等,数着时钟周期去拉高拉低引脚。这种方法代码简单,但CPU被完全独占,无法执行其他任务,且时序容易受中断干扰。
  • 中断驱动的优势:我们的方案是中断驱动的。大部分时间里,CPU可以自由执行主程序。只有当“事件”发生时(如需要发送一个bit,或到了该采样接收bit的时刻),TIM硬件才会产生中断,CPU跳转到中断服务程序(ISR)进行极短时间的处理(设置下一个时间点、读写引脚),然后立刻返回。这极大地降低了CPU占用率。
  • 时序精度的保障:所有关键的时间点(1个比特的时间长度,即比特时间)都由TIM的硬件计数器来度量和管理,而不是靠软件延时循环。这保证了即使在有其它中断干扰的情况下,通信时序的“时钟基准”依然是稳定和准确的。中断服务程序本身的执行时间(中断延迟)虽然会影响绝对时间点,但我们可以通过计算将其补偿掉。

这个方案的设计哲学很明确:将时间敏感的操作交给硬件定时器,软件只负责逻辑和状态管理。这样,我们就能用一个没有硬件UART的MCU,或者利用多余的TIM通道,“无中生有”地创造出稳定可用的串行通信接口。

3. 两种实现模式详解:从简到繁

原文档提供了两种模式:正常模式(Normal Mode)和增强模式(Enhanced Mode)。我们可以把它们理解为“基础版”和“豪华版”。理解这两种模式,有助于你根据项目需求做出选择。

3.1 正常模式:轻量级全双工

正常模式追求的是极致的精简和效率。它实现了最核心的8位数据、1位停止位、无校验位的格式。

3.1.1 核心寄存器与变量在RAM中,我们需要定义5个变量来模拟硬件SCI的寄存器:

  1. 状态寄存器(rSCSR):一个字节,其中我们只使用4个位。
    • RPF(接收进行标志):1表示正在接收一帧数据。
    • TPF(发送进行标志):1表示正在发送一帧数据。
    • SCRF(接收器满标志):1表示接收数据寄存器里有新数据可读。
    • SCTE(发送器空标志):1表示发送数据寄存器为空,可以写入新数据。
  2. 数据寄存器(共4个):为了解耦,发送和接收各需要两个寄存器。
    • rSCRDR(接收数据寄存器):用户从这里读取接收到的数据。
    • rSCRSR(接收移位寄存器):在接收中断中,数据位被逐个移入这里。
    • rSCTDR(发送数据寄存器):用户把要发送的数据写到这里。
    • rSCTSR(发送移位寄存器):在发送中断中,数据位被从这里逐个移出到引脚。

注意:这种“双缓冲”结构是关键。用户操作rSCTDRrSCRDR,而中断服务程序操作rSCTSRrSCRSR。当一帧发送/接收完成时,数据在两个寄存器间搬运。这避免了用户程序和中断程序直接操作同一变量导致的数据冲突,是实现稳定全双工的基础。

3.1.2 接收流程拆解接收过程是一个由硬件事件(起始位)触发,并由定时器精确调度的状态机。

  1. 起始位捕获:初始化时,接收通道(如TIM通道0)配置为“输入捕获,下降沿触发”。RX引脚空闲时为高电平。当起始位(低电平)到来时,硬件瞬间记录下此刻计数器的值(T0),并产生中断。
  2. 首次延时计算:在输入捕获中断服务程序(ISR)中,我们不能立刻去采样第一个数据位,因为此时刚好是位边界。我们需要在比特时间的“中间”去采样,以获得最稳定的值。文档指出,采样点应在比特时间的30%到70%之间。因此,我们设置第一个输出比较点为T0 + 1.3 * BitTime - PinCheckLatency
    • BitTime:根据波特率计算出的一个比特所占的计数器周期数。BitTime = Timer_Freq / BaudRate
    • PinCheckLatency:从进入输出比较中断,到执行读取RX引脚状态的那条指令,中间所有指令消耗的CPU周期数。这是一个需要精确计算的常量,用于补偿软件延迟,确保采样点在比特时间中心。
  3. 数据位采样:进入第一个输出比较中断后,读取RX引脚电平,移入接收移位寄存器(rSCRSR)。然后,为下一个比特设置新的输出比较点:当前时间 + 1 * BitTime。重复此过程8次,接收8个数据位。
  4. 停止位与结束:第9个输出比较中断对应停止位。我们采样停止位(应为高电平,如果为低则说明帧错误,但正常模式不检测)。然后,将rSCRSR中的数据搬运到rSCRDR,置位SCRF标志通知主程序,清除RPF标志,并将通道重新配置为输入捕获模式,等待下一个起始位。

3.1.3 发送流程拆解发送过程是由用户程序调用发送函数启动的。

  1. 启动发送:用户将数据写入rSCTDR,然后调用SCISend函数。该函数检查TPFSCTE标志,若空闲,则将数据从rSCTDR拷贝到rSCTSR,并计算第一个输出比较点:当前计数器值 + 1 * BitTime。将发送通道(如TIM通道1)配置为“输出比较,匹配时清零”(即输出低电平),以产生起始位。
  2. 数据位移出:进入发送通道的输出比较中断。根据rSCTSR当前最低位(LSB)是0还是1,将通道配置为“匹配时清零”或“匹配时置位”,以输出对应的电平。然后,将rSCTSR右移一位,并为下一个比特设置新的输出比较点:当前时间 + 1 * BitTime
  3. 停止位与结束:发送完第8个数据位后,在第9个中断中,将通道配置为“输出比较,匹配时置位”,以产生停止位(高电平)。停止位发送完毕后,检查SCTE标志。如果SCTE=1(发送数据寄存器空),说明没有新数据要发,则关闭通道中断;如果SCTE=0,说明用户已经写入了下一个数据,则通道立即配置为输出低电平,开始发送下一帧的起始位,实现自动背靠背(back-to-back)发送。

3.2 增强模式:功能完备的解决方案

增强模式在正常模式的基础上,增加了工业级应用常需要的功能,代码更复杂,但更健壮。

3.2.1 扩展的寄存器集RAM变量增加到11个,主要包括:

  1. 配置寄存器(rSCCR):允许用户动态配置。
    • M:数据位长度(8/9位)。
    • PEN,PTY:奇偶校验使能与类型(奇校验/偶校验)。
    • SB:停止位数量(1/2位)。
    • TEN,REN:发送/接收使能。
    • TIEN,RIEN:发送完成/接收完成中断使能。
  2. 状态寄存器(rSCSR1, rSCSR2):除了基本标志,增加了错误标志。
    • FE(帧错误):停止位采样到低电平。
    • PE(奇偶校验错误):计算的奇偶位与接收的不符。
    • ORE(溢出错误):上一帧数据还未被读取,新一帧数据已经接收完成。
  3. 扩展的数据寄存器:为了支持9位数据,接收和发送的数据/移位寄存器都扩展为16位(高字节存第9位)。

3.2.2 关键功能实现

  1. 奇偶校验
    • 发送:在发送中断中,每发送一个数据位,就与一个临时奇偶变量进行异或(XOR)运算。所有数据位发送完后,根据配置的奇偶类型(PTY),计算出需要发送的奇偶校验位,并在下一个比特时间发出。
    • 接收:在接收中断中,每接收一个数据位,同样进行异或运算。接收到奇偶校验位后,与计算值比较,若不一致则置位PE错误标志。
  2. 多停止位与帧错误检测:根据SB配置,在奇偶位(或无奇偶位)之后,会进行1次或2次停止位采样。每次采样时检查引脚是否为高电平,如果不是,则置位FE帧错误标志。这能有效检测通信线路上的干扰或双方波特率微小失配导致的错位。
  3. 溢出错误处理:在完成一帧接收,准备将数据从移位寄存器搬运到数据寄存器前,先检查SCRF标志。如果SCRF=1(数据寄存器未读),说明上一帧数据还未被主程序取走,新数据就来了。此时置位ORE溢出错误标志,但新数据仍然会覆盖旧数据。这是UART的典型行为,主程序必须及时读取数据以避免丢失。
  4. 完成中断:如果配置寄存器中的TIENRIEN被置位,则在一帧数据发送完成或接收完成时,软件会跳转到用户定义的SCITXEMPTYSCIRXFULL子程序。这为用户提供了事件驱动的编程接口,但要注意,这些代码在中断上下文中执行,必须非常简短。

实操心得:增强模式虽然功能强大,但中断服务程序(ISR)的执行时间也显著增加。这会直接影响系统能支持的最高波特率。在资源紧张的HC08上,你需要仔细权衡是否需要这些高级功能。很多时候,正常模式加上软件层面的简单校验(如和校验),已经能满足大多数应用需求。

4. 波特率计算与性能极限分析

这是软件SCI设计的精髓,也是调试中最容易出错的地方。一切的核心都围绕着“比特时间”这个基本单位展开。

4.1 比特时间的计算

比特时间(BitTime)是传输一个比特所需要的定时器计数周期数。BitTime = Timer_Input_Frequency / Desired_Baud_Rate

例如,TIM时钟源为2.4576MHz,目标波特率为9600bps,则:BitTime = 2,457,600 / 9,600 = 256个计数周期。

在代码中,BitTime通常被存储为一个16位整数(BITHI:BITLO)。

4.2 关键延时:PinCheckLatency

这是软件SCI特有的一个概念。在接收时,我们从“输出比较匹配”的中断发生,到实际执行BRCLRBRSET指令去读取RX引脚状态,中间有一段固定的指令执行时间。这段周期数就是PinCheckLatency

为什么它如此重要?回顾接收时序:我们在检测到起始边沿后,设置第一个输出比较点为T0 + 1.3 * BitTime - PinCheckLatency

  • 1.3 * BitTime:是为了让采样点落在第一个数据比特时间的中心(从起始边沿算起,经过1.5个比特时间采样是最理想的,但1.3已经为中断延迟留出了余量)。
  • - PinCheckLatency:是为了补偿从进入中断到执行采样指令这段时间。如果不减去这个延迟,实际采样点就会比预期晚,可能滑出70%的稳定窗口,导致采样错误。

如何计算PinCheckLatency?你需要手动数出从输出比较中断入口到采样指令之间,所有指令的CPU总线周期数。以文档中正常模式的一段汇编为例:

RX_ISR: ;[9] 中断响应本身消耗9个周期 PSHH ;[2] 保护寄存器 BCLR CH0F, TSC0 ;[4] 清除中断标志 BRSET RPF, rSCSR, rxinprog ;[5] 判断是否正在接收 ... (跳转或执行其他路径) rxinprog: CLC ;[1] 清除进位标志 BRCLR RPIN, PTD, nocarry ;[.r...] !!!这就是采样指令!!!

你需要将BRCLR指令之前的所有指令周期相加。注意,BRCLR指令本身的周期数可能随条件变化(.r...表示可能为3或4周期,取决于跳转是否发生)。你必须按最坏情况(即最长路径)来计算。文档中给出的例子是26个周期,这是一个需要你根据自己代码精确计算的关键常数。

4.3 最高波特率估算

软件SCI的性能是有上限的,它受限于两个因素:

  1. 中断服务程序(ISR)的执行时间:无论是发送ISR还是接收ISR,其执行时间(MaxCyclesRx,MaxCyclesTx)必须小于一个比特时间(BitTime)。否则,当前一个中断还没处理完,下一个比特时间又到了,会导致时序完全混乱。
  2. 全双工冲突:在全双工模式下,发送和接收中断可能几乎同时发生。最坏情况是,接收中断刚进入,正在执行PinCheckLatency期间的指令时,发送中断发生了。我们必须确保,从接收采样点(比特时间的30%处)到下一个采样点(130%处)这0.4 * BitTime的窗口内,能够容纳下整个发送ISR的执行时间。即MaxCyclesTx < 0.4 * BitTime

因此,最高波特率的计算公式为:Max_BaudRate = Timer_Freq / MAX(MaxCyclesTx/0.4, MaxCyclesRx + MaxCyclesTx)

举例说明: 假设TIM频率为2.4576MHz,测得正常模式下:

  • MaxCyclesRx = 73cycles
  • MaxCyclesTx = 88cycles

计算:

  • 条件1:MaxCyclesTx / 0.4 = 88 / 0.4 = 220cycles
  • 条件2:MaxCyclesRx + MaxCyclesTx = 73 + 88 = 161cycles
  • 取较大值:MAX(220, 161) = 220cycles
  • 最高波特率:2,457,600 / 220 ≈ 11,170 bps

这意味着,在此配置下,理论最高安全波特率约为9600bps(因为9600bps对应的BitTime=256,大于220)。如果强行使用19200bps(BitTime=128),则128 < 220,不满足条件,通信极可能出错。

避坑指南:在项目初期就必须进行这个计算。选择一个远低于理论极限的波特率(例如,极限是20k,就用9600)会给系统留出充足的余量,以应对中断嵌套、其他高优先级中断干扰等实际情况。盲目追求高波特率是软件SCI不稳定最常见的根源。

5. 实战代码结构与调试要点

理解了原理,我们来看如何组织代码。虽然原文提供的是汇编代码,但其逻辑框架完全适用于C语言实现(在资源允许的情况下,用C可读性和可维护性更好)。

5.1 软件模块划分

一个清晰的软件SCI驱动应包含以下部分:

  1. 初始化函数SCI_Init()
    • 配置TX、RX引脚方向(TX输出,RX输入)。
    • 配置TIM基本时钟(禁止分频或设置合适分频)。
    • 初始化所有状态标志和软件寄存器(RPF,TPF,SCRF,SCTE清零,数据寄存器清零)。
    • 配置接收通道为输入捕获(下降沿),使能其中断。
    • 配置发送通道(但先不使能输出比较中断)。
    • 启动TIM自由运行计数器。
  2. 发送函数SCI_SendByte(uint8_t data)
    • 检查TPFSCTE标志,判断发送器是否就绪(非阻塞式设计通常需要循环等待或返回忙状态)。
    • 关中断(防止竞态条件)。
    • 将数据写入rSCTDR,清除SCTE标志。
    • 如果当前不在发送中(TPF == 0),则启动发送流程:拷贝数据到rSCTSR,设置TPF,计算第一个输出比较点(当前计数器值 + BitTime),配置发送通道为“匹配时清零”并使能中断。
    • 开中断。
  3. 接收查询函数SCI_ReceiveByte(uint8_t *data)
    • 检查SCRF标志。
    • 如果SCRF == 1,则将rSCRDR中的数据复制到*data,清除SCRF标志,返回成功。
    • 否则,返回无数据状态。
  4. 中断服务程序TIM0_CH0_IRQHandler(接收) 和TIM0_CH1_IRQHandler(发送)
    • 接收ISR:这是最复杂的部分。它是一个状态机,根据当前正在接收的是第几位(起始位、数据位、停止位)来执行不同操作。核心是:读取引脚、移位、设置下一个比较点、在结束时搬运数据并置位标志。
    • 发送ISR:相对简单。根据要发送的位值,配置输出比较动作为“置位”或“清零”,移位,设置下一个比较点。发送完停止位后,检查SCTE,决定是关闭发送还是立即开始下一帧。

5.2 调试技巧与常见问题排查

软件SCI的调试离不开逻辑分析仪或示波器。光看代码跑不通是常态。

  1. 问题:通信完全无反应,TX引脚没有波形。

    • 排查:首先检查SCI_Init是否被正确调用,TIM时钟是否使能。用示波器看TX引脚初始电平是否为高(空闲状态)。单步调试发送函数,看是否成功进入了发送ISR。检查计算BitTime的公式和数值是否正确,特别是TIM的输入时钟频率是否是你预期的值(注意分频器设置)。
  2. 问题:能发送,但不能接收;或接收数据全是乱码。

    • 排查:这是最常见的问题。90%的原因出在波特率不匹配或**PinCheckLatency计算错误**。
      • 波特率:用逻辑分析仪同时抓取TX和RX波形。测量一个比特的实际宽度,计算实际波特率,与预设值对比。确保通信双方(你的MCU和上位机/另一设备)的波特率设置完全一致,包括数据位、停止位、校验位。
      • PinCheckLatency:在接收ISR中设置一个调试引脚,在进入ISR时拉高,在执行采样指令前拉低。用逻辑分析仪测量这个脉冲的宽度,换算成CPU周期数,与你代码中计算的值对比。务必使用编译后的汇编列表文件来精确计数,C语言的一条语句可能对应多条汇编指令。
      • 采样点:在接收ISR中,在采样指令前后翻转一个调试引脚。在逻辑分析仪上,将这个翻转点叠加在RX数据波形上。它应该稳稳地落在每个数据比特的中间区域(30%-70%)。如果偏左或偏右,调整1.3 * BitTime中的系数(例如微调到1.35或1.25)或重新校准PinCheckLatency
  3. 问题:高波特率下通信不稳定,偶发错误。

    • 排查:这通常是CPU负载过高或中断冲突导致的。
      • 计算负载:重新评估你的ISR执行时间(MaxCycles)是否真的小于BitTime。确保你计算的是最坏情况下的周期数,包括所有条件分支中较长的那一条。
      • 中断优先级:确保TIM通道的中断优先级足够高,不会被其他长时间的中断(如ADC转换完成中断)阻塞。如果无法提高优先级,则必须降低波特率,或者优化其他ISR,缩短其执行时间。
      • 全局中断:在操作关键的软件标志或数据寄存器(如rSCTDRrSCTSR之间拷贝数据)时,必须使用关中断/开中断(DisableIRQ/EnableIRQ)进行保护,防止数据在搬运过程中被中断破坏。
  4. 问题:增强模式下的奇偶校验或帧错误频繁发生。

    • 排查:首先在无校验、1位停止位的简单模式下测试,确保基本通信是好的。然后逐一开启功能。
      • 奇偶校验错:检查发送方和接收方的奇偶校验配置(奇校验/偶校验/无)是否完全一致。单步调试发送和接收ISR中的奇偶计算部分,验证算法是否正确。
      • 帧错误:用逻辑分析仪观察停止位的位置和电平。帧错误通常意味着波特率仍有微小偏差,或者线路噪声较大。尝试降低波特率,或在硬件上增加适当的滤波电路。

实现一个稳定的软件SCI,是对开发者理解MCU定时器、中断和异步通信时序的一次综合考验。它没有硬件SCI那么“傻瓜式”,但带来的灵活性和对系统理解的深度是无可替代的。当你看到自己用代码“雕刻”出的规整串行波形,与外部设备成功握手通信时,那种成就感是直接调用硬件库函数无法比拟的。希望这篇结合了原始文档精华和个人实践经验的总结,能帮你少走弯路,顺利攻克这个嵌入式开发中的经典挑战。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询