外设驱动库开发笔记8:GPIO模拟I2C驱动

I2C总线简单方便,是我们经常使用的一种总线。但有时候我们的MCU没有足够多的I2C控制器来实现我们的应用,所幸我可以使用普通的GPIO引脚来模拟低速的I2C总线通信。这一节我们就来实现使用软件通过普通GPIO操作I2C设备的驱动。

1、功能概述

I2C总线使用两条线:串行数据(SDA)和串行时钟(SCL)。所有I2C主设备和从设备仅与这两条线连接。每个设备可以是发射器,接收器或两者。有些设备是主设备,它们生成总线时钟并在总线上启动通信,其他设备是从设备并响应总线上的命令。为了与特定设备通信,每个从设备必须具有总线上唯一的地址。I2C主设备(通常是微控制器)不需要地址,因为没有其他设备向主设备发送命令。总线设备连接示意图如下:

1.1I2C的传输过程

I2C总线有标准、快速和高速多种速度模式;也有7位地址和10位地址多种地址格式,但不管什么样的模式其数据传输格式都可以划分为3个阶段:起始阶段、数据传输阶段和终止阶段。如下图:

1.1.1、起始阶段

I2C总线不工作的情况下,SDA(数据线)SCL(时钟线)上的信号均为高电平。如果此时主机需要发起新的通信请求,那么需要首先通过SDASCL发出起始标志。当SCL为高电平时,SDA电平从高变低,这一变化表示完成了通信的起始条件。

在起始条件和数据通信之间,通常会有延时要求,具体的指标会在设备厂商的规格说明书中给出。

1.1.2、数据传输阶段

I2C总线的数据通信是以字节(8)作为基本单位在SDA上进行串行传输的。一个字节的传输需要9个时钟周期。其中,字节中每一位的传输都需要一个时钟周期,当新的SCL到来时,SCL为低电平,此时数据发送方根据当前传输的数据位控制SDA的电平信号。如果传输的数据位为"1",就将SDA电平拉高;如果传输的数据位为"0",就将SDA的电平拉低。当SDA上的数据准备好之后,SCL由低变高,此时数据接收方将会在下一次SCL信号变低之前完成数据的接收。当8位数据发送完成后,数据接收方需要一个时钟周期以使用SDA发送ACK信号,表明数据是否接收成功。当ACK信号为"0"时,说明接收成功;为"1"时,说明接收失败。每个字节的传输都是由高位(MSB)到低位(LSB)依次进行传输。

I2C总线协议中规定,数据通信的第一个字节必须由主机发出,内容为此次通信的目标设备地址和数据通信的方向(/)。在这个字节中,第17位为目标设备地址,第0位为通信方向,当第0位为"1"时表示读,即后续的数据由目标设备发出主机进行接收;当第0位为"0"时表示写,即后续的数据由主机发出目标设备进行接收。在数据通信过程中,总是由数据接收方发出ACK信号。

1.1.3、终止阶段

当主机完成数据通信,并终止本次传输时会发出终止信号。当SCL 是高电平时,SDA电平由低变高,这个变化意味着传输终止。

1.2I2C的传输格式

根据I2C总线的技术标准,I2C总线上的数据传输方式有3种:主站向从站写数据方式;主站从从站读数据方式;读写组合的方式。下面将就这几种方式简单说明。

1.2.1、写数据格式

主站向从站写数据方式是主栈发送数据给从站。传输方向没有改变,从站接收主站发过来的每一个字节。具体格式如下图:

1.2.2、读数据格式

主站从从站读数据方式,主站在发送第一个字节之后,立即接收从站数据。也就是说在第一次确认的时刻,主发送器变成了主接收器,从属接收器变成了从属发送器。第一个确认仍然由从站生成。主站则生成后续的确认。停止条件由主主站生成,它在停止条件之前发送一个非确认应答。具体格式如下图:

1.2.3、读写组合格式

