8位MCU软件模拟SCI与ADC:时序控制与RC充放电测量实践
1. 项目概述与核心价值如果你正在捣鼓一块老旧的Freescale现NXPHC908JB8微控制器手头恰好有USB08评估板并且想在不依赖专用硬件模块的情况下实现串口通信和模拟量采集那么你找对地方了。这个项目正是关于如何在资源极其有限的8位MCU上用纯软件“抠”出SCI串行通信接口和ADC模数转换器功能。代码源自一份二十多年前的Motorola官方参考手册但其中蕴含的“用软件模拟硬件时序”的思想至今在成本敏感、引脚资源紧张或者需要高度定制化通信协议的嵌入式场景中依然闪烁着实用主义的光芒。简单来说这个项目解决了两个核心问题第一当MCU没有硬件SCI模块时如何用两个普通的GPIO口实现稳定的异步串行通信第二当没有集成ADC时如何利用GPIO和定时器通过测量RC电路充放电时间来实现模拟电压的数字化。这不仅仅是让一块老开发板“活过来”更是一次深入MCU最底层——直接操作寄存器、精确计算指令周期、理解中断与轮询本质的绝佳实践。无论你是想修复一个老旧设备学习嵌入式底层编程的精髓还是在资源受限的新项目中寻求极致的成本优化方案这些代码和思路都能提供直接的参考。2. 核心思路与方案选型解析2.1 为什么选择软件模拟在HC908JB8这类早期的8位MCU上外设资源往往比较精简。USB08评估板虽然主打USB功能但其主控芯片可能并未配备独立的硬件SCI和ADC模块或者项目为了节省成本、复用引脚需要将有限的硬件资源用于更关键的USB通信。此时软件模拟Software-Emulated就成了必然选择。软件模拟的核心思想是“用时间换资源”和“用代码换硬件”。它不依赖于特定的硬件外设电路而是通过GPIO通用输入输出的位操作配合精准的延时或定时器来模拟出特定通信协议或采样过程的时序。这种方法的优势在于极高的灵活性——波特率、数据格式、ADC采样率等参数都可以通过软件调整并且不占用额外的硬件成本。但挑战也同样明显它极度消耗CPU时间尤其是在通信或采样期间CPU会被完全占用并且对代码的时序精度要求极为苛刻任何中断或意外的代码执行路径变化都可能导致通信失败或采样错误。2.2 整体架构设计从提供的源代码文件U08232.C,U08ADC.C,U08KEY.C等和链接文件USB08.LKF可以看出整个项目是一个典型的基于中断驱动的前后台系统。后台主循环 (main函数)位于u08main.c虽未提供源码但从调用关系可推断负责整体的任务调度例如处理USB事务、响应上位机命令、协调SCI发送和ADC采集的触发。中断服务程序 (ISR)如isrKey()处理按键中断isrUSB()处理USB中断。这些高优先级例程负责响应外部异步事件确保实时性。软件模拟模块软件SCI (U08232.C)一个完全由GPIO和延时循环实现的异步串行收发器。软件ADC (U08ADC.C)利用GPIO控制RC电路并通过定时器TCNT测量电容放电时间来实现模数转换。按键扫描 (U08KEY.C)利用端口变化中断KBI实现按键检测。硬件抽象层通过hc08jb8.h头文件对MCU的所有寄存器进行了符号化定义使得代码可以直接通过PTC、DDRC、TSC等名称访问硬件提高了可读性和可移植性。这种架构确保了在实现复杂USB设备功能的同时还能挤出CPU资源来完成自定义的串口通信和模拟量读取是嵌入式系统中资源管理的经典案例。3. 软件SCI通信的深度解析与实现3.1 异步串行通信基础与软件实现原理标准的异步串行通信如RS-232一帧数据包含1个起始位低电平、5-9个数据位通常8位LSB先行、可选的校验位、1个或多个停止位高电平。软件SCI的核心任务就是用GPIO口的高低电平变化精确地模拟出这个波形。关键挑战在于比特时间的精确控制。在硬件SCI中由一个专用的波特率发生器负责产生精准的时钟。在软件中我们必须用CPU指令来“拼凑”出这个时间。代码中选择了系统的定时器TIM作为时间基准其计数器运行在3MHz周期0.333µs。以9600波特率为例每个比特的持续时间是104.1666µs换算成定时器时钟周期就是104.1666µs / 0.333µs ≈ 312.5个周期。由于函数调用、循环判断本身也有指令开销代码中在计算延时循环次数时需要减去这个“Overhead”代码中注释为~20个时钟周期最终通过调整循环次数来逼近目标时间。3.2 代码逐行解读与实操要点让我们深入U08232.C的关键函数3.2.1 硬件映射与初始化 (initSSCI)#define setTxLow() (PTC ~0x01) // PTC0置低 #define setTxHigh() (PTC | 0x01) // PTC0置高 #define enaTxOut() (DDRC | 0x01) // 设置PTC0为输出 #define tstRxLvl() (PTA 0x80) // 读取PTA7电平 #define enaRxIn() (DDRA ~0x80)// 设置PTA7为输入注意这是硬件依赖最强的部分。移植到其他MCU或板子时必须根据实际电路连接修改这些宏定义。TX和RX引脚通常需要连接电平转换芯片如MAX232才能与PC串口通信。initSSCI()函数非常简单将TX引脚初始化为输出高电平空闲状态将RX引脚初始化为输入。这符合RS-232标准空闲时线路处于标记Mark高电平状态。3.2.2 比特延时函数 (delayHalfBit,delayBitTime)void delayHalfBit() { _asm(\ lda #120\n\ __dhbl:deca\n\ nop\n\ bne __dhbl\n\ ); }这里直接嵌入了汇编代码来实现精准延时。lda #120加载立即数120到累加器Adeca递减Anop空操作用于微调时间bne __dhbl循环直到A为0。这段循环的总时钟周期数需要精心计算以匹配半个比特时间对于9600波特率约为52µs。使用汇编是为了消除C语言编译器优化带来的不确定性确保延时绝对精确。3.2.3 发送一个字节 (putSSCI)这是软件SCI最核心的部分流程如下保存状态与关中断首先保存当前CCR条件码寄存器状态然后禁用全局中断。这是至关重要的发送过程中如果被中断打断会导致波形畸变通信必然失败。发送起始位调用setTxLow()将TX线拉低持续一个完整的比特时间delayBitTime()。发送8个数据位LSB先行n8; do { if((c1)0) setTxLow(); // 如果当前位是0输出低电平 else setTxHigh(); // 如果当前位是1输出高电平 delayBitTime(); // 保持一个比特时间 c 1; // 准备下一个位 } while(--n);通过c1取出字节的最低位LSB先发送然后右移依次发送所有位。发送停止位调用setTxHigh()将TX线拉回高电平并持续两个比特时间。这是为了给接收端足够的恢复时间确保帧结束的识别提高通信可靠性。恢复中断状态恢复之前保存的CCR重新允许中断。3.2.4 接收一个字节 (getSSCI)接收过程是发送的逆过程但更复杂因为要检测起始位并采样数据关中断与等待起始位同样先关中断。然后while(tstRxLvl()!0) ;循环等待RX线从高电平变为低电平起始位下降沿。采样点对齐检测到起始位后调用delayHalfBit()延时半个比特时间。这样做的目的是将采样点对准每个数据位的中间时刻这是抗干扰能力最强的位置。循环采样8个数据位do { delayBitTime(); // 延时一个完整比特时间到达下一个数据位的中间 c 1; // 为接收新位做准备先右移原有数据 if(tstRxLvl()!0) c | 0x80; // 如果RX线为高则将当前位的MSB置1 } while(--n);注意这里接收到的位被放到了字节的高位c | 0x80因为之前有c 1所以实际是LSB先入但通过移位操作在内存中重组成了正确的字节。检查停止位再次delayBitTime()后检查RX线是否为高电平。如果不是则可能发生了帧错误Framing Error代码中留有处理接口注释部分。恢复中断并返回。实操心得软件SCI非常脆弱。除了确保延时精准必须在putSSCI和getSSCI的整个过程中禁止中断。任何细微的时序偏差都会导致误码。在实际项目中如果系统必须响应其他紧急中断可以考虑将软件SCI的优先级提到最高或者使用硬件定时器产生精确的比特中断来驱动状态机但这会大大增加代码复杂度。4. 软件ADC采集的实现与优化4.1 基于RC充放电时间的ADC原理当MCU没有集成ADC时一种经典的低成本替代方案是利用GPIO和内部定时器通过测量RC电路的充放电时间来间接测量电压。其基本原理如下RC电路将一个已知阻值的电阻R和一个待测电压源V_in驱动的电容C串联。电容两端的电压V_c随时间按指数规律变化V_c V_in * (1 - e^(-t/RC))。测量思想将电容连接到MCU的一个GPIO引脚。该引脚可以配置为输出高电平对电容充电、输出低电平对电容放电或高阻输入测量电容电压。通过控制引脚状态并利用MCU的输入捕获功能或简单的高低电平判断测量电容电压从一种状态变化到另一种状态例如从V_high放电到MCU输入逻辑低阈值V_IL所需的时间t。电压计算由于时间t与V_in成反比电压越高电容充电到阈值所需时间越长或放电时间越短通过测量这个时间并结合已知的R、C值以及MCU的IO电平阈值可以反推出V_in的值。为了简化计算并消除R、C绝对精度和阈值电压的影响代码中采用了比例测量法。4.2 代码流程与双周期测量法U08ADC.C中的getSADC函数是实现这一方法的核心。它采用了“校准-测量”双周期法来提高精度和稳定性4.2.1 校准周期 (Calibration Cycle)PTD ~0x78; DDRD | 0x78; // 将PTD[3..6]设为输出低电平放电 PTE | 0x07; DDRE | 0x07; // 将PTE[0..2]ADC通道设为输出高电平充电 for(zz0;zz1000;zz--) ; // 短暂延时确保电容充满电 DDRE ~0x07; // 将PTE[0..2]设为高阻输入停止驱动 t0 TCNT; // 记录当前定时器计数值 while((PTE channel) ! 0) ; // 等待电容通过内部/外部电阻放电至低电平 t1 TCNT; // 记录放电结束时间 t1 - t0; // 得到完全放电时间t1对应参考电压通常是VCC这个周期的目的是测量一个“基准时间”t1。此时电容从已知的VCC通过GPIO输出高电平得到放电到0V通过GPIO输出低电平的PTD引脚。这个时间t1与VCC和RC时间常数有关作为后续计算的基准。4.2.2 采集周期 (Acquisition Cycle)DDRD ~0x38; // 将PTD[3..5]设为高阻断开放电回路 DDRE | 0x07; // PTE再次输出高电平对电容充电此时电压为待测电压V_in for(zz0;zz1000;zz--) ; // 充电延时 DDRE ~0x07; // PTE高阻输入 t0 TCNT; // 开始计时 while((PTE channel) ! 0) ; // 等待电容通过**未知的外部电阻如光敏、热敏电阻**放电至低电平 t2 TCNT; t2 t2 - t0 - t1 - 100; // 计算净放电时间t2减去校准时间和固定偏移这个周期是实际的测量周期。关键区别在于放电回路不再是固定的PTD下拉电阻而是通过channel选择的外部传感器电阻如电路图中的R1光敏、R2热敏、R3电位器。电容从V_in开始放电放电速度由外部电阻R_x决定。因此测量到的时间t2反映了R_x的大小进而反映了外部模拟量光照、温度、位置的变化。4.2.3 结果计算 (scaleSADC)t2 scaleSADC(t1,t2);scaleSADC函数包含内联汇编的核心操作是计算(t2 4) / (t1 4)即(t2 * 16) / (t1 / 16)。这本质上是在计算t2 / t1的比例并进行了定点数缩放可能是为了将结果映射到一个合适的范围如0-255或0-1023以模拟硬件ADC的分辨率。最终返回值t2就是一个与输入电压或传感器阻值成比例的数字量。注意事项精度与速度这种方法精度受限于定时器分辨率、GPIO阈值电压的离散性以及RC元件本身的温度漂移。它不适合高速或高精度采集但对于温度、光照等慢变化信号是可行的。CPU占用while循环等待放电完成是阻塞式的在此期间CPU无法执行其他任务。测量一个点可能需要几毫秒到几十毫秒。电路连接必须严格按照评估板原理图连接传感器和电容。电容值的选择至关重要它决定了放电时间常数进而影响测量范围和耗时。抗干扰软件ADC易受噪声干扰。可以在代码中增加多次采样取平均的滤波算法。5. 系统集成与工程构建要点5.1 多模块协同与中断处理整个项目并非孤立地运行SCI和ADC。从VECJB8.C中断向量表和USB08.MAP链接映射文件可以看出系统是一个多任务协作的整体中断向量表将isrKey按键、isrUSBUSB等中断服务程序与特定的中断源绑定。isrDummy是空的中断处理程序用于未使用的中断源。主循环 (main)在u08main.c中源码未提供但可推断主函数会调用initSSCI(),initKey(),initSADC()进行初始化然后进入一个无限循环。在这个循环中它可能定期轮询或响应事件来调用getSADC()读取传感器数据并通过putSSCI()将数据发送出去或者通过USB接口上传。资源冲突软件SCI和软件ADC都重度依赖CPU时间和定时器资源。必须仔细设计它们的调用时机避免冲突。例如不能在getSSCI接收数据的过程中启动ADC测量否则会导致串口数据错乱。通常采用状态机或严格的顺序执行来规避。5.2 开发环境与构建过程项目使用的是Cosmic Software的HC08 C编译器工具链。这对于今天的大多数开发者来说可能是个陌生的环境。关键文件解析USB08.LKF链接器命令文件。它定义了内存布局seg .text -b 0xdc00 -n .text代码段.text从地址0xDC00开始。seg .const -a .text常量段紧接代码段之后。seg .bsct -b 0x0040 -n .bsct零页变量从0x0040开始。seg .ubsct -a .bsct -n .ubsct和seg .data -a .ubsct数据段。def __stack0x013f栈指针初始化为0x013F。最后列出了需要链接的目标文件crtsjb8.o启动代码、u08main.o、u08key.o、u08adc.o以及数学库等。BUILD.BAT批处理构建脚本。cx6808 -v -l u08main.c u08adc.c u08key.c vecjb8.c调用编译器编译C源文件。clnk -m usb08.map -o usb08.h08 usb08.lkf调用链接器根据.lkf文件生成.h08目标文件并生成映射文件.map。chex -fm -h -o usb08.s19 usb08.h08将目标文件转换为Intel HEX格式的.s19文件用于烧录到MCU的Flash中。USB08.MAP内存映射文件。这是链接后生成的对于调试至关重要。它显示了每个段segment的起始地址、结束地址和长度。每个模块object file中的函数和变量被分配到了哪个地址。栈使用情况例如_getSADC使用了18字节栈空间。所有全局符号函数、变量的地址。移植到现代IDE如果你想在Keil、IAR或基于GCC的现代嵌入式IDE中复现此项目需要做以下工作创建新工程选择正确的HC908JB8器件型号。将.c和.h源文件添加到工程。根据USB08.LKF和USB08.MAP的信息在IDE的链接器设置中手动配置代码、数据、栈的起始地址和大小。重写或替换crtsjb8.s启动文件因为不同编译器的启动代码和汇编语法可能不兼容。根据新编译器的语法调整源代码中可能存在的编译器特定扩展如interrupt中断函数声明、_asm内联汇编语法。6. 常见问题、调试技巧与实战建议6.1 软件SCI通信失败排查如果串口通信无法建立可以按照以下步骤排查电平与硬件连接首要检查确认TX/RX引脚是否通过电平转换芯片如MAX232正确连接到了PC的串口或USB转串口适配器。用示波器或逻辑分析仪直接测量TX引脚波形是最直接的方法。引脚配置双重检查U08232.C开头的宏定义确保setTxLow/High、tstRxLvl操作的寄存器位与你的实际电路完全一致。波特率校准这是软件SCI最常见的问题源。代码中的delayHalfBit循环次数是针对特定CPU时钟3MHz和编译器优化等级计算出来的。如果你的系统时钟不同或者使用了不同的编译器导致生成的汇编指令周期数变化必须重新计算延时。校准方法编写一个简单的测试程序循环发送字符U二进制01010101是很好的方波。用示波器测量TX引脚上一个完整比特的时间调整delayHalfBit中的循环计数直到实测波特率与目标值如9600的误差在可接受范围内通常要求2%。中断干扰确保在putSSCI和getSSCI函数执行期间全局中断是关闭的disableINTR()。如果系统中存在不可屏蔽的中断或高优先级中断需要考虑更复杂的调度策略。帧格式匹配确认PC端串口助手的设置波特率、数据位、停止位、校验位与代码实现严格一致。代码中实现了1位起始位、8位数据位、1位停止位、无校验位。6.2 软件ADC读数不准或不稳定RC时间常数选择电容C和放电电阻R包括传感器电阻的乘积RC决定了放电时间。RC太大测量时间过长CPU占用高RC太小时间太短定时器分辨率不够误差大。经验值通常选择RC时间常数使得在测量范围内放电时间在几百微秒到几十毫秒之间。可以先用固定电阻测试观察t1和t2的数值是否在合理的范围内避免溢出或过小。GPIO内部上拉/下拉initSADC()中有一行POCR ~0x80; // disable PTE20P这是禁用PTE端口的内部上拉电阻。如果启用它会显著影响RC放电曲线导致测量错误。务必根据硬件设计确认是否需要关闭。电源噪声与去耦模拟测量对电源噪声非常敏感。确保为MCU和传感器电路提供干净、稳定的电源并在VCC和GND之间靠近芯片位置放置去耦电容通常为100nF。软件滤波单次采样容易受噪声影响。可以在getSADC函数外部增加软件滤波。最简单的是连续采样N次然后取平均值。更高级的可以用中值滤波去掉最大最小值再平均或一阶低通滤波new_value α * old_value (1-α) * sample。定时器溢出代码中使用了TCNT自由运行定时器。注意TCNT是16位还是8位是否会溢出在t2 t2 - t0 - t1 - 100;的计算中如果t0很小而t2很大且TCNT发生了溢出回零计算结果将是错误的。需要增加溢出处理逻辑。6.3 项目集成与优化建议状态机重构当前的getSSCI和getSADC都是阻塞函数。在复杂的系统中这可能导致其他任务如USB响应被延迟。可以考虑将其改造成非阻塞状态机。例如SCI接收可以设计成在起始位中断中启动然后在定时器中断中逐位采样。ADC测量也可以分解为“启动充电”、“等待延时”、“开始放电计时”、“等待放电完成”、“计算”等多个状态分散到主循环的不同周期中执行。使用硬件定时器为了解放CPU并提高时序精度强烈建议使用MCU的硬件定时器TIM来产生精确的比特率中断驱动SCI或者用来测量ADC的放电时间输入捕获功能。这能极大提高系统的可靠性和实时性。添加调试输出在调试阶段可以利用软件SCI本身来回传调试信息。例如在关键函数入口、出口或错误处调用putSSCI发送特定的字符或字符串到PC帮助定位问题。功耗考虑while循环等待是“忙等待”功耗较高。如果设备是电池供电需要考虑在等待期间让CPU进入低功耗模式WAIT或STOP并通过外部中断或定时器唤醒。但这需要仔细设计确保不破坏SCI和ADC的时序。代码封装与可移植性将U08232.C和U08ADC.C中的硬件相关宏定义提取到单独的board.h配置文件中。提供清晰的初始化、发送、接收、读取等API接口。这样当更换MCU或调整引脚时只需修改配置文件核心算法代码无需变动。通过这个项目你收获的远不止是让一块老开发板跑通。你深入理解了异步串行通信和RC式ADC的底层物理原理掌握了在资源受限环境下用软件创造“硬件”的经典方法并亲身体验了嵌入式开发中从位操作、时序控制到系统集成、调试排错的完整流程。这些技能在你未来面对任何看似“不可能”的硬件限制时都将成为你手中最有力的工具。