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

告别时序图恐惧症:手把手教你用C语言实现IIC通信(附完整代码)

告别时序图恐惧症:手把手教你用C语言实现IIC通信(附完整代码)

第一次看到IIC时序图时,那些高低电平的波形就像天书一样让人头疼。作为嵌入式开发者,我们常常需要把抽象的时序图转化为实实在在的代码。本文将带你一步步拆解IIC通信的每个环节,用最直观的方式教你如何"看图写代码"。

1. IIC通信基础回顾

IIC(Inter-Integrated Circuit)是一种简单、双向二线制的同步串行总线,只需要两根线(SDA和SCL)就能实现设备间的通信。在开始编码前,我们需要明确几个关键概念:

  • 起始条件(START):SCL为高电平时,SDA从高电平跳变到低电平
  • 停止条件(STOP):SCL为高电平时,SDA从低电平跳变到高电平
  • 数据有效性:在SCL高电平期间,SDA必须保持稳定
  • 应答信号(ACK):每传输完一个字节后,接收方需要发送一个低电平应答
// 示例:GPIO初始化代码 void IIC_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; // 配置SCL和SDA引脚为开漏输出 GPIO_InitStruct.Pin = IIC_SCL_PIN | IIC_SDA_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(IIC_PORT, &GPIO_InitStruct); // 初始状态:SCL和SDA都置高 HAL_GPIO_WritePin(IIC_PORT, IIC_SCL_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(IIC_PORT, IIC_SDA_PIN, GPIO_PIN_SET); }

2. 从时序图到代码:基础信号实现

2.1 起始信号生成

起始信号是IIC通信的第一个动作,它的时序要求非常严格:

  1. SDA线先置高
  2. SCL线置高
  3. 等待至少4.7μs(标准模式)
  4. SDA线拉低
  5. 等待至少4μs
  6. SCL线拉低
