PIC18微控制器软件模拟SPI驱动EEPROM实战

张开发
2026/4/18 9:25:39 15 分钟阅读

分享文章

PIC18微控制器软件模拟SPI驱动EEPROM实战
1. PIC18微控制器与SPI EEPROM接口设计概述在嵌入式系统开发中非易失性存储器的使用几乎无处不在。Microchip的25XXX系列SPI EEPROM因其体积小、接口简单、可靠性高等特点成为许多嵌入式项目的首选存储方案。然而并非所有微控制器都配备硬件SPI模块或者在某些情况下硬件SPI引脚可能已被其他功能占用。这时通过GPIO模拟SPI协议的技术方案就显得尤为重要。我最近在一个工业传感器项目中就遇到了这种情况。项目使用的是PIC18F1220微控制器需要频繁记录传感器数据到25LC040 EEPROM中。由于硬件SPI模块已被其他外设占用最终选择了软件模拟SPI的方案。实测下来这种方案在10MHz系统时钟下能达到约500kHz的实际通信速率完全满足项目需求。2. 硬件连接与电路设计2.1 引脚连接方案根据Microchip官方应用笔记的建议PIC18F1220与25XXX系列EEPROM的标准连接方式如下PIC18F1220 25XXX系列EEPROM RB2(SCK) ---- SCK(6) RB1(SDO) ---- SI(5) RB0(SDI) ---- SO(2) RB3(CS) ---- CS(1) VCC ---- VCC(8), HOLD(7), WP(3) GND ---- VSS(4)注意WP(写保护)和HOLD(暂停)引脚需要通过10KΩ电阻上拉到VCC除非你需要使用这些功能。在我们的基础应用中通常保持写保护禁用状态。2.2 电源与去耦设计在实际电路布局中有几点需要特别注意在EEPROM的VCC引脚附近放置0.1μF的陶瓷去耦电容如果传输线较长(10cm)建议在SCK线上串联33Ω电阻以减少振铃避免将SPI信号线与高频或模拟信号线平行走线我在第一次设计时忽略了去耦电容结果在批量生产中出现约5%的设备偶发写入错误。后来在每片EEPROM的电源引脚添加104电容后问题完全解决。3. SPI协议软件模拟实现3.1 SPI模式配置25XXX系列EEPROM支持SPI模式0和模式3即时钟极性(CPOL)0时钟空闲时为低电平时钟相位(CPHA)0数据在时钟的第一个边沿采样对应的时序特性如下参数 最小值 典型值 最大值 SCK频率 0 - 3/20MHz CS到SCK建立时间 100ns - - SCK高低时间 100ns - - 数据保持时间 10ns - -3.2 GPIO模拟SPI的核心函数以下是使用C18编译器实现的关键函数// SPI初始化 void SPI_Init(void) { TRISBbits.TRISB0 1; // SDI输入 TRISBbits.TRISB1 0; // SDO输出 TRISBbits.TRISB2 0; // SCK输出 TRISBbits.TRISB3 0; // CS输出 LATBbits.LATB3 1; // CS初始高电平 LATBbits.LATB2 0; // SCK初始低电平 } // SPI单字节传输 uint8_t SPI_Transfer(uint8_t data) { uint8_t i, received 0; for(i0; i8; i) { LATBbits.LATB1 (data 0x80) ? 1 : 0; // 设置SDO data 1; __delay_us(1); // 满足建立时间 LATBbits.LATB2 1; // SCK上升沿 __delay_us(1); received 1; if(PORTBbits.RB0) received | 1; LATBbits.LATB2 0; // SCK下降沿 __delay_us(1); } return received; }提示__delay_us()依赖于系统时钟配置在使用前需正确设置_XTAL_FREQ宏。如果使用不同频率晶振需要调整延时参数以满足时序要求。4. EEPROM操作指令集实现4.1 基本指令框架所有EEPROM操作都遵循相同的基本流程拉低CS使能器件发送指令字节发送地址(1或2字节取决于容量)发送/接收数据拉高CS结束传输void EEPROM_WriteEnable(void) { LATBbits.LATB3 0; // CS低 SPI_Transfer(0x06); // WREN指令 LATBbits.LATB3 1; // CS高 __delay_us(10); } uint8_t EEPROM_ReadStatus(void) { uint8_t status; LATBbits.LATB3 0; // CS低 SPI_Transfer(0x05); // RDSR指令 status SPI_Transfer(0x00); LATBbits.LATB3 1; // CS高 return status; }4.2 字节写入操作字节写入是EEPROM最基本的操作时序要求最为严格void EEPROM_WriteByte(uint16_t addr, uint8_t data) { // 等待上次写入完成 while(EEPROM_ReadStatus() 0x01); // 发送写使能 EEPROM_WriteEnable(); // 开始写入序列 LATBbits.LATB3 0; // CS低 if(addr 0x100) { SPI_Transfer(0x02); // WRITE指令 SPI_Transfer((uint8_t)addr); // 地址低字节 } else { SPI_Transfer(0x02 | ((addr8) 0x01)); // 包含A8位 SPI_Transfer((uint8_t)addr); // 地址低字节 } SPI_Transfer(data); // 写入数据 LATBbits.LATB3 1; // CS高 // 等待写入完成 while(EEPROM_ReadStatus() 0x01); }常见问题写入操作后必须检查WIP(Write In Progress)位确保写入完成才能进行下一步操作。忽略这一步是新手最常见的错误。4.3 页写入操作页写入可以显著提高多字节写入效率但需注意页边界限制void EEPROM_PageWrite(uint16_t addr, uint8_t *data, uint8_t len) { uint8_t i; // 检查页边界 if(len 16) len 16; // 25LC040页大小为16字节 if((addr/16) ! ((addrlen-1)/16)) { len 16 - (addr % 16); // 调整长度不跨页 } // 等待上次写入完成 while(EEPROM_ReadStatus() 0x01); // 发送写使能 EEPROM_WriteEnable(); // 开始写入序列 LATBbits.LATB3 0; // CS低 if(addr 0x100) { SPI_Transfer(0x02); // WRITE指令 SPI_Transfer((uint8_t)addr); // 地址低字节 } else { SPI_Transfer(0x02 | ((addr8) 0x01)); // 包含A8位 SPI_Transfer((uint8_t)addr); // 地址低字节 } for(i0; ilen; i) { SPI_Transfer(data[i]); // 写入数据 } LATBbits.LATB3 1; // CS高 // 等待写入完成 while(EEPROM_ReadStatus() 0x01); }4.4 读取操作实现读取操作相对简单不需要写使能也没有延时要求uint8_t EEPROM_ReadByte(uint16_t addr) { uint8_t data; LATBbits.LATB3 0; // CS低 if(addr 0x100) { SPI_Transfer(0x03); // READ指令 SPI_Transfer((uint8_t)addr); // 地址低字节 } else { SPI_Transfer(0x03 | ((addr8) 0x01)); // 包含A8位 SPI_Transfer((uint8_t)addr); // 地址低字节 } data SPI_Transfer(0x00); // 读取数据 LATBbits.LATB3 1; // CS高 return data; } void EEPROM_SequentialRead(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t i; LATBbits.LATB3 0; // CS低 if(addr 0x100) { SPI_Transfer(0x03); // READ指令 SPI_Transfer((uint8_t)addr); // 地址低字节 } else { SPI_Transfer(0x03 | ((addr8) 0x01)); // 包含A8位 SPI_Transfer((uint8_t)addr); // 地址低字节 } for(i0; ilen; i) { buf[i] SPI_Transfer(0x00); // 连续读取 } LATBbits.LATB3 1; // CS高 }5. 高级功能与优化技巧5.1 写保护配置25XXX系列EEPROM提供了灵活的写保护机制可以通过状态寄存器配置void EEPROM_WriteStatus(uint8_t status) { // 等待上次写入完成 while(EEPROM_ReadStatus() 0x01); // 发送写使能 EEPROM_WriteEnable(); // 写入状态寄存器 LATBbits.LATB3 0; // CS低 SPI_Transfer(0x01); // WRSR指令 SPI_Transfer(status); LATBbits.LATB3 1; // CS高 // 等待写入完成 while(EEPROM_ReadStatus() 0x01); }状态寄存器各位含义位7-4: 保留(始终为0) 位3: BP1 - 块保护 位2: BP0 - 块保护 位1: WEL - 写使能锁存(只读) 位0: WIP - 写入进行中(只读)块保护组合BP1 BP0 | 保护范围 0 0 | 无保护 0 1 | 高1/4阵列保护 1 0 | 高1/2阵列保护 1 1 | 整个阵列保护5.2 性能优化技巧延时优化根据实际时序测量调整延时参数在可靠性和速度间取得平衡。例如在10MHz系统时钟下__delay_us(1)实际延时约3.6μs远高于要求的最小100ns可以适当减少。循环展开对SPI_Transfer函数进行循环展开可以提升约20%的速度uint8_t SPI_Transfer_Fast(uint8_t data) { uint8_t received 0; LATBbits.LATB1 (data 0x80) ? 1 : 0; data 1; LATBbits.LATB2 1; received | PORTBbits.RB0; LATBbits.LATB2 0; received 1; LATBbits.LATB1 (data 0x80) ? 1 : 0; data 1; LATBbits.LATB2 1; received | PORTBbits.RB0; LATBbits.LATB2 0; received 1; // ...重复6次... return received; }批量写入策略对于频繁的小数据写入可以设计RAM缓冲区积累到页大小后再一次性写入减少EEPROM写入次数延长寿命。6. 常见问题与调试技巧6.1 典型问题排查表现象可能原因解决方案无法写入数据1. 未发送WREN指令检查写使能流程2. WP引脚未上拉确保WP引脚通过10K上拉3. 块保护使能读取状态寄存器检查BP位读取数据全为0xFF1. CS信号问题用示波器检查CS时序2. 器件未正确供电检查VCC电压(2.5-5.5V)3. 器件损坏更换EEPROM芯片偶发数据错误1. 电源噪声增加去耦电容2. 未等待写入完成检查WIP位或增加足够延时3. 信号完整性差缩短走线或串联阻尼电阻6.2 调试工具推荐逻辑分析仪Saleae Logic系列或DSView配合廉价克隆探头可以完美解码SPI信号验证时序。示波器技巧触发模式设置为正常触发源选择CS信号时间基准调整到能显示完整传输帧(通常1-2μs/div)检查SCK占空比和数据建立/保持时间MPLAB ICD调试// 在关键位置添加调试输出 #define DEBUG_PRINT(msg) { \ printf([%s:%d] , __FILE__, __LINE__); \ printf msg; \ } // 示例使用 DEBUG_PRINT((Write addr0x%04X, data0x%02X\n, addr, data));6.3 可靠性增强措施写入验证重要数据写入后应立即读取验证错误计数记录写入失败次数超过阈值报警数据校验添加CRC校验或校验和磨损均衡对于频繁更新数据动态分配存储位置我在一个数据记录仪项目中实现了简单的磨损均衡算法将EEPROM寿命从约10万次提升到超过50万次写入。核心思路是使用地址偏移量轮询#define EEPROM_SIZE 512 #define RECORD_SIZE 16 static uint16_t write_index 0; void WriteRecord(uint8_t *data) { uint16_t addr write_index * RECORD_SIZE; if(addr RECORD_SIZE EEPROM_SIZE) { write_index 0; addr 0; } EEPROM_PageWrite(addr, data, RECORD_SIZE); write_index; // 验证写入 uint8_t buf[RECORD_SIZE]; EEPROM_SequentialRead(addr, buf, RECORD_SIZE); if(memcmp(data, buf, RECORD_SIZE) ! 0) { // 写入失败处理 Handle_WriteError(); } }7. 项目实战数据记录系统设计7.1 系统需求分析以一个工业温度记录仪为例每5分钟记录一次温度值(2字节)需要保存最近7天的数据工作环境温度-40℃~85℃电池供电要求低功耗计算存储需求每天记录次数 24×60/5 288次 7天记录量 288×7 2016条 每条记录占2字节共需要4032字节 选择25LC040(4Kbit512字节)需要8片7.2 硬件设计要点使用PIC18F1220的休眠模式降低功耗为每片EEPROM独立CS线(RB3~RB5RA0~RA3)添加I2C温度传感器(如MCP9808)设计电源管理电路不使用时切断EEPROM供电7.3 软件架构设计// 数据结构定义 typedef struct { uint16_t timestamp; // 分钟计数 int16_t temperature; // 0.1℃分辨率 } DataRecord; // 系统状态机 void System_Task(void) { static uint8_t state 0; switch(state) { case 0: // 初始化 SPI_Init(); TempSensor_Init(); state 1; break; case 1: // 等待采样间隔 if(timer_count 5*60) { timer_count 0; state 2; } break; case 2: // 采集温度 current_temp TempSensor_Read(); state 3; break; case 3: // 存储记录 DataRecord rec; rec.timestamp total_minutes; rec.temperature current_temp; EEPROM_WriteRecord(rec); state 1; break; } }7.4 低功耗优化在等待期间进入休眠模式void Enter_Sleep(void) { LATBbits.LATB3 1; // 取消选择所有EEPROM // 配置所有SPI引脚为输入 TRISBbits.TRISB0 1; TRISBbits.TRISB1 1; TRISBbits.TRISB2 1; SLEEP(); NOP(); // 唤醒后恢复 SPI_Init(); }降低工作电压在满足性能前提下使用3.3V供电可显著降低功耗。智能写入策略积累多次采样后一次性写入减少EEPROM激活时间。

更多文章