尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LPC1768 I2C工程实战:状态机、EEPROM读写与总线锁死排查

LPC1768 I2C工程实战:状态机、EEPROM读写与总线锁死排查 简介这是基于NXP LPC1768微控制器的I2C通信驱动源码包面向正在学习Cortex-M3平台I2C总线编程的嵌入式开发者。代码由其他平台工程移植而来可直接参考寄存器配置、收发时序与从机寻址逻辑帮助理解主模式下的通信流程加速项目开发中的驱动适配。压缩包共7个文件约8KB包含2个C源文件、1个头文件以及Keil工程文件uv2、内存配置文件ini、工程选项文件opt和说明文档txt。C文件分别实现底层I2C驱动与主机收发接口头文件提供寄存器和函数声明配合工程文件可在MDK中直接打开编译验证。资源已有98人学习下载适合刚接触LPC1768或需要快速移植I2C外设驱动的开发者。结合源码文件与工程配置读者能掌握从时钟初始化、启动停止条件到数据帧发送的完整实现并可根据自身板卡灵活修改引脚与速率参数。 这东西我太熟了——一看到这个压缩包名字就知道是有人把LPC1768平台的I2C工程包发出来了。LPC1768这个芯片在工业控制、物联网网关里用了十多年还不过时I2C总线又是接EEPROM、传感器、IO扩展芯片最常见的接口所以这套代码本质上就是教你在这颗NXP经典Cortex-M3上把I2C跑通、跑稳、跑出工程经验。这篇我不打算只把代码贴一遍了事而是顺着拿到这个包之后“从看懂到改好”的完整过程来讲。硬件I2C和软件模拟I2C怎么取舍、7位地址扫描怎么写、总线锁死怎么救、和STM32平台比有哪些不同点这些问题我在实际项目里都踩过整理出来一次说清楚。适合手里有LPC1768开发板、或者是想在老平台上做I2C外设驱动的朋友参考。1. 拿到LPC1768 I2C工程包之后先搞清楚硬件基础1.1 LPC1768的I2C引脚分配和内部结构LPC1768的I2C控制器有4个编号I2C0到I2C3其中I2C0、I2C1、I2C2是标准I2C接口I2C3比较特殊是带SMBus支持的版本。开发板上最常用的是I2C0默认引脚是P0.27SDA0和P0.28SCL0但LPC1768的好用之处在于引脚可以重映射——比如I2C0还能映射到P0.0/P0.1I2C1能在P0.19/P0.20、P0.29/P0.30之间切换。这意味着画PCB或接线的时候灵活性很大不用为了I2C特意绕线。内部结构上每个I2C外设都自带一个状态机控制器它不像有些单片机需要你在中断里手动模拟起始、停止、应答时序而是硬件自动完成字节传输你只需要关注状态寄存器I2STAT里的状态码。这一点很关键因为LPC1768的I2C驱动核心就是一张“状态码查表”——每个状态码对应一个动作写清楚这张表驱动就完成了一大半。1.2 打开工程先看这两个文件头文件和主循环我打开这个压缩包后第一件事是找lpc17xx_i2c.h和lpc17xx_i2c.c这是NXP官方外设库里的I2C驱动文件配合system_LPC17xx.c里时钟树配置基本能跑通。官方库的函数风格我比较熟I2C_Init()做初始化I2C_Start()发起始位I2C_SendStop()发停止位读写操作对应I2C_SendData()和I2C_GetData()。如果包里还带i2c_app.c这类应用层文件多半是封装好了的读写函数比如I2C_WriteNBytes()这种直接调就行。需要留个心眼的是不同项目的裁剪方式差别很大。有的包把中断处理函数I2C0_IRQHandler()放在main.c里有的放在外设库里有的用查询方式有的用中断加状态机的轮子。拿到代码后先把“状态码处理”和“中断入口”这两处找出来整个程序的执行脉络就清楚了。2. 看懂LPC1768 I2C的状态机机制比背代码重要2.1 状态码查表法核心转发逻辑LPC1768的I2C控制器很有意思它会在每个总线事件后自动更新I2STAT寄存器里面存的就是当前所处的状态编码。主机发送模式下0x08表示起始条件已发送0x18表示从机地址加写位已发出并且收到ACK0x28表示数据字节已发送且收到ACK主机接收模式下0x40表示地址加读位已发出且收到ACK0x50表示收到一个数据字节并已回ACK。还有0x20这种地址被NACK、0x30数据被NACK的错误状态。我习惯在主循环里用一个switch(I2C_GetStatus())来转发伪代码大致是这样uint8_t I2C_MasterSendByte(uint8_t devAddr, uint8_t data) { I2C_Start(); if (I2C_GetStatus() ! 0x08) return I2C_ERR_START; I2C_SendData(devAddr 1); // 地址左移一位是写 if (I2C_GetStatus() ! 0x18) return I2C_ERR_ADDR; I2C_SendData(data); if (I2C_GetStatus() ! 0x28) return I2C_ERR_DATA; I2C_SendStop(); return I2C_OK; }这里的核心逻辑是“每次发完后等待状态码改变再根据状态码决定下一步”。这是LPC1768的I2C和STM32的HAL库最大区别STM32的HAL库把状态码封装成回调你只需要关心事件标志LPC1768则把状态码直接暴露出来裸奔感更强但也更透明——出了问题查状态码就能定位到具体是哪一步失败。2.2 查询方式还是中断方式什么时候选哪个官方库两种方式都支持。查询方式简单直白适合单主机、低速、传输量不大的场景比如读温湿度传感器、写EEPROM。缺点是传输过程中CPU要一直等状态码没法去做别的事。中断方式适合多字节连续传输比如从AT24C256里一次读几十个字节此时每个字节都产生中断把状态码判断和数据存取放到IRQHandler里主循环去处理协议逻辑。实际项目中我一般这样定从机数量少于5个、波特率不超过100kHz用查询从机多、数据块大、波特率上400kHz甚至1MHz用中断。LPC1768的中断配置需要注意一个细节I2C0、I2C1、I2C2的中断号不一样NVIC_EnableIRQ()里填I2C0_IRQn还是I2C1_IRQn要看用哪个外设。我接过一个项目代码是从I2C0改成I2C1结果中断号没改现象是发送起始位后程序卡死查了半天才发现是NVIC里中断没使能。3. 这套代码的真正核心从GPIO模拟到硬件外设的切换3.1 为什么LPC1768很适合做软件I2C搜索热词里出现“STM32F407模拟I2C”“BH1750软件I2C”说明大家在I2C上踩过不少坑之后第一反应是绕过硬件外设、用GPIO模拟。这个思路在LPC1768上同样适用而且LPC1768的GPIO开漏配置很灵活GPIO_SetDir()把方向寄存器设好GPIO_SetValue()输出高低电平配合__NOP()延时就能写出最简单的软件I2C。但我要说点实际的LPC1768的硬件I2C没有像某些芯片那样难用它的状态机是出了名的“直白”反而比软件模拟更容易调。软件模拟唯一必须用到的场景是硬件I2C引脚被其它外设占了引脚重映射也来不及改板子这时候在任意两个GPIO上模拟比如用P1.0做SCL、P1.1做SDA配好开漏加上拉就能跑。热词里“BH1750软件I2C”很典型——光强度传感器对时序要求不高软件模拟完全没问题而且BH1750地址固定为0x23或0x5C7位地址扫描一下就能找到。3.2 地址扫描快速判断设备在不在总线上无论用什么方式驱动I2C调试第一步都是确认设备地址对不对。这个包里的代码如果没有现成的扫描函数我建议你自己加一个把总线上所有可能的7位地址0x01到0x7F逐个发地址加写位能收到ACK的就是存在的设备。void I2C_ScanBus(void) { uint8_t i; for (i 1; i 128; i) { I2C_Start(); I2C_SendData(i 1); if (I2C_GetStatus() 0x18) { printf(Found device at 0x%02X\r\n, i); } I2C_SendStop(); } }这个代码在LPC1768上跑起来的效果立竿见影。我之前调试一块板子总共接了三个I2C设备时钟芯片、EEPROM、温度传感器扫描结果只有两个地址有应答第三个怎么都不出来。后来查原理图发现它的地址引脚被拉错了地址从0x48变成了0x4F——扫描一遍地址几秒钟就定位问题比一个个设备查手册高效得多。3.3 I2C时序的关键理解热词里“I2C时序图”被反复搜索说明时序是初学者的拦路虎。其实I2C的时序核心就两条起始/停止条件要“在SCL高电平期间改变SDA”数据传输要“在SCL高电平期间保持SDA稳定、低电平期间改变SDA”。硬件I2C模式下这些由控制器自动完成但如果你用软件模拟忘记“SCL高时SDA不能变”这一条设备就会把普通数据误判成起始或停止条件表现就是读回来的数据乱跳、地址总是不应答。LPC1768的I2C时钟频率配置也很直观I2CSCLH和I2SCLL两个寄存器各设高电平和低电平时间总线时钟120MHz时要得到400kHz的I2C频率两个寄存器都配成120000000 / (2 * 400000) 150左右。我用示波器测过这个配置出来的频率是400.2kHz完全在误差范围内。4. EEPROM读写LPC1768上最容易上手的实战4.1 从AT24C系列开始的读写模版热度词里“I2C读写EEPROM代码 verilog”说明很多人是从EEPROOM开始接触I2C的。LPC1768虽然是个ARM芯片但读写AT24C系列EEPROM和单片机没什么区别。我以AT24C02为例它的容量是256字节7位地址0x50支持单字节写和页写一页8字节。硬件I2C下写单字节的流程是起始条件 → 发送设备地址写位 → 发送内部字节地址 → 发送数据 → 停止条件。看起来简单但里面有一个坑EEPROM写完一字节后需要5毫秒左右的自定时写周期在这个时间内不能给它发任何命令否则数据会丢。void EEPROM_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); I2C_SendData(0x50 1); // 设备地址写 I2C_SendData(addr); // 内部地址 I2C_SendData(data); // 要写的数据 I2C_SendStop(); delay_ms(6); // 等待写周期结束 }读取就更简单先发一个空写把内部地址设置好然后重新发起始条件发送地址读位之后每收到一个字节回ACK读到最后一个字节回NACK再发停止条件。LPC1768的状态机在这段流程里会依次经历0x08 → 0x18 → 0x28 → 0x28 → 0x10重复起始 → 0x40 → 0x50 → 0x58最后一个字节对着状态码表写思路特别清晰。4.2 连续读和跨页写的边界问题EEPROM练手时最容易出错的是跨页写。AT24C02每页8字节页写时内部地址计数器到了页边界会“回卷”也就是写到第7字节后如果继续写地址会跳回页首而不是进入下一页。有一次我写一个30字节的传感器校准数据分了好几次页写结果回读只有最后8字节是对的前面全是垃圾就是因为没处理跨页问题。正确做法是每一页最多写8字节地址超过页边界就重新发一次“设备地址页内地址”。void EEPROM_WriteBytes(uint16_t addr, uint8_t *buf, uint16_t len) { uint8_t firstPage 8 - (addr 0x07); uint8_t count (len firstPage) ? len : firstPage; // 先写第一段到页边界 // 然后循环按8字节一页继续写 }LPC1768的主频和内存跑这个逻辑游刃有余关键是时序上留够写周期时间。如果芯片上EEPROM比较大比如256Kbit的AT24C256页大小是64字节写大块数据前先用示波器看看实际写周期是不是标准5ms有些国产芯片能做到3ms以内有些要7ms我都是统一按10ms延时来的稳比快重要。5. 总线锁死问题BQ76952、D2000还有不明所以的“没反应”5.1 锁死现象和根因分析热词里“bq76952 i2c调试没反应啊”“d2000 i2c锁死问题”是广大工程师的真实心声。I2C总线锁死最常见的现象是程序跑到某个设备读写时状态码卡在0x08之后不再前进或者SCL/SDA一直卡在低电平。根因通常是三种第一种是复位期间从机把SDA拉低。比如bq76952这类电池管理芯片它内部可能在刚上电时还没完成初始化此时你发起始条件它会误判协议状态把SDA钳住。第二种是总线电平不匹配——LPC1768是3.3V逻辑如果你外接的是5V的EEPROM部分AT24C系列能到5V上拉电阻接的却是3.3V电源设备在高电平时的识别阈值会有偏差时而通信正常时而卡死。第三种是时钟拉伸clock stretching——从机让SCL保持低电平以延长时钟周期但主机驱动没处理这个状态导致等待超时。5.2 锁死之后怎么救三板斧对付总线锁死我总结了“三板斧”按顺序用软件复位I2C外设I2C_Init()重新初始化一次状态机回到IDLE。这一招对协议卡死有效但如果是从机物理上拉住了SDA软件复位无效。用GPIO把SDA/SCL两个引脚临时拽成普通输出手动发送9个时钟脉冲一个停止条件。这是I2C协议里最经典的“解除总线锁死”操作。LPC1768里把引脚从I2C模式临时切回GPIO需要改PINSEL寄存器或者直接把外设关闭用GPIO_SETVALUE翻转。这样做的好处是强制让所有从机完成内部状态复位。断电重启从机。如果是电池管理芯片、气体传感器这类带复杂内部逻辑的设备只能断它的供电等几百毫秒再上电。我在实际项目里为了这一点专门在I2C设备的电源轨上加了一个MOS管开关MCU上电后先扫描总线检测到锁死就断开设备电源重启它这样通常能恢复而不需要人跑到现场断电。5.3 从I2C地址角度调试BQ76952这类复杂芯片BQ76952这类芯片地址不是简单固定的手册里写的是可以通过引脚配置变成多个地址调试时容易对不上。热词“bq76952 i2c调试没反应”我有同感当初第一次调试这个芯片I2C扫描怎么都找不到设备百思不得其解。后来发现是芯片的REGOUT和BREG配置不对芯片根本没进入正常模式I2C端口是高阻态你发地址它自然没应答。所以碰到“没反应”优先用示波器看SDA在主机发地址时有没有波形、从机有没有拉低SDA的迹象而不是反复改代码——大概率是设备没上电、没进工作模式、或者地址配置引脚接错。6. 功能扩展I2C译码器、编码器、模拟开关一总线多用6.1 I2C IO扩展一秒钟多出8个IO口很多LPC1768项目里MCU的IO吃紧这时候I2C扩展是一招妙棋。PCF8574是最经典的8位I2C IO扩展芯片地址由A0-A2引脚决定可以接8个一片8路8片就是64路。上个项目做设备状态灯需要24个LED指示灯占PCF8574三片地址0x20-0x22我写了一个简单的写函数void PCF8574_Write(uint8_t addr, uint8_t val) { I2C_Start(); I2C_SendData((0x20 addr) 1); I2C_SendData(val); I2C_SendStop(); }这里要特别提醒PCF8574输出低电平能力强输出高电平偏弱是电流源结构接LED时最好用低电平点亮否则LED亮度不均或者驱动能力不足推不亮。这个坑是我在一次产品调试中发现的后来改成共阳极接法所有问题都没了。6.2 I2C编码器旋钮、滑块的新玩法热词“i2c编码器”指的是像AS5600这样的磁性角度传感器通过I2C可以直接读到12位角度值比传统的机械编码器定位更准、寿命更长也没有接触抖动问题。我在LPC1768上接过AS5600它地址是0x36上电后读寄存器0x0C和0x0D就能得到角度高低字节uint16_t angle; uint8_t high, low; I2C_ReadRegister(0x36, 0x0C, high); I2C_ReadRegister(0x36, 0x0D, low); angle ((uint16_t)high 8) | low;这个芯片还有个特性它默认输出PWM但I2C模式才是它的完全体。第一次调试时我只读到满量程值不变化查了资料才发现要写配置寄存器把它从PWM模式切换成I2C模式否则数据寄存器一直是初始值。类似这类带模式配置的I2C芯片很常见拿到新设备先完整读一遍手册里所有的寄存器说明能省掉后续一大串莫名其妙的调试时间。6.3 I2C和SPI、UART怎么取舍热搜词里还有一个高频问题“USART、UART、I2C、SPI区别”。借这个项目顺便说清楚——UART是异步串口收发双方各用一根线靠波特率约定采样时刻适合点对点SPI用四根线SCK、MOSI、MISO、CS速度高、全双工但要占用片选脚适合高速大吞吐设备I2C是两根线SDASCL挂多设备靠地址区分速度比SPI慢但节省引脚硬件设计也最方便。LPC1768上三者都有我一般这样分配跟PC通信或调试日志走UART跟显示屏或Flash走SPI跟传感器、EEPROM、IO扩展这类低速设备走I2C。各自用各自的强项别一个接口硬扛到底。6.4 多主多从总线上的仲裁和冲突LPC1768的I2C总线是支持多主机仲裁的但实际工程里我几乎没见过谁在同一根总线上挂两个MCU主动发起通信。仲裁机制的原理是当两个主机同时尝试拉低SDA谁发的是高电平而对方发低电平时它会检测到总线状态和自己想发的不一致自动退出竞争。这个机制对可靠性有提升但对开发者要求也高你得处理仲裁丢失中断。我自己的习惯是宁可把总线设计成单主多从需要数据交换时用事件标志或IO握手不要让两个控制器在I2C总线上“抢话”。把时间花在硬件握手逻辑上比在仲裁代码上死磕划算得多。7. 移植到其它平台时的几条实战记忆7.1 从LPC1768到STM32F407模拟I2C热词里“STM32F407模拟I2C”搜的人不少我也移植过。LPC1768的状态机驱动风格和STM32的HAL库差别挺大但核心思路是一样的都是“发起起始 → 发送地址 → 等待应答 → 发数据/读数 → 停止”。差别最大的是STM32的HAL用阻塞超时机制LPC1768是用状态码查询所以移植时不要太纠结于“把函数一一对应”而是按协议阶段重新组织代码。今天你写LPC1768的状态机判断明天改写STM32的HAL库只要把7个状态的逻辑吃透了哪套API都只是换皮。7.2 软件I2C的兼容性设计如果要写一套软件I2C驱动同时兼容LPC1768和STM32建议直接把SDA/SCL操作封装成宏或底层函数上层只保留起始、停止、读写字节、等待ACK的函数。这个思路和LPC1768官方库的做法是一致的。软件I2C最大的优势就是“一套代码跑所有MCU”——我在多个项目里验证过只要底层接口用宏定义隔离开换芯片时只需要改时针延时和引脚配置协议层代码完全不动。像BH1750这类时序宽容度大的传感器软件I2C跑得非常稳读回来的光照度数据和硬件I2C几乎一致。7.3 回读校验的重要性不管是硬件I2C还是软件I2C我都建议在写EEPROM这类非易失存储后做一次回读校验。LPC1768跑起来很容易写完之后发起读操作对比读回的数据和要写的原始数据不一致就报错或者重写。我见过不少“数据隔几天丢一次”的案例最后定位都是EEPROM写时序不严格回读校验能提前把它揪出来。加这个逻辑代码量很小但可靠性提升一大截。8. 最后补充几个容易忽略但在实际工程里决定成败的细节这部分内容不是官方库文档里会写的是我在这类项目里反复验证过才会分享出来的LPC1768的I2C引脚是开漏结构外部必须有上拉电阻一般用4.7kΩ总线速度快、设备多的时候降到2.2kΩ或1kΩ。没有上拉电阻总线上的波形会很难看设备时好时坏。多个I2C设备时要给每个设备预留独立的“软件开关”芯片使能控制调试时可以单独复位某一个从机而不影响其它设备这在排查“没反应”类问题时省下大量时间。I2C总线波形异常时先别急着改代码用示波器看SDA/SCL的上升沿。如果上升沿很缓多半是上拉电阻太大或者总线寄生电容太大这种情况下只要把上拉改小一点、或者把I2C频率降到100kHz就稳定了代码层面改来改去是治标不治本。如果I2C总线上既有3.3V设备又有5V设备电平常要也要设计好最好加电平转换芯片或MOS管双向电平转换电路否则高温或干扰下总会出现不明所以的通信错误。说到底LPC1768的I2C外设给我最大的感受是“透明”——每个状态码都摆在明面上出问题时只要顺着状态码查一遍基本能知道是主机的问题还是从机的问题。这个包如果能看懂状态码、用好扫描函数、处理好应答超时就已经能覆盖绝大多数I2C应用场景。剩下的经验就是在不断调试和踩坑中积累的祝你的总线稳定运行点个灯、读个传感器全程顺利。本文还有配套的精品资源点击获取
返回列表