void IIC_Start(void) { // SDA和SCL初始状态都为高 SDA_HIGH(); SCL_HIGH(); delay_us(5); // 满足tSU;STA时间要求 SDA_LOW(); // 产生起始条件 delay_us(4); SCL_LOW(); // 准备数据传输 delay_us(1); }

注意:实际延时时间需要根据具体MCU时钟频率调整,这里只是示例值

2.2 停止信号生成

停止信号标志着一次通信的结束,其实现与起始信号类似但顺序相反:

void IIC_Stop(void) { SDA_LOW(); // 确保SDA为低 SCL_LOW(); // 确保SCL为低 delay_us(1); SCL_HIGH(); // 先拉高SCL delay_us(4); SDA_HIGH(); // 再拉高SDA,产生停止条件 delay_us(5); // 满足tBUF时间要求 }

3. 数据传输与应答处理

3.1 字节发送流程

发送一个字节需要严格按照时序图操作,每个bit的传输都包含以下步骤:

  1. SCL拉低
  2. 设置SDA为当前bit值
  3. 延时满足tLOW时间
  4. SCL拉高
  5. 延时满足tHIGH时间
  6. SCL再次拉低
void IIC_SendByte(uint8_t byte) { for(int i=0; i<8; i++) { SCL_LOW(); delay_us(1); // 从最高位开始发送 if(byte & 0x80) { SDA_HIGH(); } else { SDA_LOW(); } byte <<= 1; delay_us(3); SCL_HIGH(); delay_us(5); SCL_LOW(); delay_us(1); } // 释放SDA线,准备接收ACK SDA_HIGH(); }

3.2 应答检测实现

每个字节传输后都需要检测从设备的应答信号:

uint8_t IIC_Wait_Ack(void) { uint8_t ack = 0; SCL_LOW(); delay_us(1); // 释放SDA线,切换为输入模式 SDA_INPUT(); delay_us(1); SCL_HIGH(); delay_us(2); // 读取SDA状态 if(SDA_READ() == GPIO_PIN_RESET) { ack = 1; // 收到ACK } else { ack = 0; // 未收到ACK } SCL_LOW(); SDA_OUTPUT(); // 切换回输出模式 delay_us(1); return ack; }

4. 完整通信流程实现

4.1 写操作流程

一个完整的IIC写操作通常包含以下步骤:

  1. 发送起始信号
  2. 发送设备地址(7位地址 + 写标志位0)
  3. 等待应答
  4. 发送寄存器地址
  5. 等待应答
  6. 发送数据
  7. 等待应答
  8. 发送停止信号
void IIC_Write(uint8_t devAddr, uint8_t regAddr, uint8_t data) { IIC_Start(); // 发送设备地址(7位) + 写标志(0) IIC_SendByte((devAddr << 1) & 0xFE); if(!IIC_Wait_Ack()) { IIC_Stop(); return; // 无应答,退出 } // 发送寄存器地址 IIC_SendByte(regAddr); if(!IIC_Wait_Ack()) { IIC_Stop(); return; } // 发送数据 IIC_SendByte(data); if(!IIC_Wait_Ack()) { IIC_Stop(); return; } IIC_Stop(); delay_ms(10); // 等待写入完成 }

4.2 读操作流程

读操作比写操作稍复杂,通常需要先发送寄存器地址,然后再发起读请求:

uint8_t IIC_Read(uint8_t devAddr, uint8_t regAddr) { uint8_t data = 0; // 第一阶段:发送寄存器地址 IIC_Start(); IIC_SendByte((devAddr << 1) & 0xFE); // 写模式 IIC_Wait_Ack(); IIC_SendByte(regAddr); IIC_Wait_Ack(); // 第二阶段:重新启动并读取数据 IIC_Start(); IIC_SendByte((devAddr << 1) | 0x01); // 读模式 IIC_Wait_Ack(); data = IIC_ReadByte(); IIC_Send_NAck(); // 发送非应答信号 IIC_Stop(); return data; }

5. 实际应用中的优化技巧

5.1 延时优化

不同IIC设备对时序要求不同,可以通过宏定义灵活调整延时参数:

// 根据不同模式定义延时 #ifdef IIC_STANDARD_MODE #define IIC_DELAY_US 5 #elif defined IIC_FAST_MODE #define IIC_DELAY_US 1 #else #define IIC_DELAY_US 3 // 默认值 #endif

5.2 错误处理机制

完善的错误处理能提高代码健壮性:

#define IIC_TIMEOUT 1000 // 超时时间(ms) IIC_Status IIC_WriteWithRetry(uint8_t devAddr, uint8_t regAddr, uint8_t data) { uint32_t startTime = HAL_GetTick(); IIC_Status status = IIC_ERROR; while((HAL_GetTick() - startTime) < IIC_TIMEOUT) { IIC_Write(devAddr, regAddr, data); // 验证写入是否成功 uint8_t readBack = IIC_Read(devAddr, regAddr); if(readBack == data) { status = IIC_OK; break; } delay_ms(10); } return status; }

5.3 多设备管理

当总线上有多个IIC设备时,可以通过地址扫描检测可用设备:

void IIC_ScanDevices(void) { printf("Scanning IIC devices...\n"); for(uint8_t addr = 0x08; addr < 0x78; addr++) { IIC_Start(); IIC_SendByte((addr << 1) & 0xFE); // 尝试写模式 if(IIC_Wait_Ack()) { printf("Device found at 0x%02X\n", addr); } IIC_Stop(); delay_ms(1); } }
http://www.jsqmd.com/news/901416/

相关文章:

  • 开发者如何运用设计思维与创新方法解决技术难题
  • 从电机到屏幕:用STM32CubeMX+编码器+OLED,做个实时转速显示的小项目
  • 直流微电网并联变换器环流抑制:自适应下垂控制原理与工程实践
  • 2025-2026年变频器风机品牌推荐:TOP5评测市场份额防高温案例价格 - 品牌推荐
  • 别只当它是个编辑器:挖掘Dreamweaver CS6里那些被遗忘的‘高级’功能(AP Div与行为篇)
  • AI应用开发新范式:从直觉驱动到评估驱动开发(EDD)
  • AI结构化推理:从“诚实失败”到深度思考的工程实践
  • SARscape数据处理必备:离线环境下手动准备SRTM1 DEM的完整流程与文件管理心得
  • Stresser与DDoS攻击:地下产业链的技术原理与防御实践
  • 别再让电脑偷偷费电了!手把手教你开启PCIe ASPM,笔记本续航立竿见影
  • Matlab进阶技巧:巧用repelem函数实现图像像素缩放与数据可视化美化
  • 告别Win11内存焦虑:深入dwm.exe与Intel核显驱动的‘爱恨纠葛’及一劳永逸的修复法
  • 构建本地语音AI助手:从意图识别到工具调用的完整实现
  • 构建稳健预测引擎:时序特征工程防泄露核心方法论
  • 机器人运动控制中的观察空间与动作空间设计
  • 用PyTorch和VGG16预训练权重,从零搭建Unet语义分割模型(附完整代码)
  • pywinauto-打开程序+连接已打开的程序
  • 巨有科技:乡村市集的 “在地化” 密码——跳出同质化,做有根的烟火气
  • 告别RAM焦虑:手把手教你用Vitis SDK为MicroBlaze制作QSPI Flash启动的Bootloader
  • Cadence CIS库添加元件不显示?手把手教你排查SPB17.4配置的5个关键点
  • 别再只调颜色了!Echarts地图的visualMap组件,这5个隐藏功能让你的数据可视化更专业
  • 阿波罗11号代码考古:从历史源码看嵌入式系统的并发隐患与设计权衡
  • 2026年活动隔断/玻璃隔断/铝合金隔断/办公隔断厂家推荐榜:宴会厅隔断与医院移动隔断墙的匠心之选 - 品牌企业推荐师(官方)
  • AI如何重塑2026年Web开发:从意图驱动到智能工具链
  • 2026年镭雕粉与钛白粉供应厂家实力精选:东莞成硕塑料的深度观察 - 品牌企业推荐师(官方)
  • 从资助到投资:构建数据驱动的价值转化模型与自动化管道
  • 2026年SaaS构建成本全解析:AI辅助、外包与无代码路径深度对比
  • 从聊天机器人到AI操作系统:核心技术架构与应用场景深度解析
  • DeeplabV3+语义分割实战:如何用Keras在Colab上免费跑通你的第一个分割项目?
  • Ubuntu 18.04无线网卡驱动安装避坑指南:从lspci查型号到github找r8168驱动