组合格式就是读和写是接连完成的。在传输中改变方向时,启动条件和从地址都要重复,但R/W位要倒过来。如果主接收器发送一个重复启动条件,它在重复启动条件之前发送一个非确认应答,但不会有停止条件。具体格式如下图:

2、驱动设计与实现

我们已经了解了I2C协议的基本内容,接下来我们需要考虑如何实现这一协议。实现了这一协议也就完成通过GPIO模拟I2C的驱动。

2.1、对象定义

我们们依然采用基于对象的操作来实现。所以在使用对象之前,我们需要得到对象。接下来我们就考虑GPIO模拟I2C的对象问题。

2.1.1、对象的抽象

一般的,作为一个对象肯定包括属性和操作。所以我们考虑GPIO模拟I2C的对象也要从这两方面来进行。

首先来考虑GPIO模拟I2C对象的属性。作为属性应该是必要的且能标识对象特点的参数。我们模拟的I2C其实是主站,作为主站没有地址,所以地址不需要作为属性。但通讯速度却是主站需要控制的,所以我们将速度设置为GPIO模拟I2C的一个属性。除此之外,作为主站没有必须要记录的参数了。

还需要考虑GPIO模拟I2C对象的操作。既然是使用GPIO模拟I2C,那么I2C的两根总线SCLSDA都需要主站操作GPIO来实现,所以控制SCL和控制SDA的行为都是对象的操作。除了控制总线我们还需要从总线读取数据,所以从SDA读取数据也是对象的一个操作。还有如延时等操作与具体的平台关系很大,我们也将其作为操作以便在具体的平台初始化。

根据上述的分析,我们可以抽象得到GPIO模拟I2C的对象类型如下:

typedef struct SimuI2CObject{uint32_t period;                            //确定速度为大于0K小于等于400K的整数,默认为100Kvoid (*SetSCLPin)(SimuI2CPinValue op);      //设置SCL引脚void (*SetSDAPin)(SimuI2CPinValue op);      //设置SDA引脚uint8_t (*ReadSDAPin)(void);                //读取SDA引脚位void (*Delayus)(volatile uint32_t period);  //速度延时函数
}SimuI2CObjectType;

2.1.2、对象的初始化

我们已经得到了GPIO模拟I2C的对象,但对象必须要初始化之后才可以操作,所以这里就需要考虑如何对对象进行初始化。一般来说,初始化函数需要处理几个方面的问题。一是检查输入参数是否合理;二是为对象的属性赋初值;三是对对象作必要的初始化配置。据此我们设计GPIO模拟I2C对象的初始化函数如下:

/* GPIO模拟I2C通讯初始化 */
void SimuI2CInitialization(SimuI2CObjectType *simuI2CInstance,uint32_t speed,SimuI2CSetPin setSCL,SimuI2CSetPin setSDA,SimuI2CReadSDAPin readSDA,SimuI2CDelayus delayus)
{if((simuI2CInstance==NULL)||(setSCL==NULL)||(setSDA==NULL)||(readSDA==NULL)||(delayus==NULL)){return;}simuI2CInstance->SetSCLPin=setSCL;simuI2CInstance->SetSDAPin=setSDA;simuI2CInstance->ReadSDAPin=readSDA;simuI2CInstance->Delayus=delayus;/*初始化速度,默认100K*/if((speed>0)&&(speed<=400)){simuI2CInstance->period=500/speed;}else{simuI2CInstance->period=5;}/*拉高总线,使处于空闲状态*/simuI2CInstance->SetSDAPin(Set);simuI2CInstance->SetSCLPin(Set);
}

2.2、对象操作

我们已经定义了对象类型,也实现了对象的初始化函数,接下来我们就需要考虑封装对象的操作了。根据前面我们对I2C协议的了解,需要实现的操作主要有:向从站写数据、从从站读数据、先向从站写而后接着读数据以及基于这三种模式的组合操作。

2.2.1、向从站写数据操作

