资讯详情

单片机C语言项目实战:IIC驱动24C04与数码管动态显示

📅 2026/10/28 8:58:39 | 华诺云谱 👁 阅读
单片机C语言项目实战:IIC驱动24C04与数码管动态显示
简介这是一套面向单片机初学者的IIC-24C04与数码管C语言程序设计及仿真资源以Keil C为开发平台完整演示了IIC总线对EEPROM 24C04的读写操作并将读取的数据通过数码管显示出来。压缩包共十五个文件大小约五十三千字节内容涵盖C源码、Keil工程配置、编译生成文件、Proteus仿真电路及备份文件等从程序编写、编译链接到硬件仿真各环节均有对应目录结构一目了然。已有三百二十一人学习下载适合正在学习单片机IIC通信协议、外部存储器扩展或数码管驱动的读者。通过源码和仿真工程可以直观掌握IIC总线的起始、停止、应答机制熟悉24C04的页写与顺序读指令流程同时参考数码管段码与位选控制方法对完成课程设计或二次开发非常有帮助无论是实验实训还是项目开发都能获得完整的实践参考。1. 单片机C语言程序设计 IIC-24C04与数码管这套源码到底解决了什么说到单片机C语言程序设计里最容易被劝退的组合IIC-24C04与数码管绝对排得上号。IIC 协议看着就两条线真调起来全是玄学24C04 跟 24C02 长得一样地址机制却暗藏区别数码管能亮不难难的是动态扫描不闪不拖影。这套源码把三段东西串成一条完整链路模拟 IIC 驱动读写 24C04 EEPROM再把读出来的数据送到数码管显示数据掉电不丢。适合刚学完 51 基础、想拿一个完整外设项目练手的同学也适合做小型存储方案时抄驱动。拿到手不是跑通就完事关键是弄懂每一行时序背后的为什么。2. IIC 协议与 24C04 读写先啃透时序再写代码2.1 为什么 IIC 必须开漏输出推挽会翻车上拉电阻取多大网上经常有人问IIC 为什么不能用推挽这个问题问得很有水平。IIC 是线与结构任何设备都能把 SDA 拉低如果两边都用推挽输出一个输出高一个输出低直接形成短路轻则通信乱码重则烧引脚。所以 SDA 和 SCL 必须开漏再靠外部上拉电阻把电平拉高。51 单片机的 P0 口本身是开漏P1/P2/P3 内部有弱上拉但驱动能力不够稳实际调 IIC 我一般都会外接上拉电阻。上拉电阻取值有一个经验区间标准模式 100kHz 用 4.7kΩ快速模式 400kHz 用 2.2kΩ 或 1kΩ总线器件多、走线长时适当减小。总线速率推荐上拉电阻适用场景100kHz 标准模式4.7kΩ走线短、器件少开发板常见400kHz 快速模式2.2kΩ走线稍长、速率要求高1kΩ总线电容大、多从机波形上升沿严重拖尾时判断电阻是否合适用示波器看 SDA 上升沿如果波形像台阶一样缓慢爬升说明电阻太大或总线电容偏大优先把 4.7k 换成 2.2k。这一步在很多开发板上是被忽略的因为 24C04 本身速率低勉强能通但一接第二个 IIC 从机就翻车。2.2 起始、停止与应答位时序图上三个最容易抄错的点IIC 时序的核心就四个动作起始、停止、发送字节、接收字节。写代码前先把时序图画在纸上对照 24C04 数据手册看。// IIC 引脚定义STC89C52P2.0 接 SDAP2.1 接 SCL sbit IIC_SCL P2^1; sbit IIC_SDA P2^0; void IIC_Delay(void) { _nop_(); // 12MHz 晶振下约 1us决定时钟频率 } void IIC_Start(void) // 起始条件SCL 高电平期间 SDA 由高拉低 { IIC_SDA 1; IIC_SCL 1; IIC_Delay(); IIC_SDA 0; IIC_Delay(); IIC_SCL 0; // 拉低 SCL准备传输数据 } void IIC_Stop(void) // 停止条件SCL 高电平期间 SDA 由低拉高 { IIC_SDA 0; IIC_SCL 1; IIC_Delay(); IIC_SDA 1; IIC_Delay(); }起始和停止的条件必须严格遵守SCL 高的时候 SDA 跳变才有效SCL 低的时候 SDA 变化只是准备数据。新手最容易抄错的地方是顺序比如起始条件写成先拉低 SDA 再拉高 SCL从机根本识别不了。代码里 IIC_Delay 就是位周期基础一个完整的位周期包含 SDA 建立、SCL 高电平、SCL 低电平三段12MHz 的 51 单片机上大约 4~5us折算下来约 200kHz处于标准模式和快速模式之间对 24C04 没压力。如果换 11.0592MHz 晶振延时变化可忽略但如果换到 STM32必须重新评估这个延时。应答位是第二个高频翻车点。从机接收到一个字节后会把 SDA 拉低作为 ACK主机读数据时则要主动释放 SDA让从机控制总线。所以发送字节后要等 ACK接收字节前要释放 SDA。unsigned char IIC_WaitAck(void) // 主机发送完一个字节后等待从机应答 { unsigned char timeout 255; IIC_SDA 1; // 释放 SDA从机才能拉低 IIC_SCL 1; IIC_Delay(); while (IIC_SDA) // SDA 仍为高说明没有应答 { if (--timeout 0) // 超时退出防止死等 { IIC_SCL 0; return 1; // 返回 1 表示无应答 } IIC_Delay(); } IIC_SCL 0; IIC_Delay(); return 0; // 返回 0 表示应答正常 }这段代码里我做了一个很多教程不会做的事超时计数。学习板上的裸机 IIC 例程普遍不判超时一旦从机异常拉死 SDA程序就卡死在 while 循环里。加一个 255 次的超时判断虽然简单但能救回大量调试时间。注意 IIC_WaitAck 返回 1 表示无应答调用时要主动检查返回值不要默认每次都能等到 ACK。2.3 24C04 的 512 字节地址它与 24C02 的页选择差在哪24C04 和 24C02 外观、引脚几乎一样但地址机制完全不同。24C02 是 256 字节字地址 8 位器件地址 0xA0 | (A22) | (A11) | A0。24C04 是 512 字节内部其实是两个 256 字节的块字地址需要 9 位最高位 A8 被挪进了器件地址的第 1 位bit1也就是 P0 位。// 24C04 写一个字节 void AT24C04_WriteByte(unsigned char page, unsigned char addr, unsigned char dat) { IIC_Start(); // 起始条件 IIC_SendByte(0xA0 | (page 1)); // 器件地址 页选择位 if (IIC_WaitAck()) return; // 无应答则退出 IIC_SendByte(addr); // 页内地址 0x00~0xFF if (IIC_WaitAck()) return; IIC_SendByte(dat); // 写入数据 if (IIC_WaitAck()) return; IIC_Stop(); // 停止条件 delay_ms(5); // 等待写周期完成 }对照原理图逐行说明0xA0 的低四位分别是 A2、A1、P0、R/W当开发板上 A0~A2 全部接地时基地址就是 0xA0。page 取 0 或 1对应 24C04 的页选择位它与内部字地址 A8 绑定所以 page 1 后要按位或到器件地址里。addr 是页内地址范围 0~255。如果直接拿 24C02 的驱动来写 24C04把 page 参数去掉、固定 0xA0那你永远只能操作前 256 字节后一半看着能读能写实际全是镜像数据。写周期延时是 24C04 最容易漏的坑。数据手册给的是 tWR 典型 5ms意思是写入一个字节后EEPROM 内部还要把数据固化进非易失存储这段时间芯片不接受新操作。写完立刻读大概率读到旧数据。所以我习惯在每次写完成后加一个 delay_ms(5)宁可多等也不能翻车。读操作比写操作多一个重复起始条件。先写器件地址和字地址把内部指针指到目标位置然后再发一次起始条件和读方向的器件地址从机才会把数据放到 SDA 上。unsigned char AT24C04_ReadByte(unsigned char page, unsigned char addr) { unsigned char dat; IIC_Start(); IIC_SendByte(0xA0 | (page 1)); // 写方向先定位地址 if (IIC_WaitAck()) return 0xFF; IIC_SendByte(addr); if (IIC_WaitAck()) return 0xFF; IIC_Start(); // 重复起始条件 IIC_SendByte(0xA1 | (page 1)); // 读方向 if (IIC_WaitAck()) return 0xFF; dat IIC_ReceiveByte(); // 接收 8 位数据 IIC_Stop(); return dat; }这里要提醒一个细节按 IIC 规范主机在读最后一个字节之前应该发送 NACK表示别再发了。这个单字节读示例里直接 Stop 了大多数情况下 24C04 能容忍但如果你做的是连续读最后一个字节前一定记得把 SDA 置 1在第 9 个时钟不发应答否则从机不知道你要结束。3. 数码管显示段码表、动态扫描与消影顺序3.1 共阴共阳段码表一个反了整屏乱码数码管段码本质上就是一张 8 位字体表a~g 七个段加一个小数点 dp对应字节的 bit0~bit7。共阳和共阴的段码完全相反共阳极数码管公共端接高电平某段接低电平才点亮共阴极相反。很多开发板用共阳数码管段码表长这样。// 共阳极段码表低电平点亮 code unsigned char seg_tab[] { 0xC0, // 0 0xF9, // 1 0xA4, // 2 0xB0, // 3 0x99, // 4 0x92, // 5 0x82, // 6 0xF8, // 7 0x80, // 8 0x90, // 9 0x88, // A 0x83, // b 0xC6, // C 0xA1, // d 0x86, // E 0x8E // F };这段表怎么验证对不对把 0xC0 按二进制拆开1100 0000bit0~bit6 对应 a~g 段bit6 是 1说明 g 段灭只有 a~f 亮组成的字形正好是 0。如果用的是共阴极数码管段码整体取反0 就是 0x3F。判断自己板子是共阳还是共阴最简单的办法是万用表二极管档测公共端和任一段脚或者直接看原理图。注意 51 单片机的 P0 口内部没有上拉是开漏结构。如果 P0 接共阳数码管的段选输出高电平那段全靠外部上拉排阻开发板上通常已经集成但自己画板一定要加 10k 排阻否则数码管亮度会明显不均匀甚至缺笔画。3.2 动态扫描轮流亮背后的位选与延时考量动态显示的原理是轮流亮。人眼有视觉暂留只要刷新率够高四个数码管轮流点亮看起来就是同时亮。相比静态显示动态扫描省掉了 8 个锁存器代价是程序要持续刷新并且消影顺序必须正确。我见过太多人把消影放在送段码之后结果前一位置数切换后留有残影。unsigned char num_buf[4]; // 待显示数值元素为 0~9 void display_scan(unsigned char *buf) { unsigned char i; for (i 0; i 4; i) { P0 0xFF; // 先灭所有段消影 P2 (P2 0x0F) | (0x10 i); // P2.4~P2.7 逐位选通 P0 seg_tab[buf[i]]; // 再送段码 delay_short(1); // 保持约 1ms } }这段程序里顺序是严格固定的先 P00xFF 熄灭再切位选最后送段码。如果反着来位选切到下一个数码管时P0 还残留着上一个数字的段码人眼看到的就是前一位的鬼影。位选线 P2.4~P2.70x10 i 在 i0 时选中第一个数码管i3 时选中第四个理解这个位操作就可以改成任意引脚组合。每位保持 1ms四位数码管一轮扫描 4ms刷新率 250Hz完全不会闪烁。如果每位延时超过 5ms刷新率掉到 50Hz 以下肉眼会明显看到闪烁延时太短则亮度不足因为每个数码管只有 1/4 的时间在亮。亮度不均的常见原因就是四个位选延时不一致程序里用循环就天然一致手工写四条赋值语句反而容易出错。3.3 读回来的数据怎么送到数码管一次完整的数据流把 24C04 和数码管串起来逻辑其实很简单先往 EEPROM 写几个数字掉电后再读出来显示证明数据没丢。数据流是 EEPROM → 字节变量 → 显示缓冲区 → 段码查表 → P0 口。void main(void) { unsigned char i; // 第一次上电写入需要保存的数据 AT24C04_WriteByte(0, 0, 1); // 第 0 页地址 0 存 1 AT24C04_WriteByte(0, 1, 6); // 第 0 页地址 1 存 6 AT24C04_WriteByte(0, 2, 8); // 第 0 页地址 2 存 8 AT24C04_WriteByte(0, 3, 4); // 第 0 页地址 3 存 4 delay_ms(5); // 写周期保护两次写之间必须间隔 while (1) { // 每次上电读回 EEPROM 数据并显示 for (i 0; i 4; i) { num_buf[i] AT24C04_ReadByte(0, i); } display_scan(num_buf); } }注意 num_buf 里存的是 0~9 的数值display_scan 内部还要经过 seg_tab 查表转成段码。如果直接把读回来的字节当段码送数码管数值 1 会显示成 0xF9 代表的字形看起来就是乱的。这块稍微想一下就能理解EEPROM 里存的是数据段码表做的是数据到显示字形的映射两者不要混。这套源码的典型应用就是记住用户的最终选择或计数器的掉电值上电后自动恢复到断电前的状态。4. 源码结构拆解驱动分层与关键函数逐行说明4.1 工程文件怎么组织iic.c、at24c04.c、seg_display.c 各管什么拿到源码先别急着烧录看一遍文件结构再动手。规范的 51 工程会把驱动按外设拆分这样换芯片或换板子时只需要改底层不需要动业务逻辑。这套源码的驱动层一般拆成三个模块加主文件。文件职责对外提供接口iic.c / iic.h模拟 IIC 底层时序IIC_Start、IIC_Stop、IIC_SendByte、IIC_ReceiveByte、IIC_WaitAckat24c04.c / at24c04.h24C04 读写封装AT24C04_WriteByte、AT24C04_ReadByte、AT24C04_PageWriteseg_display.c / seg_display.h数码管扫描显示display_scan、seg_tab 段码表main.c业务逻辑编排读写 EEPROM刷新显示这个分层的意义在于IIC 时序层只关心波形不关心操作的是 EEPROM 还是传感器AT24C04 层只关心地址和数据不关心信号怎么产生显示层只关心段码和位选。你要移植到 STM32只需要重写 iic.c 这个文件上层函数一行不用改。4.2 IIC_SendByte 与 IIC_ReceiveByte逐位填充与超时判断发送字节的代码看起来简单但每一位的顺序都有讲究。SDA 的数据必须在 SCL 低电平时准备好SCL 高电平时从机采样所以标准流程是先放数据再拉高 SCL延时再拉低 SCL。void IIC_SendByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { IIC_SDA (dat 0x80) 7; // 先放最高位到 SDA IIC_SCL 1; // SCL 拉高从机采样 IIC_Delay(); IIC_SCL 0; // SCL 拉低准备下一位 IIC_Delay(); dat 1; // 左移次高位移到最高位 } }最高位优先是 IIC 协议的规定与串口的低位优先正好相反写寄存器配置时尤其要小心。dat 1 放在延时之后是因为 SCL 已经拉低了此时改变 SDA 不影响从机采样。整个循环执行完SDA 处于最后一位的值所以紧接着要调用 IIC_WaitAck 释放总线。接收字节要处理的方向相反从机在 SCL 高电平期间把数据放到 SDA主机在高电平期间读取。主机必须在开始接收前主动把 SDA 置 1因为 SDA 是开漏结构主机不释放从机拉不动线。unsigned char IIC_ReceiveByte(void) { unsigned char i, dat 0; IIC_SDA 1; // 释放 SDA交给从机控制 for (i 0; i 8; i) { IIC_SCL 1; // SCL 拉高数据有效 IIC_Delay(); dat 1; // 先左移再判断当前位 if (IIC_SDA) dat | 0x01; IIC_SCL 0; IIC_Delay(); } return dat; }注意 dat 1 必须放在读 SDA 之前这样第一次循环 dat 左移后仍是 0读到最高位放进 bit0第二次左移后最高位移到 bit1 位置以此类推。如果顺序写反收到的数会整体颠倒这是很隐蔽的 bug。4.3 从 51 移植到 STM32这几行代码必须改很多人把 51 上的模拟 IIC 直接搬到 STM32发现通信怪异原因是 GPIO 配置和延时系统完全变了。STM32 的 GPIO 要配成开漏输出模式同时开启内部上拉或外接上拉电阻。千万别配成推挽输出这就回到了 2.1 节说的线与他问题。延时也要重新适配。51 的nop() 是 1 个机器周期STM32 主频 72MHz 时一个空循环的时间完全不同。我一般会用 DWT 或 SysTick 做微秒级延时把 IIC_Delay 替换成独立的 delay_us(5) 或 delay_us(2)按目标速率换算。位操作也要改STM32 操作单个引脚建议用库函数或 BSRR 寄存器像 51 那样直接 sbit 定义的方式不存在。再就是上下拉策略。51 的 P0 口必须靠外部上拉STM32 内部有可编程上拉电阻偷懒时可以开启内部上拉但内部上拉阻值在 30~50kΩ适合 100kHz 标准模式跑 400kHz 最好还是外部 2.2k 上拉。5. 避坑手册IIC数码管联调的 5 个高频翻车点5.1 通信异常全是 0xFF、波形有台阶现象调用 AT24C04_ReadByte 永远返回 0xFF或者第一次上电能写第二次就失效。原因读回来全 0xFF通常是器件地址错误或 SDA 根本没有波形。可以先量一下 IIC_Start 之后 SDA 是否被拉低如果没有说明引脚定义错了或者上拉电阻缺失。全是 0xFF 也有可能是应答等待失败后返回的默认值我在 AT24C04_ReadByte 里失败就 return 0xFF所以要区分是读到了真 0xFF 还是通信失败。解决用 GPIO 翻转的方式观察 IIC_Start/IIC_Stop 的时序或者用逻辑分析仪抓 SDA/SCL 波形确认地址字节是 0xA0 还是 0xA2。通信失败时优先查三处引脚有没有接反、上拉电阻有没有虚焊、page 参数和器件地址的匹配关系。现象示波器量 SCL 和 SDA上升沿跟缓坡一样波形出现明显台阶。原因上拉电阻偏大总线电容把上升沿拉缓了尤其是在用杜邦线连接的时候几十 pF 的寄生电容再乘上 4.7k 电阻RC 时间常数就上去了。解决把上拉电阻从 4.7k 换成 2.2k如果还不行就换 1k同时尽量缩短杜邦线长度。另外检查示波器探头本身也有电容用 10X 档位可以减少探头负载对波形的影响。5.2 写入失败地址页选错、写周期没等够现象24C04 页选择完全复制 24C02 的驱动写 page1 的数据读出来的还是 page0 的旧值。或者写进去当时能读出来断电重启数据丢了。原因24C04 的页选择位在器件地址的第 1 位不是字地址的最高位。如果按 24C02 的思路把地址写到 0x01FF实际上字地址字节只取了低 8 位高位的 1 被完全忽略相当于还在第一页反复写。写周期问题则是 tWR 没等够EEPROM 内部擦写还没完成。解决严格按 AT24C04_WriteByte 的签名传 page 和 addr 两个参数器件地址用 0xA0 | (page 1)字地址只传 0~255。写周期用延时 5ms 兜底再进行写后读回验证。量产场景要更严谨可以写完后发一个 ACK 轮询命令直到器件回复 ACK 表示内部写完成不过裸机例程里延时足够。5.3 显示异常拖影、乱码、亮度不均现象数码管切换数字时能看到上一帧的残影尤其在数字变化第一时间最明显。另一个常见现象是某一位明显偏暗。原因消影顺序不对。动态扫描时位选切换的瞬间 P0 还保留着上一个位的段码新的位一选通立刻把上一帧的段码显示出来持续几百微秒就形成鬼影。亮度不均则是位选延时不一致或者三极管驱动能力差异。解决严格按照「先消影 → 再位选 → 后段码」的顺序扫描。先 P00xFF 把所有段熄灭再切换 P2 位选最后才送当前位的段码。如果位选用的是 P2 直接驱动检查每个 I/O 的灌电流能力共阳数码管位选是低电平选通驱动不足就把位选改成 74HC138 译码器加三极管扩流。每位保持时间用循环控制保持恒定。现象数码管显示内容和 EEPROM 里存的数值对不上比如存了 3 显示出来是 E。原因把读回来的数值直接当成段码送 P0 了。EEPROM 里存的是数值 0~9而 P0 要的是段码 0xC0~0xF9中间缺了查表这一步。解决加一层 seg_tab[buf[i]] 映射确认 num_buf 存的是数值而不是段码。现象整片数码管闪烁亮度一直跳。原因主循环里扫描间隔不均匀。如果在循环里插入了 IIC 读写操作IIC 通信期间数码管停止刷新每一帧的间隔被拉长且不稳定人眼就感知到闪烁。解决IIC 一次性把 4 个字节读完后再进入显示循环或者用定时器中断做固定节拍的扫描把显示刷新和 EEPROM 读写解耦。我一般会规定扫描周期 4ms用定时器中断里置一个标志位主循环只做数据读写两件事互不干扰。6. 验证三步走抓波形、掉电测试、页写优化6.1 逻辑分析仪抓 IIC 波形怎么算合格写完驱动别急着信任把 SDA 和 SCL 接到逻辑分析仪上抓一次完整读写过程。采样率至少 1MHz触发方式设为 SDA 下降沿。抓到的波形里应该能看到起始条件 SDA 先于 SCL 拉低、停止条件 SCL 先于 SDA 拉高每个字节 8 个 SCL 脉冲第 9 个脉冲高电平时 SDA 被从机拉低这是 ACK。如果 SCL 频率明显高于 400kHz说明延时太短24C04 可能不稳定把 IIC_Delay 加大再试。6.2 掉电保持测试验证数据真的写进去了代码烧进板子后做这样一轮测试先把一组测试数比如 1、6、8、4 写入 24C04 的 page 0确认数码管显示 1684然后直接断电等几秒再上电程序从 EEPROM 读回数据如果数码管仍然显示 1684说明写入和读回链路都正常。这个测试要特别注意别在写入过程中断电写满 5ms 前断电很可能留下脏数据。如果想测试写入的健壮性可以用示波器监控电源在延时 5ms 的不同时刻断电多次上电看数据是否稳定。6.3 进阶页写优化与多机通信如果数据量超过一个字节逐字节写入效率太低24C04 支持页写单次最多连续写 16 字节。实现页写时最关键的约束是跨页保护写地址加上长度不能超过页边界否则数据会回卷到本页开头覆盖前面的内容。实际项目里常见做法是把大块数据按 16 字节切分从页边界对齐开始分成多次页写完成。void AT24C04_PageWrite(unsigned char page, unsigned char addr, unsigned char *p, unsigned char len) { unsigned char i; if (addr len 16) // 超过页边界则截断 len 16 - addr; IIC_Start(); IIC_SendByte(0xA0 | (page 1)); IIC_WaitAck(); IIC_SendByte(addr); IIC_WaitAck(); for (i 0; i len; i) { IIC_SendByte(p[i]); IIC_WaitAck(); } IIC_Stop(); delay_ms(5); }页写还有一个隐藏收益写周期从每字节 5ms 变成每页 5ms写 16 字节的时间从 80ms 降到 5ms这对做产品固件升级、参数存储这类场景非常重要。但页写的 ACK 时机和字节写不同页写过程中第 16 个字节后器件进入内部写周期此时不再响应 ACK后续没有等待 ACK 的步骤直接 Stop 即可。从那以后我每次写完 IIC 驱动都强制走一遍抓波形加掉电测试的流程宁可多看几眼波形也不愿意到现场才发现数据丢。这套源码把 IIC、EEPROM、数码管三条线一次打通希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