当前位置: 首页 > news >正文

STM32F103 KEIL工程:软硬双模I²C驱动24Cxx EEPROM + 实时LCD状态显示

本文还有配套的精品资源,点击获取

简介:一套开箱即用的STM32F103嵌入式工程,基于KEIL MDK-ARM v5.x环境构建,支持I²C通信的两种实现方式——软件模拟(MyIIC)和硬件外设。工程内置完整24Cxx系列EEPROM(兼容24C02、24C04等)读写驱动,可稳定完成字节/页写入、随机/顺序读取操作;所有操作结果通过LCD模块实时显示,包括地址、数据、操作状态及错误提示。配套基础外设驱动齐全:LED指示、独立按键检测、串口调试输出(USART)、系统延时(delay)、中断向量配置(stm32f10x_it)、时钟与GPIO初始化等。源码全部采用标准C编写,模块划分清晰:main.c为主控逻辑,myiic.c实现位 banged I²C时序,24cxx.c封装设备协议层,lcd.c管理显示刷新。所有.c文件均附带.crf编译中间文件和.d依赖文件,无需额外配置即可一键编译生成.axf可执行镜像,直接烧录运行。适用于I²C底层原理学习、EEPROM数据持久化开发、LCD人机交互验证等典型嵌入式应用场景。

1. 项目概述:为什么这个I²C工程值得你花30分钟认真读完

我带过十几届嵌入式方向的毕业设计,也帮不少中小公司做过原型验证,发现一个特别普遍的现象:学生和初级工程师一提到I²C,脑子里立刻蹦出“起始信号、地址字节、ACK、数据字节、停止信号”这些教科书术语,但真让他用STM32F103在KEIL里跑通一次24C02写入并显示到LCD上,十有八九卡在时序不对、地址没对齐、ACK没拉低、或者LCD初始化黑屏——不是不会查手册,而是缺一套把协议、寄存器、引脚、延时、状态反馈全部串起来的真实工作流。这个工程就是为解决这个问题而生的。

它不是一个只讲理论的Demo,也不是一个封装过度、看不到底层细节的黑盒库。它是一套“可拆解、可调试、可验证”的完整闭环:从GPIO口线一根根模拟SCL/SDA的电平翻转(MyIIC),到调用STM32标准外设库的I²C硬件模块(I2C1_Init);从24Cxx芯片手册里抠出来的7位设备地址+页写入时序约束,到LCD上实时刷出“ADDR: 0x50 → WRITE OK”这样的肉眼可确认结果;甚至包括按键触发写操作、LED指示忙状态、串口输出十六进制原始数据用于交叉验证——所有环节都暴露在源码里,没有魔法,只有逻辑。

关键词STM32F103, I²C驱动, 24Cxx, LCD显示, KEIL工程不是标签,而是五个必须打通的关卡。你不需要先搞懂整个HAL库,也不必从零手写启动文件;只要打开KEIL MDK-ARM v5.x(我实测v5.36和v5.38均完美兼容),加载IIC.uvprojx(注意:原文中.uvguix是GUI配置文件,真正工程是.uvprojx,这点我在实际部署时踩过坑),点击Build,看到“0 Error(s), 0 Warning(s)”,再烧进板子,就能亲眼看到LCD第一行显示“24C02 READY”,第二行滚动着当前写入地址和数据值。这种“所见即所得”的确定性,对建立嵌入式开发信心至关重要。尤其适合两类人:一是刚学完《嵌入式系统原理》还在对着I²C波形图发懵的学生;二是手头有个新项目要用EEPROM存校准参数,但不想花三天去调通底层驱动的工程师。它不教你“什么是I²C”,它直接给你一把已经磨好的刀,切开第一个24C02芯片。

2. 整体架构与双模设计逻辑:软硬两条路,为什么都要走?

2.1 双模I²C的本质不是“多一种选择”,而是“分层验证”