向从站写数据包括向从站写命令、地址以及设定数据等。如向一个或多个存储地址写数据,需要先写存储起始地址再写需要保存的数据。所有的数据都是从主站发往从站,包括启动通讯、下发数据、停止通讯这一过程。具体的实现如下:

/* 通过模拟I2C向从站写数据 */
SimuI2CStatus WriteDataBySimuI2C(SimuI2CObjectType *simuI2CInstance,uint8_t deviceAddress,uint8_t *wData,uint16_t wSize)
{//启动通讯SimuI2CStart(simuI2CInstance);//发送从站地址(写)SendByteBySimuI2C(simuI2CInstance,deviceAddress);if(SimuI2CWaitAck(simuI2CInstance,5000)){return I2C_ERROR;}while(wSize--){SendByteBySimuI2C(simuI2CInstance,*wData);if(SimuI2CWaitAck(simuI2CInstance,5000)){return I2C_ERROR;}wData++;simuI2CInstance->Delayus(10);}SimuI2CStop(simuI2CInstance);return I2C_OK;
}

2.2.2、自从站读数据操作

读从站数据操作其实就是先向从站发送站地址(读),然后接收数据。一般存储器不会使用到这种模式,而对于向一些设备获取数据会有这种模式,如MS5803压力触感器。其过程是先启动通讯,再从主站发送包含读的从站地址,然后主站接收自从站返回的数据,然后停止通讯。具体的实现过程如下:

/* 通过模拟I2C自从站读数据 */
SimuI2CStatus ReadDataBySimuI2C(SimuI2CObjectType *simuI2CInstance,uint8_t deviceAddress,uint8_t *rData, uint16_t rSize)
{//启动通讯SimuI2CStart(simuI2CInstance);//发送从站地址(读)SendByteBySimuI2C(simuI2CInstance,deviceAddress+1);if(SimuI2CWaitAck(simuI2CInstance,5000)){return I2C_ERROR;}simuI2CInstance->Delayus(1000);while(rSize--){*rData=RecieveByteBySimuI2C(simuI2CInstance);rData++;if(rSize==0){IIC_NAck(simuI2CInstance);}else{IIC_Ack(simuI2CInstance);simuI2CInstance->Delayus(1000);}}//结束通讯SimuI2CStop(simuI2CInstance);return I2C_OK;
}

2.2.3、先写后读组合操作

对于组合操作则是写数据并读数据连续进行。这就像从某一存储地址读数据一样,先发送要读的其实地址,然后接收读出来的数据。其一般过程是:先启动通讯,然后写数据,接着重启通讯,然后读数据,最后停止通讯。具体的实现过程如下:

/* 通过模拟I2C实现对从站先写数据紧接读数据组合操作 */
SimuI2CStatus WriteReadDataBySimuI2C(SimuI2CObjectType *simuI2CInstance,uint8_t deviceAddress, uint8_t *wData,uint16_t wSize,uint8_t *rData, uint16_t rSize)
{//启动通讯SimuI2CStart(simuI2CInstance);//发送从站地址(写)SendByteBySimuI2C(simuI2CInstance,deviceAddress);if(SimuI2CWaitAck(simuI2CInstance,5000)){return I2C_ERROR;}while(wSize--){SendByteBySimuI2C(simuI2CInstance,*wData);if(SimuI2CWaitAck(simuI2CInstance,5000)){return I2C_ERROR;}wData++;simuI2CInstance->Delayus(10);}//再启动SimuI2CStart(simuI2CInstance);//发送从站地址(读)SendByteBySimuI2C(simuI2CInstance,deviceAddress+1);if(SimuI2CWaitAck(simuI2CInstance,5000)){return I2C_ERROR;}while(rSize--){*rData=RecieveByteBySimuI2C(simuI2CInstance);rData++;if(rSize==0){IIC_NAck(simuI2CInstance);}else{IIC_Ack(simuI2CInstance);}}//结束通讯SimuI2CStop(simuI2CInstance);return I2C_OK;
}

3、驱动的使用

