MC68HC908 ROM编程例程深度解析:FLASH操作、缺陷规避与Bootloader实践
1. 项目概述深入MC68HC908的ROM编程例程在嵌入式系统开发领域尤其是针对那些资源受限的8位微控制器如何安全、可靠地对片内FLASH存储器进行编程一直是一个既基础又关键的技术点。这不仅仅是把数据写进去那么简单它涉及到精确的时序控制、电压管理、错误处理以及与外部工具的通信协议。很多开发者习惯于依赖现成的编程器或集成开发环境IDE提供的“一键下载”功能但当你需要实现产品的现场固件升级FOTA、参数在线配置或者开发自己的引导加载程序Bootloader时就必须深入到硬件底层理解FLASH编程的每一个时钟周期。Freescale现为NXP的一部分的MC68HC908系列微控制器作为经典的8位产品线其设计哲学非常具有代表性将复杂但标准的底层操作固化在ROM中以库函数的形式提供给开发者。这相当于在芯片内部预置了一个“硬件驱动库”。我们今天要拆解的正是这份名为《Using MC68HC908 On-Chip FLASH Programming Routines》的应用笔记及其附带的ROM例程源码。这份资料对于仍在维护或开发基于HC908老项目的工程师来说是无价之宝对于学习嵌入式底层编程的学生和爱好者而言它也是一份绝佳的、来自工业级的汇编语言和硬件接口编程范本。这套ROM例程包含了五个核心函数GETBYTE串行接收、RDVRRNG读取/验证、PRGRNGE编程、ERARNGE擦除和DELNUS延时。它们共同构成了一套完整的FLASH操作工具箱。但这份文档的价值远不止于提供几个调用地址它更揭示了在资源极度紧张可能只有几十字节RAM和几百字节ROM空间的环境下如何编写高效、健壮的代码。例如如何在不影响实时性的情况下服务看门狗COP如何计算并保证精确的微秒级延时以及如何巧妙地利用栈空间来传递参数和管理临时变量。接下来我将结合自己多年在8位机上的调试经验带你逐行剖析这些代码并分享那些数据手册上不会写的实践细节和避坑指南。2. 核心例程源码深度解析与设计逻辑官方文档提供了五个例程的流程图和完整的汇编源码。我们不要孤立地看每一行代码而是要先理解其整体的设计框架和数据结构这是高效使用它们的前提。2.1 统一的数据结构参数传递的约定在调用RDVRRNG、PRGRNGE和ERARNGE这三个与存储范围操作相关的例程前必须在RAM中建立一个特定的数据结构。这个结构起始于RAM $08地址是例程与调用者之间的“合同”。地址偏移变量名大小描述与要点RAM$08CTRLBYT1字节控制字节。bit 6是关键0选择页擦除$40选择整片擦除。其他位保留必须为0。RAM$09CPUSPD1字节CPU速度参数。计算公式为4 * fop (MHz)。例如总线频率fop为2.4576MHz时此处应填入4 * 2.4576 ≈ 10即$0A。这是一个非常容易出错的地方务必使用四舍五入后的整数值它直接关系到编程和擦除时序的准确性。RAM$0ALADDR2字节范围的结束地址高字节在前低字节在后。注意它表示的是操作范围的最后一个字节的地址而非范围大小。RAM$0CDATA可变长度数据数组。对于编程操作这里存放待写入FLASH的数据对于验证操作这里存放待比较的预期数据。其长度至少应等于你要操作的数据量。实操心得CPUSPD的计算与验证文档说CPUSPD 4 * fop但fop是内部总线频率它可能由外部晶振经过锁相环PLL分频而来。最稳妥的做法是在代码中通过测量已知的定时器周期或利用芯片的时钟输出功能来反推实际运行频率而不是完全信任理论计算值。我曾在一个项目中因为忽略了PLL的锁定时间导致fop实际未达到预期值结果FLASH编程全部失败排查了很久。除了上述RAM中的结构调用时还需通过CPU寄存器传递参数H:X寄存器对存放操作范围的起始地址(FADDR)。累加器A在调用RDVRRNG时用于选择模式。A0为“发送输出”模式通过通信口读出数据A≠0为“验证”模式与DATA数组比较。这种“寄存器固定RAM区域”的混合参数传递方式在ROM空间有限的8位MCU中很常见它节省了用于传递大量参数的栈空间。2.2GETBYTE异步串行接收的精准实现GETBYTE例程用于从监控模式通信口通常是PTA0或PTB0接收一个字节。它实现了一个简单的异步串行通信协议NRZ 无校验位1个停止位。GETBYTE: BRSET 0, PTA, GETBYTE ; 等待起始位低电平 JSR GET_BIT ; 尝试接收完整的起始位 BCS GETBYTE ; 成功(GET_BIT会检查起始位有效性) LDA #$80 ; 初始化接收移位寄存器 GBIT: JSR GET_BIT ; 接收一个数据位 RORA ; 将C位收到的数据位移入A的最高位 BCC GBIT ; 循环8次接收8个位 STOPBIT: JSR GET_BIT ; 接收停止位 RTS关键点解析波特率校准注释中提到BITTIMING9(1710*23)256CYCLES2.4576MHZ104µs9600BAUD。这是一个循环周期必须恒定的经典实现。GET_BIT子例程的每条指令周期数都经过精心设计确保每位数据的采样点都在位周期的中心。这意味着你不能在调用GETBYTE期间使能中断否则会破坏周期精度导致数据错乱。错误处理GET_BIT子例程会检查起始位和停止位的有效性。如果停止位不是高电平即帧错误GETBYTE返回时进位标志C会被清零。调用者必须检查C位如果C0则本次接收的数据无效。硬件准备通信口必须配置为输入并且外部需要上拉电阻。这是很多新手容易忽略的没有上拉高电平无法被可靠识别。2.3RDVRRNG读取与验证的二合一设计这个例程非常巧妙地将“读取数据并发送”和“读取数据并验证”两个功能合二为一通过累加器A的值来切换模式。流程核心对应流程图初始化在栈上开辟空间用于存放校验和、数据索引和验证状态。循环主体服务COPSTA $FFFF防止看门狗复位。从H:X指向的FLASH地址读取一个字节。判断模式A0发送模式直接调用PUT_BYTE一个未公开的ROM例程将字节发送出去。验证模式将读取的字节与DATA数组中的对应字节比较。若不同则将验证状态标记为失败$7E同时用读取值更新DATA数组便于调试查看实际值。更新校验和简单的累加和。地址H:X递增数据索引递增。判断是否到达结束地址LADDR。返回将验证状态FF为成功$7E为失败转移到C位校验和放在A中返回。注意事项COP服务的潜在陷阱文档特别指出即使用户模式使能了COP且使用了短超时设置在“发送模式”下由于PUT_BYTE例程的执行时间可能较长仍有可能发生COP超时复位。如果你的应用必须使能COP那么在调用RDVRRNG进行大数据块读取前临时延长COP超时时间。或者采用“验证模式”分批读取并在每次调用间隙服务COP。最根本的评估PUT_BYTE在特定波特率下的耗时确保其在COP窗口内。2.4PRGRNGE灵活的FLASH编程引擎这是最复杂的例程它允许对任意地址范围无需对齐行或页边界进行编程极大地提升了灵活性。设计亮点与步骤分析中断屏蔽一进入就SEI关闭中断确保编程和擦除的关键时序微秒级不被中断打断。行边界处理代码首先计算起始地址FADDR到其所在行末尾的字节数#BYTES TO END OF ROW。这是为了处理跨行编程的情况。编程过程以“行”为单位进行每完成一行会重新计算下一个行的起始。编程状态机对应流程图PRGSTP1-PRGSTP12步骤1-3设置FLASH控制寄存器FLCR的编程位PGM并读取块保护寄存器FLBPR以启动编程序列。步骤4-6使能高压HVEN并等待建立时间TNVS和TPGS。步骤7-9核心编程循环。从DATA数组取数据写入目标地址然后等待编程时间TPROG。这里有一个精妙的设计每编程6个字节就会调用一个本地子程序CLR_P_H临时关闭PGM和HVEN位并服务一次COPSTA $FFFF然后再重新开启高压继续编程。这解决了长序列编程过程中COP超时的问题。步骤10-12完成一行或整个范围后清除HVEN和PGM位等待高压保持时间TNVH。设备差异对于JB8和JL/JK3(E)等型号其FLBPR并非FLASH存储器因此在编程前用户软件必须额外执行解锁FLASH的操作否则编程会失败。这是一个重要的设备兼容性细节。2.5ERARNGE擦除操作及其已知缺陷ERARNGE例程用于页擦除或整片擦除。其逻辑相对直接根据CTRLBYT设置擦除模式配置FLCR寄存器施加高压并等待特定的擦除时间TERASE或TMERASE。然而文档第2节明确指出了一个关键缺陷Bug在部分型号如GR4/8, JB8, JL/JK3等上进行页擦除操作时由于在TERASE延时循环中服务了COPSTA $FFFF可能导致页擦除不彻底甚至意外擦除向量表所在页。根本原因分析FLASH擦除操作需要持续施加一个高压脉冲。在原始的ERARNGE代码中为了服务COP会在延时循环中执行STA $FFFF。这条指令本身会访问地址总线在某些特定的芯片硬件时序下可能会干扰到FLASH阵列内部的高压生成电路导致擦除电压不稳定或提前终止。整片擦除MASS因为时序参数TMERASE和循环次数不同可能避开了这个敏感的时间窗口。影响范围这个Bug不是软件逻辑错误而是与特定硅片版本的硬件时序相关。它会导致产品FLASH的耐久性远低于数据手册标称的10k次可能在几百次擦写后就出现数据错误。因此对于受影响的器件绝对不能直接使用ROM中的ERARNGE进行页擦除。2.6DELNUS可预测的精确延时DELNUS是一个通用的微秒级延时例程被PRGRNGE和ERARNGE调用。其延时公式为总周期数 5 (延时时间[µs] / 12) * (3 * (4*fop - 3) 9)简化后约为总周期数 ≈ 5 延时时间[µs] * fop [MHz]调用前需设置X寄存器延时时间(µs) / 12。例如需要100µs延时fop4MHz则计算X 100 / 12 ≈ 8。累加器ACPUSPD 4 * fop。同上例A 4 * 4 16 ($10)。使用技巧如果你需要非12µs整数倍的延时可以组合多个DELNUS调用或者在其前后添加少量NOP指令进行微调。在编写自己的延时函数时DELNUS的这段双循环嵌套的汇编代码DBNZA和DBNZX是学习如何编写与频率无关的精确延时程序的绝佳范例。3. 页擦除缺陷的实战解决方案既然直接调用ROM中的ERARNGE进行页擦除有风险官方文档在第10节和第12节提供了完整的解决方案。这个方案的核心思想是将存在风险的擦除时序代码复制到RAM中执行并移除其中服务COP的指令。3.1 解决方案原理与步骤工作区代码PageErase的执行流程如下代码复制将ROM中ERARNGE例程的页擦除部分从开始到设置高压并启动延时循环之前共72字节复制到RAM的指定区域RamStart$0A开始。关键修补修改擦除时间对于非JB8器件将循环次数从支持1ms擦除ECALLS 5改为支持4ms擦除ECALLS $14。这是为了确保达到10k次耐久性的最小擦除时间要求。JB8器件本身已使用4ms参数无需修改。移除COP服务将复制到RAM代码中的STA $FFFF指令服务COP替换为LDA $FFFF。LDA是读操作不会在地址总线上产生写脉冲从而避免了对FLASH擦除时序的干扰。添加跳转在复制到RAM的代码末尾添加一条跳转指令JMP使其跳转回ROM中ERARNGE例程的“步骤7”即擦除延时结束后的清理部分。这样危险的延时部分在RAM中执行且无COP干扰安全的收尾工作仍由可靠的ROM代码完成。调用执行像调用普通例程一样设置好CTRLBYT、CPUSPD和H:X后调用这个位于FLASH中的PageErase例程。它会自动完成复制、修补、跳转和执行的全过程。3.2 代码实现与关键配置以下是工作区代码的关键部分解析和配置要点; 1. 设备选择必须根据目标芯片修改 ;$SETNOT JB8 ; 默认非JB8器件 $SET JB8 ; 如果是JB8器件则启用此设置 ; 2. 本程序在FLASH中的存放地址强烈建议放在受FLBPR保护的区域 locFLASH: equ $FDC0 ; 3. RAM起始地址因器件而异 ; 对于 908GR4/8, 908KX2/8, 908JB8: RamStart: equ $0040 ; 对于 908JK1(E), 908JL/JK3(E): RamStart equ $0080 RamStart: equ $0040 ; 以HC908GR8为例 ; 4. ROM中DELNUS例程的地址因器件而异 ; 对于 908GR4/8: DELNUS: equ $1D96 ; 对于 908KX2/8: DELNUS: equ $12C3 ; 对于 908JK1/JL3等: DELNUS: equ $FD21 DELNUS: equ $1D96 ; 以HC908GR8为例 PageErase: pshx ; 保存传入的地址H:X pshh ldhx #CodeSize ; 设置复制字节数 Code2RAM: lda DELNUS-1,x ; 从ROM (DELNUS附近) 复制代码到RAM sta codeRAM-1,x dbnzx Code2RAM ; 循环复制 ; --- 关键修补开始 --- $IFNOT JB8 ; 如果不是JB8 lda #ECALLS ; ECALLS $14 (对应4ms) sta locECALLS ; 修改RAM中循环次数的值 $ENDIF lda #exLDA ; exLDA $C6 (LDA指令的操作码) sta locCOP ; 将RAM中的STA $FFFF改为LDA $FFFF ; --- 关键修补结束 --- mov #exJMP, CodeJmp ; 在复制代码后添加JMP指令 ldhx #jmpAddr ; jmpAddr指向ROM中ERARNGE的“步骤7” sthx CodeJmp1 ; 设置跳转地址 pulh ; 恢复要擦除的页内地址到H:X pulx jsr ramERARNGE ; 执行RAM中的修补后例程 rts3.3 使用示例与重要警告调用这个工作区例程的方式与调用原始ERARNGE类似但必须确保COP已被禁用MOV #$00, CTRLBYT ; 选择页擦除模式 MOV #$0A, CPUSPD ; 设置CPU速度例如 fop2.4576MHz - $0A LDHX #$F000 ; 加载要擦除的页内的任意地址 JSR PageErase ; 调用工作区例程致命警告与实操要点COP必须禁用工作区代码明确不支持COP。因为在修补代码中已经移除了STA $FFFF指令。如果在调用PageErase期间COP使能系统必定会复位。务必在调用前通过CONFIG寄存器或代码关闭COP。FLASH保护强烈建议将PageErase例程本身存放在受FLBPR保护的FLASH区域。否则在擦除其他页时有可能意外擦除这段代码本身导致程序崩溃。JB8/JL3/JK3的特殊性对于这些型号除了使用工作区在调用PRGRNGE编程前还需额外的步骤解锁FLASH因为它们的FLBPR不是FLASH单元。这通常涉及向某个特定的控制寄存器写入密钥序列请务必查阅具体型号的数据手册。RAM空间占用该方案需要约72字节的RAM空间从RamStart$0A开始。在资源非常紧张的项目中需要仔细规划内存布局确保该区域不会被栈或其它变量覆盖。4. 典型调用场景与问题排查实录理解了原理和缺陷解决方案后我们来看看如何在实际项目中调用这些例程并分享一些调试过程中常见的“坑”。4.1 完整编程-验证流程示例一个健壮的FLASH更新流程通常包含擦除、编程、验证三个步骤。以下是一个对$F000到$F01F区域进行更新的示例框架; 假设 DATA 数组从 $0050 开始 FADDR_START EQU $F000 FADDR_END EQU $F01F DATA_AREA EQU $0050 ; 1. 准备待编程数据 (例如填充为 $55, $AA 交替模式) LDHX #0 LDA #$55 LOAD_LOOP: STA DATA_AREA,X EOR #$FF ; $55 异或 $FF $AA AIX #1 CPHX #$20 ; 32字节 BNE LOAD_LOOP ; 2. 设置参数 MOV #$00, CTRLBYT ; 页擦除模式 MOV #$0A, CPUSPD ; fop 2.4576MHz LDHX #FADDR_END STHX LADDR ; 设置结束地址 ; 3. 执行页擦除 (使用工作区例程假设已禁用COP) LDHX #FADDR_START JSR PageErase ; 使用修补后的例程 ; 4. 执行编程 LDHX #FADDR_START JSR PRGRNGE ; 调用ROM编程例程 ; 5. 执行验证 LDHX #FADDR_START LDA #$01 ; 非0选择验证模式 JSR RDVRRNG BCC VERIFY_FAILED ; 如果C0验证失败 ; 验证成功A中为校验和可选检查 BRA UPDATE_SUCCESS VERIFY_FAILED: ; 处理验证失败重试、报错等 ; DATA_AREA 中现在存放的是从FLASH读出的实际值可用于调试4.2 常见问题排查速查表在实际使用这些例程时你可能会遇到以下问题。这里提供一个快速排查指南问题现象可能原因排查步骤与解决方案编程/擦除后验证失败1.CPUSPD计算错误。2. 目标地址受FLBPR保护。3. 页擦除使用了有Bug的ROM例程。4. 电源电压不稳或不在规范内。1. 确认fop频率重新计算并设置CPUSPD。2. 检查FLBPR寄存器值确保目标地址未处于保护区间。3. 对于受影响器件务必使用工作区PageErase例程。4. 测量Vdd电压确保在编程/擦除要求的范围内通常较宽但边缘值可能导致失败。调用PRGRNGE或ERARNGE后程序跑飞1. 栈溢出覆盖了关键变量或返回地址。2. 中断在编程/擦除期间发生。3. 对非FLASH地址进行了操作。1. 确保有足够的栈空间。PRGRNGE需7字节栈ERARNGE需5字节。优化调用层次。2. 例程已用SEI关中断但调用前若处于高优先级中断中需谨慎。确保不会发生嵌套中断导致意外开中断。3. 检查传入的H:X和LADDR地址是否在有效的FLASH地址范围内。GETBYTE接收数据错误1. 通信端口未配置为输入或缺少上拉。2. 系统频率fop与波特率不匹配。3. 在接收期间发生了中断。1. 确认DDRA0或DDRB0相应位为0输入并在MCU引脚外部连接4.7k-10k上拉电阻。2. 核对文档Table 1确认芯片型号和fop下的标准波特率。GETBYTE波特率是固定的。3. 在调用GETBYTE前必须用SEI禁止中断。使用工作区PageErase后系统复位1. COP未禁用。2. RAM代码区域被意外修改。1.这是最常见原因。在调用PageErase前确认CONFIG寄存器或相关COP控制位已将其禁用。2. 检查是否有其他代码或中断服务程序使用了RamStart$0A之后的区域。确保该区域专用于此例程。对JB8/JL3等编程失败未在编程前解锁FLASH。对于这些型号FLBPR是独立寄存器。在调用PRGRNGE或ERARNGE前必须按照数据手册向FLBPR写入特定的解锁序列通常是两个连续的写操作。4.3 调试技巧与高级应用利用验证模式调试当编程失败时不要只检查返回状态。调用RDVRRNG的验证模式A≠0失败后DATA数组会被替换为FLASH中实际读出的值。将这些值与你的预期值对比可以判断是哪些位出了问题是0写成了1还是1写成了0这有助于定位是时序问题、电压问题还是地址问题。构建简易Bootloader结合GETBYTE和PUT_BYTE需从ROM中查找其地址或自己实现一个串口发送函数你可以构建一个通过串口接收新固件并写入FLASH的Bootloader。流程通常是接收固件长度和校验和 - 擦除目标区域 - 分块接收并编程 - 逐块验证 - 跳转到新程序。切记在Bootloader中妥善处理中断向量表的重映射。时序参数的微调虽然ROM例程的延时参数是经过验证的但在极端温度或电压下如果仍出现偶发编程失败可以尝试微调DELNUS的输入参数适当增加X寄存器的值即增加延时。但这需要非常谨慎最好在芯片数据手册规定的极限参数范围内进行。内存与栈的规划这是8位项目成败的关键。在链接器脚本或汇编文件中明确定义DATA数组的起始地址如RAM$0C并为其预留足够空间。同时根据你的调用深度预留充足的栈空间通常从RAM末尾向下生长。务必确保栈、变量区和PageErase工作区三者不发生重叠。一个实用的方法是在项目启动代码中用特定值如$AA填充全部RAM在调试时观察这些值何时被修改从而判断栈的溢出情况。深入理解这套ROM编程例程不仅能让你搞定MC68HC908的FLASH操作其背后体现的精确时序控制、资源受限环境下的编程、硬件缺陷的软件规避等思想对于任何底层嵌入式开发都具有普遍的指导意义。它提醒我们即使是最基础的存储操作也充满了细节与挑战而阅读官方文档、理解源码、并亲自实践验证是掌握这些技能的唯一途径。