很多人初看这个工程会疑惑:既然硬件I²C外设更高效,为什么还要费劲写一套软件模拟(MyIIC)?这不是重复造轮子吗?我的答案很直接:因为硬件I²C是个“黑箱”,而MyIIC是你的示波器探针。在真实项目中,我遇到过三次典型故障,全靠MyIIC定位:

  • 一次是客户PCB把SCL和SDA走线长度差了15cm,硬件I²C在400kHz下偶发丢ACK,但MyIIC在100kHz下稳定运行——这立刻指向信号完整性而非代码问题;
  • 一次是EEPROM批次变更,新芯片要求起始信号后等待3μs才发地址,硬件I²C的自动时序无法微调,而MyIIC里delay_us(3)一行就解决了;
  • 还有一次是客户误将PA15(JTAG-SWCLK)复用为SDA,硬件I²C初始化失败且无明确报错,但MyIIC用PB6/PB7完全不受影响,快速排除了引脚冲突。

所以这个工程的双模设计,核心逻辑是:MyIIC作为“可信基准”,硬件I²C作为“性能目标”。所有24Cxx的操作函数(如AT24CXX_WriteOneByte)都通过宏开关切换底层实现:

// 24cxx.h 中定义 #define USE_HARDWARE_I2C 1 // 0=MyIIC, 1=Hardware I2C #if USE_HARDWARE_I2C #include "stm32f10x_i2c.h" #define I2C_Start() I2C_GenerateSTART(I2C1, ENABLE) #define I2C_SendByte(x) I2C_SendData(I2C1, x) #else #include "myiic.h" #define I2C_Start() MyIIC_Start() #define I2C_SendByte(x) MyIIC_Send_Byte(x) #endif

这种设计让验证变得极其简单:先用MyIIC确保24Cxx芯片本身、接线、电源都没问题;再切到硬件I²C,如果失败,问题一定出在时钟配置、引脚复用或中断优先级上,而不是协议理解错误。这是嵌入式调试最高效的“分治法”。

2.2 模块化分层:为什么main.c只有87行,却能控制一切?

