你对IIC(I2C)协议的理解难道还只是在调用HAL库或其他库的封装好的函数的层次吗?-------关于硬件IIC电路的讲解和面向对象架构的模拟IIC的程序设计! 目录一、先讲解硬件电路以及时序图。IIC协议概述IIC硬件电路上拉电阻的选择总线电容IIC协议细节起始条件地址传输数据传输停止条件IIC时序图二、讲解代码模拟IIC的关键点(这部分纯人工踩坑的经验哈)Github链接https://github.com/ywa152/WA-LearingPacks.githttps://github.com/ywa152/WA-LearingPacks.gitB站视频讲解https://www.bilibili.com/video/BV1kKJAzyEY6/?spm_id_from333.1387.homepage.video_card.clickvd_source1a92331248fcc8901a7a007eb40010a3https://www.bilibili.com/video/BV1kKJAzyEY6/?spm_id_from333.1387.homepage.video_card.clickvd_source1a92331248fcc8901a7a007eb40010a3一、先讲解硬件电路以及时序图。IIC协议概述IICInter-Integrated Circuit协议也称为I²C是一种由Philips公司开发的双线串行通信协议。它广泛应用于微控制器、传感器、EEPROM等设备之间的通信。IIC协议具有简单、灵活、低功耗等特点适合短距离、低速率的通信场景。IIC硬件电路IIC协议使用两条信号线进行通信SDASerial Data Line和SCLSerial Clock Line。SDA用于数据传输SCL用于同步时钟信号。这两条线都是开漏输出因此需要上拉电阻连接到电源电压。上拉电阻的选择上拉电阻的阻值通常在1kΩ到10kΩ之间具体值取决于总线电容和通信速率。较大的上拉电阻会降低功耗但会增加信号上升时间从而限制通信速率。较小的上拉电阻可以提高通信速率但会增加功耗。总线电容总线电容是影响IIC通信速率的重要因素。较大的总线电容会延长信号上升时间从而限制通信速率。为了减少总线电容应尽量缩短总线长度并减少连接到总线的设备数量。IIC协议细节IIC协议采用主从架构主设备负责发起通信并控制时钟信号从设备响应主设备的命令。通信过程包括起始条件、地址传输、数据传输和停止条件。起始条件起始条件是主设备发起通信的信号。当SCL为高电平时SDA从高电平变为低电平表示起始条件。地址传输起始条件后主设备发送7位或10位的从设备地址。地址的最后一位表示读写操作0表示写操作1表示读操作。从设备在接收到地址后会发送一个应答信号ACK表示已接收到地址。数据传输地址传输后主设备开始发送或接收数据。每个字节传输后接收方会发送一个应答信号ACK或非应答信号NACK。ACK表示接收方已成功接收数据NACK表示接收方未成功接收数据或通信结束。停止条件停止条件是主设备结束通信的信号。当SCL为高电平时SDA从低电平变为高电平表示停止条件。IIC时序图下图是IIC协议的时序图展示了起始条件、地址传输、数据传输和停止条件的时序关系。在时序图中SDA和SCL的波形清晰地展示了IIC协议的各个阶段。起始条件和停止条件分别由SDA的下降沿和上升沿表示。地址传输和数据传输阶段SDA在SCL的上升沿或下降沿进行数据采样。IIC协议是一种简单、灵活、低功耗的双线串行通信协议广泛应用于各种嵌入式系统中。通过理解IIC协议的硬件电路和通信细节可以更好地设计和优化IIC通信系统。时序图清晰地展示了IIC协议的各个阶段有助于深入理解IIC协议的工作原理。二、讲解代码模拟IIC的关键点(这部分纯人工踩坑的经验哈)那么首先要搭建模拟IIC的几个基本函数函数功能分别是IIC起始信号函数IIC结束信号函数IIC等待应答函数IIC发送应答函数IIC发送1个字节数据函数IIC接收(也可以叫做“读取”)1个字节数据函数。那么这些基础的函数不必多做解释了。让我们一起3、2、1上代码3、2、1上代码3、2、1上代码I2C_HandleDef wai2c0; I2C_HandleDef wai2c1; I2C_HandleDef wai2c2; I2C_HandleDef wai2c3; /* ********************************************************************************************************* * 函 数 名: IIC_Start * 功能说明: CPU发起IIC总线启动信号 * 形 参无 * 返 回 值: 无 ********************************************************************************************************* */ void IIC_Start(I2C_HandleDef *wai2c) { /* 当SCL高电平时SDA出现一个下跳沿表示IIC总线启动信号 */ IIC_SDA_1(wai2c); IIC_SCL_1(wai2c); IIC_Delay(wai2c); IIC_SDA_0(wai2c); IIC_Delay(wai2c); IIC_SCL_0(wai2c); IIC_Delay(wai2c); } /* ********************************************************************************************************* * 函 数 名: IIC_Start * 功能说明: CPU发起IIC总线停止信号 * 形 参无 * 返 回 值: 无 ********************************************************************************************************* */ void IIC_Stop(I2C_HandleDef *wai2c) { /* 当SCL高电平时SDA出现一个上跳沿表示IIC总线停止信号 */ IIC_SDA_0(wai2c); IIC_SCL_1(wai2c); IIC_Delay(wai2c); IIC_SDA_1(wai2c); } /* ********************************************************************************************************* * 函 数 名: IIC_SendByte * 功能说明: CPU向IIC总线设备发送8bit数据 * 形 参_ucByte 等待发送的字节 * 返 回 值: 无 ********************************************************************************************************* */ void IIC_Send_Byte(I2C_HandleDef *wai2c,uint8_t _ucByte) { uint8_t i; /* 先发送字节的高位bit7 */ for (i 0; i 8; i) { if (_ucByte 0x80) { IIC_SDA_1(wai2c); } else { IIC_SDA_0(wai2c); } IIC_Delay(wai2c); IIC_SCL_1(wai2c); IIC_Delay(wai2c); IIC_SCL_0(wai2c); if (i 7) { IIC_SDA_1(wai2c); // 释放总线 } _ucByte 1; /* 左移一个bit */ IIC_Delay(wai2c); } } /* ********************************************************************************************************* * 函 数 名: IIC_ReadByte * 功能说明: CPU从IIC总线设备读取8bit数据 * 形 参无 * 返 回 值: 读到的数据 ********************************************************************************************************* */ uint8_t IIC_Read_Byte(I2C_HandleDef *wai2c,uint8_t ack) { uint8_t i; uint8_t value; /* 读到第1个bit为数据的bit7 */ value 0; for (i 0; i 8; i) { value 1; IIC_SCL_1(wai2c); IIC_Delay(wai2c); if (IIC_SDA_READ(wai2c)) { value; } IIC_SCL_0(wai2c); IIC_Delay(wai2c); } if(ack0) IIC_NAck(wai2c); else IIC_Ack(wai2c); return value; } /* ********************************************************************************************************* * 函 数 名: IIC_WaitAck * 功能说明: CPU产生一个时钟并读取器件的ACK应答信号 * 形 参无 * 返 回 值: 返回0表示正确应答1表示无器件响应 ********************************************************************************************************* */ uint8_t IIC_Wait_Ack(I2C_HandleDef *wai2c) { uint8_t re; IIC_SDA_1(wai2c); /* CPU释放SDA总线 */ IIC_Delay(wai2c); IIC_SCL_1(wai2c); /* CPU驱动SCL 1, 此时器件会返回ACK应答 */ IIC_Delay(wai2c); if (IIC_SDA_READ(wai2c)) /* CPU读取SDA口线状态 */ { re 1; } else { re 0; } IIC_SCL_0(wai2c); IIC_Delay(wai2c); return re; } /* ********************************************************************************************************* * 函 数 名: IIC_Ack * 功能说明: CPU产生一个ACK信号 * 形 参无 * 返 回 值: 无 ********************************************************************************************************* */ void IIC_Ack(I2C_HandleDef *wai2c) { IIC_SDA_0(wai2c); /* CPU驱动SDA 0 */ IIC_Delay(wai2c); IIC_SCL_1(wai2c); /* CPU产生1个时钟 */ IIC_Delay(wai2c); IIC_SCL_0(wai2c); IIC_Delay(wai2c); IIC_SDA_1(wai2c); /* CPU释放SDA总线 */ } /* ********************************************************************************************************* * 函 数 名: IIC_NAck * 功能说明: CPU产生1个NACK信号 * 形 参无 * 返 回 值: 无 ********************************************************************************************************* */ void IIC_NAck(I2C_HandleDef *wai2c) { IIC_SDA_1(wai2c); /* CPU驱动SDA 1 */ IIC_Delay(wai2c); IIC_SCL_1(wai2c); /* CPU产生1个时钟 */ IIC_Delay(wai2c); IIC_SCL_0(wai2c); IIC_Delay(wai2c); }那么看到这里你会发现我的每一个驱动函数都有一个共同的形参而这个形参的数据类型是我自定义的结构体嘿嘿嘿嘿嘿嘿嘿嘿这个时候一谈到结构体那就必然是“面向对象编程”的代码架构条件反射你都要这样去猜那么为什么要“面向对象”去做这样的“操作”呢那么话不多说先展示再讲解。// I2C句柄结构体保存各实例引脚配置 typedef struct { uint8_t scl_group; // SCL引脚组 uint32_t scl_pin; // SCL引脚号 uint8_t sda_group; // SDA引脚组 uint32_t sda_pin; // SDA引脚号 uint8_t delayus; } I2C_HandleDef; // 外部声明4个I2C实例 extern I2C_HandleDef wai2c0; extern I2C_HandleDef wai2c1; extern I2C_HandleDef wai2c2; extern I2C_HandleDef wai2c3;从我的结构体内容不难看出我需要在我初始化的时候把我需要用到的引脚都初始化一边这就需要他的引脚组号和引脚Pin号以及模拟IIC的延时的时间(单位:us)这样的操作是为了方便自定义IIC的引脚以及耗时让他在初始化的时候直接搞完所有操作后期调用就十分方便了。上初始化代码void WA_I2C_Init(I2C_HandleDef *wai2c,uint8_t sclgroup,uint32_t sclpin,uint8_t sdagroup,uint32_t sdapin,uint8_t delayus) { wai2c-scl_group sclgroup; wai2c-scl_pin sclpin; wai2c-sda_group sdagroup; wai2c-sda_pin sdapin; wai2c-delayus delayus; if(wai2c-scl_group GPIOA) { GPIOA_ModeCfg(wai2c-scl_pin,GPIO_ModeIN_PU); GPIOA_ResetBits(wai2c-scl_pin); } if(wai2c-scl_group GPIOB) { GPIOB_ModeCfg(wai2c-scl_pin,GPIO_ModeIN_PU); GPIOB_ResetBits(wai2c-scl_pin); } if(wai2c-sda_group GPIOA) { GPIOA_ModeCfg(wai2c-sda_pin,GPIO_ModeIN_PU); GPIOA_ResetBits(wai2c-sda_pin); } if(wai2c-sda_group GPIOB) { GPIOB_ModeCfg(wai2c-sda_pin,GPIO_ModeIN_PU); GPIOB_ResetBits(wai2c-sda_pin); } }那么最关键的一步要来咯就是他的SCL,SDA引脚电平拉高拉低的操作这一步非常非常关键你们想想一般的模拟IIC都是宏定义一个某某函数名然后去调用你的HAL库或者标准库或者其他库的底层拉高引脚拉低引脚电平的函数那么这就会出现一个问题你这样的操作会影响IIC时序因为频繁的调用函数函数里嵌套其他函数会影响栈的调用以及影响IIC引脚电平变换的响应速度这就会导致你模拟IIC读取需要高精度时序的IIC外设的时候他的读函数读取错误信息甚至读取不了数据。那么这个时候怎么解决呢那就直接对寄存器操作下面演示我在沁恒国产蓝牙芯片CH585环境下的对GPIO口引脚电平的寄存器操作函数吧void IIC_SCL_1(I2C_HandleDef *wai2c) /* SCL 1 */ { if(wai2c-scl_group GPIOA) R32_PA_DIR ~wai2c-scl_pin; if(wai2c-scl_group GPIOB) R32_PB_DIR ~wai2c-scl_pin; } void IIC_SCL_0(I2C_HandleDef *wai2c) { if(wai2c-scl_group GPIOA) R32_PA_DIR | wai2c-scl_pin; /* SCL 0 */ if(wai2c-scl_group GPIOB) R32_PB_DIR | wai2c-scl_pin; /* SCL 0 */ } void IIC_SDA_1(I2C_HandleDef *wai2c) { if(wai2c-sda_group GPIOA) R32_PA_DIR ~wai2c-sda_pin; /* SDA 1 */ if(wai2c-sda_group GPIOB) R32_PB_DIR ~wai2c-sda_pin; /* SDA 1 */ } void IIC_SDA_0(I2C_HandleDef *wai2c) { if(wai2c-sda_group GPIOA) R32_PA_DIR | wai2c-sda_pin; /* SDA 0 */ if(wai2c-sda_group GPIOB) R32_PB_DIR | wai2c-sda_pin; /* SDA 0 */ } uint32_t IIC_SDA_READ(I2C_HandleDef *wai2c) { uint32_t i; i GPIOB_ReadPortPin(wai2c0.sda_pin); /* 读SDA口线状态 */ return i; }显然这样一来我能同时使用多对IIC引脚并且在我的结构的作用下这几对IIC引脚能同时使用互补影响而且他们的延时时间也可以不一样这就是面向对象编程的代码架构那么最后还有一个重要的点就是IIC设备的设备地址了注意是设备地址不是寄存器地址设备地址是有7bit、8bit的区分的这其实跟硬件没啥大关系主要是你在网上找相关IIC设备的驱动代码的时候他有时候给7bit地址有时候给8bit地址那么你在IIC写函数和读函数的时候总得先传入一个设备地址吧那么怎么让他模拟IIC自动识别是7bit还是8bit到底设备地址呢话不多说先上代码//IIC连续写 //addr:器件地址 //reg:寄存器地址 //len:写入长度 //buf:数据区 //返回值:0,正常 // 其他,错误代码 uint8_t WA_Write_Len(I2C_HandleDef *wai2c,uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf ) { uint8_t i; uint16_t t; uint8_t dev_write; dev_write ((addr1)|0); MPU_IIC_Start(wai2c); MPU_IIC_Send_Byte(wai2c,dev_write);//假设是7bit while(MPU_IIC_Wait_Ack(wai2c)) { t; if(t 30) { MPU_IIC_Stop(wai2c); //产生一个停止条件 dev_write addr 0xFE; MPU_IIC_Start(wai2c); MPU_IIC_Send_Byte(wai2c,dev_write);//假设是7bit MPU_IIC_Wait_Ack(wai2c); break; } } MPU_IIC_Send_Byte(wai2c,reg); //写寄存器地址 MPU_IIC_Wait_Ack(wai2c); //等待应答 for(i0; ilen; i) { MPU_IIC_Send_Byte(wai2c,buf[i]); //发送数据 if(MPU_IIC_Wait_Ack(wai2c)) //等待ACK { MPU_IIC_Stop(wai2c); return 1; } } MPU_IIC_Stop(wai2c); return 0; } //IIC连续读 //addr:器件地址 //reg:要读取的寄存器地址 //len:要读取的长度 //buf:读取到的数据存储区 //返回值:0,正常 // 其他,错误代码 uint8_t WA_Read_Len(I2C_HandleDef *wai2c,uint8_t addr,uint8_t reg,uint8_t len,uint8_t *buf ) { uint8_t t; uint8_t dev_write,dev_read; dev_write ((addr1)|0); dev_read ((addr1)|1); MPU_IIC_Start(wai2c); MPU_IIC_Send_Byte(wai2c,dev_write);//假设是7bit while(MPU_IIC_Wait_Ack(wai2c)) { t; if(t 30) { MPU_IIC_Stop(wai2c); //产生一个停止条件 // flag 1; dev_write addr 0xFE; dev_read (addr | 0x01); MPU_IIC_Start(wai2c); MPU_IIC_Send_Byte(wai2c,dev_write);//假设是7bit MPU_IIC_Wait_Ack(wai2c); break; } } MPU_IIC_Send_Byte(wai2c,reg); //写寄存器地址 MPU_IIC_Wait_Ack(wai2c); //等待应答 MPU_IIC_Start(wai2c); MPU_IIC_Send_Byte(wai2c,dev_read);//发送器件地址读命令 MPU_IIC_Wait_Ack(wai2c); //等待应答 while(len) { if(len1)*bufMPU_IIC_Read_Byte(wai2c,0);//读数据,发送nACK else *bufMPU_IIC_Read_Byte(wai2c,1); //读数据,发送ACK len--; buf; } MPU_IIC_Stop(wai2c); //产生一个停止条件 return 0; }很简单只需要挨个挨个试试就行了先把你的驱动代码里的设备地址按7bit输入然后看他的应答状态如果没有应答那就重启IIC输入8bit的格式就OK啦为什么我强调这一点呢只要是因为我理解大家新手入门嵌入式那必定都绕不开HAL库的学习在我从ST芯片跨越到国产芯片的过程中我踩过很多雷这个就是我踩过的雷HAL库里的IIC都是硬件IIC他的IIC相关函数都全部封装好了而且都有自动识别设备地址是7bit、还是8bit、甚至是10bit的功能但是一般人根本没注意这一点啊得亏我不是一般人嘿嘿那么我在做这款沁恒CH585的WA库开发的国产中我就需要考虑这款蓝牙芯片的IIC资源这块所以为了拓宽这IIC引脚资源不得不用模拟IIC那么我在做模拟IIC通信的过程中就用到了俩不同的IIC外设这俩外设驱动代码里的设备地址分别是7bit和8bit所以我就考虑这些个点做了一个我能力范围内比较“完美”的整体代码架构的设计。代码源码以及关联到这篇文章啦自行查阅我的注释也挺多的不理解的可以慢慢理解嘿嘿嘿原创不易多多支持现在我把代码架构思路的设计分享给了诸位未来的天之骄子诸位请多多点赞关注吧原创的道路真的任重道远求支持啦