1. 项目概述为什么要在PIC18上模拟Microwire如果你手头的PIC18单片机项目需要存储一些配置参数、校准数据或者运行日志外挂一颗串行EEPROM电可擦可编程只读存储器是个非常经典且可靠的选择。在众多串行总线协议中除了大家耳熟能详的I²C和SPIMicrowire也是一种由美国国家半导体现已被TI收购推出的、结构简单且历史悠久的同步串行通信协议。它常见于一些老型号的EEPROM芯片上比如93C46、93C56、93C66系列。那么问题来了PIC18单片机本身可能并没有硬件Microwire控制器。为了驱动这颗EEPROM最直接、成本最低的方案就是“软件模拟”也就是用普通的GPIO口通过精确的时序控制来“扮演”Microwire协议的主机。这听起来像是“用软件造轮子”但在资源受限的单片机开发中这恰恰是体现工程师功力的地方。它避免了更换硬件或增加额外芯片的成本让你能充分利用手头已有的物料完成设计。整个项目的核心就是理解Microwire协议的“交通规则”然后用单片机的代码去精准地指挥那几个GPIO口的电平跳变完成数据的写入和读取。2. Microwire协议核心原理与时序拆解在动手写代码之前我们必须吃透协议。Microwire协议是一个全双工、同步串行协议通常需要三根线有时是四根。理解这几根线的作用和它们之间的时序关系是成功模拟的关键。2.1 信号线定义与角色分工典型的Microwire接口需要以下信号线CS (Chip Select片选) 主机控制。拉高时EEPROM被选中准备接收指令或数据拉低时EEPROM进入低功耗待机状态并忽略其他信号线上的任何变化。这是协议的门控信号。SK (Serial Clock串行时钟) 主机产生并输出给EEPROM。所有数据的输入和输出都严格以这个时钟的边沿为基准。它是整个通信节奏的“指挥棒”。DI (Data In数据输入) 主机输出EEPROM输入。用于主机向EEPROM发送指令码、地址和数据。DO (Data Out数据输出) EEPROM输出主机输入。用于EEPROM向主机回传数据或状态。注意有些EEPROM如93C46在特定封装下DO和DI是复用的通过一个引脚DI/DO分时进行输入输出。我们的模拟驱动需要能兼容这两种情况。2.2 指令集与通信帧格式Microwire EEPROM的操作是通过发送特定的指令来实现的。一个完整的操作周期始于CS的上升沿终于CS的下降沿。在这个周期内主机需要先发送一个指令字Instruction Word。一个标准的指令字通常由三部分组成起始位 (Start Bit) 固定为逻辑‘1’。它告诉EEPROM一个指令周期开始了。操作码 (Opcode) 2位定义要执行的操作如读、写、擦除等。地址 (Address) 位数取决于EEPROM的容量。例如93C461Kbit128x8或64x16需要7位或6位地址。以93C46在8位组织模式128x8下的“读”指令为例其指令字结构为1起始位 10读操作码 A6 A5 A4 A3 A2 A1 A07位地址。主机需要将这个10位的指令字从高位MSB到低位LSB在SK时钟的上升沿或下降沿具体看芯片手册通过DI线一位一位地发送出去。发送完指令字后对于读操作EEPROM会紧接着在接下来的时钟周期里从DO线将指定地址的数据8位从高位到低位输出。对于写操作主机在发送完指令字后需要紧接着发送要写入的8位数据。2.3 关键时序参数与“软件模拟”的挑战这是软件模拟的精髓所在也是最容易出错的地方。我们必须严格遵守芯片数据手册Datasheet中规定的时间参数。以Microchip的93AA46为例几个关键参数如下tSKL (SK Clock Pulse Width Low/High) SK时钟低电平和高电平的最小持续时间。比如典型值为250ns。这意味着我们在翻转SK引脚电平后必须用延时程序确保低电平或高电平保持足够长的时间。tCSS (CS Setup Time) 在发送第一个SK时钟之前CS必须保持高电平的最小时间。例如100ns。tCSH (CS Hold Time) 在最后一个SK时钟之后CS必须保持高电平的最小时间。例如100ns。tDI SU (DI Setup Time)和tDI H (DI Hold Time) 数据DI在SK时钟有效边沿前后的建立和保持时间。这意味着我们需要在翻转SK时钟之前提前设置好DI的数据并保持稳定在SK时钟翻转之后DI数据还需再保持一段时间。tDO V (DO Valid Time) SK时钟有效边沿之后DO数据有效的最长时间。我们的程序必须在这个时间之后再去读取DO引脚的状态才能得到正确的数据。软件模拟的挑战就在于我们需要用单片机的普通GPIO和循环延时或定时器来满足这些纳秒级或微秒级的时序要求。PIC18单片机在20MHz主频下一个指令周期是200ns这为我们用软件循环实现精确延时提供了基础但同时也要求代码必须高度优化避免因函数调用、中断干扰等因素引入不可控的时序抖动。注意不同品牌、不同型号的Microwire EEPROM其指令集细节、时序参数甚至信号有效边沿是SK上升沿采样还是下降沿采样都可能不同。驱动开发的第一步永远是仔细阅读你手头那颗EEPROM的官方数据手册这是唯一权威的参考。3. PIC18单片机软件模拟驱动设计与实现有了理论武装我们就可以开始设计驱动了。我们的目标是编写一个可靠、清晰且易于移植的驱动模块。3.1 硬件连接与GPIO初始化假设我们使用PIC18F45K20并选择以下引脚连接RB0作为CS输出RB1作为SK输出RB2作为DI输出RB3作为DO输入配置为上拉输入因为Microwire的DO在空闲时通常为高阻态启用内部上拉可以保证稳定的空闲电平首先在初始化函数中配置这些引脚的方向和初始状态。// Microwire引脚定义 #define MW_CS LATBbits.LATB0 #define MW_SK LATBbits.LATB1 #define MW_DI LATBbits.LATB2 #define MW_DO PORTBbits.RB3 // 引脚方向寄存器 #define MW_CS_TRIS TRISBbits.TRISB0 #define MW_SK_TRIS TRISBbits.TRISB1 #define MW_DI_TRIS TRISBbits.TRISB2 #define MW_DO_TRIS TRISBbits.TRISB3 void MICROWIRE_Init(void) { // 配置CS, SK, DI为输出并初始化为低电平 MW_CS_TRIS 0; MW_SK_TRIS 0; MW_DI_TRIS 0; MW_CS 0; MW_SK 0; MW_DI 0; // 配置DO为输入并启用内部上拉如果MCU支持 MW_DO_TRIS 1; // 假设PIC18F45K20的PORTB有弱上拉需全局启用 INTCON2bits.RBPU 0; // 使能PORTB弱上拉 }3.2 核心底层时序函数为了实现精确延时我们需要一个微秒级的延时函数。可以使用内置的__delay_us()宏依赖于_XTAL_FREQ定义或者为了更精确的控制编写基于指令周期的空循环延时。#define _XTAL_FREQ 20000000 // 20MHz // 一个简单的微秒延时函数实际精度需校准 static void mw_delay_us(unsigned int us) { while(us--) { __delay_us(1); // 编译器内置宏 } } // 更精确的短延时用于满足tSKL等参数 static void mw_short_delay(void) { // 根据主频计算出的空循环实现约250ns延时 // 例如NOP(); NOP(); NOP(); NOP(); _asm nop _endasm _asm nop _endasm _asm nop _endasm _asm nop _endasm }3.3 位读写与指令/数据收发函数这是驱动层最核心的部分。我们需要实现两个基本函数mw_send_bit()和mw_read_bit()并在此基础上构建发送指令字和接收数据字节的函数。关键点在于确定采样边沿。假设我们的EEPROM在SK的上升沿采样DI数据并在SK的上升沿更新DO数据。那么模拟过程如下// 发送一位数据bit到EEPROM static void mw_send_bit(uint8_t bit) { // 1. 准备数据在SK上升沿到来之前提前设置好DI电平 MW_DI (bit ! 0); mw_short_delay(); // 满足tDI_SU (数据建立时间) // 2. 产生SK上升沿 MW_SK 1; mw_short_delay(); // 满足tSKH (SK高电平时间) // 3. 产生SK下降沿完成一个时钟周期 MW_SK 0; // 这里可以加一个短延时但DI数据需在下降沿后继续保持tDI_H时间 // 由于我们紧接着会设置下一个bit的DI值所以通常能满足 } // 从EEPROM读取一位数据 static uint8_t mw_read_bit(void) { uint8_t bit_val; // 1. 产生SK上升沿EEPROM会在此后将下一位数据放到DO线上 MW_SK 1; mw_short_delay(); // 满足tSKH并等待tDO_VDO数据有效 // 2. 在SK高电平期间读取DO引脚状态 bit_val MW_DO; // 3. 产生SK下降沿为下一个周期做准备 MW_SK 0; mw_short_delay(); // 满足tSKL return (bit_val ! 0); } // 发送一个字节8位指令或数据MSB first static void mw_send_byte(uint8_t data) { for(int8_t i 7; i 0; i--) { mw_send_bit((data i) 0x01); } } // 接收一个字节8位数据MSB first static uint8_t mw_receive_byte(void) { uint8_t data 0; for(int8_t i 7; i 0; i--) { if(mw_read_bit()) { data | (1 i); } } return data; }3.4 高层应用函数封装基于底层的位操作函数我们可以封装出用户友好的应用层函数如MW_ReadByte,MW_WriteByte,MW_WriteEnable等。#define MW_OPCODE_READ 0x02 // 假设操作码定义需根据实际芯片调整 #define MW_OPCODE_WRITE 0x01 #define MW_OPCODE_EWEN 0x00 // 写使能指令示例 uint8_t MW_ReadByte(uint8_t addr) { uint8_t data; // 启动传输 MW_CS 1; __delay_us(1); // 满足tCSS // 发送读指令字 (1 操作码 地址) mw_send_bit(1); // 起始位 mw_send_bit((MW_OPCODE_READ 1) 1); // 操作码高位 mw_send_bit(MW_OPCODE_READ 1); // 操作码低位 // 发送地址7位 for(int8_t i 6; i 0; i--) { mw_send_bit((addr i) 0x01); } // 接收数据字节 data mw_receive_byte(); // 结束传输 __delay_us(1); // 满足tCSH MW_CS 0; return data; } void MW_WriteByte(uint8_t addr, uint8_t data) { // 1. 发送写使能指令如果需要 MW_WriteEnable(); // 2. 启动传输发送写指令字和数据 MW_CS 1; __delay_us(1); mw_send_bit(1); // 起始位 mw_send_bit((MW_OPCODE_WRITE 1) 1); mw_send_bit(MW_OPCODE_WRITE 1); // 发送地址 for(int8_t i 6; i 0; i--) { mw_send_bit((addr i) 0x01); } // 发送数据 mw_send_byte(data); __delay_us(1); MW_CS 0; // 3. 等待写入完成Polling MW_WaitForWriteComplete(); }MW_WaitForWriteComplete()函数通常通过发送一个“读状态”指令如果芯片支持或简单地延时一个芯片手册规定的“页写周期时间”tWR典型值3-10ms来实现。4. 驱动开发中的关键调试技巧与避坑指南软件模拟协议驱动调试起来往往比硬件外设更棘手因为时序问题非常隐蔽。下面分享几个我实践中总结的关键技巧和常见坑点。4.1 调试工具与手段逻辑分析仪是必需品 没有逻辑分析仪调试这种时序驱动的难度会呈指数级上升。一个哪怕是最基础的8通道逻辑分析仪配合Sigrok/PulseView软件也能让你清晰地看到CS、SK、DI、DO四根线上的波形。你可以直接测量tSKH、tSKL、tDI_SU等时间参数与数据手册对比一目了然。示波器辅助看噪声 逻辑分析仪看逻辑示波器看模拟质量。如果读写不稳定可以用示波器检查电源纹波、信号线上的振铃或过冲。在SK或CS线上串联一个22-100欧姆的小电阻有时能有效改善信号完整性。软件仿真 在MPLAB X IDE或类似环境中进行软件仿真单步执行代码观察GPIO寄存器的变化可以帮助理解代码流程但无法替代真实硬件的时序测试。4.2 常见问题排查速查表现象可能原因排查思路与解决方案完全无法通信读回全是0xFF或0x001. 硬件连接错误线接反、虚焊。2. 电源问题EEPROM未供电或电压不足。3. CS信号时序错误tCSS/tCSH不满足。4. 指令字格式错误起始位、操作码、地址位宽不对。1. 用万用表检查连通性和电源电压。2. 用逻辑分析仪抓取完整通信波形首先检查CS信号宽度和SK时钟是否存在。3. 核对数据手册确认芯片的组织模式8位/16位和指令集逐位比对发送的指令字。偶尔能读写但数据错误或不稳定1. 时序参数处于临界状态延时刚刚满足最小值受温度、电压波动影响。2. 中断干扰在位读写函数中被高优先级中断打断。3. 信号完整性差长线、无阻抗匹配导致边沿畸变。4. DO引脚未正确配置如应上拉却未启用。1.增加时序裕量将所有软件延时如mw_short_delay适当加长20%-50%。这是最有效的手段之一。2.关闭全局中断在MW_ReadByte和MW_WriteByte等关键函数的首尾使用GIE 0;和GIE 1;禁用中断。3. 检查PCB走线缩短连接距离在信号线上串联小电阻。写操作成功但读回数据不对或无法写入1. 写使能EWEN指令未发送或格式错误。2. 写周期等待时间tWR不足在写入完成前就进行了读操作。3. 页写边界处理错误跨页写入未分两次操作。1. 确认写操作前必须发送正确的写使能指令序列。2. 在MW_WriteByte函数后增加足够长的延时如10ms或实现状态轮询如果芯片支持。3. 查阅手册明确芯片的“页”大小如93C46是16字节一页在驱动中实现地址检查避免跨页连续写。驱动在仿真时正常烧录后不正常1. 编译器优化级别影响高优化可能删除或重排延时循环。2. 系统时钟配置与实际不符_XTAL_FREQ宏定义错误。1. 将关键的延时函数和位操作函数声明为static或使用volatile关键字修饰循环变量防止被优化。2. 检查配置位Configuration Bits中关于振荡器的设置确保与代码中定义的时钟频率一致。用示波器测量一下实际的系统时钟。4.3 提升驱动鲁棒性与可移植性的技巧将时序参数宏定义 不要将延时时间硬编码在函数里。在头文件中用宏定义所有关键时序如#define T_SK_HIGH_US 0.5这样更换不同型号的EEPROM时只需修改头文件即可。// microwire_cfg.h #define MW_T_CSS_US 1.0 // CS建立时间 #define MW_T_CSH_US 1.0 // CS保持时间 #define MW_T_SK_HALF_US 0.25 // SK半周期用于产生上升沿和下降沿的间隔抽象硬件层 将MW_CS1、MW_DI0等具体引脚操作封装成宏或函数例如MW_SET_CS_HIGH()。当需要更换单片机引脚时只需修改这一层抽象上层应用代码完全不动。添加超时机制 在MW_WaitForWriteComplete函数中不要无限等待。可以设置一个超时计数器比如循环检查5000次超时后返回错误标志避免程序死锁。编写完备的测试程序 开发一个测试函数顺序执行“写全0xAA-读回校验-写全0x55-读回校验”的操作。这是快速验证驱动基本功能是否正常的最佳方法。5. 软件模拟驱动的性能考量与替代方案评估虽然软件模拟提供了最大的灵活性但它并非没有代价。我们需要客观评估其优缺点以决定在当前项目中是否是最佳选择。5.1 软件模拟的优缺点分析优点成本为零 无需额外的硬件芯片充分利用MCU的GPIO。灵活性极高 可以模拟几乎任何同步串行协议不受MCU硬件外设的限制。引脚可任意分配 可以根据PCB布局灵活选择空闲的GPIO。缺点占用CPU资源 在读写数据期间CPU被完全占用特别是如果禁用了中断无法处理其他任务不适合实时性要求高的系统。时序精度依赖主频 延时基于指令周期系统主频变化如切换节能模式会破坏时序导致通信失败。代码复杂度与维护成本 需要自己实现所有底层时序代码量较大调试耗时。速度较慢 软件循环产生的时钟频率远低于硬件SPI可能只有几十到几百KHz不适合大数据量传输。5.2 替代方案使用硬件SPI模块模拟许多PIC18单片机自带硬件SPIMSSP模块。虽然Microwire和SPI协议并不完全相同但二者都是同步串行协议且很多Microwire EEPROM可以兼容SPI的“模式0”CPOL0 CPHA0。在这种情况下我们可以尝试用硬件SPI来驱动。操作步骤硬件连接 将MCU的SPI SCK接EEPROM的SKSPI MOSI接DISPI MISO接DO。CS引脚仍然用普通GPIO控制。配置SPI 将SPI配置为主机模式时钟极性CPOL0空闲时为低电平时钟相位CPHA0数据在SCK上升沿采样。这与之前假设的Microwire时序一致。驱动修改 发送指令字和数据时不再使用软件位操作而是调用SPI的字节发送函数如WriteSPI()。接收数据时通过发送哑元数据Dummy Byte如0xFF来触发SPI时钟从而读取MISO上的数据。优势 通信速度大幅提升可达MHz级别且CPU占用率极低发送/接收由硬件完成CPU可处理中断或其它任务。挑战 需要确认EEPROM是否完全兼容SPI模式0。有些Microwire芯片的指令字长度不是8的整数倍如10位用硬件SPI发送时需要做特殊处理例如先发送指令字的高8位再处理剩下的位这会增加一些软件复杂性。5.3 项目选型建议选择软件模拟如果 项目成本极其敏感所使用的MCU没有空闲的硬件SPI需要驱动的Microwire设备协议特殊与SPI兼容性差数据读写频率很低如几分钟存一次数据对CPU占用不敏感。优先尝试硬件SPI模拟如果 MCU有硬件SPI且引脚允许EEPROM数据手册明确支持SPI模式0或经测试可以兼容系统对通信速度或CPU效率有要求。在我最近的一个温湿度记录仪项目中我选择了软件模拟。因为该产品使用的PIC18F型号的硬件SPI已被用于连接RF模块而EEPROM仅用于每小时存储一次校准数据数据量极小。软件模拟驱动稳定可靠地运行了数年充分证明了在合适的场景下这是一个简洁高效的解决方案。关键在于你对协议的理解要足够深对时序的控制要足够细并且留出充足的调试和测试时间。