这个工程的源码结构看似简单,但每一层都有明确职责边界,杜绝了新手常犯的“所有逻辑堆在main里”的毛病。我来拆解它的数据流:

  1. 应用层(main.c):只做三件事——初始化所有外设、进入主循环、响应按键事件。它不关心I²C怎么发信号,也不管LCD像素怎么点亮,只调用LCD_ShowString()AT24CXX_WriteOneByte()这样的语义化接口。比如写入操作:
    c if(key == KEY0_PRES) { // 按键0按下 addr = (addr + 1) % 256; // 地址自增 data = (data + 1) % 256; // 数据自增 res = AT24CXX_WriteOneByte(addr, data); // 调用驱动层 if(res == 0) { LCD_ShowString(1,0,"WRITE OK! "); // 显示层反馈 LED0 = 0; // LED指示成功 } else { LCD_ShowString(1,0,"WRITE FAIL! "); LED0 = 1; } }
    这里res返回值直接来自24cxx.c,而LCD_ShowString调用的是lcd.c里的函数。main.c就像一个冷静的指挥官,只下达“写地址X数据Y”这样的命令,不插手执行细节。

  2. 设备驱动层(24cxx.c):这是真正的“翻译官”。它把应用层的抽象请求,翻译成I²C总线上的具体动作。关键点在于它严格遵循24Cxx系列芯片的数据手册:
    - 设备地址计算:24C02是7位地址0x50(A2A1A0=000),但I²C协议要求左移1位并补R/W位,所以写操作地址是0xA0,读操作是0xA1
    - 页写入保护:24C02一页8字节,写入地址不能跨页,否则后半页数据丢失。代码里有明确检查:
    c if((addr & 0x07) + len > 8) { // 跨页检测 return 1; // 返回错误 }
    - ACK超时处理:硬件I²C的I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)可能因总线干扰失败,这里设置了50ms超时重试,避免死等。

  3. 硬件抽象层(myiic.c / stm32f10x_i2c.c):MyIIC.c是纯GPIO操作,核心是四个函数:MyIIC_Start()MyIIC_Stop()MyIIC_Send_Byte()MyIIC_Read_Byte()。以MyIIC_Start()为例:
    c void MyIIC_Start(void) { SDA_OUT(); // SDA设为推挽输出 IIC_SDA = 1; // SDA拉高 IIC_SCL = 1; // SCL拉高 delay_us(4); // 保持时间≥4.7μs(24C02手册要求) IIC_SDA = 0; // SDA下降沿→起始信号 delay_us(4); IIC_SCL = 0; // SCL拉低,进入数据传输态 }
    每个delay_us(x)都对应手册里的最小时间参数,这是软件模拟能成功的根基。而硬件I²C部分,则集中在I2C1_Init()函数里配置:时钟分频器(I2C_ClockSpeed=100000)、占空比(I2C_DutyCycle=I2C_DutyCycle_2)、应答使能(I2C_Ack=ENABLE)——这些参数不是随便填的,它们决定了SCL方波的高/低电平时间,必须匹配24Cxx的时序要求(标准模式100kHz,快速模式400kHz)。

  4. 外设支撑层(lcd.c, key.c, led.c等):LCD显示不是简单“打印字符串”,它涉及FSMC总线配置(如果你用并口LCD)、ILI9341初始化序列(128条寄存器写入)、GRAM地址设置。这个工程用的是常见的1602字符型LCD(4-bit模式),所以lcd.cLCD_Write_Com()函数要精确控制RS/RW/EN时序,比如EN脉冲宽度必须≥450ns。而按键消抖用的是“两次采样间隔10ms”的经典方案,比单纯延时更可靠。

这种分层不是为了炫技,而是为了让你在调试时能精准定位问题。如果LCD不显示,先看LCD_Init()是否执行;如果显示乱码,检查LCD_Write_Data()的时序;如果按键无反应,跟踪KEY_Scan()的返回值。每一层都是独立的验证单元。

3. 核心细节解析:从I²C时序到LCD刷新,每一个“为什么”都藏着经验

3.1 MyIIC的时序精度:为什么用SysTick做微秒延时,而不是for循环?

新手常犯的错误是用for(i=0;i<10;i++);这种空循环做延时,但KEIL编译优化等级一变,延时就失效。这个工程采用SysTick定时器实现精准微秒级延时,核心在delay.c

static u8 fac_us=0; // us延时倍乘数 static u16 fac_ms=0; // ms延时倍乘数 void delay_init(u8 SYSCLK) { SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); // SysTick时钟为HCLK/8 fac_us = SYSCLK/8; // 例如SYSCLK=72MHz → fac_us=9, 即1us=9个SysTick计数 fac_ms = (u16)fac_us*1000; } void delay_us(u32 nus) { u32 temp; SysTick->LOAD = nus * fac_us; // 自动重装载值 SysTick->VAL = 0x00; // 清空当前计数器 SysTick->CTRL = 0x01; // 使能SysTick do { temp = SysTick->CTRL; } while((temp & 0x01) && !(temp & (1<<16))); // 等待计数完成 SysTick->CTRL = 0x00; // 关闭SysTick SysTick->VAL = 0x00; // 清空计数器 }

为什么必须这么麻烦?因为24C02的时序要求极其苛刻:
- 起始信号:SCL高时SDA由高→低,且SCL高电平时间≥4.7μs;
- 数据建立时间:SDA在SCL低电平时改变,且SCL低电平时间≥4.7μs;
- ACK时隙:主设备释放SDA后,从设备必须在SCL高电平期间将SDA拉低,且SCL高电平时间≥4μs。

如果用for循环,编译器优化-O2会把循环展开,导致延时严重缩水。而SysTick基于硬件定时器,不受编译优化影响,误差<1%。我在实测中对比过:72MHz系统下,delay_us(5)实测波形为5.02μs,完全满足24C02手册要求。这是软件模拟I²C能稳定运行的物理基础。

3.2 24Cxx地址映射与页写入:为什么24C02最大地址是255,而24C04是511?

24Cxx系列的命名规则直接反映容量:“02”=2Kbit=256字节,“04”=4Kbit=512字节。但地址线设计有玄机。24C02只有8根地址线(A0-A7),所以地址范围0x00~0xFF(256个字节)。而24C04需要9根地址线,但芯片只有A0-A2三个硬件引脚,第9位地址(A8)由设备地址的最低位(A0)决定!这就是为什么24C04需要两个设备地址:0x50(A0=0)对应地址0x000~0x1FF,0x51(A0=1)对应0x200~0x3FF。

这个工程的AT24CXX_WritePage()函数巧妙处理了这一点:

u8 AT24CXX_WritePage(u16 addr, u8 *buf, u8 len) { u8 i; u8 dev_addr = 0xA0; // 默认24C02地址 if(EE_TYPE == 4) { // 如果是24C04 dev_addr = (addr < 512) ? 0xA0 : 0xA2; // A8由addr最高位决定 addr &= 0x1FF; // 取低9位 } // 后续页写入逻辑... }

这里EE_TYPE24cxx.h中定义为宏,编译时指定芯片型号。如果不做这个判断,直接用24C02的代码去驱动24C04,写入地址>255时就会写到错误的物理位置。我在帮一家传感器公司做校准参数存储时,就因忽略这点导致温度补偿表错位,花了两天才定位到。

3.3 LCD实时刷新策略:为什么不用中断刷新,而用主循环轮询?

很多教程喜欢用定时器中断每100ms刷新LCD,但这个工程坚持在while(1)主循环里调用LCD_Refresh()。原因很实在:中断刷新会引入竞态条件,而轮询刷新能保证状态绝对一致。具体来说:

  • 当I²C正在写EEPROM时(耗时约10ms),如果LCD刷新中断在此时触发,它可能读取到addrdata变量的中间状态(比如addr已更新但data还没写),导致LCD显示“ADDR: 0x15 DATA: 0x00”这种错误组合;
  • 更严重的是,如果LCD驱动本身用了全局缓冲区(如lcd_buffer[32]),中断里修改缓冲区,主循环里又在往里写,缓冲区会错乱。

解决方案是“状态快照”:在主循环每次迭代开始时,用局部变量保存当前要显示的所有状态:

while(1) { u16 cur_addr = addr; u8 cur_data = data; u8 cur_res = last_write_result; LCD_Clear(); LCD_ShowString(0,0,"24C02 TEST"); LCD_ShowNum(1,0,"ADDR:",cur_addr,3,16); // 显示地址 LCD_ShowNum(1,6,"DATA:",cur_data,3,16); // 显示数据 if(cur_res == 0) { LCD_ShowString(1,12,"OK"); } else { LCD_ShowString(1,12,"ERR"); } delay_ms(200); // 刷新间隔 }

所有显示内容都基于这一帧的快照,I²C操作在后台异步进行,互不干扰。虽然牺牲了毫秒级实时性,但换来了100%的状态一致性——对于调试和演示,这比“看起来更流畅”重要得多。

3.4 KEIL工程配置的关键陷阱:为什么.crf和.d文件必须齐全?

原文提到“.crf编译中间文件”和“.d依赖文件”,这看似是细节,实则是工程能否“开箱即用”的命门。.crf(C Reference File)是KEIL编译器生成的符号引用信息,包含函数调用关系、变量定义位置;.d(Dependency File)记录每个.c文件依赖哪些头文件(如#include "24cxx.h")。当KEIL增量编译时,它通过.d文件判断:如果24cxx.h被修改,那么所有包含它的.c文件(main.c, myiic.c等)都需要重新编译。

如果没有.d文件,KEIL只能全量编译,速度慢十倍;如果.crf缺失,调试时无法在源码行设置断点,只能看到汇编指令。我在教学中见过太多学生抱怨“明明改了myiic.c,debug时断点不生效”,最后发现是工程里删掉了.crf文件。这个工程保留全套中间文件,意味着你双击IIC.uvprojx后,KEIL能立即识别出main.c依赖24cxx.h,而24cxx.h又依赖myiic.h,从而构建出正确的编译依赖图。这是专业工程和玩具Demo的根本区别。

4. 实操过程详解:从KEIL新建工程到LCD显示“WRITE OK”的完整路径

4.1 KEIL环境准备与工程加载(5分钟)

第一步永远是环境确认。这个工程基于KEIL MDK-ARM v5.x,强烈建议使用v5.36或v5.38(v5.39之后对旧版ST标准库支持变弱)。安装完成后:

  1. 打开KEIL,点击Project → Open Project...,选择解压后的IIC.uvprojx文件(注意不是.uvguix,那是GUI配置);
  2. 如果提示“Device not found”,说明缺少STM32F10x Device Family Pack。点击Pack Installer图标(小盒子),搜索“STM32F10x DFP”,安装最新版(我用的是2.3.0);
  3. 加载后,在Project → Options for Target 'Target 1'中检查:
    -Device选项卡:选中STM32F103C8(主流小容量芯片);
    -Target选项卡:Crystal/Ceramic Resonator8000000(外部晶振8MHz),这是标准配置;
    -Output选项卡:勾选Create HEX File,方便后续用ST-Link Utility烧录;
    -C/C++选项卡:Define框里确保有STM32F10X_MD, USE_STDPERIPH_DRIVER(这是标准外设库的编译开关)。

提示:如果编译报错undefined symbol SystemInit,说明system_stm32f10x.c没被加入工程。右键Source Group 1Add Existing Files to Group...,添加该文件。这是新手最高频的错误。

4.2 硬件连接与引脚映射(3分钟)

这个工程默认引脚分配是经过验证的稳定组合,务必按此接线,避免自行更改引发冲突:

功能STM32引脚说明
MyIIC SCLPB6复用为I²C1_SCL,也可作普通GPIO
MyIIC SDAPB7复用为I²C1_SDA,也可作普通GPIO
LCD RSPA0寄存器选择(0=指令,1=数据)
LCD RWPA1读写选择(0=写,1=读),通常接地
LCD ENPA2使能信号,高脉冲触发
LCD D4-D7PA3-PA64-bit数据总线
LED0PA8板载LED,低电平点亮
KEY0PA9独立按键,按下接地

注意:PB6/PB7同时是JTAG的SWDIO/SWCLK,如果用ST-Link下载,需确保JTAG未被禁用。在system_stm32f10x.cSystemInit()函数末尾,有RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_AFIO, ENABLE); GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这行代码禁用了JTAG,只保留SWD,所以PB6/PB7可安全用作I²C。

4.3 编译、下载与首次运行(2分钟)

点击KEIL工具栏的Build按钮(锤子图标),观察底部Build Output窗口:
- 如果出现0 Error(s), 0 Warning(s),说明编译成功,生成IIC.axf
- 点击Download按钮(向下箭头),KEIL自动调用ST-Link驱动烧录;
- 烧录完成后,板子自动复位,LCD第一行应显示“24C02 READY”,第二行显示初始地址和数据(如“ADDR: 000 DATA: 000”)。

此时按下KEY0,LCD第二行变为“WRITE OK”,LED0熄灭;再按一次,地址和数据自增,继续显示“WRITE OK”。这就是最简验证路径。

4.4 深度验证:用串口抓取原始I²C数据流

仅仅看LCD显示还不够,真正的验证要看总线上的原始数据。工程已集成USART1(PA9/PA10),波特率115200:

  1. 用USB转TTL模块连接PA9(TX)、PA10(RX)、GND到电脑;
  2. 打开串口助手(如XCOM),设置波特率115200,无校验;
  3. main.cwhile(1)循环里,添加串口输出:
    c printf("Write Addr: 0x%02X, Data: 0x%02X, Result: %d\r\n", addr, data, res);
  4. 每次按键后,串口会打印类似Write Addr: 0x01, Data: 0x02, Result: 0的字符串。

更重要的是,你可以用逻辑分析仪(如Saleae)抓取PB6/PB7波形,对照串口输出的地址和数据,逐比特验证起始信号、地址字节(0xA0)、数据字节(0x01)、ACK信号——这才是I²C学习的终极验证方式。我在带学生时,会让每个人用Saleae截图发到群里,比对波形是否符合24C02手册Figure 9的时序图。

4.5 双模切换实战:从MyIIC到硬件I²C的无缝迁移

现在验证MyIIC稳定后,我们切换到硬件I²C,体验性能差异:

  1. 打开24cxx.h,将#define USE_HARDWARE_I2C 0改为1
  2. 检查I2C1_Init()函数(在24cxx.c中),确认I2C_ClockSpeed=100000(100kHz);
  3. 重新编译下载。

你会发现操作响应明显更快,LCD刷新更顺滑。但这时可以故意制造一个故障来测试双模价值:把PB6和PB7的杜邦线拔掉一根,再按KEY0——MyIIC会立即报错(因为GPIO读不到ACK),而硬件I²C可能卡死在I2C_CheckEvent()。这证明MyIIC不仅是备用方案,更是你的硬件诊断工具。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 问题速查表:高频故障与一键修复

现象可能原因排查步骤修复方法
LCD全黑,无任何显示LCD背光未供电或RS/RW/EN时序错误用万用表测LCD VCC/GND是否5V;用示波器看PA0(PA2)是否有脉冲检查LCD_Init()LCD_Write_Com(0x38)是否执行;确认PA1(RW)是否接地
LCD显示乱码(如“□□□□”)字符集不匹配或数据线接反用万用表测PA3-PA6电压,正常应随数据变化检查LCD_Write_Data()LCD_Dat = dat是否正确;确认D4-D7与PA3-PA6一一对应
按键无反应按键电路未上拉或KEY_Scan()逻辑错误用万用表测PA9对地电阻,按下时应≈0Ωkey.c中确认KEY0 = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_9);检查RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE)是否开启时钟
I²C写入失败(res≠0)SCL/SDA上拉电阻缺失或阻值过大用万用表测PB6/PB7对VCC电阻,应为4.7kΩ在PB6/PB7与3.3V间各加一个4.7kΩ上拉电阻(开发板通常已内置)
硬件I²C编译报错“I2C1未定义”标准外设库未包含I²C驱动文件在KEIL工程中查看stm32f10x_i2c.c是否在Source Group中右键Source Group 1Add Existing Files,添加stm32f10x_i2c.cstm32f10x_i2c.h

5.2 独家避坑技巧:来自十年现场调试的经验

技巧1:用LED做I²C状态指示器,比串口更直观
不要只依赖串口打印,把LED变成协议指示灯。在MyIIC_Start()开头加LED1=0;,结尾加LED1=1;,这样每次起始信号产生,LED就闪一下。我曾用这招在一分钟内定位到客户PCB上SCL线虚焊——LED完全不闪,而串口还显示“正在写入”。

技巧2:EEPROM写入前必须等待“就绪”
24Cxx写入后内部需要10ms完成擦写,期间发送任何I²C信号都会失败。工程里AT24CXX_WaitEepromReady()函数用“发送设备地址+读位,检测ACK”来轮询,但新手常忽略:这个轮询必须在每次写操作后立即执行,不能等到下次按键。我在main.c里把它放在AT24CXX_WriteOneByte()返回后:

res = AT24CXX_WriteOneByte(addr, data); AT24CXX_WaitEepromReady(); // 关键!必须紧跟写操作

技巧3:LCD初始化失败时,强制复位比重试更有效
如果LCD_Init()第一次失败,反复调用它可能无效。我在lcd.c里加了硬件复位逻辑:

void LCD_Reset(void) { RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2); delay_ms(10); GPIO_ResetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2); delay_ms(10); }

