MC68HC908GR/GZ单片机片上FLASH例程深度解析与实战指南
1. 项目概述与核心价值
在嵌入式开发领域,尤其是面对那些资源受限、没有内置硬件编程接口的老牌8位微控制器时,如何安全、高效地操作其内部的FLASH存储器,一直是一个既基础又充满挑战的课题。MC68HC908GR/GZ系列单片机,作为Freescale(现NXP)经典HC08架构的代表,其内部集成的FLASH支持例程,为开发者提供了一个极其宝贵的“官方外挂”。这些固化在ROM中的代码,封装了FLASH编程、擦除、验证所需的所有底层时序和电压控制逻辑,让我们得以在用户模式下,通过简单的函数调用,就能完成复杂的存储操作,而无需深究其内部电荷泵如何工作,或是精确计算每个微妙级的延时。
这项技术的核心价值在于标准化与降本增效。想象一下,如果没有这些例程,每个工程师都需要根据数据手册的电气特性,从头编写底层驱动,不仅容易因时序偏差导致编程失败或器件损坏,更会带来巨大的重复开发和测试成本。Motorola(后来的Freescale)将这些复杂操作固化为ROM例程,相当于为所有使用该系列MCU的开发者提供了一套经过严格验证、稳定可靠的“标准工具库”。无论是进行产品开发阶段的固件烧录,还是产品上市后通过通信接口实现现场固件升级(FOTA的早期雏形),亦或是存储设备运行时的关键参数,这套机制都至关重要。它直接关系到产品的可靠性、可维护性和生命周期成本。
本文将以MC68HC908GR16/GZ16/GZ8为例,深入剖析其片上FLASH支持例程的调用方法、工作原理、关键参数配置以及实战中的避坑指南。我将结合自己多年在工业控制设备开发中使用HC08系列MCU的经验,不仅告诉你这些例程怎么用,更会解释为什么这么用,以及在实际项目中可能遇到哪些“坑”和应对技巧。无论你是正在维护一个遗留的HC08项目,还是出于学习目的研究经典嵌入式设计,这篇文章都将为你提供一份详尽的参考手册。
2. FLASH存储结构与片上例程总览
2.1 FLASH物理结构与操作原理
MC68HC908GR/GZ系列MCU采用的是一种名为“SuperFlash”的分栅(Split-Gate)存储单元技术,授权自Silicon Storage Technology (SST)。这种结构相较于早期的EPROM或EEPROM,在可靠性、编程速度和功耗上取得了更好的平衡。
其核心操作基于两种物理机制:
- 编程(Program):采用沟道热电子注入(Channel Hot Electron Injection)。简单理解,就是在控制栅施加高电压,同时在漏极施加电压,使沟道中的电子获得足够能量(变“热”),越过硅-氧化硅的势垒,被注入到浮栅中。浮栅捕获电子后,晶体管的阈值电压升高,在读取时被识别为逻辑‘0’(对于HC08 FLASH,通常已擦除状态为‘1’,编程后为‘0’)。编程操作以字节为单位进行。
- 擦除(Erase):采用福勒-诺德海姆隧穿(Fowler-Nordheim Tunnelling)。在源极施加高电压,控制栅接地,浮栅中的电子在强电场作用下,穿越薄氧化层隧穿到源极,从而实现擦除。擦除操作通常以页(Page)或整片(Mass)为单位。GR/GZ的FLASH组织为每32字节为一个行(Row),每64字节(两行)为一个页(Page)。
所有编程和擦除所需的高压均由芯片内部的电荷泵(Charge Pump)从单一的VDD电源产生,这大大简化了外部电路设计。官方标称这类FLASH可承受至少10,000次编程/擦除周期,足以满足绝大多数应用场景。
2.2 片上ROM例程生态系统
芯片的ROM中预置了5个核心例程,它们通过一个跳转表(Jump Table)提供固定入口地址,确保了未来ROM代码更新时的向前兼容性。这五个例程构成了一个完整的FLASH操作闭环:
| 例程名 | 入口地址 | 核心功能 | 简要描述 |
|---|---|---|---|
| GetByte | $1C00 | 数据接收 | 通过PTA0引脚以串行方式接收一个字节数据,使用与监控模式相同的NRZ协议和波特率。 |
| RDVRRNG | $1C03 | 读取与验证 | 读取指定范围的FLASH数据,并可选择:a) 通过PTA0发送出去;b) 与RAM中的DATA数组进行校验。 |
| PRGRNGE | $1C09 | 编程 | 将RAM中DATA数组的数据编程到指定的FLASH地址范围。支持2.0 - 8.4 MHz总线频率。 |
| ERARNGE | $1C06 | 擦除 | 擦除指定的一个FLASH页(64字节)或整个FLASH阵列。支持2.0 - 8.4 MHz总线频率。 |
| DELNUS | $1C0C | 延时 | 产生一个可编程的精确延时,被PRGRNGE和ERARNGE内部调用,也可由用户独立使用。 |
关键经验:务必使用这些固定的入口地址来调用例程,绝对不要试图直接调用ROM中的子函数(如
GetBit,PutByte)或猜测其他地址。跳转表是保持兼容性的生命线。
2.3 关键变量与内存布局
所有例程都依赖于一片位于零页(Zero Page, RAM地址$0040起)的特定RAM区域进行参数传递。理解这片内存的布局是正确调用的前提。
表:FLASH例程关键变量定义
| 地址 | 变量名 | 大小 | 描述与用途 |
|---|---|---|---|
$40-$47 | (保留) | 8字节 | 保留供未来使用,用户程序不应使用。 |
$48 | CTRLBYT | 1字节 | 控制字节。仅ERARNGE使用其位6:1= 整片擦除 (Mass Erase),0= 页擦除 (Page Erase)。 |
$49 | CPUSPD | 1字节 | CPU速度参数。PRGRNGE和ERARNGE的关键参数。计算公式:CPUSPD = ceil(fop(MHz) * 2)。例如,fop=4.2MHz,则CPUSPD=9。设置错误将导致编程/擦除时序错误而失败。 |
$4A-$4B | LADDR | 2字节 | 范围末地址。16位地址,$4A为高字节,$4B为低字节。被RDVRRNG和PRGRNGE用于指定操作范围的结束地址。 |
$4C起 | DATA | 可变 | 数据数组起始地址。用于存放待编程或待校验的数据。必须位于零页,且其大小必须与要编程或校验的字节范围严格匹配。 |
| H:X寄存器 | - | 2字节 | 范围起始地址。在RDVRRNG和PRGRNGE中,存放操作范围的起始地址;在ERARNGE中,存放待擦除页或阵列内的任意一个地址。 |
避坑指南一:零页(Zero Page)约束。
DATA数组必须完全位于零页(地址$0000-$00FF)。这是因为HC08的某些寻址模式(如STA DATA,x)在零页内效率更高,且ROM例程的代码基于此假设编写。如果你的数据超过256字节,必须分多次调用例程,每次确保DATA数组的索引不超过零页边界。一个常见的错误是直接定义一个大型数组并跨越$0100,这将导致例程访问错误的内存位置。
3. 核心例程详解与调用实战
3.1 GetByte:串行字节接收
GetByte例程是数据输入的基础,常用于通过简单的单线串口从主机(如PC)下载程序或数据到RAM。它复用监控模式(Monitor Mode)的通信引脚PTA0和协议。
工作原理:该例程等待PTA0引脚出现一个由高到低的下降沿(起始位),然后以固定的波特率采样后续的8个数据位和1个停止位。其波特率由内部总线频率fop决定:
- 对于GZ8/GZ16:
波特率 = fop / 278 - 对于GR16:
波特率 = fop / 256
调用要点与陷阱:
- 硬件准备:PTA0引脚外部必须接上拉电阻,确保空闲时为高电平。
- 软件配置:调用前,必须将PTA0配置为输入(
DDRA0=0)。 - 中断与看门狗:该例程不屏蔽中断(I位不置1),也不服务看门狗(COP)。这意味着如果接收一个字节的时间过长,可能触发COP复位。因此,在调用
GetByte的循环中,用户程序必须自行处理COP或确保接收间隔短于COP超时时间。 - 错误处理:返回时,若进位标志
C=0,表示帧错误(未检测到有效的停止位),此时累加器A中的数据不可信。
示例代码:安全接收一个字节
GETBYTE equ $1C00 bclr 0, DDRA ; 确保PTA0为输入模式 jsr GETBYTE ; 调用接收例程 bcc FRAME_ERROR ; 如果C=0,跳转到帧错误处理程序 ; 此时A中为有效接收数据 sta RECEIVED_DATA ; ... 处理数据 FRAME_ERROR: ; 错误处理:例如重试、记录错误、系统复位等 bra RESET_COM_LINK3.2 RDVRRNG:读取与校验的瑞士军刀
RDVRRNG功能强大,提供两种模式,通过调用前累加器A的值来切换。
两种工作模式:
- 发送模式(Send-Out):
A = $00。将指定FLASH地址范围的数据,通过PTA0以串行方式发送出去。适用于读取FLASH内容并传输到上位机进行备份或分析。 - 校验模式(Verify):
A ≠ $00(通常用$FF或$55等非零值)。读取FLASH数据,并与零页DATA数组中预先存放的预期数据逐字节比较。
关键流程与返回值:
- 参数设置:
H:X设为首地址,LADDR设为末地址。对于校验模式,还需提前将预期数据填充到DATA数组。 - 执行过程:例程依次读取FLASH,根据模式选择发送或比较。在校验模式中,任何不匹配都会导致
DATA数组中对应位置被替换为实际读出的FLASH值。 - 返回值:
- A寄存器:始终存放读取的所有数据的校验和(Checksum),即所有字节累加和的低8位。
- C标志位(仅校验模式有效):
C=1表示所有字节校验通过;C=0表示至少有一个字节不匹配。 - H:X寄存器:指向刚操作范围的下一个地址,便于连续操作。
示例代码:校验已编程的FLASH内容假设我们刚刚向FLASH地址$C000-$C01F(一个32字节行)编程了交替的$55和$AA,现在需要验证。
RDVRRNG equ $1C03 ; 步骤1:在DATA数组中填充预期数据 ($55, $AA, $55, $AA...) ldhx #$0000 ; 数组索引 lda #$55 ; 起始值 FillLoop: sta DATA, x ; 存储到DATA数组 eor #$FF ; $55异或$FF = $AA,实现交替 aix #$01 ; 索引加1 cphx #$20 ; 比较是否达到32字节 ($20) bne FillLoop ; 步骤2:设置操作范围 ldhx #$C01F ; 末地址 sthx LADDR ldhx #$C000 ; 首地址 ; 步骤3:调用校验模式 (A != 0) lda #$FF ; 任意非零值,选择校验模式 jsr RDVRRNG ; 步骤4:检查结果 bcc VERIFY_FAILED ; C=0,校验失败 ; C=1,校验成功。A中为校验和,可选择性记录或忽略 bra PROGRAM_OK VERIFY_FAILED: ; 校验失败处理。此时DATA数组中已被替换为实际读出的FLASH值, ; 可以遍历DATA数组,找出哪些地址的数据不匹配。避坑指南二:校验和的用途。
RDVRRNG返回的校验和是所有读取字节的累加和,而非CRC等复杂校验。它主要用于快速检查数据传输过程(在发送模式下)是否有严重错误,不能作为数据绝对正确的唯一依据。在校验模式下,应主要依赖C标志位和DATA数组的比对结果。
3.3 PRGRNGE:FLASH编程的核心引擎
PRGRNGE例程负责将DATA数组中的数据“烧写”进FLASH。这是最常用也最需要小心操作的例程。
核心算法与时序控制: FLASH编程需要精确的高压脉冲时间(tprog,典型值30-40µs)。PRGRNGE内部通过DELNUS延时例程和CPUSPD参数来保证这一点。其编程算法遵循一个多步骤序列(见原文流程图):
- 设置编程位(PGM)。
- 读取FLASH块保护寄存器(FLBPR)——这是一个必要的锁存操作。
- 向目标FLASH地址写入任意数据(触发内部状态机)。
- 等待建立时间(
Tnvs)。 - 使能高压(HVEN)。
- 等待保持时间(
Tpgs)。 - 写入实际数据字节。
- 等待精确的编程时间(
tprog,由CPUSPD决定)。 - 循环7-8步,直到当前行(Row)的所有字节写完。
- 清除PGM位。
- 等待高压保持时间(
Tnvh)。 - 清除HVEN位。
CPUSPD的计算与选择: 这是成功编程的关键。CPUSPD = ceil(fop(MHz) * 2)。你必须根据系统实际运行的总线频率fop来计算。
- 例1:使用内部RC振荡器,频率为4.9152 MHz。
CPUSPD = ceil(4.9152 * 2) = ceil(9.8304) = 10 ($0A)。 - 例2:使用外部8MHz晶体,经过PLL倍频到32MHz,再4分频得到
fop=8MHz。CPUSPD = ceil(8 * 2) = 16 ($10)。
编程范围与DATA数组: 编程范围由H:X(首地址)和LADDR(末地址)定义,不需要对齐到行或页的边界。但DATA数组的大小必须严格等于(LADDR - H:X + 1)。编程数据必须预先存入DATA数组。
示例代码:编程一个完整的64字节页
PRGRNGE equ $1C09 ; 步骤1:填充DATA数组(例如,填充1-64的序列) ldhx #$0000 ; 数组索引 clra ; 从0开始 FillLoop: inca ; A从1递增到64 sta DATA, x ; 存储到DATA数组 aix #$01 ; 索引加1 cphx #$40 ; 比较是否达到64字节 ($40) bne FillLoop ; 步骤2:设置CPU速度参数 (假设fop=2.0MHz) mov #$04, CPUSPD ; CPUSPD = ceil(2.0 * 2) = 4 ; 步骤3:设置编程范围 (页: $C000 - $C03F) ldhx #$C03F ; 末地址 sthx LADDR ldhx #$C000 ; 首地址 ; 步骤4:调用编程例程 jsr PRGRNGE ; 注意:PRGRNGE不返回成功状态!必须后续调用RDVRRNG进行验证。致命陷阱:编程前的擦除检查。
PRGRNGE例程不会检查目标FLASH区域是否已被擦除(即为全$FF)。FLASH编程只能将‘1’变为‘0’,不能将‘0’变回‘1’。如果目标地址原有数据是$00(所有位为0),试图将其编程为$55(01010101) 将会失败,因为‘0’位无法再被改变。因此,在调用PRGRNGE之前,必须确保目标区域已被ERARNGE正确擦除。
3.4 ERARNGE:页擦除与整片擦除
ERARNGE用于擦除FLASH,可以选择擦除一个64字节的页,或者擦除整个FLASH阵列。
操作模式选择: 通过CTRLBYT变量的位6控制:
- 页擦除(Page Erase):
CTRLBYT的位6清零。H:X寄存器只需指向待擦除页内的任意地址即可。 - 整片擦除(Mass Erase):
CTRLBYT的位6置1。H:X寄存器指向FLASH阵列内的任意地址。
擦除保护与安全机制:
- 块保护(Block Protect):如果目标FLASH区域被FLASH块保护寄存器(FLBPR)保护,擦除操作将静默失败。要擦除受保护区域,必须在
IRQ引脚上施加特定的测试高压(Vtst)以旁路保护。这在量产编程器中常见,在用户应用中需谨慎。 - 安全字节(Security Byte):如果FLASH安全校验失败,在正常监控模式下将无法访问FLASH。但文档指出,通过调用
ERARNGE进行整片擦除并将H:X设置为FLBPR的地址,可以覆盖安全机制并擦除整个阵列。这是一个非常重要的后门,但也意味着如果安全字节设置不当,可能导致意外全擦。
示例代码:擦除单个页
ERARNGE equ $1C06 ; 步骤1:设置CPU速度参数 (假设fop=4.9152MHz) mov #$0A, CPUSPD ; CPUSPD = ceil(4.9152 * 2) = 10 ($0A) ; 步骤2:选择页擦除模式 bclr 6, CTRLBYT ; 清除CTRLBYT的位6 ; 步骤3:设置目标页内的一个地址 (例如页 $E100-$E13F) ldhx #$E121 ; 可以是该页内任意地址,如 $E121 ; 步骤4:调用擦除例程 jsr ERARNGE ; 擦除操作需要数毫秒,例程内部会处理延时。3.5 DELNUS:精准延时发生器
DELNUS是一个通用的延时子程序,延时周期数公式为:延时周期 = 3 * A * X + 8。其中A需≥4,X需≥1。它被PRGRNGE和ERARNGE内部用于产生tprog、Terase等关键时序。
独立使用示例:产生一个约100µs的延时(假设fop=4MHz,周期0.25µs)。
DELNUS equ $1C0C ; 目标延时 = 100µs / 0.25µs/周期 = 400周期 ; 公式:400 = 3*A*X + 8 => 3*A*X ≈ 392 ; 取 A=8, X=16,则 3*8*16+8=392 周期 (98µs),接近目标。 lda #$08 ; A=8 ldx #$10 ; X=16 jsr DELNUS ; 实际延时 = 8 + (3*8*16) = 392周期计算技巧:当需要精确延时时,先根据总线频率计算所需周期数,再反推A和X的值。由于A≥4,X≥1,有时无法得到精确值,选择最接近的整数组合即可,微秒级延时对精度要求不苛刻。
4. 实战流程、问题排查与高级技巧
4.1 完整的FLASH操作标准流程
一个健壮的FLASH操作(如固件更新)应遵循以下步骤:
初始化与参数计算:
- 根据系统时钟
fop,计算并设置CPUSPD。 - 初始化
DATA数组指针和其他应用变量。
- 根据系统时钟
擦除目标区域(必须):
- 确定需要擦除的范围(页或整片)。
- 设置
CTRLBYT和H:X,调用ERARNGE。 - (可选但推荐)擦除后,可以读取该区域验证是否为全
$FF。
准备编程数据:
- 将待写入的数据(例如从串口接收、从其他存储器加载或计算生成)填充到零页的
DATA数组中。确保数组大小与编程范围匹配。
- 将待写入的数据(例如从串口接收、从其他存储器加载或计算生成)填充到零页的
编程操作:
- 设置
H:X(首地址)和LADDR(末地址)。 - 调用
PRGRNGE。
- 设置
验证编程结果(强烈推荐):
- 使用
RDVRRNG的校验模式,将DATA数组(仍存放着期望数据)与刚编程的FLASH区域比较。 - 检查返回的C标志位。如果失败,需要分析原因(通常是擦除不彻底或电源不稳)。
- 使用
后续处理:
- 验证成功后,可以跳转到新程序执行,或更新程序指针。
4.2 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 编程/擦除后验证失败 | 1.CPUSPD设置错误。2. 目标区域未正确擦除(非全 $FF)。3. 电源电压不稳或过低。 4. FLASH被保护(FLBPR)。 | 1. 重新计算fop和CPUSPD,确保MCU时钟配置正确。2. 擦除后,先读取FLASH确认是否为 $FF。3. 检查VDD电压,编程/擦除时要求电压在规范范围内(通常4.5V-5.5V)。 4. 检查FLBPR寄存器值,或尝试在 IRQ加Vtst高压旁路保护。 |
| 调用例程后系统死机或复位 | 1. 看门狗(COP)超时。 2. 栈溢出。 3. 中断在关键时序中触发。 | 1.PRGRNGE和ERARNGE会服务COP,但GetByte和DELNUS不会。确保在长循环接收数据时定期服务COP。2. 检查每个例程的栈使用量(见原文表格),确保RAM空间足够,尤其是中断嵌套时。 3. PRGRNGE和ERARNGE会自动置位I位屏蔽中断,但GetByte不会。在调用GetByte前可手动SEI,调用后CLI。 |
DATA数组数据错乱 | 1.DATA数组跨越零页边界。2. 数组大小与编程/校验范围不匹配。 3. 在填充数组时索引计算错误。 | 1. 确保DATA数组定义在$004C起始,且大小不超过($0100 - $004C)。2. 编程前,计算 LADDR - H:X + 1,确保与填充的数据量一致。3. 使用仿真器或调试器,单步跟踪查看 DATA区域在调用例程前后的内存内容。 |
| 整片擦除后程序丢失 | 意外执行了整片擦除。 | 1. 检查代码中CTRLBYT位6的设置逻辑,确保只有在明确意图时才置位。2.关键保护:将设置 CTRLBYT和调用ERARNGE的代码放在独立的、不易被意外执行的函数中,甚至添加软件开关(如特定密码序列)。 |
| 通信(GetByte/PutByte)失败 | 1. PTA0引脚未上拉。 2. 波特率计算错误。 3. 发送/接收双方帧格式不匹配。 | 1. 确认硬件上PTA0有上拉电阻(通常10kΩ)。 2. 根据芯片型号(GZ/GR)和 fop精确计算波特率,并与主机匹配。3. 确保使用8位数据位、无校验、1位停止位(8N1)的NRZ格式。 |
4.3 高级技巧与优化建议
批量编程优化:
PRGRNGE支持连续编程跨行数据。最有效的方式是每次编程一整行(32字节)或一整页(64字节),这样可以减少行间高压开关的次数。对于大数据块,应组织数据以行/页为边界进行分批操作。CPUSPD的动态计算:如果你的系统频率可变(例如有省电模式),可以在调用FLASH例程前,动态计算CPUSPD。可以将fop与一个常量表进行比较,或者使用简单的计算指令(如ASL左移一位相当于乘2,再处理进位)。安全升级设计:实现一个Bootloader时,采用“A/B备份”或“黄金镜像”策略。将FLASH划分为引导区、主程序区A、主程序区B。Bootloader始终从引导区运行,负责验证和跳转到有效的程序区。新固件下载到空闲区,验证通过后再擦除旧区并切换指针。这样即使升级中途断电,也至少有一个可启动的版本。
利用校验和进行快速完整性检查:在将数据写入
DATA数组前,先计算其校验和并存储。在调用PRGRNGE编程后,再用RDVRRNG的发送模式读回数据,计算读回数据的校验和进行比对。这比逐字节验证更快,可用于快速判断大块数据是否整体写入成功(尽管不能定位单个错误)。调试辅助:监控FLASH控制寄存器(FLCR)。在调试异常问题时,可以在调用例程前后读取FLCR寄存器(地址
$FE08)。观察PGM、HVEN、ERASE等位的状态变化,可以帮助判断例程是否正常执行了关键步骤。
最后需要强调的是,这些ROM例程是芯片厂商提供的宝贵资产,但它们运行在用户模式下,对中断和时序有严格要求。在实际项目中,尤其是在干扰较大的工业环境,务必确保电源质量,并在关键FLASH操作期间关闭不必要的全局中断,做好异常处理,防止程序跑飞导致FLASH被意外修改。通过深入理解本文剖析的每个细节,你应当能自信地在MC68HC908GR/GZ平台上驾驭片上FLASH,构建出稳定可靠的嵌入式系统。
