MC68HC908片上FLASH编程实战:ROM例程调用、页擦除Bug规避与Bootloader设计 1. 项目概述深入理解MC68HC908的片上FLASH编程如果你正在或曾经使用过Freescale现NXP的MC68HC908系列8位微控制器那么对片上FLASH进行编程无论是用于产品量产、固件升级还是现场调试都是绕不开的核心技能。这个系列以其高性价比和丰富的外设在十多年前的汽车电子、家电控制和工业传感器等领域占据了相当大的市场份额。即便在今天维护和升级这些“老兵”系统的需求依然存在。这些芯片内部ROM中预置了一组FLASH编程例程这原本是厂商提供给开发者的一个强大工具包旨在简化自编程Bootloader或编程器软件的开发。理论上你只需要按照文档调用几个固定的入口地址传入正确的参数就能轻松完成读取、擦除和编程操作。然而在实际项目中我踩过最大的坑恰恰就来自于对这些“黑盒”例程的盲目信任。官方应用笔记AN1831虽然提供了函数说明和调用示例但其中潜藏的细节陷阱、特定型号的兼容性问题尤其是那个著名的“页擦除Bug”如果没有足够的实战经验去解读和规避很可能会导致产品批量烧录失败甚至意外擦除关键的中断向量区让整个系统“变砖”。本文的目的就是结合我多年在工控和消费电子领域使用HC908系列的经验为你彻底拆解这份应用笔记。我不会仅仅翻译手册而是会聚焦于如何安全、高效地使用这些ROM例程重点剖析那个让人头疼的页擦除问题及其根治方案并分享一系列从调试中总结出来的参数计算技巧、内存布局规划和避坑指南。无论你是正在维护旧有项目还是出于学习目的研究经典的8位MCU自编程机制这些从一线实战中沉淀下来的细节都能让你少走弯路。2. 核心例程功能解析与设计逻辑MC68HC908系列芯片将FLASH操作例程固化在ROM中主要提供了五个核心函数GETBYTE,RDVRRNG,PRGRNGE,ERARNGE和DELNUS。这种设计非常巧妙它把最复杂、最依赖精确时序的底层脉冲生成和电压控制逻辑用经过硅验证的ROM代码实现保证了最高的可靠性。开发者只需在RAM中运行一个“管理器”程序负责组织数据、设置参数并调用这些ROM函数从而大大降低了自编程软件开发的难度和风险。2.1 各例程的角色与协同关系我们可以把这五个例程看作一个流水线上的不同工位GETBYTE (字节接收)这是通信的“入口”。它通过芯片的监控模式通信引脚通常是PTA0或PTB0接收一个字节数据。其价值在于如果你在编写一个通过串口接收固件数据的Bootloader可以直接调用它来获取数据而无需自己实现底层的串口字节接收和波特率匹配逻辑。需要注意的是它不负责回显ECHO这提升了传输效率但要求上位机有独立的校验机制如Checksum。RDVRRNG (读取/验证范围)这是一个“质检员”身兼两职。当累加器A设置为$00时它扮演读取并发送的角色将指定FLASH地址范围的数据通过通信引脚发送出去常用于读取芯片内容进行校验。当A非零时它则进行验证操作将指定FLASH范围的数据与RAM中DATA数组的数据逐字节比较并返回比较结果通过CCR寄存器的C标志位和整个数据块的校验和。PRGRNGE (编程范围)这是“写入工”。负责将RAM中DATA数组的数据编程到FLASH的指定地址范围。它非常“耿直”不检查目标地址是否已擦除未擦除的位只能从1变为0无法从0变回1也不在编程后进行自动验证。因此安全的操作流程必须是先擦除确保全为1再调用PRGRNGE写入最后调用RDVRRNG的验证模式进行确认。ERARNGE (擦除范围)这是“清空工”。它根据控制字节CTRLBYT的值执行页擦除$00或整片擦除$40。这里埋藏着一个关键陷阱对于GR、JB、JL/JK等系列的部分型号其页擦除功能存在Bug可能导致擦除不彻底或误擦向量表。这是本文后续需要重点攻克的问题。DELNUS (延时)这是“计时员”。为一个可编程的延时循环被PRGRNGE和ERARNGE内部调用以产生精确的FLASH编程/擦除时序。你也可以独立调用它来实现微秒级延时。其延时公式为3 * A * X 8个CPU周期其中A通常设置为4 * fopfop为内部总线频率。2.2 关键数据结构RAM中的“控制台”所有例程除了GETBYTE都通过一个位于RAM固定偏移位置的数据结构来传递参数。这个结构起始于RAM基地址 $08是调用者与被调用ROM例程之间的“约定”。理解它是正确调用的前提。偏移地址 (相对RAM基址)变量名大小描述与设置要点$08CTRLBYT1字节控制字节。仅用于ERARNGE。$00页擦除$40整片擦除。务必在调用前正确设置。$09CPUSPD1字节速度参数。值为4 * fop其中fop是内部总线频率(MHz)。例如fop2.4576MHz时CPUSPD10 ($0A)。这是精确延时的关键计算错误会导致擦写失败或FLASH寿命缩短。$0A, $0BLADDR2字节结束地址。需要操作读、写、验证的FLASH地址范围的最后一个字节的地址。注意是包含性的。$0C 开始DATAN字节数据数组。存放待编程的数据或验证时用于比较的数据。其长度由你需要编程的字节数决定。实操心得一CPUSPD的计算与取舍手册要求CPUSPD 4 * fop。这里的fop是你的MCU实际运行的总线频率。例如使用外部4MHz晶振内部PLL未启用则fop4MHzCPUSPD16 ($10)。但这里有个细节fop必须是调用擦写例程时MCU的实际频率。如果你的Bootloader为了高速接收数据而临时提高了总线频率就必须用提高后的频率来计算CPUSPD。我曾在一个项目中Bootloader运行在8MHz而主程序运行在4MHz忘记切换CPUSPD导致编程一直失败。一个稳妥的做法是在调用任何FLASH操作例程前重新计算并赋值一次CPUSPD。3. 页擦除Page EraseBug的深度剖析与根治方案这是MC68HC908 FLASH编程中最著名、也最危险的“坑”。应用笔记AN1831的Section 2和Section 10专门讨论了这个问题。简单来说在部分型号如MC68HC908GR4/8, JB8, JL/JK系列的ROM例程ERARNGE中当执行页擦除操作时由于在关键的擦除延时(tErase)期间错误地服务了看门狗COP可能导致两个严重后果目标页未能完全擦除某些字节仍为0。更致命的是可能意外擦除中断向量所在的页面。这意味着如果你直接调用存在Bug的ROM例程进行页擦除轻则数据残留导致后续编程错误重则擦除复位向量导致芯片下次无法启动。3.1 官方解决方案的利弊分析应用笔记给出了两种规避方案方案一自行编写页擦除例程存储在FLASH中使用时复制到RAM执行。优点完全自主可控可以优化代码避开所有ROM例程的Bug。缺点需要开发者深入理解FLASH控制器的寄存器操作和精确时序实现门槛高。且需要占用一部分FLASH空间来存储这个自编的擦除函数。方案二使用应用笔记附录提供的“补丁”代码Workaround Code。优点相对简单。它本质上是一个“安全壳”通过将ROM中ERARNGE的部分关键代码拷贝到RAM中执行并跳过其中服务COP的指令从而规避了Bug。缺点需要额外占用约72字节的RAM空间用于存放拷贝的代码并且要求将这段“补丁”代码本身存放在受FLASH块保护寄存器(FLBPR)保护的区域内防止它自己被意外擦除。对于大多数需要快速解决问题、不想重新发明轮子的工程师来说方案二是更实用和可靠的选择。下面我将详细拆解这个方案的具体实现和注意事项。3.2 “补丁”代码Workaround的实现机理与部署要点AN1831的Section 12提供了完整的汇编源代码。它的核心逻辑并不复杂定位与拷贝代码首先计算出ROM中ERARNGE例程里从“页擦除步骤1”到“步骤6”的72字节机器码的起始地址。RAM执行将这72字节代码拷贝到RAM中一个固定的偏移地址RAM基址 $0A开始的位置。跳转执行然后跳转到RAM中的这段代码开始执行。由于拷贝到RAM的代码已经移除了服务COP的指令因此擦除操作可以在不受干扰的情况下完成。部署与调用步骤编译与存放将Section 12的汇编代码一个名为PAGE_ERASE的子程序编译后链接到你的工程中。最关键的一步是必须通过链接器脚本或指令将这段代码定位到FLASH中一个受保护的地址空间。通常你可以将其放在紧挨着中断向量表之前的某个固定页并确保FLBPR寄存器的设置保护了这个区域。例如如果你的FLASH结束于$FFFF向量表在$FFC0-$FFFF你可以将PAGE_ERASE放在$FF00-$FFBF这个页并设置FLBPR保护$FF00以上的空间。调用前准备与调用原始ERARNGE进行页擦除一样你仍然需要正确设置CTRLBYT为$00CPUSPD为4*fop并在H:X寄存器中放入目标页内的任意一个地址。调用不再使用jsr ERARNGEROM地址而是使用jsr PAGE_ERASE你存放的FLASH地址。实操心得二FLASH保护寄存器的“陷阱”对于JB8和JL/JK系列手册特别指出其FLASH块保护寄存器(FLBPR)本身不是FLASH存储器这意味着芯片复位后FLASH默认处于保护状态。你必须先在用户程序中向FLBPR写入相应的值来解除保护然后才能进行擦写操作。而像QY系列FLBPR本身是FLASH可以通过编程器一次性配置好。这是一个极易忽略的差异点。在编写通用Bootloader时务必根据芯片型号判断是否需要以及如何解除保护。一个常见的做法是在代码开头读取某个标志引脚或检查RAM中的标志决定是否执行解除保护序列。4. 完整编程流程实操与参数详解掌握了核心例程和避开了最大的坑之后我们来演练一个完整的FLASH操作流程擦除一页、编程数据、并进行验证。我们以MC68HC908QY4为例假设总线频率fop 2.4576MHz目标页地址为$EE00-$EE3F64字节页。4.1 步骤一初始化与参数计算首先我们需要定义例程入口地址和数据结构在RAM中的位置。根据表6QY4的例程入口在$2800附近。; 定义例程入口地址 (以MC68HC908QY4为例) GETBYTE EQU $2800 RDVRRNG EQU $2803 ERARNGE EQU $2806 ; 注意如果芯片有页擦除Bug应使用补丁代码地址 PRGRNGE EQU $2809 DELNUS EQU $280C ; 定义RAM数据结构起始地址 (假设RAM从$0080开始) RAM_BASE EQU $0080 CTRLBYT EQU RAM_BASE$08 CPUSPD EQU RAM_BASE$09 LADDR EQU RAM_BASE$0A ; 2字节 DATA EQU RAM_BASE$0C ; 数据数组开始 ; 计算 CPUSPD: 4 * 2.4576 ≈ 9.8304四舍五入为10 ($0A) ; 注意此处取整可能导致微小时序误差但对于2.4576MHz是官方推荐值。4.2 步骤二执行页擦除在擦除前必须确保目标页未被FLBPR保护。对于QY4我们假设已解除保护。; 准备页擦除参数 MOV #$00, CTRLBYT ; 选择页擦除模式 MOV #$0A, CPUSPD ; fop 2.4576MHz - CPUSPD $0A ; H:X 装入目标页内的任意地址例如$EE00 LDHX #$EE00 ; 调用擦除例程 ; 如果是GR/JB/JL/JK等有Bug的型号此处应调用自编的 PAGE_ERASE JSR ERARNGE ; 或 JSR PAGE_ERASE (补丁) ; 擦除操作需要数毫秒例程内部有延时此处无需额外等待。注意事项擦除后的状态检查ERARNGE例程不会返回擦除成功或失败的状态。标准的做法是在擦除后立即对目标页进行读取验证。一个简单的检查是读取该页的若干个地址例如首、中、尾确认其值是否为$FF已擦除状态。这可以及早发现因保护寄存器设置错误或电压不稳导致的擦除失败。4.3 步骤三准备数据并编程接下来我们将一个预设的数据模式例如$55, $AA交替编程到刚擦除的页中。; 1. 向DATA数组填充待编程数据 (64字节) LDHX #0 ; 索引清零 LDA #$AA ; 初始值 FILL_LOOP: COMA ; A取反 $AA - $55, $55 - $AA STA DATA, X ; 存入DATA数组 AIX #1 ; 索引加1 CPHX #64 ; 是否填满64字节 BNE FILL_LOOP ; 2. 设置编程参数 ; CPUSPD 已在擦除时设置如果频率未变可沿用。 ; 设置操作范围$EE00 到 $EE3F LDHX #$EE3F ; 结束地址 (Last Address) STHX LADDR LDHX #$EE00 ; 起始地址 (First Address) 放在 H:X 中 ; 3. 调用编程例程 JSR PRGRNGE ; 编程完成同样无直接状态返回。4.4 步骤四验证编程结果编程完成后必须进行验证确保数据被正确写入。; 使用RDVRRNG的验证模式 ; DATA数组中已经是期望的数据刚填充的 LDHX #$EE3F ; 设置验证范围的结束地址 STHX LADDR LDHX #$EE00 ; 设置起始地址于H:X LDA #$01 ; A置为非零值选择“验证”模式 JSR RDVRRNG ; 检查结果 BCC VERIFY_FAILED ; 如果C位被清除验证失败 ; 如果C位被置位验证成功 ; 此时累加器A中为读取数据的校验和可用于高级校验。 VERIFY_FAILED: ; 验证失败处理流程 ; 可以重试编程或标记错误标志或进入故障安全模式。4.5 独立延时函数DELNUS的使用示例假设我们需要在编程流程中插入一个精确的500微秒延时用于等待外部电路稳定fop 4MHz。计算总周期数500µs * 4MHz 2000 cycles。计算A值A CPUSPD 4 * fop 16 ($10)。计算X值调用DELNUS本身需要JSR等指令产生开销。假设调用前设置A和X用了5个周期JSR用了5个周期总开销C_overhead 10 cycles。所需DELNUS产生的周期数 2000 - C_overhead 1990 cycles。根据公式DELNUS_cycles 3*A*X 8 1990。解得X (1990 - 8) / (3 * 16) 1982 / 48 ≈ 41.29。取整X 41 ($29)。计算实际延时实际周期 10 (3*16*41 8) 10 1976 1986 cycles。对应时间1986 / 4MHz 496.5µs误差在可接受范围内。DELAY_500US: LDA #$10 ; A 16 (4 * 4MHz) LDX #$29 ; X 41 JSR DELNUS RTS实操心得三DELNUS的精度与优化DELNUS的延时是“阻塞式”的且期间不服务COP。如果延时较长需注意看门狗超时。对于更精确的延时可以牺牲一些代码空间用循环嵌套来实现。此外公式中的8cycles和调用开销是固定值在精确计时时应纳入计算。对于非关键时序如LED闪烁粗略估算即可。5. 常见问题排查与调试经验实录在实际开发中调用这些ROM例程时遇到的问题往往不是语法错误而是逻辑和状态问题。下面是我总结的几个典型故障场景和排查思路。5.1 问题一编程或擦除操作完全无效果FLASH内容不变可能原因1FLASH处于保护状态。排查检查FLASH块保护寄存器(FLBPR)的值。确认你要操作的地址范围不在被保护的区块内。对于JB8/JL/JK系列确认已在代码中执行了正确的解保护写入操作。技巧在尝试擦写前先读取FLBPR的值并打印出来如果支持串口或者将其暂存到某个RAM变量中供调试器查看。可能原因2CPUSPD参数设置错误。排查确认你计算CPUSPD所用的fop是MCU在执行擦写例程时的实际总线频率而不是芯片标称频率或软件配置的理想频率。检查时钟配置寄存器如ICGC、OSCCTL。技巧在调用擦写例程的代码附近通过翻转一个GPIO引脚并用示波器测量来间接验证当前代码的执行速度是否与预期频率相符。可能原因3电源电压或稳定性不满足要求。排查FLASH编程/擦除对电源电压(Vdd)有严格要求通常需要在标称范围内如3.0V-3.6V。使用不稳定的电源或电压偏低会导致操作失败。技巧在调试阶段使用实验室稳压电源供电并监控电源纹波。确保在擦写操作瞬间电源没有大的跌落。5.2 问题二验证阶段随机失败但重新编程有时又能成功可能原因1目标页未完全擦除。排查在调用PRGRNGE之前增加一个检查步骤读取目标地址范围内的几个样本点如开始、中间、结束地址确认其值是否为$FF。如果不是说明之前的擦除不成功。根源这很可能就是遇到了“页擦除Bug”。你正在使用的芯片型号恰好是受影响型号并且你直接调用了有Bug的ROMERARNGE。解决立即换用应用笔记提供的“补丁”代码Workaround进行页擦除。可能原因2数据缓冲区(DATA数组)在编程过程中被意外修改。排查如果DATA数组区域与堆栈(Stack)或其它动态变量区域重叠在PRGRNGE或RDVRRNG例程执行期间它们会使用一些堆栈空间可能会覆盖数据。确保DATA数组有独立且充足的空间。技巧在调用PRGRNGE之后、验证之前再次读取DATA数组的内容与原始数据对比确认未被破坏。5.3 问题三调用例程后程序跑飞或看门狗复位可能原因1堆栈溢出。排查每个ROM例程都会消耗一定的堆栈空间见应用笔记Table 2从3到7字节不等。如果你的应用程序堆栈本身已经接近RAM顶部调用这些例程可能导致溢出覆盖关键数据或返回地址。解决重新规划内存布局确保堆栈有足够的安全空间。一个保守的做法是在调用FLASH操作例程前临时将堆栈指针(SP)指向一个更安全的区域如一个预留的缓冲区操作完成后再恢复。可能原因2在FLASH例程执行期间发生了中断。排查PRGRNGE和ERARNGE会屏蔽中断设置I位但GETBYTE、RDVRRNG和DELNUS不会。如果在这三个例程执行期间发生了中断而中断服务程序(ISR)试图访问正在被操作的FLASH区域例如代码在FLASH中会导致不可预知的行为。解决在调用任何FLASH操作例程包括DELNUS之前最好手动清除中断允许位CLI指令的逆操作是SEI这里应为SEI屏蔽中断。或者确保你的所有ISR都位于RAM中执行。可能原因3使用了受影响的页擦除例程误擦了向量表。现象页擦除操作后系统无法再次启动或复位。解决这是最严重的情况。一旦向量表被擦通常需要借助外部编程器如BDM、JTAG恢复。预防是关键对于有Bug的型号绝对不要直接调用ROM的ERARNGE进行页擦除。务必使用补丁代码并在补丁代码中严格校验传入的地址避免其落在向量表页面。5.4 调试辅助技巧LED或GPIO状态指示在Bootloader的关键阶段开始擦除、开始编程、验证成功/失败控制一个LED或GPIO引脚输出不同的电平或脉冲。用逻辑分析仪或示波器捕捉这些信号可以清晰地看到程序执行到哪一步失败。RAM日志在RAM中开辟一小块区域作为日志缓冲区。在每个关键操作后将一个特定的状态码写入日志。即使程序跑飞通过调试器查看这块RAM区域也能知道崩溃前最后执行的操作是什么。简化复现在排查问题时构建一个最小测试工程。只包含最基础的时钟初始化、GPIO初始化用于指示和FLASH操作代码。排除其他复杂驱动和业务逻辑的干扰让问题更容易暴露。6. 工程实践构建一个稳健的Bootloader框架基于上述例程和避坑经验我们可以勾勒出一个用于MC68HC908的简易Bootloader框架。这个框架运行在RAM中通过串口接收新固件并安全地更新到FLASH中。6.1 内存布局规划这是Bootloader设计的第一步也是最重要的一步决定了系统的稳定性和可维护性。Bootloader代码区通常放在FLASH的起始地址如$E000。上电后从这里开始执行。这个区域必须足够大容纳Bootloader的所有代码包括FLASH操作补丁、通信协议解析等。应用程序区放在Bootloader之后如$E800开始。Bootloader负责将接收到的固件数据编程到这个区域。中断向量重映射MC68HC908通常允许重映射中断向量表。Bootloader和应用程序应有各自的中断向量表。Bootloader运行时向量表指向自己的ISR跳转到应用程序前需要修改向量表基址寄存器如IVBR指向应用程序的向量表。关键数据/标志区预留一小块FLASH区域如某个固定页的最后几个字节用于存储Bootloader版本、应用程序校验和、升级标志等。这个区域应被FLBPR保护防止在应用程序崩溃时被意外修改。RAM分配明确划分堆栈区、全局变量区、DATA数组区、以及可能用到的补丁代码拷贝区。确保它们互不重叠。6.2 安全升级流程设计一个健壮的Bootloader不仅仅是能写FLASH更要能应对各种异常。上电/复位判断首先检查升级标志存放在受保护的FLASH标志区。如果标志有效则进入升级模式否则直接跳转到应用程序。升级模式 a.通信握手与上位机建立可靠的连接交换版本、波特率等信息。 b.接收数据与校验分块接收固件数据。每接收一块计算CRC或Checksum并与上位机发送的校验和比对。只有校验通过的数据块才被暂存到RAM的DATA数组中。 c.擦除目标页在编程前使用安全的页擦除补丁擦除目标应用程序页。擦除后可选进行空白检查读回是否为$FF。 d.编程与验证调用PRGRNGE编程紧接着调用RDVRRNG进行验证。必须验证通过才能进行下一块的操作。e.全程超时与看门狗管理在长时间的数据接收或擦写过程中要合理服务看门狗。但要注意在补丁代码执行的擦除关键期内不能服务看门狗。跳转与恢复 a. 所有数据块接收、编程、验证成功后更新升级标志为“成功”并计算整个应用程序的校验和存入标志区。 b. 执行软复位或直接初始化应用程序的堆栈和PC指针跳转到应用程序入口。 c. 应用程序启动后应首先检查自身的完整性比如校验和如果损坏应能主动设置升级标志并触发复位让系统回到Bootloader模式。6.3 代码结构示例伪代码逻辑; *********************************************************** ; MC68HC908 Bootloader 核心逻辑框架 ; *********************************************************** Main: ; 1. 初始化时钟、看门狗、串口、GPIO ; 2. 检查升级标志 LDA UPGRADE_FLAG CMP #$A5 BNE Jump_To_App ; 3. 进入升级模式 Upgrade_Mode: ; 3.1 与上位机握手 JSR Establish_Communication BCC Upgrade_Failed ; 3.2 循环接收数据包 Packet_Loop: JSR Receive_Packet_With_CRC BCC Packet_Error ; 数据包在 RX_BUFFER, 长度已知 ; 3.3 计算目标FLASH地址 ; 根据数据包序号计算 ; 3.4 擦除目标页 (使用安全的补丁代码!) ; 设置 CTRLBYT, CPUSPD, H:X JSR SAFE_PAGE_ERASE ; 这是我们的补丁函数 ; 可选进行空白检查 JSR Check_If_Erased BCC Erase_Failed ; 3.5 将数据从RX_BUFFER复制到DATA数组 JSR Copy_To_DATA_Array ; 3.6 编程 ; 设置 LADDR, H:X (FADDR) JSR PRGRNGE ; 3.7 验证 ; 设置 LADDR, H:X, A (非零) JSR RDVRRNG BCC Program_Failed ; 3.8 发送本包成功应答给上位机 ; 3.9 判断是否所有包完成 ; 若完成则设置最终成功标志复位 ; 若未完成跳回 Packet_Loop Jump_To_Application: ; 禁用中断设置应用程序堆栈指针 ; 跳转到应用程序入口地址 (例如 $E800) JMP $E800 Upgrade_Failed: Erase_Failed: Program_Failed: ; 错误处理点亮错误灯等待超时后尝试跳转或复位 ; 可以记录错误码到RAM供调试 JMP Error_Handler ; *********************************************************** ; 安全的页擦除补丁函数 (基于AN1831 Section 12) ; 输入: CTRLBYT$00, CPUSPD已设置, H:X页内地址 ; 输出: 无 ; 注意: 此函数自身必须存放在受FLBPR保护的FLASH区域 ; *********************************************************** SAFE_PAGE_ERASE: ; 将ROM中部分ERARNGE代码拷贝到RAM (例如 COPY_DEST) ; 跳转到RAM中的代码执行 ; 具体代码见AN1831 Section 12 RTS最后我想分享一点个人体会。处理MC68HC908这类老型号的底层编程就像与一位经验丰富但脾气古怪的老工匠合作。ROM例程是它提供的现成工具非常强大但你必须完全读懂它的“说明书”数据手册和应用笔记特别是那些用星号标注的“注意事项”和“勘误”。那个页擦除Bug就是最典型的例子它提醒我们在嵌入式开发中尤其是涉及底层硬件操作时对任何第三方代码哪怕是厂商提供的都要保持审慎必须通过严格的测试和交叉验证才能投入生产。每一次成功的在线升级背后都是对这些细节的反复打磨和验证。