LCD_Init()开头调用它,能解决80%的初始化黑屏问题。

技巧4:KEIL调试时,用“Memory Window”直接读EEPROM
不用写读函数,直接在KEIL调试模式下:View → Memory Windows → Memory 1,输入地址0x08000000(假设EEPROM映射到此),就能看到刚写入的数据。这是验证写入是否成功的最快方法。

6. 工程扩展与进阶实践:从学会到精通的三步跃迁

6.1 第一步:增加CRC校验,让数据存储更可靠

24Cxx只是存储介质,但数据完整性需要软件保障。在24cxx.c中添加CRC8校验:

u8 CRC8(u8 *data, u8 len) { u8 crc = 0; for(u8 i=0; i<len; i++) { crc ^= data[i]; for(u8 j=0; j<8; j++) { if(crc & 0x80) crc = (crc << 1) ^ 0x07; else crc <<= 1; } } return crc; } // 写入时附加CRC u8 buf[17]; // 16字节数据 + 1字节CRC for(u8 i=0; i<16; i++) buf[i] = data[i]; buf[16] = CRC8(buf, 16); AT24CXX_WriteBuffer(addr, buf, 17);

读取后校验CRC8(read_buf, 16) == read_buf[16],不等则报错。这在工业环境中防止数据篡改至关重要。