前面已经设计并实现了GPIO模拟I2C通讯的驱动,下面我们还需要使用此驱动设计一个简单的应用以验证驱动设计的是否合理。

3.1、声明并初始化对象

在应用一个对象前,我们需要先得到这个对象。前面我们已经抽象了GPIO模拟I2C通讯的对象类型,这里我们将使用此对象类型声明一个对象变量。具体形式如下:

SimuI2CObjectType simuI2C;

声明了这个对象变量并不能立即使用,我们还需要使用驱动中定义的初始化函数对这个变量进行初始化。这个初始化函数所需要的输入参数如下:

SimuI2CObjectType *simuI2CInstance

uint32_t speed

SimuI2CSetPin setSCL

SimuI2CSetPin setSDA

SimuI2CReadSDAPin readSDA

SimuI2CDelayus delayus

对于这些参数,对象变量我们已经定义了。而通讯速度根据实际情况选择就好了,最大不超过500K,默认是100K。主要的是我们需要定义几个函数,并将函数指针作为参数。这几个函数的类型如下:

typedef void (*SimuI2CSetPin)(SimuI2CPinValue op);        //设置SDA引脚

typedef uint8_t (*SimuI2CReadSDAPin)(void);                  //读取SDA引脚位

typedef void (*SimuI2CDelayus)(volatile uint32_t period);    //速度延时函数

对于这几个函数我们根据样式定义就可以了,具体的操作可能与使用的硬件平台有关系。具体函数定义如下:

//设置SCL引脚
static void SetSCLPin(SimuI2CPinValue op)
{if(op==Set){HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET);}else{HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_RESET);}
}//设置SDA引脚
static void SetSDAPin(SimuI2CPinValue op)
{if(op==Set){HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_SET);}else{HAL_GPIO_WritePin(GPIOB,GPIO_PIN_7,GPIO_PIN_RESET);}
}//读取SDA引脚位
static uint8_t ReadSDAPin(void)
{return (uint8_t)HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_7);
}

对于延时函数我们可以采用各种方法实现。我们采用的STM32平台和HAL库则可以直接使用HAL_Delay()函数。于是我们可以调用初始化函数如下:

SimuI2CInitialization(&simuI2C100SetSCLPinSetSDAPinReadSDAPinHAL_Delay);

这里我们将其设为100I2C通讯接口。

3.2、基于对象进行操作

我们定义了对象变量并使用初始化函数给其作了初始化。接着我们就来考虑操作这一对象获取我们想要的数据。我们在驱动中已经封装了读从站、写从站以及读写混合操作,接下来我们使用这一驱动开发我们的应用实例。

这里我们考虑使用驱动读写一个I2C接口的存储器,我们向某一个地址写入数据和读出数据,我们假定存储器较小地址是8位的。

//从Memery中读取数据
void ReadDataFromMem(uint8_t deviceAddress, uint8_t memAdd,uint8_t *rData, uint16_t rSize)
{    WriteReadDataBySimuI2C(&simuI2C,deviceAddress,&memAdd,1,rData,rSize);
}//向Memery中写数据
void WriteDataToMem(uint8_t deviceAddress,uint8_t memAdd,uint8_t *wData,uint16_t wSize)
{uint8_t data[10];uint16_t size=0;data[size++]=memAdd;for(int i=0;i<wSize;i++){data[size++]=wData[i];}WriteDataBySimuI2C(&simuI2C,deviceAddress,wData,size);
}

在这一例中,我们实现了对8位地址的存储器的数据写入和读出操作,根据封装的驱动函数很容易实现。

4、应用总结

我们使用GPIO模拟的I2C协议在STM32平台上与多个设备进行通讯,如SHT20温湿度传感器、TSEV01CL55红外温度传感器、MLX90614红外温度传感器等,等到的结果非常好,即使在长达1米的通讯线路上都没有问题。

使用本驱动是需要注意一点,因为在I2C总线中SDA是双向的,所以在模拟式需要将模拟SDA的引脚配置为开漏模式,否则就需要控制其方向。

