带你深入理解一下如何设计一个兼容性强(可跨芯片跨环境)的模拟IIC代码架构,小猫老弟!

 目录

一、先讲解硬件电路以及时序图。 

IIC协议概述

IIC硬件电路

上拉电阻的选择

总线电容

IIC协议细节

起始条件

地址传输

数据传输

停止条件

IIC时序图

 二、讲解代码模拟IIC的关键点(这部分纯人工踩坑的经验哈!)


Github链接:
https://github.com/ywa152/WA-LearingPacks.githttps://github.com/ywa152/WA-LearingPacks.git

B站视频讲解:

https://www.bilibili.com/video/BV1kKJAzyEY6/?spm_id_from=333.1387.homepage.video_card.click&vd_source=1a92331248fcc8901a7a007eb40010a3https://www.bilibili.com/video/BV1kKJAzyEY6/?spm_id_from=333.1387.homepage.video_card.click&vd_source=1a92331248fcc8901a7a007eb40010a3

一、先讲解硬件电路以及时序图。 

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,所以我就考虑这些个点,做了一个我能力范围内比较“完美”的整体代码架构的设计。

        代码源码以及关联到这篇文章啦!,自行查阅!,我的注释也挺多的,不理解的可以慢慢理解,嘿嘿嘿!!!!!原创不易,多多支持!

        现在我把代码架构思路的设计分享给了诸位未来的天之骄子,诸位!请多多点赞关注吧!,原创的道路真的任重道远,本人目前双非大二在读,求支持啦!!!!!!!

  

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值