1. 项目概述与核心价值
在嵌入式开发领域,尤其是面对一些资源受限的经典8位微控制器时,我们常常会遇到一个现实问题:芯片本身没有集成硬件SPI或I2C等串行通信外设,但项目又需要使用外部的串行EEPROM来存储关键数据。这时候,“Bit-Banging”(位敲击)这项古老的软件技艺就派上了大用场。它本质上就是用通用I/O口(GPIO)和精准的软件延时,来模拟出特定通信协议所需的全部时序波形。今天要深入探讨的,就是基于摩托罗拉(现恩智浦)经典MCU——MC68HC705J1A,与同样经典的93C56串行EEPROM,实现一套完整、可靠的软件驱动接口。这套方案虽然基于上世纪90年代的文档,但其设计思想、代码结构以及对时序的精准把控,至今仍是嵌入式软件工程师理解底层通信和资源受限编程的绝佳范例。
这个项目的核心价值在于“知其然,更知其所以然”。它不仅仅是一段能用的代码,更是一个完整的工程实践案例,涵盖了从硬件接口设计、通信协议解析、软件状态机实现,到系统可靠性增强(如看门狗、掉电保护)的全过程。对于从事消费电子(如老式电视、录像机的频道记忆)、工业控制(设备参数存储)或任何需要低成本、小容量非易失存储的场景,这种通过软件模拟驱动EEPROM的思路都具有直接的参考意义。即使你手头不是J1A和93C56,理解了这套方法,也能轻松移植到其他MCU和类似的93XX系列EEPROM上。
2. 硬件接口设计与信号解析
要实现软件驱动,第一步必须吃透硬件连接和每个引脚的角色。MC68HC705J1A是一款小巧的20引脚MCU,而93C56是一个8引脚的EEPROM。它们的对话只需要四根线,这构成了整个通信的物理基础。
2.1 核心信号线定义与MCU端口配置
93C56的串行接口主要依赖四个引脚:
- CS (Chip Select):片选信号。这是通信的“总开关”,必须拉高才能启动与EEPROM的任何对话,操作结束后必须拉低。它决定了EEPROM是否监听总线上的其他信号。
- SK (Serial Clock):串行时钟。由MCU产生,用于同步每一位数据的传输。每一个时钟的上升沿或下降沿(取决于器件)定义了数据采样或输出的时刻,是通信的“心跳”。
- DI (Data Input):数据输入。MCU通过这根线向EEPROM发送指令、地址和数据。
- DO (Data Output):数据输出。EEPROM通过这根线向MCU回传数据,例如执行读操作时的存储内容。
在MC68HC705J1A上,我们使用其端口A(PORTA)来模拟这些信号。根据附录A的电路图,典型的连接和软件定义如下:
CS equ 0 ; PORTA.0 连接至 93C56 的 CS 引脚 SER_CLK equ 1 ; PORTA.1 连接至 93C56 的 SK 引脚 SER_OUT equ 2 ; PORTA.2 连接至 93C56 的 DI 引脚 SER_IN equ 3 ; PORTA.3 连接至 93C56 的 DO 引脚初始化时,需要将SER_OUT和SER_CLK对应的端口位设置为输出(写1到DDRA相应位),将SER_IN对应的端口位设置为输入(写0到DDRA相应位)。CS虽然也是输出,但通常会在每次通信前后动态控制。
注意:93C56的DO引脚是开漏输出,这意味着它只能主动拉低电平,而不能驱动高电平。因此,必须在MCU端为该I/O口启用内部上拉电阻,或者像原理图中那样,在外部连接一个上拉电阻到VCC,以确保当EEPROM不输出数据时,该线能被拉至高电平,MCU才能正确读取到逻辑‘1’。J1A的I/O口在复位后内部有下拉,但作为输入读取外部信号时,确保明确的上拉是可靠通信的关键,这一点在移植到其他MCU时务必检查。
2.2 电源与可靠性设计考量
原始应用笔记中特别提到了两个增强可靠性的设计,这体现了工业级产品的严谨性:
- 软件看门狗(COP):MC68HC705J1A内置看门狗定时器。在所有的软件延时循环(如
J9356_WAIT)和主循环中,都插入了STA COPR指令来“踢狗”,防止程序跑飞导致EEPROM通信时序错乱,甚至陷入死循环。这在有强电磁干扰或电源波动的环境中至关重要。 - 低电压抑制电路(MC34064):这是一个独立的硬件电路,用于监控MCU的供电电压(VDD)。当电压低于某个阈值(例如4.5V)时,它会强制拉低MCU的RESET引脚,使系统保持复位状态,防止MCU在电压不足的情况下执行错误的写操作,从而保护EEPROM中的数据。对于非易失性存储器,防止在非正常电压下写入是基本的设计原则。
这些外围设计虽然不是软件驱动的核心,但却是保证整个存储系统长期稳定运行的基石。在实际项目中,尤其是电池供电或工业环境,必须认真考虑。
3. 93C56通信协议与指令集深度解析
软件模拟的核心,就是精确地再现93C56所期望的通信协议。这是一个同步串行协议,所有数据都在SK时钟的同步下,以最高位(MSB)在先的方式传输。
3.1 指令帧格式与位传输时序
每一次完整的操作(读、写、擦除等)都始于MCU拉高CS信号,然后发送一个完整的指令帧。帧的构成根据指令类型有所不同,但都遵循“起始位+操作码+地址+数据(可选)”的结构。
以**写操作(WRITE)**为例,其完整的帧格式为:
- 起始位:
CS从低变高,标志着传输开始。 - 操作码(3位):
101(二进制),表示这是一个写命令。 - 地址(8位):A7-A0,指定要写入的16位寄存器地址(93C56有256个地址)。
- 数据(16位):D15-D0,要存储的16位数据。
MCU需要严格按照以下时序操作:
- 在
SK为低电平时,准备好DI(即SER_OUT)上的数据位(1或0)。 - 将
SK拉高,产生一个上升沿。93C56会在SK上升沿采样DI上的数据。 - 将
SK拉低,完成一个时钟周期。 - 重复以上步骤,发送完所有位。
**读操作(READ)**的流程略有不同:MCU先发送操作码110和8位地址,随后,MCU需要再产生一个额外的SK时钟脉冲(即“虚时钟”),之后93C56才会在DO线上输出数据。MCU在随后的16个SK时钟的上升沿从DO(即SER_IN)读取数据位。
3.2 七条核心指令详解
93C56支持7条指令,驱动必须完整实现它们以提供灵活的控制:
| 指令助记符 | 操作码 (二进制) | 地址字段 (A7-A0) | 数据字段 | 功能描述 |
|---|---|---|---|---|
| EWEN | 100 | 11XXXXXX | 无 | 擦写使能。在执行任何写或擦除操作前,必须首先发送此命令,相当于给EEPROM“解锁”。 |
| WRITE | 101 | A7-A0 | D15-D0 | 写数据。向指定地址写入16位数据。 |
| WRAL | 100 | 01XXXXXX | D15-D0 | 全写。向所有地址写入相同的16位数据(用于批量初始化)。 |
| ERASE | 111 | A7-A0 | 无 | 擦除。将指定地址的数据擦除为全1(0xFFFF)。 |
| ERAL | 100 | 10XXXXXX | 无 | 全擦除。将所有地址的数据擦除为全1。 |
| READ | 110 | A7-A0 | D15-D0 (输出) | 读数据。从指定地址读取16位数据。 |
| EWDS | 100 | 00XXXXXX | 无 | 擦写禁止。在所有写/擦除操作完成后发送,将EEPROM“上锁”,防止意外修改。 |
实操心得:
EWEN和EWDS指令的地址字段是固定的(11XXXXXX和00XXXXXX),通常直接填充0xC0和0x00即可。WRAL和ERAL的地址字段也是固定的(01XXXXXX和10XXXXXX)。理解这一点可以避免在配置指令时对地址位的困惑。一个关键的安全操作流程是:EWEN-> (写/擦除操作) ->EWDS。养成这个习惯能极大避免程序异常时对EEPROM的误写。
4. 软件驱动架构与核心子程序实现
驱动代码采用模块化设计,分为高层命令子程序(如J9356_READ)和底层位操作支持子程序(如J9356_WR_OP)。这种结构清晰,易于调试和维护。
4.1 内存变量定义与初始化
驱动需要4个字节的RAM来暂存通信过程中的数据:
OPCODE rmb 1 ; 存储3位操作码(实际占用一个字节,高位有效) ADDR rmb 1 ; 存储8位目标地址 DATA_H rmb 1 ; 数据高字节 DATA_L rmb 1 ; 数据低字节在调用具体命令子程序前,主程序需要将相应的参数加载到这些变量中。例如,执行写操作前,需要设置好ADDR、DATA_H和DATA_L。
端口初始化代码(J9356_START)除了设置数据方向寄存器DDRA,还将PORTA初始化为0x80。查看J1A数据手册可知,这通常是为了将某些未使用的端口设置为已知状态(如禁用上拉),但核心是正确配置SER_OUT、SER_CLK为输出,SER_IN为输入。
4.2 底层位传输子程序剖析
这是驱动中最精妙的部分,直接实现了协议的物理层。
J9356_WR_OP(写3位操作码) 和J9356_WR_ADDR(写8位地址):这两个子程序逻辑完全相同,只是循环次数(3 vs 8)和操作的数据源(OPCODEvsADDR)不同。以J9356_WR_ADDR为例:
- 设置循环计数器X=8。
- 检查
ADDR寄存器的最高位(bit7)是1还是0。 - 根据检查结果,将
SER_OUT引脚置高或置低。 - 将
SER_CLK引脚拉高再拉低,产生一个时钟脉冲。93C56在SK上升沿采样DI,因此数据必须在SK拉高前稳定。 - 将
ADDR寄存器算术左移(ASL),将下一位移动到bit7位置,为发送下一位做准备。 - 递减X,若不为零则跳回步骤2。
J9356_WR_DATA(写16位数据):逻辑与写地址类似,但需要处理两个字节(DATA_H和DATA_L)。技巧在于使用ASL DATA_L和ROL DATA_H两条指令的配合。ASL将DATA_L左移,其最高位(原bit7)移入处理器的进位标志(C)。紧接着ROL将DATA_H连同进位标志一起左移,这样就把DATA_L的最高位移到了DATA_H的最低位,同时DATA_H原来的最高位移到了C标志位。下一轮循环,检查的就是DATA_H的bit7(即上一轮从C标志位移入的DATA_L的最高位)。如此循环16次,就完成了16位数据的串行化输出。
J9356_RD_DATA(读16位数据):这是唯一从EEPROM读取数据的子程序。流程如下:
- 在发送完读命令和地址后,MCU先产生一个额外的时钟脉冲(在
J9356_READ子程序中完成),通知EEPROM开始输出数据。 - 设置循环计数器X=16。
- 读取
SER_IN引脚状态。代码使用BRCLR指令判断引脚是否为低,若为低则C标志位被清零,若为高则C标志位保持为1(取决于具体指令序列,此处逻辑需结合上下文理解)。关键在于,需要将引脚电平状态转移到C标志位。 - 使用
ROL指令循环左移DATA_L和DATA_H。ROL会将C标志位移入目标寄存器的最低位,同时将目标寄存器的最高位移出到C标志位。通过先移DATA_L再移DATA_H,并利用C标志位在两个字节间传递数据,巧妙地完成了16位数据的串行组装。 - 产生时钟脉冲(
SK拉高再拉低),EEPROM会在SK上升沿后更新DO引脚输出下一位数据。 - 循环16次。
J9356_WAIT(等待写周期结束):EEPROM在执行写或擦除操作时,需要一定时间(典型值3-5ms)将数据写入存储单元。在此期间,其DO引脚会保持低电平(忙状态),完成后恢复高阻态(由上拉电阻拉高)。此子程序在拉高CS后,循环检测SER_IN引脚,直到其为高电平才返回。循环中必须插入看门狗复位操作(STA COPR),防止等待时间过长导致看门狗溢出复位。
4.3 高层命令子程序流程
每个高层命令子程序都是底层子程序的组合。以J9356_WRITE为例:
- 加载写操作码
0xA0到OPCODE。 - 拉高
CS。 - 调用
J9356_WR_OP发送3位操作码。 - 调用
J9356_WR_ADDR发送8位地址。 - 调用
J9356_WR_DATA发送16位数据。 - 将
SER_OUT拉低(确保结束时DI线处于确定状态)。 - 拉低
CS,结束指令帧。 - 调用
J9356_WAIT,等待EEPROM内部写操作完成。 - 返回。
J9356_READ的流程略有不同,在发送完操作码和地址后,需要手动产生一个时钟脉冲(BSET SER_CLK,PORTA和BCLR SER_CLK,PORTA),然后才能调用J9356_RD_DATA读取数据。
5. 完整测试流程与代码实战分析
附录C提供的测试代码是一个极佳的学习模板,它演示了如何集成所有驱动子程序,并构建一个完整的自检流程。
5.1 测试主程序逻辑
测试程序J9356_START的执行序列清晰地展示了一个安全、完整的EEPROM操作流程:
- 初始化与使能:初始化端口,调用
EWEN解锁EEPROM。 - 安全擦除:调用
ERAL全擦除,确保从一个已知状态(全0xFFFF)开始测试。在实际产品中,若非必要慎用全擦除,建议按地址擦除。 - 写入测试数据:向地址
0x00写入数据0xAA55,向地址0x20写入数据0x1234。 - 回读验证:分别从地址
0x00和0x20读取数据,存入临时变量TEST1-TEST4。 - 结果判断与指示:比较读回的数据与写入的是否一致。如果全部正确,则点亮一个LED(通过清除PORTA的某个位,假设连接了LED);如果有任何错误,则保持LED熄灭。
- 看门狗维护与循环:进入一个无限循环,持续“踢”看门狗,防止复位。
这个测试覆盖了使能、擦除、写、读等核心操作,并通过LED给出了直观的通过/失败指示,是一个非常经典的硬件功能测试范例。
5.2 关键代码片段解读
以写数据到地址0x00为例,汇编代码清晰地展示了参数传递过程:
lda #$00 ; 将地址0x00加载到累加器A sta ADDR ; 存入ADDR变量 lda #$AA ; 将数据高字节0xAA加载到A sta DATA_H ; 存入DATA_H lda #$55 ; 将数据低字节0x55加载到A sta DATA_L ; 存入DATA_L jsr J9356_WRITE ; 调用写子程序这种将参数存入预定RAM位置,再由子程序读取的模式,是汇编语言中常见的参数传递方式,清晰且高效。
在结果检查部分(J9356_CKSUM),代码使用了CMPA(比较)和BNE(不相等则跳转)指令进行逐字节比对。任何不匹配都会跳转到J9356_BRANCH,跳过点亮LED的指令。
6. 移植、调试与常见问题排查
虽然这份代码是针对特定MCU和EEPROM的,但其思想可以移植到任何具有GPIO的微控制器上,如8051、PIC、AVR乃至STM32等。移植的关键在于理解并重现时序。
6.1 移植到其他平台的核心步骤
- 引脚重映射:将
CS、SK、DI、DO的定义改为新MCU的对应GPIO引脚。 - GPIO操作抽象:将
BSET(置位)、BCLR(清零)、BRCLR(判断)等位操作指令,替换为新MCU的GPIO库函数或寄存器直接操作(如GPIO_SetBits、GPIO_ResetBits、GPIO_ReadInputDataBit)。 - 延时调整:原代码依赖MCU指令周期产生时序,未使用软件延时循环。在移植到更高主频的MCU(如ARM Cortex-M)时,必须加入微秒级的延时(
__nop()或delay_us),以确保满足93C56数据手册中对SK时钟高低电平最小宽度、CS建立/保持时间等参数的要求。这是移植中最容易出错的地方。 - 看门狗处理:如果新平台没有看门狗或机制不同,需移除或替换
STA COPR这样的看门狗服务操作。 - 编译器与语法适配:将汇编指令改为C语言代码。位操作、循环、移位等逻辑需要对应转换。
6.2 调试技巧与常见问题
在调试这类Bit-Banging驱动时,逻辑分析仪或示波器是必不可少的工具。将四根信号线连接到仪器上,可以直观地看到波形是否符合93C56的时序图。
常见问题速查表:
| 问题现象 | 可能原因 | 排查思路 |
|---|---|---|
| 完全无法通信,读回全是0或0xFF | 1. 硬件连接错误(线接反、虚焊)。 2. CS信号未正确控制。3. DO引脚未加上拉电阻。4. 电源问题。 | 1. 用万用表检查连通性。 2. 用示波器看 CS信号是否在操作期间拉高。3. 确认 DO线有上拉。4. 测量VCC和GND电压是否稳定。 |
| 能写但读回数据错误 | 1. 时序不满足,特别是时钟频率太快或脉冲宽度不足。 2. 读数据时,未在发送地址后产生那个额外的“虚时钟”。 3. 读数据子程序中,位组装逻辑错误(C标志位处理)。 | 1.用逻辑分析仪捕获完整波形,与数据手册时序图对比,重点检查tSKH,tSKL,tDI SU等参数。2. 检查 J9356_READ子程序中,在调用J9356_RD_DATA前是否有SK脉冲。3. 单步调试,检查 DATA_H/L在J9356_RD_DATA中的变化。 |
| 写操作失败(验证读回不一致) | 1. 未发送EWEN指令或序列错误。2. 写周期等待时间不足( J9356_WAIT失效)。3. 电源电压在写操作期间跌落。 | 1. 确认每次写/擦除前都成功执行了EWEN。2. 检查 J9356_WAIT循环是否正常退出(SER_IN是否变高)。可尝试延长等待时间。3. 检查电源负载能力和去耦电容。 |
| 偶尔出现数据错误 | 1. 电磁干扰(EMI)。 2. 看门狗复位打断了长时间操作(如全擦除)。 3. 中断打断了Bit-Banging时序。 | 1. 优化布线,缩短信号线,增加滤波电容。 2. 在长耗时操作(如 WAIT)循环内确保定期“踢狗”。3.在Bit-Banging关键序列(发送/接收一位数据)期间,必须关闭全局中断。 |
独家避坑技巧:在C语言实现中,一个稳健的做法是将所有对
CS、SK、DI、DO引脚的操作封装成宏或内联函数。例如:#define EEPROM_CS_HIGH() GPIO_SetBits(EEPROM_PORT, CS_PIN) #define EEPROM_CS_LOW() GPIO_ResetBits(EEPROM_PORT, CS_PIN) #define EEPROM_SK_HIGH() GPIO_SetBits(EEPROM_PORT, SK_PIN) #define EEPROM_SK_LOW() GPIO_ResetBits(EEPROM_PORT, SK_PIN) #define EEPROM_DI_HIGH() GPIO_SetBits(EEPROM_PORT, DI_PIN) #define EEPROM_DI_LOW() GPIO_ResetBits(EEPROM_PORT, DI_PIN) #define EEPROM_DO_READ() GPIO_ReadInputDataBit(EEPROM_PORT, DO_PIN)这样不仅代码清晰,而且当需要更换引脚时,只需修改一处。另外,务必为每个
SK高低电平的变化之间插入微秒级的延时(Delay_us(1)),这是在高主频MCU上成功驱动低速外设的黄金法则。
通过透彻理解这份来自摩托罗拉的应用笔记,我们不仅获得了一个可用的93C56驱动,更重要的是掌握了一套在资源受限环境下,通过软件精确控制硬件时序的方法论。这种底层驾驭能力,是嵌入式工程师从“会用库”到“懂原理”的关键跨越。当你下次遇到一个没有硬件SPI却要驱动串行器件的问题时,希望这份详细的拆解能给你带来清晰的思路和足够的信心。