ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32H743驱动EEPROM:寄存器库I2C读写与GPIO模拟实战解析

STM32H743驱动EEPROM:寄存器库I2C读写与GPIO模拟实战解析 简介本资源是一套面向嵌入式开发工程师与STM32进阶学习者的EEPROM驱动实践方案聚焦STM32H7系列尤其H743在无外部EEPROM芯片时利用片内Flash模拟EEPROM的寄存器级实现。它解决了非易失数据存储中擦写控制、状态校验、跨型号移植等关键工程问题适用于工业控制参数保存、设备配置固化等典型场景。压缩包含150个文件以86个头文件.h定义寄存器映射与接口函数和58个源文件.c涵盖Flash操作、读写封装、错误处理及H7系列通用适配逻辑为主体辅以Keil工程配置.uvprojx/.uvoptx、启动代码.s、固件库.lib及可执行镜像.hex总大小832KB。已有128人下载学习提供完整可编译工程支持Keil/IAR/STM32CubeIDE直接构建代码结构清晰分层包含Flash_ACR/KEYR/SR/CR等核心寄存器操作封装、擦写状态轮询机制、OPERR/WR_PG_ERR等错误标志解析并预留DMP、LCD、LTDC等外设协同接口便于集成到复杂系统中。STM32H743驱动EEPROM基于寄存器库的I2C读写方案完整复盘如果你正在STM32H7系列单片机上做数据存储大概率会遇到一个绕不开的需求把掉电不能丢的参数、校准值、运行日志存到外部EEPROM里。网上搜H743驱动EEPROM的资料不少但大多数是HAL库版本的要么代码臃肿、要么移植到H750和H723时改来改去很麻烦。这篇文章我边做边写把手头这套基于寄存器库寄存器直接操作的I2C读写EEPROM驱动完整梳理一遍包含引脚设计、时序实现、24C02读写协议、实测结果和踩过的坑适合正在做H7系列裸机或RTOS项目的开发者参考。先说结论这套驱动我在STM32H743上配合24C02跑通了单字节读写、连续读写、页写和随机读都验证过稳定性没问题。文章里给出的代码可以直接抄但如果只看完不搞清楚I2C时序和EEPROM协议换一颗芯片、换一个引脚照样会翻车。所以我会把每一步为什么这么写也讲明白。1. 寄存器库方案在H7平台上为什么值得选1.1 H743的存储体系决定了外挂EEPROM是个必然选项STM32H743内置的Flash虽然有2MB但它的擦写次数上限是1万次左右而且按扇区擦除动辄几十KB起步。如果项目里要频繁修改一些配置参数比如PID系数、设备地址、校准系数直接写内部Flash会面临两个问题一是寿命不够一天写几十次一年多就把Flash写废了二是Flash擦写要阻塞式等待而且不能字节级改写改一个字节得先读出一整页、擦除、再写回。所以不少H7方案里都会外挂一颗EEPROM常见的是24C02、24C64这类I2C接口芯片。24C系列的优势就在这儿字节级擦写、100万次擦写寿命、5ms以内的写周期、掉电数据保留100年。H743负责跑主逻辑参数和小批量数据交给EEPROMFlash只管固件和不太频繁升级的大块数据各司其职。1.2 寄存器库与HAL库、标准库的取舍逻辑很多人在H7上拿到工程第一件事就是打开STM32CubeMX点一点生成HAL库工程。这样做开发效率确实高但代价是代码量和执行效率都不尽如人意。HAL库的I2C驱动函数一层套一层HAL_I2C_Mem_Write进去之后要过超时判断、状态机切换、中断回调光函数调用栈就有四五层。在需要频繁读写的场景下这种抽象带来的时延和代码体积并不划算。寄存器库驱动的思路完全不同不依赖HAL层的状态机和抽象接口直接操作外设寄存器或者直接用GPIO模拟时序。执行路径最短一个字节发送就是几条寄存器操作指令。调试的时候你能非常清楚地看到总线上每一拍发生了什么不会被库函数包装挡住。这对学习I2C协议本身也更有帮助——你能完全掌控时序细节。注意这里说的“寄存器库驱动”本质上是两种做法的统称。一种是用H7的硬件I2C外设寄存器操作另一种是用GPIO模拟I2C时序。对于EEPROM这种低速器件我更推荐GPIO模拟原因下面详细说。1.3 硬件I2C外设的坑与GPIO模拟的可靠性对比STM32H7的硬件I2C其实和F1系列完全不是一回事。H7用的是全新的I2C IP功能更强支持Fast-mode Plus、SMBus、时间戳等等但Bug也比老平台更刁钻。我刚开始尝试直接用硬件I2C寄存器版本发现SCL/SDA的时序配置参数PRESC、SCLDEL、SDADEL如果不小心配错设备地址直接发不出去还有总线忙标志BUSY在异常停止后不会自动清一旦总线卡死整个I2C外设就锁住了必须复位外设才能恢复。GPIO模拟I2C完全没有这个问题。两根线用开漏输出加上拉通过翻转GPIO寄存器来产生时序主动权完全在自己手里。对24C02这种最高时钟1MHz的设备来说GPIO翻转速度绰绰有余。而且GPIO模拟代码可以在H743、H750、H723、H725之间无缝移植只需要改引脚定义和时钟使能不用管外设寄存器差异。相比之下HAL库的I2C驱动在不同系列之间有时配置项都对不上。我之前在某篇帖子里看到有同行用H743硬件I2C中断方式跑24C256共享同一套总线还要挂多个从机结果调试了两周最后发现是SMBus超时逻辑一直在误触发。你要是遇到类似的情况我的建议很直接低速从机一律GPIO模拟别折腾硬件I2C那套智能功能了。2. 驱动设计前的硬件准备与引脚规划2.1 最小硬件连接与上拉电阻怎么选EEPROM驱动的硬件连接非常简单以24C02和STM32H743为例信号EEPROM引脚STM32H743引脚备注SDA5PB7开漏输出外部上拉SCL6PB6开漏输出外部上拉A0/A1/A21/2/3GND设备地址全为0WP7GND写保护关闭VCC83.3V加100nF去耦电容GND4GND共地上拉电阻的选择有个细节。24C02的I2C总线要求上升时间在快速模式400kHz下不超过300ns标准模式100kHz下不超过1000ns。上拉电阻太大上升沿太慢时序就测不过太小灌电流超了引脚可能扛不住。3.3V供电下我实测2.2kΩ到4.7kΩ都能正常工作建议用4.7kΩ起步信号线长了再换2.2kΩ。如果板上还有其他I2C设备并联上拉电阻的计算要考虑所有设备的输入电容具体公式是Rp(max) 1μs / (Cbus × 0.8473)Cbus是总线上所有电容之和自己算一下比较稳。2.2 STM32H743的GPIO时钟配置细节H7系列GPIO的初始化比F1复杂一些主要体现在时钟使能上。H743的GPIO挂在AHB4总线上使能GPIOB时钟要操作RCC-AHB4ENR寄存器。同时如果代码在外部中断里使用I2C还要单独使能SYSCFG时钟这个后面在坑的部分详细说。GPIO配置成开漏时有个容易忽略的点——开漏输出模式下输出数据寄存器ODR必须写1否则引脚会一直输出低电平把总线拉死。很多人在F103上习惯了先配置复用功能再开I2C外设往往没注意到这个细节。模拟I2C里把引脚设成开漏输出后务必将对应ODR位置1这样引脚实际上处于高阻状态靠外部上拉到高电平才能正常通信。还有一个问题是内部上拉要不要使能。如果板上已经加了外部上拉电阻内部上拉可以不开。但如果你临时搭电路、没来得及焊上拉电阻可以把内部上拉打开应急使用。注意内部上拉和外部4.7kΩ并联之后的等效阻值会偏低高速通信时可能让信号边沿变差所以正式产品里我通常关掉内部上拉只用外部上拉。3. I2C总线寄存器级驱动的核心实现这部分的代码是整个驱动的基石全部基于寄存器直接操作不依赖任何HAL库函数。我用的是PB6/PB7读者换成其他引脚时只需要修改端口和引脚号宏。3.1 引脚宏定义与基础时序原语GPIO模拟I2C最关键的是把SCL和SDA的拉高、拉低封装成原语函数并且要把延时函数做到可控。#define I2C_GPIO_PORT GPIOB #define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PIN GPIO_PIN_7 #define SCL_H() (I2C_GPIO_PORT-BSRR I2C_SCL_PIN) #define SCL_L() (I2C_GPIO_PORT-BSRR (uint32_t)I2C_SCL_PIN 16) #define SDA_H() (I2C_GPIO_PORT-BSRR I2C_SDA_PIN) #define SDA_L() (I2C_GPIO_PORT-BSRR (uint32_t)I2C_SDA_PIN 16) #define SDA_READ() ((I2C_GPIO_PORT-IDR I2C_SDA_PIN) ? 1 : 0) static void i2c_delay_us(uint32_t us) { uint32_t count us * (SystemCoreClock / 1000000U) / 4U; while (count--) { __NOP(); } }BSRR寄存器的用法值得解释一下。BSRR低16位写1对应引脚置高高16位写1对应引脚置低两条操作互不影响而且写0不会有任何效果。用BSRR而不是ODR的好处是单个寄存器操作就能改变引脚状态不需要读-改-写从执行效率上说更优也不存在被中断打断导致输出错误的风险。I2C这种时序敏感的场景安全第一。延时函数的计算逻辑是SystemCoreClock是H743的主频480MHz除以1000000得到每秒有多少个周期再乘以微秒数就得到需要的周期数。再除以4是因为__NOP()空指令在Cortex-M7上大约4个周期执行一跳。注意这个4是经验值编译器优化等级不同可能有差异所以用的时候最好用逻辑分析仪实际测一下。我在-Ofast优化下测试这个除法系数是准的。3.2 起始停止与字节收发逻辑I2C的时序协议里起始条件START是SCL高电平时SDA从高变低停止条件STOP是SCL高电平时SDA从低变高。这个顺序反了总线会识别错误。void i2c_start(void) { SDA_H(); SCL_H(); i2c_delay_us(5); SDA_L(); i2c_delay_us(5); SCL_L(); } void i2c_stop(void) { SDA_L(); SCL_H(); i2c_delay_us(5); SDA_H(); i2c_delay_us(5); } uint8_t i2c_send_byte(uint8_t data) { uint8_t i; uint8_t ack; for (i 0; i 8; i) { if (data 0x80) { SDA_H(); } else { SDA_L(); } data 1; i2c_delay_us(2); SCL_H(); i2c_delay_us(2); SCL_L(); i2c_delay_us(2); } SDA_H(); // 释放SDA从机控制应答 i2c_delay_us(2); SCL_H(); i2c_delay_us(2); ack SDA_READ(); // 读取应答位 SCL_L(); i2c_delay_us(2); return ack ? 1 : 0; // 返回1表示无应答0表示有应答 } uint8_t i2c_recv_byte(uint8_t ack) { uint8_t i; uint8_t data 0; SDA_H(); // 释放SDA让从机驱动 for (i 0; i 8; i) { data 1; SCL_H(); i2c_delay_us(2); if (SDA_READ()) { data | 0x01; } SCL_L(); i2c_delay_us(2); } if (ack) { SDA_L(); // 主机发送应答 } else { SDA_H(); // 主机发送非应答 } i2c_delay_us(2); SCL_H(); i2c_delay_us(2); SCL_L(); i2c_delay_us(2); SDA_H(); return data; }发送字节返回值的定义这里埋了个约定i2c_send_byte返回1表示无应答返回0表示有应答。这个设计是为了写EEPROM的时候能直接循环检测设备忙的时候无应答返回1继续轮询设备空闲时有应答返回0跳出循环。代码逻辑简洁不需要反相判断算是一个实用的小设计。i2c_recv_byte最后一个参数ack的含义也要注意向从机发送应答位时传1发送非应答位时传0。很多人容易搞反——“主机接收完最后一个字节应该发NACK”这句话是对的但在代码里对应的是调用i2c_recv_byte(0)因为最后一个字节不需要从机继续发数据主机回一个非应答告诉它“我读够了”这个逻辑要理清楚。4. 24C02的读写协议层实现底层GPIO时序搞定之后EEPROM的读写协议就变成了单纯的通信逻辑问题跟H7是哪个型号没关系了。这一层主要解决三个问题设备地址怎么拼、读写的命令序列是什么、跨页边界怎么处理。4.1 设备地址构造与常用的地址宏24C02的设备地址是高7位固定的1010开头加上A2A1A0三个引脚组成的硬件地址最后一位是读写控制位。我的连接方案里A2A1A00所以设备地址是0xA0写和0xA1读。#define EEPROM_DEV_ADDR_W 0xA0 #define EEPROM_DEV_ADDR_R 0xA1 #define EEPROM_PAGE_SIZE 8 #define EEPROM_SIZE 256注意24C02的容量是256字节对应的内部字地址是8位。如果你换用24C64容量是8KB字地址需要16位发送地址时得多发一个字节命令序列完全不同。4.2 写操作单字节写、页写与跨页拆分写操作的完整时序是主机发START然后发设备地址写位0xA0等从机应答发字地址等应答发数据等应答最后发STOP。看到这里你会发现EEPROM内部收到命令之后并不是立刻把数据写进存储单元的它先把数据锁存到页缓冲然后内部定时器启动编程这个过程一般需要5ms。单字节写实现如下void eeprom_write_byte(uint16_t addr, uint8_t data) { i2c_start(); i2c_send_byte(EEPROM_DEV_ADDR_W); i2c_send_byte((uint8_t)addr); i2c_send_byte(data); i2c_stop(); eeprom_wait_ready(); }页写和单字节写的区别在于只需要发送一次设备地址和字地址然后可以连续发送最多8个字节24C02页大小是8字节从机内部会自动把页缓冲里的数据一次性写入存储阵列效率比逐字节写高很多。但这里有个大坑——页写不能跨页边界。因为字地址的低3位到达页边界7之后再加1会回卷到页起始位置如果中间某次写操作跨页了数据不会写到下一页而是覆盖当前页开头的数据。所以在提供连续写函数时我做了跨页拆分的处理void eeprom_write_bytes(uint16_t addr, uint8_t *buf, uint16_t len) { uint16_t offset 0; while (len 0) { uint8_t page_remain EEPROM_PAGE_SIZE - (addr % EEPROM_PAGE_SIZE); uint8_t chunk (len page_remain) ? len : page_remain; i2c_start(); i2c_send_byte(EEPROM_DEV_ADDR_W); i2c_send_byte((uint8_t)(addr 0xFF)); for (uint8_t i 0; i chunk; i) { i2c_send_byte(buf[offset i]); } i2c_stop(); eeprom_wait_ready(); addr chunk; offset chunk; len - chunk; } }这个函数的逻辑是每次发送前先计算当前页剩余空间如果剩余空间不够本次要写的长度就只写到页边界为止然后等写周期结束再从新页继续。addr % EEPROM_PAGE_SIZE算的是当前地址在页内的偏移EEPROM_PAGE_SIZE - 页内偏移就是离页边界还剩多少字节。这样无论从哪里开始写、写多长都不会踩到页回卷的坑。4.3 写周期等待的两种策略与推荐做法EEPROM写入一次需要5ms这期间不接受任何新的写命令。等待写周期结束有两种做法第一种是固定延时写完就delay_ms(5)。简单粗暴但问题是如果连续写很多字节固定延时浪费的时间相当可观。而且不同批次、不同温度下EEPROM的实际写时间会有差异固定延时要么偏保守浪费性能要么偏激进导致写失败。第二种是ACK轮询也就是我之前在i2c_send_byte里埋的那个返回值。EEPROM在内部编程期间不会对主机发出的设备地址做出应答。所以写完数据后可以反复发START设备地址直到收到应答为止void eeprom_wait_ready(void) { uint16_t timeout 1000; i2c_start(); while (i2c_send_byte(EEPROM_DEV_ADDR_W) ! 0) { if (--timeout 0) { break; // 超时保护防止死循环 } } i2c_stop(); }这个函数在主循环里会持续重试最多尝试1000次每次重试大约耗时几十微秒。EEPROM写完需要5ms而一次探测只要几十微秒所以实际等待的时间非常接近真实写周期效率比固定延时高一个量级。另外这里加了超时保护万一从机异常挂死也不会让程序卡在死循环里。4.4 读操作当前地址读与随机读的完整实现读操作分两种当前地址读和随机读。当前地址读是直接从EEPROM内部地址指针指向的位置读一个字节不需要发送字地址。每次读写操作之后内部地址指针会自动加1。在连续读场景里这个特性可以大大简化逻辑。随机读则需要先发一个伪写入操作来加载字地址发START发设备地址写位发字地址然后重新发START发设备地址读位最后读数据。注意这个伪写操作不需要发STOP直接连接REPEATED START即可。uint8_t eeprom_read_byte(uint16_t addr) { uint8_t data; i2c_start(); i2c_send_byte(EEPROM_DEV_ADDR_W); // 伪写加载字地址 i2c_send_byte((uint8_t)addr); i2c_start(); // REPEATED START i2c_send_byte(EEPROM_DEV_ADDR_R); data i2c_recv_byte(0); // 主机发送NACK i2c_stop(); return data; }随机读里的REPEATED START和STOPSTART有什么区别微妙但关键REPEATED START期间总线没有被释放主机一直保持着总线所有权。STOPSTART之间总线有一段空闲时间总线所有权可能被其他主机抢走。在多主机系统里随机读必须用REPEATED START保持原子性。模拟I2C里这两者就差一个i2c_start()的调用时机但协议上的意义完全不同。连续读的终止条件同样要小心。EEPROM在读到最后一个字节地址255之后会回卷到0所以如果读的长度超过剩余地址空间数据会从头开始。连续读的最后一个字节必须发NACK之前的字节都要发ACK否则EEPROM会认为主机没读完继续输出数据导致总线时序混乱。实际实现时循环里判断是否最后一字节即可void eeprom_read_bytes(uint16_t addr, uint8_t *buf, uint16_t len) { i2c_start(); i2c_send_byte(EEPROM_DEV_ADDR_W); i2c_send_byte((uint8_t)addr); i2c_start(); i2c_send_byte(EEPROM_DEV_ADDR_R); for (uint16_t i 0; i len; i) { uint8_t ack_flag (i len - 1) ? 0 : 1; buf[i] i2c_recv_byte(ack_flag); } i2c_stop(); }这段代码里最后一个字节明确传了0NACK前面的都传1ACK读多字节时就非常安全。5. 驱动初始化与API封装底层时序和EEPROM协议都就绪之后最后一步是把它们封装成上层可以直接调用的API并完成GPIO初始化。这一层解决的问题是上层逻辑不用关心I2C时序、不用关心EEPROM协议只管“往哪个地址写什么数据”就行。void eeprom_init(void) { GPIO_InitTypeDef gpio_init {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio_init.Pin I2C_SCL_PIN | I2C_SDA_PIN; gpio_init.Mode GPIO_MODE_OUTPUT_OD; gpio_init.Pull GPIO_PULLUP; gpio_init.Speed GPIO_SPEED_FREQ_VERY_HIGH; HAL_GPIO_Init(I2C_GPIO_PORT, gpio_init); SCL_H(); SDA_H(); }等等这里是HAL库的初始化函数。不是说好了寄存器库吗这里说明一下我的做法GPIO初始化这类一次性配置用HAL库函数并没有性能问题而且代码可读性更好。真正的性能关键路径——字节收发、时序产生——全部是寄存器操作。这种“初始化用库数据通路用寄存器”的组合是实际项目里最常见的写法既保证了可维护性又不牺牲热路径效率。如果你连GPIO初始化都想用纯寄存器核心也就几行RCC-AHB4ENR | RCC_AHB4ENR_GPIOBEN; GPIOB-MODER ~(GPIO_MODER_MODE6 | GPIO_MODER_MODE7); GPIOB-MODER | (GPIO_MODER_MODE6_1 | GPIO_MODER_MODE7_1); // 输出模式 GPIOB-OTYPER | (GPIO_OTYPER_OT6 | GPIO_OTYPER_OT7); // 开漏 GPIOB-PUPDR | (GPIO_PUPDR_PUPD6_0 | GPIO_PUPDR_PUPD7_0); // 上拉 GPIOB-OSPEEDR | (GPIO_OSPEEDR_OSPEED6 | GPIO_OSPEEDR_OSPEED7); // 高速 GPIOB-BSRR I2C_SCL_PIN | I2C_SDA_PIN; // ODR置1两种写法都能工作哪种看着舒服用哪种。API封装方面我提供了几个常规函数函数功能eeprom_write_byte(uint16_t addr, uint8_t data)写单个字节eeprom_write_bytes(uint16_t addr, uint8_t *buf, uint16_t len)连续写自动跨页eeprom_read_byte(uint16_t addr)随机读单个字节eeprom_read_bytes(uint16_t addr, uint8_t *buf, uint16_t len)连续读eeprom_wait_ready()等待写周期结束这些函数已经足够覆盖绝大多数应用场景了。如果你的项目需要频繁读写大量数据建议把底层收发函数的延时精简同时检查编译器优化等级是否到了-O2以上否则空循环延时在高主频下会显得异常缓慢。6. 实测结果、波形验证与避坑总结最后这部分是驱动跑通之后做的验证工作顺便把我在整个过程中踩过的坑集中列出来。别小看这些内容有时候排查一个怪异现象花费的时间比写整个驱动还多。6.1 回读校验与性能测试数据我的测试场景是通过串口发送命令H743配合24C02执行写入和回读比较数据是否一致。测试步骤如下写入向地址0x00开始写入0x00到0xFF共256个字节每字节递增。等待调用eeprom_wait_ready()等待全部写完。回读从地址0x00开始读回256个字节逐字节比对。结果全部一致连续跑了100轮没有出现一次错误。用逻辑分析仪抓SDA/SCL波形主要验证了三个关键点起始条件确实是SCL高时SDA拉低停止条件确实是SCL高时SDA拉高。数据传输时SDA只在SCL低电平期间变化SCL高电平期间SDA保持稳定。ACK位在SCL第9个时钟沿采样主机释放SDA后从机能正常拉低。性能方面在SCL频率约100kHz延时2微秒GPIO翻转开销实际频率约90多kHz下单字节写加等待大约5.5ms其中5ms是EEPROM内部写周期连续写256字节因页写功能只需要32次写操作总耗时约180ms。如果换成固定延时5ms做法同样写256字节需要1280ms足足慢了7倍。这就是ACK轮询带来的实际收益。6.2 H7平台上最容易踩的四个坑坑一引脚配置成开漏后忘了ODR置1。症状是总线一开始就是低电平逻辑分析仪看不到任何波形从机无响应。原因是开漏输出的输出级只能拉低不能主动拉高必须靠外部上拉ODR写1才能让引脚释放为高阻。我在第一次配置后忘了这句GPIOB-BSRR SCL_PIN | SDA_PIN排查了十来分钟才反应过来。坑二H7的GPIO外部中断唤醒后配置丢失。H743进入Standby模式再唤醒GPIO配置会全部复位。如果你的产品有低功耗需求唤醒后务必重新调用eeprom_init()否则I2C总线等于没接。坑三总线死锁的恢复。EEPROM在读操作中途如果主机异常复位SDA线可能被从机拉低导致总线卡死。恢复方法是在初始化时向SCL发送9个时钟脉冲让从机释放SDA。我在eeprom_init里加了一段恢复逻辑// 总线恢复如果SDA被拉低向SCL发9个脉冲 if (!SDA_READ()) { for (uint8_t i 0; i 9; i) { SCL_H(); i2c_delay_us(5); SCL_L(); i2c_delay_us(5); } }坑四延时函数的优化等级问题。前文提过空循环延时在不同编译优化等级下执行时间不同。如果代码在调试模式-O0下正常切换到Release-Ofast后I2C时序快了好几倍EEPROM就会间歇性写失败。建议延时函数内部用volatile修饰计数变量或者在关键路径上直接查看反汇编确认周期数。我的i2c_delay_us里用的是__NOP()配合注释里的常数在固定优化等级下是稳定的。6.3 移植到其他H7型号时要注意的差异这套驱动严格说是围绕H743写的但H7系列内部GPIO模块设计基本一致移植非常简单。主要检查三个地方GPIO时钟使能寄存器在AHB4ENR里的位是否一致、引脚号是否冲突、SystemCoreClock是否配置正确。H750、H723、H725这些型号的GPIOB时钟使能位是一样的。但要注意部分H7型号的低功耗模式下GPIO保持逻辑不同从低功耗唤醒后同样需要重新初始化。另外H743的PB6和PB7如果和板上其他外设复用比如UART1的TX/RX、FDCAN需要检查引脚冲突。我之前在一个板子上遇到PB6被调试器占用I2C波形就一直不对后来把SCL换到PB8才解决。如果你换的EEPROM型号变了比如从24C02换成24C64需要改动三处地址宏16位地址、页大小24C64页大小是32字节、设备地址位数24C64地址仍然7位。其他逻辑不用动。我个人在实际项目里的体会是GPIO模拟I2C驱动EEPROM这件事难度不在代码本身而在于你是否真的理解了I2C的总线协议和EEPROM的内部机制。寄存器库驱动逼着你去面对每一个时序细节反而让你对这套总线的理解比用HAL库的人深得多。如果你打算在产品里长期跑EEPROM强烈建议自己动手把底层时序写一遍调试一次比读十遍手册都有用。最后给一个扩展方向如果项目里I2C总线上不止挂EEPROM一个设备可以把这套驱动抽象成总线层和设备层——总线层管理SCL/SDA时序和START/STOP设备层负责各自的协议。这样以后加传感器、加RTC代码复用率会高很多。本文还有配套的精品资源点击获取
返回列表