1. 项目概述与核心价值
如果你正在使用飞思卡尔(现恩智浦)的MC68HC908AS60A这类8位微控制器,并且需要在产品中保存一些掉电不丢失的数据,比如设备的校准参数、用户的配置信息或者运行状态日志,那么你一定会和它的EEPROM打交道。EEPROM,也就是电可擦可编程只读存储器,是这类嵌入式系统的“记忆核心”。它不像RAM一断电就失忆,也不像Flash那样擦写起来麻烦,它允许你在程序运行中,用特定的指令去修改里面的数据,这为产品带来了极大的灵活性。
但是,直接操作EEPROM的硬件时序是个精细活,时序不对、电压不稳,轻则数据写不进去,重则可能损坏存储单元。MC68HC908AS60A提供了一个非常贴心的功能,叫做AUTO模式。这个模式本质上是一个硬件状态机,你只需要通过配置几个控制寄存器告诉它“做什么”(擦除还是编程)和“在哪里做”,它就会自动完成所有复杂的电压切换和定时操作,大大简化了软件开发的难度,也提高了操作的可靠性。
然而,官方文档往往只给流程图和代码片段,对于刚接触这款芯片或者对底层操作不熟的工程师来说,理解这些代码“为什么这么写”以及“如何安全地用到自己的项目里”,中间还有不少沟壑。我最近在为一个老产品做功能升级,就重新啃了一遍AN2156这份应用笔记,把里面的AUTO模式编程与擦除源码和流程图彻底捋了一遍。这篇文章,我就结合自己的调试经验,把这套机制的里里外外、每个关键步骤背后的考量,以及实际移植和使用中容易踩的坑,给你掰开揉碎了讲清楚。无论你是要维护遗留代码,还是在新设计中应用此功能,相信都能找到直接的参考。
2. EEPROM AUTO模式工作原理深度解析
在深入代码之前,我们必须先搞清楚AUTO模式到底在背后帮我们做了什么。如果你把它想象成一个高度自动化的“烧录机器人”,那么我们的程序就是给这个机器人下达指令的指挥官。
2.1 EEPROM编程与擦除的物理本质
EEPROM的每个存储单元都是一个浮栅晶体管。写入(编程)数据,本质上是向浮栅注入电子,使其阈值电压升高,代表存储了‘0’;擦除数据,则是将浮栅上的电子拉走,降低其阈值电压,代表存储了‘1’(或擦除状态)。这个过程需要精确的高压脉冲和严格的时序。
手动模式(非AUTO模式)下,程序员需要严格按照数据手册的时序图,像操作精密仪器一样,按顺序设置EELAT(EEPROM锁存使能)、写入数据、设置EEPGM(EEPROM编程/擦除使能),并等待特定的时间(t_{NVS}/t_{NVP})。这个过程不仅代码繁琐,而且极易因中断干扰或时序计算偏差导致失败。
2.2 AUTO模式的自动化机制
AUTO模式就是为了消除上述复杂性而生的。当你设置了AUTO位和EELAT位(在某些版本中需同时设置),并启动操作(设置EEPGM)后,芯片内部的硬件状态机就会接管后续所有操作:
- 自动电压切换:硬件会自动产生并管理编程或擦除所需的高压。
- 精准内部定时:硬件使用独立的EEPROM时钟源(由
CONFIG2寄存器配置)和分频器(EExDIVH/L)来生成精确的脉冲宽度,完全不受CPU主频波动或中断的影响。 - 操作序列控制:自动完成所需的电压建立、脉冲施加和恢复序列。
我们的软件工作因此被简化为三步:配置 -> 触发 -> 等待完成。这极大地提升了代码的健壮性和可移植性。
2.3 关键控制寄存器(EExCR)位定义解读
理解代码的关键在于理解EE1CR和EE2CR这两个控制寄存器(分别控制EEPROM阵列1和2)。代码中使用了大量的位掩码操作,我们来明确每个位的角色:
- EEPGM (位0):这是启动键。向该位写1,将启动一个由
EERAS1:0和AUTO位定义的编程或擦除周期。硬件会在操作完成后自动清除此位。因此,软件可以通过轮询此位是否为0来判断操作是否结束。 - EELAT (位1):地址/数据锁存使能。在向EEPROM地址写入数据之前,必须将此位置1,以锁存地址总线。在AUTO模式下,它通常与
AUTO位同时设置。 - ERASE (位2):保留位,在AS60A中未使用。擦除类型由
EERAS1:0决定。 - EERAS1, EERAS0 (位4, 位3):擦除类型选择。
00: 保留。01: 字节擦除。10: 块擦除(根据芯片定义,通常为32或64字节)。11: 整体擦除(擦除整个EEPROM阵列)。
- AUTO (位5):自动模式使能。将此位置1,即启用上述的硬件自动操作序列。这是使用AUTO模式的核心。
在源码中,auto_byteprogram(%00000110) 等常量,就是EERAS1:0、EELAT和AUTO位的组合值。例如,00000110表示:EERAS1:0=00(编程),EELAT=1,AUTO=1。
3. 源码流程图与主程序(AutoEEPROM.mrt)拆解
官方流程图(Figure 25)给出了主程序的骨架,但结合源码我们能理解更多细节。主程序AutoEEPROM.mrt是一个完整的演示流程,它执行了一次擦除和一次编程。
3.1 初始化与配置阶段
Start: mov #$71,config-1 ;Turn off the COP, but leave the LVI on lda #$98 ;Select bus clock as reference clock sta config-2 ; source lda #$80 ;For setting a constant timebase of 35us sta EE1DIVH ; write $80 AND $56 to EExDIVH and sta EE2DIVH ; EExDIVL, respectively lda #$56 ; Note: If the EExDIVHNVR and EExDIVLNVR sta EE1DIVL ; registers are programmed with proper sta EE2DIVL ; values, this step is not necessary- 关闭COP,启用LVI:
CONFIG1寄存器。看门狗(COP)可能在EEPROM操作期间复位MCU,因此必须先关闭。而低电压抑制(LVI)建议保持开启,以确保操作期间电压稳定,防止在电压不足时进行写操作导致数据损坏。 - 配置EEPROM时钟源:
CONFIG2寄存器。这里写入$98,选择了内部总线时钟作为EEPROM时钟的参考源。这是最常见的配置。 - 设置EEPROM时钟分频器(
EExDIVH/L):这是保证时序正确的关键。EEPROM编程/擦除需要精确的35μs高压脉冲。脉冲宽度由公式t = (EExDIV值) / f_{EEPCLK}决定。示例中写入$80和$56,是针对2.4576MHz的总线时钟计算得出的分频值,以确保得到35μs的脉冲。
重要提示:数据手册强调,如果芯片出厂时或之前已在非易失性寄存器
EExDIVHNVR和EExDIVLNVR中烧录了正确的值,则无需在每次上电后重新配置EExDIVH/L。硬件会自动从NVR中加载。但在初次使用或不确定时,在软件中配置是更稳妥的做法。我个人的经验是,在产品代码中总是显式配置一次,不依赖NVR的默认值,这样代码行为更确定。
3.2 数据与地址准备
lda #$55 ;Write data $55 to RAM buffer sta data ldhx #$0634 ;Load EEPROM_addr with address of where sthx EEPROM_addr ; the byte should be erased and programmed- 准备编程数据:将待编程的数据(示例为
$55)存入一个RAM变量(data)。注意,擦除操作不需要此数据。 - 设定目标地址:将16位的EEPROM目标地址(示例为
$0634)存入变量EEPROM_addr。这里演示的是对同一地址先擦除后编程。
3.3 调用AUTOroutine执行操作
lda #auto_bulkerase. ;Select Bulk, Block or Byte Erase jsr AutoRoutine ;Erase the selected EEPROM size using AUTO Mode lda #auto_byteprogram. ;Select Byte Program jsr AutoRoutine ;Program one byte using AUTO Mode bra * ;无限循环,程序结束- 擦除操作:将擦除模式常量(如
auto_bulkerase)加载到累加器A,然后调用AutoRoutine子程序。擦除会使目标单元所有位变为‘1’(即$FF)。 - 编程操作:将编程模式常量(
auto_byteprogram)加载到A,再次调用AutoRoutine。编程将根据data中的值,将相应的位从‘1’变为‘0’。
操作心得:必须先擦除,后编程。因为EEPROM编程只能将位从‘1’变为‘0’。如果目标地址当前值是
$F0(11110000),你想写入$55(01010101),直接编程是无法将第7、5、3、1位从‘0’变回‘1’的。必须先擦除为$FF,再编程为$55。
4. 核心子程序AUTOroutine逐行精讲
AUTOroutine子程序(对应流程图Figure 26)是AUTO模式操作的核心引擎。它严格按照5个步骤执行。
4.1 Step 1: 设置操作参数并启动锁存
AutoRoutine: sei ;Disable interrupts jsr WriteEECR ;Step 1 - Set up EERAS0, EERAS1 for a ; desired operation, and set EELAT and ; AUTO- 关中断(
sei):这是至关重要的安全措施。EEPROM操作对时序极其敏感,任何中断的插入都可能打断关键的配置或等待序列,导致操作失败或数据损坏。必须在操作开始前关闭,并在操作完成后(cli)再打开。 - 调用
WriteEECR:这是Step 1。根据传入的A寄存器中的模式常量(如%00000110),以及全局变量EEPROM_addr中的地址,WriteEECR子程序会判断该地址属于EEPROM1还是EEPROM2,并向对应的EExCR寄存器写入这个常量。此操作同时完成了三件事:通过EERAS1:0选择操作类型,并设置了EELAT和AUTO位。此时,硬件已经准备好了,地址被锁存。
4.2 Step 2: 写入数据(仅编程操作需要)
lda data ;Step 2 - For programming, copy one byte ldhx EEPROM_addr ; data from the RAM buffer to the sta ,X ; appropriate EEPROM location- 仅当进行编程操作时,此步骤才有意义。它将
data中的字节写入由H:X寄存器对指向的EEPROM地址。由于Step 1已经设置了EELAT位,这个写入操作的数据会被锁存到EEPROM的数据锁存器中,等待后续的编程脉冲。对于擦除操作,此步骤虽然也会执行(因为代码是共享的),但写入的数据是无效的,硬件会忽略它。
4.3 Step 3: 启动编程/擦除周期
lda #eepgm. ;Step 3 - Set EEPGM bit jsr WriteEECR- 向
EExCR寄存器写入eepgm.(即%00000001,仅EEPGM位为1)。这个写操作是一个“触发”信号。硬件检测到EEPGM位被置1,且AUTO位已置1,便会立即启动内部的高压定时器,开始执行编程或擦除的物理过程。
4.4 Step 4: 等待操作完成
Clear_EEPGM1: lda EE1CR ;Step 4 - Wait until EEPGM bit is cleared and #$01 ; Checks included for both EEPGM registers bne Clear_EEPGM1 Clear_EEPGM2: ; since the programmed byte could be in lda EE2CR ; either array and #$01 bne Clear_EEPGM2- 硬件完成操作后,会自动将
EEPGM位清零。因此,软件需要通过轮询(Polling)来等待操作结束。 - 一个精妙的细节:代码同时轮询了
EE1CR和EE2CR。这是因为WriteEECR子程序虽然根据地址写入了正确的EExCR,但EEPGM位在硬件上可能存在于两个寄存器中(具体取决于芯片设计)。为了确保万无一失,等待两个寄存器的EEPGM位都清零。这是一种非常稳健的编程实践。
4.5 Step 5: 清除锁存并清理
lda #eelat. ;Step 5 - Clear EELAT bit jsr WriteEECR lda #$00 ;Clear all bits in the EExCR sta EE1CR sta EE2CR cli ;Enable interrupts AutoRoutine_End: rts- 清除EELAT位:操作完成后,需要清除
EELAT位,退出地址/数据锁存状态,使EEPROM阵列恢复正常读取模式。 - 清零控制寄存器:作为一种良好的习惯,将两个
EExCR寄存器全部清零,确保状态干净,避免后续误操作。 - 开中断(
cli):恢复中断响应。
5. 关键工具子程序WriteEECR解析
WriteEECR(流程图Figure 27)是一个实用工具函数,它的智能之处在于自动判断地址所属的EEPROM阵列。
WriteEECR: cphx #EE2DIVHNVR ;If address >= $FF70, branch to EEPROM2 bhs EEPROM2 cphx #eeprom-1 ;If address >= $0800, write to EEPROM1 bhs EEPROM1 EEPROM2: eor EE2CR ;Write to EE2CR register sta EE2CR bra WriteEECR_End EEPROM1: eor EE1CR ;Write to EE1CR register sta EE1CR WriteEECR_End: rts- 逻辑判断:它比较
EEPROM_addr(已预先加载到H:X寄存器)与两个边界地址。- 如果地址 >=
$FF70(EE2DIVHNVR的地址),则属于EEPROM2的控制范围(这可能是一些特殊的非易失性寄存器区域,在AS60A中映射到EEPROM2控制器)。 - 否则,如果地址 >=
$0800(EEPROM阵列的起始地址?这里需要查具体数据手册,示例代码中eeprom可能是一个等于$0800的标号),则属于EEPROM1。 - 如果地址小于
$0800,则不属于EEPROM,但代码逻辑会落到EEPROM1分支?这里存在一个潜在问题。实际上,更严谨的判断应该是确定地址在哪个物理EEPROM块内。示例代码可能做了简化。
- 如果地址 >=
- 位操作技巧:
eor EE1CR/sta EE1CR。这里使用了“异或后写回”的方式。传入的A寄存器中,需要置1的位为1,需要清零的位为0。eor指令将A与当前寄存器值异或,结果中,A为1的位会翻转,A为0的位保持不变。然后写回,就实现了对指定位的设置或清除。例如,若EE1CR当前为00000000,A=00000110,异或后结果为00000110,写回即设置了EELAT和AUTO位。
6. 移植与实战应用指南
官方示例代码是一个完整的、可编译运行的演示。但要把它集成到你的实际项目中,还需要做一些调整和封装。
6.1 代码模块化与接口设计
不建议直接复制粘贴整个AutoEEPROM.mrt。应该将其核心功能封装成独立的、可重用的函数。
// 示例:C语言接口封装(需内嵌汇编或调用汇编子程序) typedef enum { EEPROM_OP_BYTE_PROGRAM = 0x06, EEPROM_OP_BYTE_ERASE = 0x0E, EEPROM_OP_BLOCK_ERASE = 0x16, EEPROM_OP_BULK_ERASE = 0x1E } eeprom_op_t; /** * @brief 在指定EEPROM地址执行AUTO模式操作 * @param addr 16位EEPROM地址 * @param data 待编程的数据(仅编程操作有效) * @param op 操作类型,见eeprom_op_t * @return 0成功,非零失败(可扩展为错误码) */ int8_t eeprom_auto_operation(uint16_t addr, uint8_t data, eeprom_op_t op); /** * @brief 初始化EEPROM模块时钟(根据需要调用) */ void eeprom_init_clock(void);将AUTOroutine的逻辑用C语言配合少量内联汇编实现,或者保留为独立的汇编模块供C调用。WriteEECR函数由于其高度硬件相关性,通常用汇编实现效率最高。
6.2 时钟配置的实战考量
示例中的时钟配置(CONFIG2=$98,EExDIV=$8056)是针对2.4576MHz总线时钟的。你的系统总线频率很可能不同。
- 计算正确的分频值:你必须根据数据手册中的公式和你的系统时钟频率
f_{BUS},重新计算EExDIVH:L的值,以确保35μs的编程/擦除脉冲。公式通常是:分频值 = t_{pulse} * f_{EEPCLK} - 1,其中f_{EEPCLK} = f_{BUS} / 分频系数(由CONFIG2决定)。务必仔细查阅MC68HC908AS60A的数据手册。 - 配置时机:初始化代码中配置一次即可。如果芯片的
EExDIVHNVR/LNVR已包含正确值,理论上可以跳过。但我强烈建议在初始化阶段显式配置,确保状态已知。
6.3 数据保护与错误处理
- 中断管理:如前所述,操作期间必须关中断。确保关中断的时间尽可能短(只覆盖
AUTOroutine调用)。在实时性要求高的系统中,需评估此段时间对中断响应的影响。 - 电源稳定性:EEPROM操作期间要求电源电压稳定。确保LVI启用,并在电源设计上考虑去耦和稳压。
- 操作验证:重要的数据写入后,应该执行一次回读验证。即,将写入的数据再读出来,与期望值比较。
- 错误标志检查:某些MCU的EEPROM模块可能有错误状态标志。虽然AS60A的AUTO模式简化了操作,但在更复杂的应用中,检查相关状态寄存器是良好的习惯。
6.4 针对不同数据长度的操作策略
- 单字节操作:如示例所示,直接调用即可。
- 多字节连续写入:切勿在循环中连续调用
AUTOroutine。每次调用都包含完整的擦除(如果需要)-编程序列。对于连续地址的多字节数据,更好的做法是:- 计算这些字节所在的“块”(Block)。如果芯片支持块擦除(如32字节块),先调用一次
块擦除操作。 - 然后,在一个循环中,逐个字节地进行
字节编程操作。注意,每次编程前仍需设置地址和数据,但擦除只需一次。
- 计算这些字节所在的“块”(Block)。如果芯片支持块擦除(如32字节块),先调用一次
- 擦除整个阵列:使用
bulk erase操作。这会擦除整个EEPROM,所有位变为0xFF。谨慎使用,且通常需要在特定条件下(如特定的配置位使能)才能执行。
7. 常见问题排查与调试技巧
即使理解了原理和代码,第一次调试时也可能遇到问题。以下是我总结的一些常见坑点和排查思路。
7.1 操作失败(数据未改变或改变不正确)
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 数据完全无变化 | 1. EEPROM写保护未解除。 2. EExDIV分频器配置错误,导致时序不满足。3. AUTO或EELAT位未正确设置。4. 目标地址错误,或地址不在有效的EEPROM空间。 | 1. 检查相关配置寄存器或选项字节的写保护位。 2. 双检查系统时钟频率和 EExDIV计算值。用示波器测量EEPROM相关引脚(如果可用)查看高压脉冲。3. 单步调试,在调用 WriteEECR后立即读取EExCR寄存器,确认位已被设置。4. 确认地址值,并查阅数据手册的内存映射图。 |
只能写0xFF或0x00 | 1. 未先擦除就编程(只能将1变0)。 2. 擦除操作本身失败,单元未恢复到 0xFF。 | 1. 确保编程操作前,目标地址已被擦除(读出来是0xFF)。2. 单独测试擦除操作,看能否将某个非 0xFF的值变为0xFF。检查擦除操作的配置常量是否正确。 |
| 数据位随机错误 | 1. 电源噪声或电压跌落。 2. 在操作完成前( EEPGM位清零前)发生了读取或其他干扰。 | 1. 加强电源滤波,确保在EEPROM操作期间系统负载稳定。 2. 确保软件严格等待 EEPGM位清零,并且在此之间不访问EEPROM地址。 |
7.2 调试方法与工具建议
- 软件仿真器:如果开发环境支持(如某些版本的CodeWarrior),可以在仿真器中单步执行代码,观察寄存器(特别是
EExCR)的变化,以及内存(EEPROM地址)的内容变化。这是理解流程最直观的方式。 - 在线调试器:通过JTAG或BDM接口进行实时调试。可以在
AUTOroutine中设置断点,检查变量和寄存器状态。注意:在EEPROM编程/擦除周期期间,CPU可能被暂停或运行异常,调试器行为可能不同,需要查阅调试器手册。 - 逻辑分析仪/示波器:这是最强大的硬件调试工具。可以抓取总线上的地址、数据信号,以及
EEPGM等控制引脚(如果引出)的波形,直观地看到操作序列和时序是否符合数据手册要求。 - “读-改-写”验证:编写一个简单的测试函数:先读取一个地址的值,打印出来;然后执行擦除,再读取打印;最后执行编程,再读取打印。通过串口输出这些信息,可以快速定位是擦除失败还是编程失败。
7.3 寿命与可靠性注意事项
EEPROM有擦写次数限制(通常为10万到100万次)。在设计软件时需注意:
- 避免频繁写入:不要将频繁变化的数据(如每秒更新的计数器)直接存到EEPROM。可以先在RAM中累加,定期或满足条件时再写入EEPROM。
- 磨损均衡:如果需要存储频繁更新的数据,可以考虑实现简单的磨损均衡算法,轮流使用EEPROM中的多个地址。
- 数据校验:存储重要数据时,除了存储数据本身,还应存储其校验和(如CRC8)或备份副本。读取时进行校验,发现错误可尝试从备份中恢复。
通过深入剖析MC68HC908AS60A的EEPROM AUTO模式源码,我们不仅看到了一段高效的底层驱动代码,更学习了一套嵌入式开发中如何安全、可靠地操作硬件模块的完整方法论。从理解硬件自动化的价值,到掌握关键寄存器的每一位含义,再到将示例代码转化为健壮的项目模块,最后用有效的工具和方法进行调试和排错——这个过程本身,就是嵌入式工程师核心能力的体现。希望这份详细的解析能让你在下次面对类似的数据手册和示例代码时,更加游刃有余。