目录
二、讲解代码模拟IIC的关键点(这部分纯人工踩坑的经验哈!)
Github链接:
https://github.com/ywa152/WA-LearingPacks.githttps://github.com/ywa152/WA-LearingPacks.git
B站视频讲解:
一、先讲解硬件电路以及时序图。
IIC协议概述
IIC(Inter-Integrated Circuit)协议,也称为I²C,是一种由Philips公司开发的双线串行通信协议。它广泛应用于微控制器、传感器、EEPROM等设备之间的通信。IIC协议具有简单、灵活、低功耗等特点,适合短距离、低速率的通信场景。
IIC硬件电路
IIC协议使用两条信号线进行通信:SDA(Serial Data Line)和SCL(Serial 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(ack==0)
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 = ((addr<<1)|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(i=0; i<len; 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 = ((addr<<1)|0);
dev_read = ((addr<<1)|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(len==1)*buf=MPU_IIC_Read_Byte(wai2c,0);//读数据,发送nACK
else *buf=MPU_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,所以我就考虑这些个点,做了一个我能力范围内比较“完美”的整体代码架构的设计。
代码源码以及关联到这篇文章啦!,自行查阅!,我的注释也挺多的,不理解的可以慢慢理解,嘿嘿嘿!!!!!原创不易,多多支持!
现在我把代码架构思路的设计分享给了诸位未来的天之骄子,诸位!请多多点赞关注吧!,原创的道路真的任重道远,本人目前双非大二在读,求支持啦!!!!!!!