新闻详情

新闻详情

首页 / 资讯中心 / 详情

海思平台GPIO模拟I2C驱动实战:从时序到源码解析

发布时间:2026/9/7 7:36:23来源:尧图网络
海思平台GPIO模拟I2C驱动实战:从时序到源码解析
简介在海思平台开发中如果硬件I2C控制器不足或为了节约资源常常使用GPIO模拟I2C总线通信这套驱动源码正好给出完整参考。压缩包很小仅有4KB里面包含三个文件一个头文件、一个C语言源文件和一个Makefile头文件定义设备结构体以及函数原型源文件实现设备初始化、移除和核心的数据传输函数Makefile负责内核模块编译配置。目前已有989人参与学习下载适合嵌入式Linux驱动开发人员。整个传输函数的逻辑细致覆盖了起始信号、停止信号、地址读写、数据收发与应答检测帮助理解怎样通过高低电平变化模拟I2C时序代码中对时序延迟和错误处理的安排同样可以为海思平台系统调试或者其他外设驱动开发提供借鉴。1. 为什么海思平台要用GPIO模拟I2C1.1 硬件I2C不够用的真实场景海思平台Hi3516、Hi3536、Hi3559这些常见IPC/NVR主控本身I2C控制器不算少一般都有两到三个硬件I2C总线但真到项目落地的时候总会有那么几回被逼到墙角。我碰到过的情况大致是这么几类一是项目改版时新加了一个传感器比如光敏、温湿度或者音频编解码芯片结果硬件上能用的I2C总线全被占满了要么接在某个引脚上但该引脚已经被复用成其它功能二是板子布线问题导致原有的I2C总线走线过长信号质量差从设备死活枚举不到这时候临时拉两根GPIO出来做软件I2C是最快的解法三是在某些定制板卡上硬件设计本来就把I2C设备挂在了GPIO上根本没有接专用的I2C控制器引脚。这种时候海思平台GPIO模拟I2C驱动就成了救命稻草。它的本质就是通过软件控制GPIO引脚的电平翻转严格模拟I2C总线的时序协议让任何挂在总线上的标准I2C从设备EEPROM、温湿度传感器、IO扩展芯片、触摸屏控制器等等感觉不到你用的是“假”I2C。对做嵌入式的朋友来说这套东西不是“会不会”的问题而是“迟早要会”的问题。1.2 模拟I2C的优劣势分析与方案取舍一定要先说清楚GPIO模拟I2C不是用来替代硬件I2C控制器的它是在特定条件下的补充方案。先看优势。第一是引脚自由不挑引脚只要是有输入输出能力的GPIO都能用不用受硬件I2C引脚复用关系的约束第二是调试方便协议栈就在自己手里每一步都可以加打印时序不对可以直接改代码不像硬件控制器出问题时只能干瞪眼第三是灵活度高想模拟多少个I2C总线都行只要GPIO够用第四是兼容性好如果从设备有时序上的特殊要求比如某些器件对SCL上升沿有要求可以随时调整软件延时。再看劣势。模拟I2C最大的问题是时序稳定性不如硬件控制器尤其是在Linux内核里做的时候一个调度延时抖动可能就让SCL的周期偏移很大碰到时序要求苛刻的从设备就头疼。其次它要占用CPU资源每次位翻转都是CPU亲自去做的大量读写时会拉高CPU占用率。最后模拟I2C很难做到高速模式一般稳定跑100Kbit/s的标准模式或者400Kbit/s的快速模式就到头了再往上就很容易出错。所以在方案取舍上我的原则是能用硬件I2C绝不用模拟只有在硬件资源受限、调试临时救火、或者对速度没要求且需要灵活调整时序的情况下才用GPIO模拟I2C。这个定位要先想清楚后面写代码的心态就完全不一样了。2. 模拟I2C的核心思路与时序控制细节2.1 先回顾I2C协议的关键时序写模拟I2C之前I2C协议本身必须吃透不然代码写出来也是一堆bug。I2C总线只有两根线一根是SCL时钟线由主机控制一根是SDA数据线双向。总线上所有设备通过地址来区分通信由主机发起。有几个关键时序节点必须烂熟于心。起始条件STARTSCL为高电平时SDA由高变低。停止条件STOPSCL为高电平时SDA由低变高。数据位的改变SDA数据必须在SCL为低电平时改变在SCL为高电平时保持稳定这样从设备才能正确采样。应答信号ACK每传输完一个字节8位后第9个时钟周期释放SDA由接收方拉低表示应答如果接收方不拉低则说明无应答NACK。GPIO模拟I2C的精髓就是把这几个时序用代码精确还原出来。实际写代码时起始条件就是“先把SDA拉高SCL拉高再拉低SDA最后拉低SCL”停止条件反过来SCL高时SDA从低变高。看起来简单但这些操作之间的延时直接决定了通信速率和稳定性。2.2 海思平台GPIO的方向配置和电平操作在海思平台上操作GPIO不同SDK版本、不同内核版本的接口差异很大。有些平台用gpiolib的标准接口有些则用SDK自带的寄存器操作接口还有的板子干脆是通过ioctl直接操作/dev/mem。这一步很关键因为模拟I2C对时序有要求接口的调用开销越小越好。如果是在内核态做最直接的当然是操作寄存器。海思的GPIO寄存器一般按bank组织每个bank有数据寄存器、方向寄存器、中断寄存器等。拿常见的Hi3516系列来说GPIO基址加上偏移就能直接读写引脚电平。但如果你用的是SDK封装好的GPIO操作函数也务必确认这些函数在调用时不会产生太多额外开销否则每个bit翻转都去走一遍spinlock加函数调用时序会稀烂。GPIO的8种工作模式这里要提一下。很多读者对“工作模式”理解成输入、输出两种其实不然完整的概念一般包含上拉pull-up、下拉pull-down、推挽输出push-pull、开漏输出open-drain、模拟输入、浮空输入等模式组合。模拟I2C设计里最理想的做法是把SDA引脚配成开漏输出如果硬件支持因为开漏输出的引脚可以主动拉低释放后靠外部上拉电阻恢复高电平这样天然符合I2C总线“线与”的特性。但海思很多GPIO不支持开漏模式那就只能在代码里手动切换SDA的方向了。具体的做法是写数据时把SDA配成输出拉低或拉高读ACK或读数据时把SDA切回输入模式然后读取引脚电平。另外一个容易踩坑的点是引脚上下拉电阻的选择。模拟I2C的SDA、SCL外部必须接上拉电阻一般4.7K到10K比较合适。如果板子上本来没画上拉电阻软件再怎么做都是白搭可能偶尔能通信但极不稳定。3. 海思平台GPIO模拟I2C驱动源码解析3.1 引脚复用与初始化配置在海思平台上用GPIO模拟I2C第一件事就是把引脚从复用功能切到GPIO功能。这个步骤如果漏了后面所有代码都是白跑。海思的引脚复用通常通过配置寄存器来实现比如在Hi3516EV200上要用某个引脚作为GPIO需要先看它是否默认被复用为其他功能如果是就要修改复用寄存器。下面是一个内核态模拟I2C驱动的初始化代码骨架#include linux/module.h #include linux/kernel.h #include linux/init.h #include linux/delay.h #include linux/gpio.h #include linux/of_gpio.h #include linux/platform_device.h #define GPIO_I2C_SCL 158 #define GPIO_I2C_SDA 159 static void gpio_i2c_init_pins(void) { /* 海思平台一般先在 DSP/系统寄存器里把引脚配成GPIO模式 再用gpio_request申请占用最后设置方向和初始电平 */ gpio_request(GPIO_I2C_SCL, i2c_scl); gpio_request(GPIO_I2C_SDA, i2c_sda); gpio_direction_output(GPIO_I2C_SCL, 1); gpio_direction_output(GPIO_I2C_SDA, 1); }这里注意gpio_request之前最好确认引脚有没有被其他驱动占用否则会返回-EBUSY。另外海思有些SDK版本的GPIO编号和原理图上标的GPIO组号不是直接对应的需要看清楚内核里的GPIO base映射关系。我曾经在Hi3536上遇到过明明在dts里把引脚功能配好了但gpio_request一直失败的情况最后发现是编号差了个base偏移白排查了半天。3.2 时序转换函数与延时控制GPIO模拟I2C的代码核心就两个字延时。总线速率靠延时控制时序稳定也靠延时控制。标准I2C模式下SCL周期大概是10us高电平和低电平各5us快速模式400Kbit/s下SCL周期2.5us高低电平各1.25us左右。但在海思嵌入式平台上软件延时函数本身的精度要实测因为内核里的ndelay、udelay在CPU频率、Cache状态不同的情况下表现会不一样。我常用的做法是先写一个带参数的延时控制函数调试时通过示波器看SCL波形再微调次数static void gpio_i2c_delay(unsigned int cycles) { volatile unsigned int i; for (i 0; i cycles; i) { /* 空循环 */ } }cycles的初值根据CPU主频估算一个空循环大概几个周期。比如主频1GHz时目标半周期5us需要大约5000个周期但如果内核开了编译器优化空循环可能被优化掉所以volatile不能省。更好的方案是直接用udelay(5)这种内核提供的忙等待接口但在内核态频繁调用udelay其实开销也不小。我实际用的比较多的是折中方案用ktime接口做精确延时或者干脆在宏里用udelay先跑通再根据示波器实测换成自旋延时。3.3 起始停止与字节读写实现下面这段是模拟I2C的核心时序代码包含了启动、停止、读写一个字节、ACK检查。这些函数在内核态或应用态都可以用只需要替换底层的GPIO操作接口。static void gpio_i2c_scl_high(void) { gpio_direction_output(GPIO_I2C_SCL, 1); } static void gpio_i2c_scl_low(void) { gpio_direction_output(GPIO_I2C_SCL, 0); } static void gpio_i2c_sda_high(void) { gpio_direction_output(GPIO_I2C_SDA, 1); } static void gpio_i2c_sda_low(void) { gpio_direction_output(GPIO_I2C_SDA, 0); } static int gpio_i2c_sda_read(void) { gpio_direction_input(GPIO_I2C_SDA); return gpio_get_value(GPIO_I2C_SDA); } static void gpio_i2c_start(void) { gpio_i2c_sda_high(); gpio_i2c_scl_high(); gpio_i2c_delay(50); gpio_i2c_sda_low(); gpio_i2c_delay(50); gpio_i2c_scl_low(); gpio_i2c_delay(50); } static void gpio_i2c_stop(void) { gpio_i2c_sda_low(); gpio_i2c_scl_high(); gpio_i2c_delay(50); gpio_i2c_sda_high(); gpio_i2c_delay(50); } static int gpio_i2c_write_byte(unsigned char data) { int i; int ack; for (i 7; i 0; i--) { if (data (1 i)) { gpio_i2c_sda_high(); } else { gpio_i2c_sda_low(); } gpio_i2c_delay(30); gpio_i2c_scl_high(); gpio_i2c_delay(50); gpio_i2c_scl_low(); gpio_i2c_delay(30); } /* 释放SDA准备读ACK */ gpio_i2c_sda_high(); gpio_i2c_delay(30); gpio_i2c_scl_high(); gpio_i2c_delay(50); ack gpio_i2c_sda_read(); gpio_i2c_scl_low(); gpio_i2c_delay(30); return (ack 0) ? 0 : -1; } static unsigned char gpio_i2c_read_byte(void) { int i; unsigned char data 0; gpio_i2c_sda_high(); gpio_direction_input(GPIO_I2C_SDA); for (i 0; i 8; i) { gpio_i2c_delay(30); gpio_i2c_scl_high(); gpio_i2c_delay(50); data 1; if (gpio_i2c_sda_read()) { data | 0x01; } gpio_i2c_scl_low(); gpio_i2c_delay(30); } gpio_direction_output(GPIO_I2C_SDA, 1); return data; }要注意的是gpio_i2c_sda_read()函数里先做了gpio_direction_input目的是在读取前把SDA切换到输入模式。海思GPIO如果输出模式下直接gpio_get_value有些平台读到的其实是输出寄存器的值而不是引脚实际电平这一点很多人都栽过。读字节的函数在每次读之前都切回输入读完一位后没有切回去而是在整个字节读完后切回输出这个细节可以省掉很多重复的方向切换提高效率。3.4 多字节读写封装给从设备传数据不可能是单字节的一般是“设备地址寄存器地址数据”的组合。所以还需要封装一个复合读写函数。以EEPROM的页写为例包括起始条件、发送7位地址和写标志、发送寄存器地址、发送一个或多个数据字节、停止条件。整个过程只需要按顺序调用前面那些底层函数就行int gpio_i2c_write_reg(unsigned char dev_addr, unsigned char reg_addr, unsigned char *buf, int len) { int i; int ret; gpio_i2c_start(); /* 发送设备地址写位 */ ret gpio_i2c_write_byte((dev_addr 1) | 0); if (ret ! 0) { gpio_i2c_stop(); return -1; } /* 发送寄存器地址 */ ret gpio_i2c_write_byte(reg_addr); if (ret ! 0) { gpio_i2c_stop(); return -1; } /* 写数据 */ for (i 0; i len; i) { ret gpio_i2c_write_byte(buf[i]); if (ret ! 0) { gpio_i2c_stop(); return -1; } } gpio_i2c_stop(); return 0; }读EEPROM的流程稍微绕一点需要先写寄存器地址然后重新发一遍起始条件即重复起始条件repeated START再发送设备地址加读位然后连续读数据最后一个字节要发NACK表示读完了最后停止。这个重复起始条件的实现直接在start之后重新写地址就行但要注意SCL/SDA的状态要正确不然从设备不会认。4. 实际调试问题与排查经验4.1 SCL频率异常和ACK读不到我在海思平台调模拟I2C遇到最多的就是ACK失败。表面上看起始条件、时钟都正常但写完设备地址后读ACK永远是高电平。这种情况先排查两件事一是从设备地址有没有写错。7位地址加上读写位之后要左移一位很多新手在地址拼接上翻车。比如设备地址是0x50AT24C02的典型地址发送时要发0xA0即0x501|0如果直接发0x50从设备根本不响应ACK自然是NACK。第二件事是查SDA方向切换的时机。我发现很多人写模拟I2C时写完最后一个数据位后直接就去读SDA但此时SDA还处于输出模式而且上一次设置的电平是1引脚被强拉高从设备想拉低也拉不动这是典型的“总线争抢”问题。正确做法是第8个时钟下降沿后先把SDA切到输入模式再拉高SCL在第9个时钟的高电平期间读取SDA。这个顺序错一步ACK读取就会失败而且偶尔成功偶尔失败的现象特别容易让人怀疑是硬件问题。SCL频率异常的问题更隐蔽。如果示波器上看到SCL波形不是方波上升沿很缓那多半是上拉电阻太大加上引脚电容太大导致的。比如用了100K欧姆的大阻值上拉SCL上升时间会明显变长超过I2C协议标准的1us之后从设备可能就采样出错了。解决方法是换小阻值上拉电阻或者降低总线速率把延时时序整体拉长。4.2 中断和系统调度对时序的干扰内核态模拟I2C还有个大敌是中断和进程调度。SCL正在高电平期间来了一个中断ISR在里面跑100us那SCL高电平就被硬生生拉长了整个I2C时序的占空比就乱了。有的从设备可以容忍这种偏差有的不行尤其是时序带检测功能的器件会直接判定为非法通信总线就挂在那里。这个问题的解决方法主要有几种。第一在做关键时序操作时关闭中断或者屏蔽本地中断但这会造成系统实时性下降能用但别多用。第二把驱动挂在实时线程里每个I2C操作是一个独立任务优先级调高减少被其他任务抢占的概率。第三降低I2C速率到标准模式100K或以下让时序裕量变大系统调度带来的抖动就不容易导致超限。如果是在应用层用/dev/mem操作GPIO做模拟I2C系统调度的干扰会更大因为用户态的线程随时可能被换出CPU。一个改善办法是用pthread_setschedparam给线程设置实时优先级配合mlockall锁定内存减少被换出的概率。但说实话应用层做模拟I2C我通常只用来调试真要做生产用的驱动还是要进内核态做或者使用海思平台自带的MISC驱动框架来挂一个字符设备把I2C逻辑放在内核里。4.3 总线死锁与多设备挂载问题GPIO模拟I2C还有一个比较头疼的故障模式是总线死锁。当从设备在某个时间点等待主机继续发时钟而主机因为流程错误提前停了SCL从设备会一直把SDA拉低导致整个总线处于“锁死”状态。这时候最直接的解决办法是连续给SCL发9个时钟脉冲来复位从设备状态机。这个技巧非常管用我写的模拟I2C驱动里一般都会内置一个gpio_i2c_recover函数在通信失败时自动触发总线恢复。多设备挂载时还有一个细节要注意。如果两个从设备的地址相同比如两块相同型号的EEPROM挂在同一条模拟I2C总线上就需要通过它们的地址引脚A0/A1/A2来区分地址。AT24C02这类EEPROM通常有3个地址引脚设计时可以通过上下拉让它们产生8个不同地址。如果硬件没有复用到地址引脚那软件怎么调也调不出来两片一样的设备这也是排查时要提前想到的。4.4 电平匹配和上拉电阻选择海思主控的IO电平通常是1.8V或者3.3V这取决于核心板的设计。如果I2C从设备的工作电平和主控不一致比如有个外设是5V供电那就必须在总线上加电平转换芯片绝对不能直接连。GPIO模拟I2C也一样SCL/SDA直接接过去轻则读不到数据重则烧毁引脚。上拉电阻的取值经验上这么选3.3V系统用4.7K到10K都行1.8V系统建议用2.2K到4.7K具体看总线挂载的设备数量和走线长度。总线设备越多、走线越长上拉电阻就要越小以保证足够的驱动能力。但上拉电阻也不能太小否则低电平下拉电流太大GPIO可能拉不低SCL/SDA低电平状态就不对了。一个简单的验证方法是设备通信时用示波器量SCL低电平确保低于设备VIL阈值一般是0.3倍VDD左右。最后的一点个人经验项目里这套GPIO模拟I2C驱动前前后后调了一个多月踩过的坑比写过的代码还多。我自己的心得就是调模拟I2C别上来就抱着示波器猛看波形先把代码逻辑理清楚从起始条件到ACK一个一个时序去核对把“什么时候切方向”“什么时候拉电平”这些顺序确定得明明白白再上板调试事半功倍。真遇到波形不对的再拿示波器和逻辑分析仪看也不晚。另外如果在海思平台上做长期稳定的项目我的建议还是优先把设备挂到硬件I2C总线上GPIO模拟I2C用作备胎方案或者临时救急就好。毕竟控制器在稳定性、功耗、传输速率上确实有天然优势。但无论如何掌握GPIO模拟I2C这套手法在做嵌入式开发的过程中绝对是值得的——它逼着你把I2C协议吃透也让你在遇到硬件资源紧张时多一条出路。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Qt翻金币游戏实战:QPainter动画与信号槽架构详解 2026/9/7 8:24:31