说到I2C总线有几个相关的总线不能不提,系统管理总线SMBus、电源系统管理总线PMBus以及TWI Bus。这些总线与I2C总线有很多的共同点,在通讯速率一致的情况下是可以通用的。

完整的源代码可在GitHub下载:https://github.com/foxclever/ExPeriphDriver

欢迎关注:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/499365.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式IAP开发笔记之一:面向STM32的BootLoader程序

对于很多人来说&#xff0c;BootLoader并不是一个陌生的词&#xff0c;甚至会经常用到它。因为在很多情况下我们都需要BootLoader程序&#xff0c;比如我们需要对系统在线升级时就需要它&#xff0c;还有当我们需要在外部存储器中运行程序时也需要用到它。在这里我们就来设计一…

外设驱动库开发笔记9:SHT1x系列温湿度传感器驱动

在我们的产品中&#xff0c;经常需要检测温湿度数据。有很多检测温湿度的方法和模块&#xff0c;其中SHT1x系列温湿度传感器就是一种成本较低使用方便的温湿度检测模块。下面我们就来说一说如何实现SHT1x系列温湿度传感器的驱动。 1、功能概述 SHT1x包括 SHT10&#xff0c; S…

Modbus协议栈应用实例之一:Modbus RTU主站应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;在这一篇中我们先来使用协议栈实现Modbus RTU主站的示例。 1、何为RTU主站 Modbus协议是…

uCOS-III应用开发笔记之一:uCOS-III在STM32的移植

uCOS-III实时操作系统在MCU平台被广泛使用&#xff0c;在这里我们将简单的记录如何将uCOS-III实时操作系统移植到目标平台上并运行。 1、必要的准备 在开始uCOS-III实时操作系统的移植前&#xff0c;我们还需要做一些必要的准备&#xff0c;如确定目标板、准备目标工程及uCOS…

外设驱动库开发笔记10:SHT2x系列温湿度传感器驱动

温湿度检测是嵌入式编程中经常应用到的一项功能。在我们的产品中亦经常使用。SHT2x系列温湿度传感器作为一种高精度低成本的集成模块&#xff0c;一直应用于我们的产品中。在这里我们讨论如何封装SHT2x系列温湿度传感器的驱动。 1、功能概述 SHT20配有一个全新设计的CMOSens芯…

Modbus协议栈应用实例之二:Modbus RTU从站应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;这一篇中我们将使用协议栈实现一个Modbus RTU从站应用。 1、何为RTU从站 Modbus协议是一…

外设驱动库开发笔记11:SHT3x系列温湿度传感器驱动

在我们的产品中经常会遇到温湿度检测的需求。可以用于检测温湿度的传感器元件也有很多。我们经常使用的SHT各系列数字温湿度传感器来实现应用需求。在这里我们将设计并实现SHT3x系列温湿度传感器的驱动。 1、功能概述 SHT3x系列温湿度传感器是适用于各种应用的高品质湿度传感…

LwIP应用开发笔记之十:LwIP带操作系统基本移植

现在&#xff0c;TCP/IP协议的应用无处不在。随着物联网的火爆&#xff0c;嵌入式领域使用TCP/IP协议进行通讯也越来越广泛。在我们的相关产品中&#xff0c;也都有应用&#xff0c;所以我们结合应用实际对相关应用作相应的总结。 1、技术准备 我们采用的开发平台是STM32F407…

ThreadX应用开发笔记之一:移植ThreadX到STM32平台

现在一些小型系统中也往往有多任务处理的需求&#xff0c;这就为实时操作系统提供了用武之地。事实上国内外各种各样的RTOS有很多&#xff0c;而且基本都在走开源的路线&#xff0c;ThreadX也不例外&#xff0c;在这一篇中我们就来学习ThreadX初步应用并将其移植到STM32平台中。…

外设驱动库开发笔记12:TSEV01CL55红外温度传感器驱动

