1. 项目概述深入MC68HC912的非易失性存储器操作在嵌入式开发的底层世界里直接与微控制器MCU的Flash和EEPROM存储器打交道是每个资深工程师必须掌握的硬核技能。这不仅仅是调用一个库函数那么简单它关乎到你的固件能否被正确烧录、设备参数能否在断电后幸存以及整个系统长期运行的可靠性。我接触过不少项目因为对这部分理解不透彻导致产品在现场出现莫名其妙的“失忆”或者根本无法启动排查起来极其痛苦。今天我们就以Freescale现NXP经典的HC12家族成员——MC68HC912DT128A/DG128A为例彻底拆解其Flash和EEPROM编程擦除的汇编级实现。这份来自原厂的应用笔记代码就像一份珍贵的“武功秘籍”直接展示了如何通过最底层的寄存器操作和精确到微秒级的延时控制来驯服这些非易失性存储器。我们将聚焦于两个核心算法用于Flash的SSTSuperFlash编程算法和用于EEPROM的AUTO模式操作。理解这些代码不仅能让你在HC12平台上游刃有余其背后关于时序、电压控制、总线操作的思想对于理解其他架构的MCU存储控制器也大有裨益。2. 核心硬件与原理深度解析在动手写代码之前我们必须先搞清楚MC68HC912DT128A/DG128A内部存储器的“脾气”。这块芯片的Flash和EEPROM并非简单的存储阵列它们内部集成了一套需要特定“仪式”才能激活的编程/擦除电路。2.1 Flash存储器结构与SST算法本质MC68HC912的Flash模块组织为页Page和行Row。根据数据手册一次擦除的最小单位通常是一个页例如2KB或4KB而编程的最小单位是一个行在提供的代码中一行是64字节32个字。这里有个关键点Flash的编程只能将位从‘1’变为‘0’而擦除操作则是将整个页的位全部恢复为‘1’。因此标准的操作流程永远是“先擦后写”。SST算法全称可能是“SuperFlash”或类似专有技术其核心目标是在满足芯片严格时序要求的前提下最小化编程一个行所需的总时间。它并不是一个复杂的软件协议而是一系列精心编排的硬件寄存器操作和精确延时。算法的精髓在于对几个关键控制位的顺序操作PGM/ERAS位告诉Flash控制器“我要开始编程或擦除了”。HVEN位控制内部高压泵的开关。给浮栅晶体管注入或移除电荷需要高于VDD的电压这个高压就是由HVEN使能后内部生成的。向特定地址的写入操作这并非写入数据而是向地址线发送一个特定的“命令”触发内部状态机的下一步动作。代码中所有的延时如tNVS、tERAS、tPGS都是芯片数据手册中规定的、必须满足的最小时间间隔。例如tNVS是“命令建立时间”tERAS是“擦除脉冲宽度”。不满足这些时序轻则编程失败重则可能损伤存储单元。2.2 EEPROM与AUTO模式的便利性相比之下EEPROM的操作对开发者友好得多。它支持字节/字编程并且通常不需要先擦除整个大块尽管擦除操作依然存在。MC68HC912的EEPROM控制器提供了一个非常方便的AUTO模式。在AUTO模式下你只需要做三件事配置EEPROG寄存器一次性设置好操作模式擦除/编程、操作粒度BULK/ROW/WORD并拉高EELAT锁存地址和AUTO位。向目标地址执行一次普通的写操作这实际上将数据和地址锁存。拉高EEPGM位启动自动过程。之后硬件会自动接管按照内置的时序发生器完成高压产生、脉冲施加、验证等所有步骤并在完成后自动清除EEPGM位。开发者只需轮询EEPGM位等待其变低即可。这大大简化了软件负担也降低了因软件延时不准而导致操作失败的风险。代码中需要配置EEDIV寄存器就是为了设置这个内部时序发生器的时钟分频以确保其时间基准符合芯片要求例如代码注释中提到的35us。关键原理提示无论是Flash的SST算法还是EEPROM的AUTO模式其底层物理过程都是通过 Fowler-Nordheim 隧穿或热电子注入等方式改变浮栅晶体管的阈值电压从而表示‘0’或‘1’。软件代码的本质是严格按照时序要求为这一物理过程提供正确的电信号控制序列。3. SST Flash编程算法汇编代码逐行精讲提供的SSTflash.mrt和SSTflash.srt文件构成了完整的Flash操作范例。我们跳过文件头部的版权信息直接切入核心。3.1 主程序SSTflash.mrt流程搭建与数据准备主程序位于SSTflash.mrt中它清晰地展示了使用SST算法对Flash进行编程的标准工作流。org RAM$50 ;代码起始地址避开RAM底部用作缓冲区 Start: lds #$4000 ;设置堆栈指针 movb #$8F,COPCTL ;使能时钟监控功能开头是标准的初始化设置堆栈、配置看门狗COPCTL。这里有个细节使能时钟监控Clock Monitor在Flash操作期间尤为重要。因为编程和擦除对时钟稳定性有要求如果时钟丢失监控电路可以触发复位防止芯片执行在异常时钟下的错误操作这可能损坏Flash。ldx #$00 ldaa #$1 Data_load: staa DATA,x ;向RAM缓冲区DATA填充数据01,02,03... inca inx cmpa #!65 ;注意这里是65因为填充了1到64 bne Data_load这段代码在RAM中准备要编程的数据。DATA是在SSTflash.var文件中定义的一个64字节缓冲区。这里它被填充为1到64的序列值。在实际项目中这里应该是你的应用程序数据比如固件代码段、配置参数等。movb #$01,PPAGE ;选择PPAGE1即Flash的页1 bclr FEEMCR,BOOTP. ;如果此页中的引导块被保护清除BOOTP位 ldx #$8002 ;加载页内任意一个对齐的字地址 jsr FlashErase ;调用子程序擦除整个选定页接下来是擦除操作。首先通过PPAGE寄存器选择要操作的Flash页该芯片采用分页机制扩展地址空间。FEEMCR是Flash和EEPROM控制寄存器BOOTP位用于保护引导块在擦除前需要确保它被清除如果该页包含引导块且被保护则无法擦除。然后将页内的一个字对齐地址如$8002加载到X寄存器并调用FlashErase子程序。为什么是任意地址因为擦除是以页为单位的只要地址落在目标页内即可硬件不关心具体的偏移。ldx #$BF40 ;加载编程行的起始地址必须是$xx00, $xx40, $xx80, $xxC0之一 ldy #DATA ;加载RAM缓冲区起始地址 jsr ProgRow ;调用子程序编程一行32个字擦除完成后进行编程。这里有一个关键约束行起始地址必须是64字节对齐的即末两位十六进制为004080C0。代码中$BF40符合要求。Y寄存器指向准备好的数据缓冲区。调用ProgRow完成一行数据的编程。Verify: movb #!32,COUNTER ;一行有32个字 ldy #DATA ldx #$BF40 Verify_Loop: ldd ,X ;从Flash地址读取一个字 cpd ,Y ;与RAM缓冲区中的数据比较 bne Error ;不相等则跳转到错误处理 dec COUNTER beq Success inx inx ;X加2指向下一个字地址 iny iny ;Y加2指向下一个数据 bra Verify_Loop Success: bra * ;编程成功原地循环 Error: bra * ;编程失败原地循环实际应跳转到错误处理程序编程后的验证环节至关重要。代码逐字比较Flash中读出的数据和RAM缓冲区中的原始数据。任何不匹配都会跳转到错误处理。在实际产品代码中Success和Error标签后绝不能是简单的bra *死循环而应该返回成功/错误标志或者触发系统复位/告警。3.2 核心子程序SSTflash.srt时序的精确舞蹈真正的魔法发生在SSTflash.srt中。我们以FlashErase页擦除子程序为例看它如何与硬件共舞。FlashErase: sei ;步骤1 - 禁止可屏蔽中断 bset FEECTL,ERAS. ;步骤1 - 设置ERAS位 std ,X ;步骤2 - 向Flash地址X指向写入任意对齐字数据第一步就拉高了中断屏蔽位SEI。这是强制要求因为在接下来的精确延时过程中任何中断的插入都会破坏时序导致擦除失败。FEECTL是Flash和EEPROM控制寄存器设置ERAS位表明这是一次擦除操作。随后向目标地址X寄存器指向的地址执行一次写操作std ,X。注意这里写入的数据D寄存器的值是无关紧要的这次写入的作用是向Flash控制器提交“命令”触发内部状态机进入擦除序列。ldaa #$1B ;步骤3 - 等待tNVS时间10.25us dbne A,* ; 1 (3 x 27) 82个周期 8MHz bset FEECTL,HVEN. ;步骤4 - 设置HVEN位开启高压步骤3是一个精确的软件延时循环等待tNVS命令建立时间。dbne A,*指令会循环递减A寄存器直到为0。计算如下dbne指令本身不跳转时1个周期跳转时3个周期。循环27次#$1B是27总周期数 1 3 * 27 82个周期。在8MHz总线频率下周期125ns82 * 125ns 10.25us正好满足tNVS要求。之后才允许开启高压HVEN。movb #!8,TIMES ;步骤5 - 等待tERAS时间8.0ms jsr ms_delay bclr FEECTL,ERAS. ;步骤6 - 清除ERAS位步骤5等待主要的擦除脉冲时间tERAS长达8ms。这里调用了ms_delay子程序通过传入TIMES8来实现8ms延时。这个延时必须足够它是电荷从浮栅移走的关键时间。之后清除ERAS位。ldd #$010B ;步骤7 - 等待tNVHL时间100.25us dbne D,* ; 1 (3 x 267) 802 cycles bclr FEECTL,HVEN. ;步骤8 - 清除HVEN位关闭高压步骤7等待tNVHL高压关闭后到下一次操作前的恢复时间。这里用了双字节循环dbne D,*循环次数为$010B267次产生约100us的延时。然后才能安全地关闭高压。ldaa #$03 ;步骤9 - 等待tRCV时间1.25us dbne A,* cli ;重新使能中断 FlashErase_End: rts最后等待一个很短的恢复时间tRCV然后重新打开中断CLI子程序返回。整个流程就像一场精心编排的芭蕾每一步的顺序和节奏都至关重要。ProgRow行编程子程序的结构与此类似但更复杂一些因为它需要在开启高压后循环写入64字节数据并且要保证每个字写入的间隔时间tFPGM约30us在规定的30-40us窗口内。代码中通过一个包含movw和特定延时循环的Copy_Loop来实现这一点。3.3 延时子程序ms_delay周期级精度ms_delay是生成毫秒级延时的通用子程序。其精妙之处在于循环次数的计算以确保在8MHz下精确产生TIMES毫秒的延时。代码注释中已经给出了计算公式延时 [{1 (2 3) * 1597 2 1 3 1 4 3} * (TIMES - 1) {1 (2 3) * 1597 2 1 3 1 4 1 5}] / 8MHz (8000 * TIMES 3) / 8MHz这体现了在资源受限的嵌入式环境中如何用汇编语言实现精确计时。在实际使用中如果总线频率不是8MHz你必须根据公式重新计算循环常数否则所有时序都会错乱。4. EEPROM AUTO模式汇编代码详解EEPROM的AUTO模式代码位于AutoEEPROM.mrt和AutoEEPROM.srt中其思路与Flash不同更侧重于寄存器的正确配置。4.1 主程序AutoEEPROM.mrt配置与调用movw #$0230,EEDIVH ;振荡器频率16MHz时设置EEDIV为$0230以获得35us时基 movw #$55AA,DATA ;将要编程的数据字$55AA存入RAM缓冲区 ldx #$0E02 ;加载要对齐的字地址 bclr EEPROT,BPROT3. ;解除目标块的保护 ldaa #auto_WORDerase. ;选择字擦除模式AUTO, BYTE, ERASE, EELAT jsr AutoRoutine ;调用AUTO模式例程执行擦除 ldaa #auto_WORDprogram. ;选择字编程模式AUTO, EELAT jsr AutoRoutine ;调用AUTO模式例程执行编程 bset EEPROT,BPROT3. ;重新保护已编程的块主程序流程非常清晰配置时钟分频器EEDIV这是AUTO模式正常工作的关键。代码中根据16MHz振荡器频率计算并设置EEDIVH:EEDIVL为$0230以确保内部编程/擦除时序的时间基准为35us。如果你用的晶振不是16MHz这个值必须重算计算公式参考芯片数据手册。准备数据与地址将想要写入的数据例如$55AA存入DATA变量2字节并将目标对齐字地址加载到X寄存器。解除块保护通过清除EEPROT寄存器相应的BPROT位允许对目标EEPROM区域进行操作。这是一个重要的安全措施防止意外写操作破坏关键数据。执行擦除与编程分别加载擦除和编程的模式字到A寄存器并调用同一个AutoRoutine子程序。模式字是在文件开头用equ定义的常量它们组合了EEPROG寄存器的各个控制位AUTO,EELAT,ERASE,BYTE等。4.2 核心子程序AutoRoutineAUTO模式的执行引擎AutoRoutine子程序是AUTO模式的核心它极其简洁因为复杂的工作都交给了硬件。AutoRoutine: sei ;步骤1 - 禁止中断 staa EEPROG ;步骤1 - 将模式字A寄存器写入EEPROG寄存器 ldd DATA ;步骤2 - 从RAM缓冲区取数据 std ,X ;步骤2 - 写入X指向的地址锁存地址和数据 bset EEPROG,EEPGM. ;步骤3 - 设置EEPGM位启动自动过程 Clear_EEPGM: brset EEPROG,EEPGM.,Clear_EEPGM ;步骤4 - 轮询等待EEPGM位自动清除 bclr EEPROG,EELAT. ;步骤5 - 清除EELAT位 movb #$80,EEPROG ;向EEPROG写入复位值可选良好习惯 cli ;重新使能中断 AutoRoutine_End: rts流程解读禁止中断同样是为了防止时序被打断。配置并启动staa EEPROG一举两得。一方面它根据调用者传入的模式字A寄存器配置了操作类型擦除/编程、块/行/字另一方面该指令本身也设置了AUTO和EELAT位使EEPROM控制器进入“地址/数据锁存”状态。触发操作紧接着的std ,X执行了一次向目标地址的写操作。在EELAT有效的情况下这个写操作并不会立即改变存储单元而是将目标地址和要写入的数据来自DATA锁存到EEPROM控制器内部。启动自动序列bset EEPROG,EEPGM.置位EEPGM位。这个信号告诉硬件“地址和数据都已就绪开始自动执行编程或擦除流程吧”。此后硬件会内部使能高压、施加脉冲、进行验证全部完成后自动清除EEPGM位。等待完成brset指令循环检测EEPGM位直到硬件将其清除表明操作完成。清理现场清除EELAT位退出锁存状态。将EEPROG恢复为复位值$80这是一个好习惯确保寄存器处于已知状态。最后重新使能中断并返回。AUTO模式的优势与陷阱优势是简单可靠硬件保证了时序。但陷阱在于EEDIV的配置和块保护状态。代码注释中特别用CAUTION标出如果EEDIV被误设为00:00或者目标地址处于保护状态批量擦除除外硬件将不会自动清除EEPGM位导致软件在此处无限循环。因此在调用AutoRoutine前必须确保EEDIV值正确且目标区域未受保护。5. 工程实践从示例代码到产品级应用原厂示例代码提供了一个可靠的起点但直接照搬到产品中是不够的。下面分享一些我将这些代码投入实际项目时的经验和关键修改点。5.1 关键参数适配与计算首先总线频率是生命线。示例代码所有延时都基于8MHz总线频率计算。你的系统频率是多少12MHz16MHz还是内部RC振荡器必须根据实际频率重新计算所有延时循环常数和EEDIV值。对于Flash的SST算法你需要根据数据手册中的tNVS、tERAS、tPGS、tFPGM、tNVHL、tRCV等时间参数以及你的总线周期重新计算dbne循环的次数。公式是所需周期数 时间要求 / 总线周期时间。ms_delay子程序也需要调整其核心循环常数代码中的#063E以确保1ms延时的准确性。对于EEPROM的AUTO模式关键是计算EEDIV。公式通常为EEDIV (fOSC * tBASE / 128) - 1其中fOSC是振荡器频率tBASE是芯片要求的基础时间单位如35us。具体公式请查阅最新的数据手册。5.2 增强鲁棒性错误处理与状态检查示例代码中的错误处理几乎是空白Error: bra *。在产品中我们必须做得更好。对于Flash操作验证失败处理Verify循环发现错误后不应死循环。可以重试几次例如3次如果仍然失败则记录错误码到特定RAM区域或备份寄存器并触发系统复位或进入安全模式。编程前检查在调用ProgRow前可以增加对目标地址区域的读取检查确保该区域已被擦除全为$FFFF。如果未擦除就编程会导致数据错误。超时机制虽然SST算法是硬延时但可以在外层调用时加入软件看门狗WDOG刷新防止程序跑飞卡死在某个循环中。对于EEPROM的AUTO模式EEPGM超时检测brset EEPROG,EEPGM.,Clear_EEPGM这个轮询循环必须加入超时机制。例如循环计数达到一个最大值对应远大于最大编程/擦除时间后如果EEPGM仍未清除则判定为操作失败执行错误恢复。ldy #TIMEOUT_VALUE Poll_EEPGM: dbne Y, NotTimeout ; 超时处理记录错误清除EEPGM/EELAT退出 bra EEPROM_Error NotTimeout: brset EEPROG,EEPGM.,Poll_EEPGM操作保护检查在尝试编程/擦除前读取EEPROT寄存器确认目标区域是否真的被解锁。这是一个防御性编程的好习惯。5.3 代码封装与可移植性为了在不同项目间复用应将Flash和EEPROM操作封装成独立的、带明确接口的函数。Flash驱动接口建议// 伪代码示例 typedef enum { FLASH_RESULT_OK, FLASH_RESULT_NOT_ERASED, FLASH_RESULT_VERIFY_FAIL, FLASH_RESULT_TIMEOUT, FLASH_RESULT_WRITE_PROTECTED } FlashResult_t; FlashResult_t Flash_ErasePage(uint16_t page_number); FlashResult_t Flash_ProgramRow(uint16_t row_address, const uint8_t *data); uint16_t Flash_CalculateChecksum(uint32_t start_addr, uint32_t length);对应的汇编函数应接受参数返回状态并且内部集成完整的擦除、编程、验证、错误处理流程。EEPROM驱动接口建议typedef enum { EEPROM_RESULT_OK, EEPROM_RESULT_PROTECTED, EEPROM_RESULT_TIMEOUT, EEPROM_RESULT_INVALID_PARAM } EEPROM_Result_t; EEPROM_Result_t EEPROM_WriteWord(uint16_t address, uint16_t data); EEPROM_Result_t EEPROM_EraseWord(uint16_t address); EEPROM_Result_t EEPROM_EraseRow(uint16_t address); // 注意EEPROM通常不需要先擦后写写操作会自动处理封装时注意将EEDIV的初始化、保护位的管理等内容也集成进去提供一个EEPROM_Init()函数。5.4 实际项目中的避坑指南中断与低功耗模式Flash/EEPROM操作期间必须禁止中断。此外也要避免进入STOP或WAIT等低功耗模式因为内部高压泵可能需要活跃的系统时钟。电压稳定性编程和擦除对供电电压(VDD)有要求。确保在操作期间电压在芯片规格书规定的范围内。在电池供电设备中如果电压过低应禁止存储操作。代码自身在Flash中的执行当你要擦写当前代码所在的Flash页时比如IAP在应用编程情况变得复杂。你必须将擦写函数和相关的数据缓冲区完整地复制到RAM中执行。因为Flash在擦写期间该页是无法读取指令的。这需要精心设计链接脚本和启动代码。EEPROM的耐久性与数据管理EEPROM有擦写次数限制通常10万-100万次。避免频繁写入同一地址。对于频繁更新的数据如运行时间计数器应采用磨损均衡算法或者配合FRAM铁电存储器使用。使用调试器在开发阶段利用调试器如PE Multilink的单步、断点功能结合示波器测量代码中注释提到的“测量点”如Port A bit 0可以直观地验证延时时序是否准确。这是调试Flash/EEPROM驱动最有效的方法。6. 常见问题与调试技巧实录即使理解了原理和代码第一次上手调试也难免踩坑。下面是我在实际项目中遇到的一些典型问题及解决方法。6.1 Flash编程验证失败现象ProgRow执行后验证循环Verify_Loop失败读回的数据与写入的不符。排查思路检查擦除是否成功在编程之前先读取目标行地址的数据。擦除后的Flash位应全为‘1’即读取值为$FFFF。如果发现不是$FFFF说明擦除步骤就失败了。重点检查FlashErase子程序传入的地址X是否在目标页内且字对齐PPAGE寄存器设置是否正确FEEMCR寄存器的BOOTP位是否已清除如果该页包含引导块用示波器或调试器检查ms_delay子程序是否真的产生了8ms延时总线频率设置对吗检查编程时序如果擦除成功但编程失败问题很可能在ProgRow的tFPGM时序上。tFPGM是连续两个字编程操作之间的时间必须在30-40us之间。代码中使用了一个ldaa #$4C的循环来产生约28.625us的延时加上循环外的指令总时间应在要求范围内。如果总线频率不是8MHz这个循环常数必须调整。使用调试器在Copy_Loop内设置断点或使用IO口翻转并配合示波器测量两次movw指令之间的实际时间。检查数据对齐与缓冲区确保Y寄存器指向的RAM缓冲区DATA确实包含了你想编程的64字节数据并且没有其他代码意外修改了这块内存。确保X寄存器指向的行起始地址是64字节对齐的末两位hex为004080C0。6.2 EEPROM的AUTO模式卡死现象程序执行到AutoRoutine中的brset EEPROG,EEPGM.,Clear_EEPGM处无限循环EEPGM位始终为1。排查步骤检查EEDIV寄存器这是最常见的原因。确认EEDIVH:EEDIVL的值是根据你的实际振荡器频率计算得出的。如果误设为0000硬件时序发生器不工作EEPGM位永远不会被清除。计算EEDIV是第一步也是必须做对的一步。检查块保护状态确认在调用AutoRoutine前已经通过bclr EEPROT, BPROTx.正确解除了目标地址所在块的保护。如果地址被保护对于非批量擦除的操作EEPGM位也不会被自动清除。可以尝试先执行一次批量擦除如果支持看是否能通过来帮助判断是否是保护问题。检查电源电压EEPROM编程/擦除对VDD电压有最低要求。如果电压过低操作可能无法完成导致EEPGM位挂起。测量系统电压是否在芯片工作范围内。加入超时跳出如前所述在产品代码中一定要为这个轮询循环加入超时机制。超时后应执行错误恢复先尝试清除EEPGM和EELAT位向EEPROG写入$00或复位值$80然后报告错误。这可以防止系统永久死锁。6.3 代码在RAM中运行的问题现象当尝试擦写当前代码所在的Flash页时程序跑飞或进入异常状态。解决方案链接脚本配置在编译器/汇编器的链接脚本.lcf, .ld文件中明确将Flash驱动函数FlashErase,ProgRow,ms_delay以及其使用的变量DATA,COUNTER,TIMES分配到RAM区域的一个固定段。启动代码复制在main函数开始前在启动代码中将这些函数和变量的二进制代码从Flash例如放在一个特殊的、不会被擦除的“引导加载程序”区域复制到RAM中指定的地址。函数指针调用在C语言中你可以将这些RAM中的函数地址赋值给函数指针然后通过函数指针来调用。在汇编中你需要知道这些例程在RAM中的确切入口地址并使用jsr跳转到那个地址。最小化RAM代码只将绝对必要的代码擦除、编程、关键延时循环复制到RAM。像数据准备、验证等逻辑可以留在Flash中。这个过程比较繁琐但却是实现固件自更新IAP功能的基础。建议在项目初期就规划好内存布局。6.4 时序测量与验证技巧在没有逻辑分析仪的情况下如何验证延时是否准确IO口调试法代码中已经给出了范例注释掉的bset/bclr PORTA,PA0.。你可以取消这些注释将一个IO口如PORTA0配置为输出。在延时循环的开始将其拉低在结束时拉高。然后用示波器测量这个低电平脉冲的宽度即可得到实际延时时间。这是调试底层驱动最直观、最可靠的方法。软件计数法在已知频率下用一个全局变量在延时函数中计数。在main函数中运行这个延时一段时间比如1秒然后通过调试器查看这个变量的值可以反推延时函数的准确性。最后请务必、务必、务必仔细阅读你所使用的MC68HC912DT128A/DG128A芯片最新版本的数据手册Data Sheet和技术参考手册Technical Reference Manual。应用笔记Application Note中的代码是基于某个特定版本的芯片和工具链而数据手册中的参数和约束才是永恒的真理。任何与手册描述不符的操作都可能带来不可预知的风险。
MC68HC912 Flash与EEPROM底层编程:SST算法与AUTO模式详解
1. 项目概述深入MC68HC912的非易失性存储器操作在嵌入式开发的底层世界里直接与微控制器MCU的Flash和EEPROM存储器打交道是每个资深工程师必须掌握的硬核技能。这不仅仅是调用一个库函数那么简单它关乎到你的固件能否被正确烧录、设备参数能否在断电后幸存以及整个系统长期运行的可靠性。我接触过不少项目因为对这部分理解不透彻导致产品在现场出现莫名其妙的“失忆”或者根本无法启动排查起来极其痛苦。今天我们就以Freescale现NXP经典的HC12家族成员——MC68HC912DT128A/DG128A为例彻底拆解其Flash和EEPROM编程擦除的汇编级实现。这份来自原厂的应用笔记代码就像一份珍贵的“武功秘籍”直接展示了如何通过最底层的寄存器操作和精确到微秒级的延时控制来驯服这些非易失性存储器。我们将聚焦于两个核心算法用于Flash的SSTSuperFlash编程算法和用于EEPROM的AUTO模式操作。理解这些代码不仅能让你在HC12平台上游刃有余其背后关于时序、电压控制、总线操作的思想对于理解其他架构的MCU存储控制器也大有裨益。2. 核心硬件与原理深度解析在动手写代码之前我们必须先搞清楚MC68HC912DT128A/DG128A内部存储器的“脾气”。这块芯片的Flash和EEPROM并非简单的存储阵列它们内部集成了一套需要特定“仪式”才能激活的编程/擦除电路。2.1 Flash存储器结构与SST算法本质MC68HC912的Flash模块组织为页Page和行Row。根据数据手册一次擦除的最小单位通常是一个页例如2KB或4KB而编程的最小单位是一个行在提供的代码中一行是64字节32个字。这里有个关键点Flash的编程只能将位从‘1’变为‘0’而擦除操作则是将整个页的位全部恢复为‘1’。因此标准的操作流程永远是“先擦后写”。SST算法全称可能是“SuperFlash”或类似专有技术其核心目标是在满足芯片严格时序要求的前提下最小化编程一个行所需的总时间。它并不是一个复杂的软件协议而是一系列精心编排的硬件寄存器操作和精确延时。算法的精髓在于对几个关键控制位的顺序操作PGM/ERAS位告诉Flash控制器“我要开始编程或擦除了”。HVEN位控制内部高压泵的开关。给浮栅晶体管注入或移除电荷需要高于VDD的电压这个高压就是由HVEN使能后内部生成的。向特定地址的写入操作这并非写入数据而是向地址线发送一个特定的“命令”触发内部状态机的下一步动作。代码中所有的延时如tNVS、tERAS、tPGS都是芯片数据手册中规定的、必须满足的最小时间间隔。例如tNVS是“命令建立时间”tERAS是“擦除脉冲宽度”。不满足这些时序轻则编程失败重则可能损伤存储单元。2.2 EEPROM与AUTO模式的便利性相比之下EEPROM的操作对开发者友好得多。它支持字节/字编程并且通常不需要先擦除整个大块尽管擦除操作依然存在。MC68HC912的EEPROM控制器提供了一个非常方便的AUTO模式。在AUTO模式下你只需要做三件事配置EEPROG寄存器一次性设置好操作模式擦除/编程、操作粒度BULK/ROW/WORD并拉高EELAT锁存地址和AUTO位。向目标地址执行一次普通的写操作这实际上将数据和地址锁存。拉高EEPGM位启动自动过程。之后硬件会自动接管按照内置的时序发生器完成高压产生、脉冲施加、验证等所有步骤并在完成后自动清除EEPGM位。开发者只需轮询EEPGM位等待其变低即可。这大大简化了软件负担也降低了因软件延时不准而导致操作失败的风险。代码中需要配置EEDIV寄存器就是为了设置这个内部时序发生器的时钟分频以确保其时间基准符合芯片要求例如代码注释中提到的35us。关键原理提示无论是Flash的SST算法还是EEPROM的AUTO模式其底层物理过程都是通过 Fowler-Nordheim 隧穿或热电子注入等方式改变浮栅晶体管的阈值电压从而表示‘0’或‘1’。软件代码的本质是严格按照时序要求为这一物理过程提供正确的电信号控制序列。3. SST Flash编程算法汇编代码逐行精讲提供的SSTflash.mrt和SSTflash.srt文件构成了完整的Flash操作范例。我们跳过文件头部的版权信息直接切入核心。3.1 主程序SSTflash.mrt流程搭建与数据准备主程序位于SSTflash.mrt中它清晰地展示了使用SST算法对Flash进行编程的标准工作流。org RAM$50 ;代码起始地址避开RAM底部用作缓冲区 Start: lds #$4000 ;设置堆栈指针 movb #$8F,COPCTL ;使能时钟监控功能开头是标准的初始化设置堆栈、配置看门狗COPCTL。这里有个细节使能时钟监控Clock Monitor在Flash操作期间尤为重要。因为编程和擦除对时钟稳定性有要求如果时钟丢失监控电路可以触发复位防止芯片执行在异常时钟下的错误操作这可能损坏Flash。ldx #$00 ldaa #$1 Data_load: staa DATA,x ;向RAM缓冲区DATA填充数据01,02,03... inca inx cmpa #!65 ;注意这里是65因为填充了1到64 bne Data_load这段代码在RAM中准备要编程的数据。DATA是在SSTflash.var文件中定义的一个64字节缓冲区。这里它被填充为1到64的序列值。在实际项目中这里应该是你的应用程序数据比如固件代码段、配置参数等。movb #$01,PPAGE ;选择PPAGE1即Flash的页1 bclr FEEMCR,BOOTP. ;如果此页中的引导块被保护清除BOOTP位 ldx #$8002 ;加载页内任意一个对齐的字地址 jsr FlashErase ;调用子程序擦除整个选定页接下来是擦除操作。首先通过PPAGE寄存器选择要操作的Flash页该芯片采用分页机制扩展地址空间。FEEMCR是Flash和EEPROM控制寄存器BOOTP位用于保护引导块在擦除前需要确保它被清除如果该页包含引导块且被保护则无法擦除。然后将页内的一个字对齐地址如$8002加载到X寄存器并调用FlashErase子程序。为什么是任意地址因为擦除是以页为单位的只要地址落在目标页内即可硬件不关心具体的偏移。ldx #$BF40 ;加载编程行的起始地址必须是$xx00, $xx40, $xx80, $xxC0之一 ldy #DATA ;加载RAM缓冲区起始地址 jsr ProgRow ;调用子程序编程一行32个字擦除完成后进行编程。这里有一个关键约束行起始地址必须是64字节对齐的即末两位十六进制为004080C0。代码中$BF40符合要求。Y寄存器指向准备好的数据缓冲区。调用ProgRow完成一行数据的编程。Verify: movb #!32,COUNTER ;一行有32个字 ldy #DATA ldx #$BF40 Verify_Loop: ldd ,X ;从Flash地址读取一个字 cpd ,Y ;与RAM缓冲区中的数据比较 bne Error ;不相等则跳转到错误处理 dec COUNTER beq Success inx inx ;X加2指向下一个字地址 iny iny ;Y加2指向下一个数据 bra Verify_Loop Success: bra * ;编程成功原地循环 Error: bra * ;编程失败原地循环实际应跳转到错误处理程序编程后的验证环节至关重要。代码逐字比较Flash中读出的数据和RAM缓冲区中的原始数据。任何不匹配都会跳转到错误处理。在实际产品代码中Success和Error标签后绝不能是简单的bra *死循环而应该返回成功/错误标志或者触发系统复位/告警。3.2 核心子程序SSTflash.srt时序的精确舞蹈真正的魔法发生在SSTflash.srt中。我们以FlashErase页擦除子程序为例看它如何与硬件共舞。FlashErase: sei ;步骤1 - 禁止可屏蔽中断 bset FEECTL,ERAS. ;步骤1 - 设置ERAS位 std ,X ;步骤2 - 向Flash地址X指向写入任意对齐字数据第一步就拉高了中断屏蔽位SEI。这是强制要求因为在接下来的精确延时过程中任何中断的插入都会破坏时序导致擦除失败。FEECTL是Flash和EEPROM控制寄存器设置ERAS位表明这是一次擦除操作。随后向目标地址X寄存器指向的地址执行一次写操作std ,X。注意这里写入的数据D寄存器的值是无关紧要的这次写入的作用是向Flash控制器提交“命令”触发内部状态机进入擦除序列。ldaa #$1B ;步骤3 - 等待tNVS时间10.25us dbne A,* ; 1 (3 x 27) 82个周期 8MHz bset FEECTL,HVEN. ;步骤4 - 设置HVEN位开启高压步骤3是一个精确的软件延时循环等待tNVS命令建立时间。dbne A,*指令会循环递减A寄存器直到为0。计算如下dbne指令本身不跳转时1个周期跳转时3个周期。循环27次#$1B是27总周期数 1 3 * 27 82个周期。在8MHz总线频率下周期125ns82 * 125ns 10.25us正好满足tNVS要求。之后才允许开启高压HVEN。movb #!8,TIMES ;步骤5 - 等待tERAS时间8.0ms jsr ms_delay bclr FEECTL,ERAS. ;步骤6 - 清除ERAS位步骤5等待主要的擦除脉冲时间tERAS长达8ms。这里调用了ms_delay子程序通过传入TIMES8来实现8ms延时。这个延时必须足够它是电荷从浮栅移走的关键时间。之后清除ERAS位。ldd #$010B ;步骤7 - 等待tNVHL时间100.25us dbne D,* ; 1 (3 x 267) 802 cycles bclr FEECTL,HVEN. ;步骤8 - 清除HVEN位关闭高压步骤7等待tNVHL高压关闭后到下一次操作前的恢复时间。这里用了双字节循环dbne D,*循环次数为$010B267次产生约100us的延时。然后才能安全地关闭高压。ldaa #$03 ;步骤9 - 等待tRCV时间1.25us dbne A,* cli ;重新使能中断 FlashErase_End: rts最后等待一个很短的恢复时间tRCV然后重新打开中断CLI子程序返回。整个流程就像一场精心编排的芭蕾每一步的顺序和节奏都至关重要。ProgRow行编程子程序的结构与此类似但更复杂一些因为它需要在开启高压后循环写入64字节数据并且要保证每个字写入的间隔时间tFPGM约30us在规定的30-40us窗口内。代码中通过一个包含movw和特定延时循环的Copy_Loop来实现这一点。3.3 延时子程序ms_delay周期级精度ms_delay是生成毫秒级延时的通用子程序。其精妙之处在于循环次数的计算以确保在8MHz下精确产生TIMES毫秒的延时。代码注释中已经给出了计算公式延时 [{1 (2 3) * 1597 2 1 3 1 4 3} * (TIMES - 1) {1 (2 3) * 1597 2 1 3 1 4 1 5}] / 8MHz (8000 * TIMES 3) / 8MHz这体现了在资源受限的嵌入式环境中如何用汇编语言实现精确计时。在实际使用中如果总线频率不是8MHz你必须根据公式重新计算循环常数否则所有时序都会错乱。4. EEPROM AUTO模式汇编代码详解EEPROM的AUTO模式代码位于AutoEEPROM.mrt和AutoEEPROM.srt中其思路与Flash不同更侧重于寄存器的正确配置。4.1 主程序AutoEEPROM.mrt配置与调用movw #$0230,EEDIVH ;振荡器频率16MHz时设置EEDIV为$0230以获得35us时基 movw #$55AA,DATA ;将要编程的数据字$55AA存入RAM缓冲区 ldx #$0E02 ;加载要对齐的字地址 bclr EEPROT,BPROT3. ;解除目标块的保护 ldaa #auto_WORDerase. ;选择字擦除模式AUTO, BYTE, ERASE, EELAT jsr AutoRoutine ;调用AUTO模式例程执行擦除 ldaa #auto_WORDprogram. ;选择字编程模式AUTO, EELAT jsr AutoRoutine ;调用AUTO模式例程执行编程 bset EEPROT,BPROT3. ;重新保护已编程的块主程序流程非常清晰配置时钟分频器EEDIV这是AUTO模式正常工作的关键。代码中根据16MHz振荡器频率计算并设置EEDIVH:EEDIVL为$0230以确保内部编程/擦除时序的时间基准为35us。如果你用的晶振不是16MHz这个值必须重算计算公式参考芯片数据手册。准备数据与地址将想要写入的数据例如$55AA存入DATA变量2字节并将目标对齐字地址加载到X寄存器。解除块保护通过清除EEPROT寄存器相应的BPROT位允许对目标EEPROM区域进行操作。这是一个重要的安全措施防止意外写操作破坏关键数据。执行擦除与编程分别加载擦除和编程的模式字到A寄存器并调用同一个AutoRoutine子程序。模式字是在文件开头用equ定义的常量它们组合了EEPROG寄存器的各个控制位AUTO,EELAT,ERASE,BYTE等。4.2 核心子程序AutoRoutineAUTO模式的执行引擎AutoRoutine子程序是AUTO模式的核心它极其简洁因为复杂的工作都交给了硬件。AutoRoutine: sei ;步骤1 - 禁止中断 staa EEPROG ;步骤1 - 将模式字A寄存器写入EEPROG寄存器 ldd DATA ;步骤2 - 从RAM缓冲区取数据 std ,X ;步骤2 - 写入X指向的地址锁存地址和数据 bset EEPROG,EEPGM. ;步骤3 - 设置EEPGM位启动自动过程 Clear_EEPGM: brset EEPROG,EEPGM.,Clear_EEPGM ;步骤4 - 轮询等待EEPGM位自动清除 bclr EEPROG,EELAT. ;步骤5 - 清除EELAT位 movb #$80,EEPROG ;向EEPROG写入复位值可选良好习惯 cli ;重新使能中断 AutoRoutine_End: rts流程解读禁止中断同样是为了防止时序被打断。配置并启动staa EEPROG一举两得。一方面它根据调用者传入的模式字A寄存器配置了操作类型擦除/编程、块/行/字另一方面该指令本身也设置了AUTO和EELAT位使EEPROM控制器进入“地址/数据锁存”状态。触发操作紧接着的std ,X执行了一次向目标地址的写操作。在EELAT有效的情况下这个写操作并不会立即改变存储单元而是将目标地址和要写入的数据来自DATA锁存到EEPROM控制器内部。启动自动序列bset EEPROG,EEPGM.置位EEPGM位。这个信号告诉硬件“地址和数据都已就绪开始自动执行编程或擦除流程吧”。此后硬件会内部使能高压、施加脉冲、进行验证全部完成后自动清除EEPGM位。等待完成brset指令循环检测EEPGM位直到硬件将其清除表明操作完成。清理现场清除EELAT位退出锁存状态。将EEPROG恢复为复位值$80这是一个好习惯确保寄存器处于已知状态。最后重新使能中断并返回。AUTO模式的优势与陷阱优势是简单可靠硬件保证了时序。但陷阱在于EEDIV的配置和块保护状态。代码注释中特别用CAUTION标出如果EEDIV被误设为00:00或者目标地址处于保护状态批量擦除除外硬件将不会自动清除EEPGM位导致软件在此处无限循环。因此在调用AutoRoutine前必须确保EEDIV值正确且目标区域未受保护。5. 工程实践从示例代码到产品级应用原厂示例代码提供了一个可靠的起点但直接照搬到产品中是不够的。下面分享一些我将这些代码投入实际项目时的经验和关键修改点。5.1 关键参数适配与计算首先总线频率是生命线。示例代码所有延时都基于8MHz总线频率计算。你的系统频率是多少12MHz16MHz还是内部RC振荡器必须根据实际频率重新计算所有延时循环常数和EEDIV值。对于Flash的SST算法你需要根据数据手册中的tNVS、tERAS、tPGS、tFPGM、tNVHL、tRCV等时间参数以及你的总线周期重新计算dbne循环的次数。公式是所需周期数 时间要求 / 总线周期时间。ms_delay子程序也需要调整其核心循环常数代码中的#063E以确保1ms延时的准确性。对于EEPROM的AUTO模式关键是计算EEDIV。公式通常为EEDIV (fOSC * tBASE / 128) - 1其中fOSC是振荡器频率tBASE是芯片要求的基础时间单位如35us。具体公式请查阅最新的数据手册。5.2 增强鲁棒性错误处理与状态检查示例代码中的错误处理几乎是空白Error: bra *。在产品中我们必须做得更好。对于Flash操作验证失败处理Verify循环发现错误后不应死循环。可以重试几次例如3次如果仍然失败则记录错误码到特定RAM区域或备份寄存器并触发系统复位或进入安全模式。编程前检查在调用ProgRow前可以增加对目标地址区域的读取检查确保该区域已被擦除全为$FFFF。如果未擦除就编程会导致数据错误。超时机制虽然SST算法是硬延时但可以在外层调用时加入软件看门狗WDOG刷新防止程序跑飞卡死在某个循环中。对于EEPROM的AUTO模式EEPGM超时检测brset EEPROG,EEPGM.,Clear_EEPGM这个轮询循环必须加入超时机制。例如循环计数达到一个最大值对应远大于最大编程/擦除时间后如果EEPGM仍未清除则判定为操作失败执行错误恢复。ldy #TIMEOUT_VALUE Poll_EEPGM: dbne Y, NotTimeout ; 超时处理记录错误清除EEPGM/EELAT退出 bra EEPROM_Error NotTimeout: brset EEPROG,EEPGM.,Poll_EEPGM操作保护检查在尝试编程/擦除前读取EEPROT寄存器确认目标区域是否真的被解锁。这是一个防御性编程的好习惯。5.3 代码封装与可移植性为了在不同项目间复用应将Flash和EEPROM操作封装成独立的、带明确接口的函数。Flash驱动接口建议// 伪代码示例 typedef enum { FLASH_RESULT_OK, FLASH_RESULT_NOT_ERASED, FLASH_RESULT_VERIFY_FAIL, FLASH_RESULT_TIMEOUT, FLASH_RESULT_WRITE_PROTECTED } FlashResult_t; FlashResult_t Flash_ErasePage(uint16_t page_number); FlashResult_t Flash_ProgramRow(uint16_t row_address, const uint8_t *data); uint16_t Flash_CalculateChecksum(uint32_t start_addr, uint32_t length);对应的汇编函数应接受参数返回状态并且内部集成完整的擦除、编程、验证、错误处理流程。EEPROM驱动接口建议typedef enum { EEPROM_RESULT_OK, EEPROM_RESULT_PROTECTED, EEPROM_RESULT_TIMEOUT, EEPROM_RESULT_INVALID_PARAM } EEPROM_Result_t; EEPROM_Result_t EEPROM_WriteWord(uint16_t address, uint16_t data); EEPROM_Result_t EEPROM_EraseWord(uint16_t address); EEPROM_Result_t EEPROM_EraseRow(uint16_t address); // 注意EEPROM通常不需要先擦后写写操作会自动处理封装时注意将EEDIV的初始化、保护位的管理等内容也集成进去提供一个EEPROM_Init()函数。5.4 实际项目中的避坑指南中断与低功耗模式Flash/EEPROM操作期间必须禁止中断。此外也要避免进入STOP或WAIT等低功耗模式因为内部高压泵可能需要活跃的系统时钟。电压稳定性编程和擦除对供电电压(VDD)有要求。确保在操作期间电压在芯片规格书规定的范围内。在电池供电设备中如果电压过低应禁止存储操作。代码自身在Flash中的执行当你要擦写当前代码所在的Flash页时比如IAP在应用编程情况变得复杂。你必须将擦写函数和相关的数据缓冲区完整地复制到RAM中执行。因为Flash在擦写期间该页是无法读取指令的。这需要精心设计链接脚本和启动代码。EEPROM的耐久性与数据管理EEPROM有擦写次数限制通常10万-100万次。避免频繁写入同一地址。对于频繁更新的数据如运行时间计数器应采用磨损均衡算法或者配合FRAM铁电存储器使用。使用调试器在开发阶段利用调试器如PE Multilink的单步、断点功能结合示波器测量代码中注释提到的“测量点”如Port A bit 0可以直观地验证延时时序是否准确。这是调试Flash/EEPROM驱动最有效的方法。6. 常见问题与调试技巧实录即使理解了原理和代码第一次上手调试也难免踩坑。下面是我在实际项目中遇到的一些典型问题及解决方法。6.1 Flash编程验证失败现象ProgRow执行后验证循环Verify_Loop失败读回的数据与写入的不符。排查思路检查擦除是否成功在编程之前先读取目标行地址的数据。擦除后的Flash位应全为‘1’即读取值为$FFFF。如果发现不是$FFFF说明擦除步骤就失败了。重点检查FlashErase子程序传入的地址X是否在目标页内且字对齐PPAGE寄存器设置是否正确FEEMCR寄存器的BOOTP位是否已清除如果该页包含引导块用示波器或调试器检查ms_delay子程序是否真的产生了8ms延时总线频率设置对吗检查编程时序如果擦除成功但编程失败问题很可能在ProgRow的tFPGM时序上。tFPGM是连续两个字编程操作之间的时间必须在30-40us之间。代码中使用了一个ldaa #$4C的循环来产生约28.625us的延时加上循环外的指令总时间应在要求范围内。如果总线频率不是8MHz这个循环常数必须调整。使用调试器在Copy_Loop内设置断点或使用IO口翻转并配合示波器测量两次movw指令之间的实际时间。检查数据对齐与缓冲区确保Y寄存器指向的RAM缓冲区DATA确实包含了你想编程的64字节数据并且没有其他代码意外修改了这块内存。确保X寄存器指向的行起始地址是64字节对齐的末两位hex为004080C0。6.2 EEPROM的AUTO模式卡死现象程序执行到AutoRoutine中的brset EEPROG,EEPGM.,Clear_EEPGM处无限循环EEPGM位始终为1。排查步骤检查EEDIV寄存器这是最常见的原因。确认EEDIVH:EEDIVL的值是根据你的实际振荡器频率计算得出的。如果误设为0000硬件时序发生器不工作EEPGM位永远不会被清除。计算EEDIV是第一步也是必须做对的一步。检查块保护状态确认在调用AutoRoutine前已经通过bclr EEPROT, BPROTx.正确解除了目标地址所在块的保护。如果地址被保护对于非批量擦除的操作EEPGM位也不会被自动清除。可以尝试先执行一次批量擦除如果支持看是否能通过来帮助判断是否是保护问题。检查电源电压EEPROM编程/擦除对VDD电压有最低要求。如果电压过低操作可能无法完成导致EEPGM位挂起。测量系统电压是否在芯片工作范围内。加入超时跳出如前所述在产品代码中一定要为这个轮询循环加入超时机制。超时后应执行错误恢复先尝试清除EEPGM和EELAT位向EEPROG写入$00或复位值$80然后报告错误。这可以防止系统永久死锁。6.3 代码在RAM中运行的问题现象当尝试擦写当前代码所在的Flash页时程序跑飞或进入异常状态。解决方案链接脚本配置在编译器/汇编器的链接脚本.lcf, .ld文件中明确将Flash驱动函数FlashErase,ProgRow,ms_delay以及其使用的变量DATA,COUNTER,TIMES分配到RAM区域的一个固定段。启动代码复制在main函数开始前在启动代码中将这些函数和变量的二进制代码从Flash例如放在一个特殊的、不会被擦除的“引导加载程序”区域复制到RAM中指定的地址。函数指针调用在C语言中你可以将这些RAM中的函数地址赋值给函数指针然后通过函数指针来调用。在汇编中你需要知道这些例程在RAM中的确切入口地址并使用jsr跳转到那个地址。最小化RAM代码只将绝对必要的代码擦除、编程、关键延时循环复制到RAM。像数据准备、验证等逻辑可以留在Flash中。这个过程比较繁琐但却是实现固件自更新IAP功能的基础。建议在项目初期就规划好内存布局。6.4 时序测量与验证技巧在没有逻辑分析仪的情况下如何验证延时是否准确IO口调试法代码中已经给出了范例注释掉的bset/bclr PORTA,PA0.。你可以取消这些注释将一个IO口如PORTA0配置为输出。在延时循环的开始将其拉低在结束时拉高。然后用示波器测量这个低电平脉冲的宽度即可得到实际延时时间。这是调试底层驱动最直观、最可靠的方法。软件计数法在已知频率下用一个全局变量在延时函数中计数。在main函数中运行这个延时一段时间比如1秒然后通过调试器查看这个变量的值可以反推延时函数的准确性。最后请务必、务必、务必仔细阅读你所使用的MC68HC912DT128A/DG128A芯片最新版本的数据手册Data Sheet和技术参考手册Technical Reference Manual。应用笔记Application Note中的代码是基于某个特定版本的芯片和工具链而数据手册中的参数和约束才是永恒的真理。任何与手册描述不符的操作都可能带来不可预知的风险。