Qt翻金币游戏实战:QPainter动画与信号槽架构详解

简介:一份基于QT5框架开发的翻金币游戏完整项目源码,适合正在学习Qt界面编程、动画特效与窗口管理的开发者作为实战参考。游戏包含开始界面、菜单界面和关卡选择界面,涵盖自定义按钮交互、金币翻转动画、多页面切换等典型功能,代码…

阅读更多 →
MMD技术进阶:从虹膜收缩特效到完整3D动画工作流 2026/9/7 8:24:31

MMD技术进阶:从虹膜收缩特效到完整3D动画工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Spring Cloud微服务实战:网约车项目核心链路与高并发方案 2026/9/7 8:24:31

Spring Cloud微服务实战:网约车项目核心链路与高并发方案

简介:OnlineTaxi 是基于 Spring Cloud 的网约车全流程实战项目,面向具备一定 Java 基础、希望学习微服务架构的开发者或相关专业学生。项目按乘客端、司机端与能力层拆分为订单、派单、乘客用户、短信、计价、验证码、钱包、支付、地图等多个服务&#x…

阅读更多 →
MATLAB实现涡格法:从原理到代码实战 2026/9/7 8:24:31

MATLAB实现涡格法:从原理到代码实战

简介:涡格法(VLM)在亚声速气动力快速评估中具有广泛应用,基于MATLAB的涡格法程序包正是面向航空工程学习者、飞行器初步设计人员以及需要快速估算升阻力特性的研究者的实用工具。资源围绕几何建模、边界条件设置、涡强分配、欧拉方…

阅读更多 →
Axios 进度捕获实战:onUploadProgress 与 onDownloadProgress 在浏览器和 Node.js 中的实现原理 2026/9/7 8:24:31

Axios 进度捕获实战:onUploadProgress 与 onDownloadProgress 在浏览器和 Node.js 中的实现原理

Axios 进度捕获实战:onUploadProgress 与 onDownloadProgress 在浏览器和 Node.js 中的实现原理 【免费下载链接】axios Promise based HTTP client for the browser and node.js 项目地址: https://gitcode.com/GitHub_Trending/ax/axios 本文基于 axios 官…

阅读更多 →
FreeRTOS官方MDK demo实战:STM32F103上跑通内核与CLI命令行 2026/9/7 8:21:31

FreeRTOS官方MDK demo实战:STM32F103上跑通内核与CLI命令行

简介:面向嵌入式初学者的FreeRTOS演示工程,在Windows下使用MDK Keil uVision4打开即可直接编译烧写,目标芯片为STM32F103。工程将FreeRTOS内核依赖完整收录,代码层面重点演示通过串口中断接收数据来触发命令行,从而在真…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