有时候我们需要检测一些无法直接接触的器件的温度。为了实现这一需求&#xff0c;我们通常会选择红外温度传感器来实现这一功能。考虑到复用的问题&#xff0c;我们一般会将操作元器件的代码抽象为驱动函数以备调用。这里我们就来设计并实现TSEV01CL55红外温度传感器的驱动。 …

FreeRTOS应用开发笔记之一:FreeRTOS在STM32的移植

FreeRTOS是如今在小型嵌入式领域应用比较广泛的一种实时操作系统。它是一种开源且免费的操作系统&#xff0c;而且移植和使用都非常的简单。在这里我们将学习并移植FreeRTOS。 1、必要的准备 工欲善其事&#xff0c;必先利其器&#xff0c;在开始学习和移植之前&#xff0c;相…

外设驱动库开发笔记13:MLX90614红外温度传感器驱动

红外温度传感器一般用于非接触式的温度检测。在我们的系统中经常会有这样的需求。所以我们将其设计为通用的驱动库以备复用。这一篇我们将讲述MLX90614红外温度传感器驱动的设计与实现。 1、功能概述 MLX90614是一种红外温度计&#xff0c;用于非接触式温度测量。红外测温是根…

Modbus协议栈应用实例之三:Modbus TCP客户端应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;这一篇中我们将解说如何使用协议栈实现一个Modbus TCP客户端。 1、何为TCP客户端 Modbus…

Modbus协议栈应用实例之四:ModbusTCP服务器应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;这一篇中我们来简述如何使用协议栈实现一个Modbus TCP服务器应用。 1、何为TCP服务器 Mo…

外设驱动库开发笔记14:DS18B20温度变送器驱动

在一时候我们需要相对简单的检测温度信号&#xff0c;而DS18B20就是一款功能和应用都相对简单的温度传感器&#xff0c;通过单线就可以实现检测温度信号的需求。这一篇我们就来实现操作DS18B20获取温度数据的驱动。 1、功能概述 DS18B20是常用的数字温度传感器&#xff0c;其…

Modbus协议栈应用实例之五:Modbus ASCII主站应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;这一篇中我们来使用协议栈实现Modbus ASCII主站应用。 1、何为ASCII主站 我们知道Modbus…

Modbus协议栈应用实例之六:Modbus ASCII从站应用

自从开源了我们自己开发的Modbus协议栈之后&#xff0c;有很多朋友建议我针对性的做几个示例。所以我们就基于平时我们的应用整理了几个简单但可以说明基本的应用方法的示例&#xff0c;这一篇中我们来使用协议栈实现Modbus ASCII从站应用。 1、何为ASCII从站 我们知道Modbus…

外设驱动库开发笔记15:DHT11温湿度传感器驱动

与DS18B20一样DHT11也是采用单总线&#xff0c;但所不同的是DHT11可同时实现温度和湿度的检测。在我们的产品中经常使用它来检测环境的温湿度信息。这一篇我们将设计并封装DHT11的驱动程序&#xff0c;以方便重复使用。 1、功能概述 DHT11数字温湿度传感器是一款含有已校准数…

外设驱动库开发笔记16:MS5536C压力变送器驱动

压力检测也是经常会遇到的需求&#xff0c;比如环境压力或者低压气体等都会用到压力检测。这类检测压力都比较低&#xff0c;一般不会超过大气压&#xff0c;有时甚至是负压。这一篇我们要讨论的MS5536C就属于这类器件。接下来我们将设计并实现MS5536C的驱动。 1、功能概述 M…

外设驱动库开发笔记17:MS5803压力变送器驱动

很多时候我们需要检测被控对象的绝对压力&#xff0c;而且在我们的多款产品中也有这样的需求。当然检测绝对压力的传感器有很多&#xff0c;我们经常使用MS5803来实现压力检测。本篇中我们将设计并实现MS5803系列压力传感器的驱动。 1、功能概述 MS5803系列产品包含压阻传感器…