6.2 第二步:移植到FreeRTOS,实现非阻塞I²C

当前工程是裸机轮询,若加入FreeRTOS,需将I²C操作封装为任务:

void I2C_Task(void *pvParameters) { while(1) { if(xQueueReceive(xI2C_Queue, &cmd, portMAX_DELAY) == pdTRUE) { switch(cmd.op) { case WRITE: AT24CXX_WriteOneByte(cmd.addr, cmd.data); break; case READ: cmd.data = AT24CXX_ReadOneByte(cmd.addr); break; } xQueueSend(xI2C_Result_Queue, &cmd, 0); } } }

这样主任务可以专注LCD刷新和按键扫描,I²C操作在后台完成,响应更及时。

6.3 第三步:升级为I²C多设备管理器

一块板子常挂多个I²C设备(EEPROM、温湿度传感器、RTC)。在24cxx.c基础上抽象出I2C_Device结构体:

typedef struct { u8 dev_addr; // 设备地址 u8 (*read)(u8 reg, u8 *buf, u8 len); u8 (*write)(u8 reg, u8 *buf, u8 len); } I2C_Device; I2C_Device eeprom_dev = {0x50, EEPROM_Read, EEPROM_Write}; I2C_Device sensor_dev = {0x40, SENSOR_Read, SENSOR_Write};

主程序通过i2c_manager_register(&eeprom_dev)注册设备,统一调度。这是向大型项目演进的必经之路。

我在实际项目中,正是从这个24Cxx工程起步,逐步加入了SPI Flash、CAN总线、USB CDC等功能,最终形成了一套完整的嵌入式固件框架。它的价值不在于代码有多炫,而在于每一个函数、每一行注释、每一个配置项,都直指嵌入式开发中最本质的问题:如何让数字世界里的0和1,在物理世界的铜线和硅片上,稳定、可靠、可验证地流动。当你亲手让LCD显示出第一个“WRITE OK”,那种掌控感,就是嵌入式工程师最纯粹的快乐。

本文还有配套的精品资源,点击获取

简介:一套开箱即用的STM32F103嵌入式工程,基于KEIL MDK-ARM v5.x环境构建,支持I²C通信的两种实现方式——软件模拟(MyIIC)和硬件外设。工程内置完整24Cxx系列EEPROM(兼容24C02、24C04等)读写驱动,可稳定完成字节/页写入、随机/顺序读取操作;所有操作结果通过LCD模块实时显示,包括地址、数据、操作状态及错误提示。配套基础外设驱动齐全:LED指示、独立按键检测、串口调试输出(USART)、系统延时(delay)、中断向量配置(stm32f10x_it)、时钟与GPIO初始化等。源码全部采用标准C编写,模块划分清晰:main.c为主控逻辑,myiic.c实现位 banged I²C时序,24cxx.c封装设备协议层,lcd.c管理显示刷新。所有.c文件均附带.crf编译中间文件和.d依赖文件,无需额外配置即可一键编译生成.axf可执行镜像,直接烧录运行。适用于I²C底层原理学习、EEPROM数据持久化开发、LCD人机交互验证等典型嵌入式应用场景。


本文还有配套的精品资源,点击获取

http://www.cnnetsun.cn/news/2813622.html

相关文章:

  • PySD终极指南:如何在Python中快速构建系统动力学模型
  • 手把手教你搞定IEEE会议投稿:从LaTeX模板到PDF eXpress避坑全流程
  • 如何在macOS上实现NTFS读写:免费开源工具的终极解决方案
  • 告别命令行恐惧:用 SRA Toolkit 的 prefetch 和 fastq-dump 轻松下载并转换宏基因组数据
  • Node.js版本太低?手把手教你用NVM切换版本,解决NPM安装时的EUNSUPPORTEDPROTOCOL错误
  • Linux内核学习轨迹第五部:反向映射RMAP机制全解析(第八小节)
  • 寻找中文 AI 的救赎:递归自我改进(RSI)如何降维打击“网络黑话”与语料污染
  • SQL语言:数字函数
  • BMI体脂率与基础代谢综合计算接口接入实践:健康评估数据的工程化处理
  • GitOps CI/CD 流水线设计:从 Git 事件到生产部署的自动化闭环
  • 电子设备接地防雷与抗干扰:原理、误区与工程实践指南
  • AVR TWI中断驱动设计:从轮询到状态机的通信效率优化
  • 全平台B站客户端终极指南:wiliwili 10分钟快速上手教程
  • Nature和Science的‘子刊宇宙’大不同:除了主刊,你更应该关注这些宝藏期刊
  • 终极指南:用Python快速获取同花顺问财数据的完整教程
  • CSDN AI数字营销企业版报价不是“问出来”的——而是靠这6项技术尽调材料+1份ROI测算模型“换来的”,附20年甲方数字化采购老炮整理的《报价谈判攻防手册》
  • 抖音视频批量下载难题:如何轻松保存无水印内容?
  • 5分钟搭建抖音直播弹幕监控系统:Go语言实现全解析
  • Cursor Pro破解工具:5分钟解锁AI编程助手的终极解决方案
  • DGL实战入门:用空手道俱乐部数据跑通GCN和GAT节点分类全流程
  • 报价延迟超72小时?CSDN AI数字营销企业版获取流程卡点全梳理,附2024Q3授权代理白名单与快速通道申请模板
  • 从算法演进到内核调优:红黑树与 B+ 树在数据库索引结构中的工程边界与退化博弈
  • Rollout
  • S32K3 eMIOS的Counter Bus机制详解:如何用两个通道生成同步PWM?附配置避坑指南
  • 抖音视频下载终极指南:3步实现无水印批量下载,免费开源工具全解析
  • iOS Swift实况图合成与播放一体化示例工程(含素材+预览UI)
  • Noto字体:为900+语言消除“豆腐块“的开源字体解决方案
  • 3分钟上手!打造你的专属WordPress博客:Argon主题深度体验指南
  • 100皇后问题的遗传算法Python实战:从卡顿到收敛全解析
  • Thought-Action-Observation闭环:AI工程化协作的核心范式