[应用相关]

STM32硬件IIC驱动设计(转载)

[复制链接]
2712|41
手机看帖
扫描二维码
随时随地手机跟帖
keaibukelian|  楼主 | 2018-12-5 12:51 | 显示全部楼层 |阅读模式
前言
stm32的硬件IIC一直是令人诟病的地方,以至于很多情况下我们不得不选择使用模拟IIC的方式来在stm32上进行iic通讯。我在stm32 iic通讯上也浪费了几多青春。。。经过不断地探索最终还是成功了(可喜可贺啊),现在把我的探索成功的经验分享出来,如果能减少读者在硬件iic上面浪费的时间,那真是太棒了!
我把驱动的一些描述做成了表格如下:

问题        描述
MCU型号        STM32F407VET6
库函数        标准外设库
操作系统        FreeRTOS


keaibukelian|  楼主 | 2018-12-5 12:52 | 显示全部楼层
关于IIC通讯
众所周知IIC是一种通讯方式。。。所以有必要先介绍一下IIC通信,省的下面不知道不知道我在写什么。当然这些都是基础,你可以选择跳过,直接看第三部分STM32的IIC

IIC是什么
说实话这个问题有点难,我就百度了一下,描述如下

IIC 即Inter-Integrated Circuit(集成电路总线),这种总线类型是由飞利浦半导体公司在八十年代初设计出来的一种简单、双向、二线制、同步串行总线,主要是用来连接整体电路(ICS) ,IIC是一种多向控制总线,也就是说多个芯片可以连接到同一总线结构下,同时每个芯片都可以作为实时数据传输的控制源。这种方式简化了信号传输总线接口。

通过这几句描述我发现——我更加不知道它是什么了。不过至少我看到一个关键字——二线制,那就表明IIC需要两根线进行通讯(不算电源和地),那么就先看一下iic硬件接口吧,这是我唯一可以看到的东西。

接口1        接口2
SCL        SDA
从名称可以看出这两个接口SCL为clock即时钟线,SDA为date即数据线。就是通过这两根线进行iic通信,相当精简的硬件连接!但是就我所知越是精简的硬件接口其软件越复杂。比如并行通信的话我们的软件直接读高低电平就好了。。。建议不懂什么是并行通信的人直接百度之,当然这并不是重点。我们的重点是串行通信,而串行通信的理论支撑是其内在的通信协议,当然iic也是个有协议的人。。。那么接下来就该我们的主角出场了——IIC协议,


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:52 | 显示全部楼层
IIC协议
说起协议我不知道你想到了什么,于是百度之:协议:共同计议,协商。其实就是人为规定了一种通讯规则,不过这个规则是给机器执行的。所以不要让协议两个字吓到。下面是两个基本的规则:

1、只要求两条总线线路 一条串行数据线 SDA 一条串行时钟线 SCL
2、每个连接到总线的器件都可以通过唯一的地址和一直存在的简单的主机/从机关系软件设定地址;主机可以作为主发送器或主机接收器
其中第一条我们已经知道了,第二条是说iic通信需要主机和从机,至于什么是主机什么是从机,一般情况下比如我使用stm32读取MPU6050那么stm32就是主机,mpu6050就是从机。还有一点iic可以支持一主多从的模式,这也就表示如果我们有多个从机设备如我既想读取MPU6050,又想读取HMC5883数据(这两个都支持iic通信)我们不需要为这两个设备分别引出两个iic接口,只需要把stm32、mpu6050、hmc5883三者的SCL和SDA引脚对应连接起来即可。那么问题来了:主机是怎么知道现在在跟哪个从机通信?其实这个问题很白痴。如果换个问题类比一下,把主机想象成一个老地主,他手下有N个奴仆(从机),地主是怎么知道现在他在跟谁说话?当然是通过名字啊。于是每个iic设备就被人为规定了“名字”,我们称为地址。如MPU6050的地址为0x68(在AD0引脚为低电平情况下)。
iic的协议还是很多的,这里就不一一列举了,新手的话知道这两个就好了。如果这些内容理解起来没毛病的话就该进入iic时序这个环节了。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:52 | 显示全部楼层
IIC通信时序
时序即电平变化的顺序,对于iic来说即SCL和SDA两根线上的电平变化顺序。对于IIC其信号主要有一下几个
开始信号、结束信号和应答/非应答信号。这就像上面的地主和奴仆例子里的,地主要命令仆役工作要有开始的信号,命令结束工作也要有停止信号,地主还要根据仆役的反馈来判断仆役是否能完成任务,如果仆役回答能够完成工作就是应答信号,若回答无法完成工作就是非应答信号。对于iic其规定了这几种信号的电平变化时序如下:

开始和结束信号


开始信号:SCL 为高电平时,SDA 由高电平向低电平跳变,开始传送数据。
结束信号:SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。

应答/非应答 信号


应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号,CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

对时序的总结如下:

1、IIC通信进行数据传输时,时钟信号为高电平期间数据线信号必须保持稳定,只有时钟信号为低电平时才允许数据信号变化。

2、SCL高电平期间,SDA由高电平变为低电平则代表起始信号,SDA由低电平变为高电平时代表停止信号。

3、起始信号和停止信号都是由主机发起,当有起始信号发出后总线处于占用状态,当停止信号被发出后总线处于空闲状态。

当我们了解到这几种时序的原理后便可以通过操作两个IO的高低电平来实现IIC通信了,这就是常说的模拟IIC的方式。以上这些知识只需要知道原理即可,如果真要写一个模拟iic的程序你还需要精确的时序,比如起始信号SDA脉冲持续的最短时间,其变为低电平后SCL至少需要保持高电平时间等等问题,所以你需要一个比较精确的微秒级延时函数。。。但这一切并不是今天我们的重点。我们的重点是STM32的硬件IIC驱动设计。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:53 | 显示全部楼层
STM32的IIC
STM32的IIC还是很强大的,只是很多功能我们根本用不上。如下是数据手册的里关于IIC的描述:

I 2 C(内部集成电路)总线接口用作微控制器和 I 2 C 串行总线之间的接口。它提供多主模式功
能,可以控制所有 I 2 C 总线特定的序列、协议、仲裁和时序。它支持标准和快速模式。它还
与 SMBus 2.0 兼容。
它可以用于多种用途,包括 CRC 生成和验证、SMBus(系统管理总线)以及 PMBus(电源
管理总线)。
根据器件的不同,可利用 DMA 功能来减轻 CPU 的工作量。

我设计驱动的目的是通过STM32读取MPU6050所以该驱动的特性是stm32作为主机而不是从机,所以想了解关于从机部分的就不用往下看了。其次由于stm32的IIC支持DMA的并且我的stm32大部分时间是在读取传感器数据。所以该驱动设计时把IIC接收部分设计为DMA接收,而发送部分没有使用DMA。关于是否使用中断,我的回答是一定要用中断,排除STM32采用查询方式各种堵死在while(1)里的囧状,使用中断还可以提升系统的性能,while(xxx);查询是要占据一定的时间的啊。所以果断采取中断处理的方式,只是stm32的中断情况比较多。所以。。。我们需要冷静地缕一缕。以下是数据手册时间。有该宝典的同学一定要珍惜啊,特别是中文版的,很好很强大。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:53 | 显示全部楼层
stm32硬件IIC通信流程
首先stm32的iic分四种模式:从发送器、从接收器、主发送器、主接收器。而上面我们也提到了只用到了主机模式,所以我们只关注主发送器、主接收器两种模式。
在主模式下,I 2 C 接口会启动数据传输并生成时钟信号。串行数据传输始终是在出现起始位时开始,在出现停止位时结束。起始位和停止位均在主模式下由软件生成
数据和地址均以 8 位字节传输,MSB 在前。起始位后紧随地址字节(7 位地址占据一个字节;10 位地址占据两个字节)。地址始终在主模式下传送。
其设计框图如下


这幅图列出了所有关于iic的寄存器,而我们真正用到最多的也就是两个控制寄存器CR1和CR2,两个状态寄存器SR1、SR2。
以下是我对这四个寄存器进行的总结性描述:

CR1:主要是控制IIC的一些信号生成(起始和结束信号等)以及是否使能IIC的某些特定功能,如应答使能是、数据校验使能等。
CR2主要控制一些IIC中断或者DMA是否开启以及配置IIC的通讯速率。因为此次设计的驱动中需要用到iic中断,所以需要了解下iic的中断。如下表:
中断名称        描述
ITBUF        缓冲中断
ITEVT        事件中断
ITERR        错误中断
在这里不细讲,下面会有介绍。只需要知道这些中断的开启都需要配置CR2相应位。

SR1和SR2里面的内容就比较杂了,不过好多功能我们用不到,只需要知道这两个寄存器存储了很多标识,如是否发送了起始位,应答失败?等状态,在程序里会有体现。
具体内容还是要参考数据手册里关于iic寄存器的描述,当然这部分也不需要记住,因为我们要调用库函数来实现很多的功能。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:54 | 显示全部楼层
接下来分析stm32的iic通讯流程,我们主要分析在主模式下如何发送以及通过DMA方式接收数据。

1、主模式下发送数据:
1、生成起始信号:CR1里的START 位置 1 后,接口会在 SR2的BUSY 位清零后生成一个起始位并切换到主模式。生成起始信号成功后SR1的SB 位会由硬件置 1 ,如果使能了事件中断(CR2的 ITEVFEN 位置 1 )则生成一个中断。这个中断在数据手册里官方命名为EV5。即为事件5中断。至于为什么从5开始,因为事件1到4在从机部分用了。。。接下来主设备会等待软件对 SR1 执行读操作,然后把从设备地址写入 DR 寄存器。只有这样才能清零SR1的SB位。

2、从地址传输,接下来从地址会通过内部移位寄存器发送到 SDA 线。stm32支持10位和7位地址。因为大多数我们接触到的都是7位地址的,所以这里只介绍7位地址位的。在 7 位寻址模式下,会发送一个地址字节。地址字节被发出后,SR1的ADDR 位会由硬件置 1 如果使能了事件中断(CR2的 ITEVFEN 位置 1 )则生成一个中断EV6。接下来主设备会等待对 SR1 寄存器执行读操作,然后对 SR2 寄存器执行读操作,只有这样才能清零SR1的ADDR 位。

3、主发送器,在发送出地址并将 ADDR 清零后,主设备会通过内部移位寄存器将 DR 寄存器中的字节发送到 SDA 线。在向数据寄存器写数据前如果开启了事件中断会进入中断EV8,接收到应答脉冲后,TxE 位会由硬件置 1 并在 ITEVFEN 和 ITBUFEN 位均置 1 时生成一个中断EV8。如果在上一次数据传输结束之前 TxE 位已置 1 但数据字节尚未写入 DR 寄存器,则 BTF 位会置 1,而接口会一直延长 SCL 低电平,等待I2C_DR 寄存器被写入,以将 BTF 清零。结束通信当最后一个字节写入 DR 寄存器后,软件会将 STOP 位置 1 以生成一个停止位EV8_2。接口会自动返回从模式(M/SL 位清零)。

结束通信:主设备会针对自从设备接收的最后一个字节发送 NACK。在接收到此 NACK 之后,从设备会释放对 SCL 和 SDA 线的控制。随后,主设备可发送一个停止位/重复起始位。

为了在最后一个接收数据字节后生成非应答脉冲,必须在读取倒数第二个数据字节后(倒数第二个 RxNE 事件之后)立即将 ACK 位清零。
要生成停止位/重复起始位,软件必须在读取倒数第二个数据字节后(倒数第二个 RxNE事件之后)将 STOP/START 位置 1。
在只接收单个字节的情况下,会在 EV6 期间(在 ADDR 标志清零之前)禁止应答并在EV6 之后生成停止位。
生成停止位后,接口会自动返回从模式(M/SL 位清零)。
以上是stm32的iic主发送模式通信流程,俗话说一图顶千字,数据手册把这个流程以图表展现出来如下:

859805c0759ed879b0.png

还是比较清楚的,只需要对照该图结合上面的文字描述来分析stm32的iic通信流程。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:54 | 显示全部楼层
2、使用 DMA 进行接收
将 I2C_CR2 寄存器中的 DMAEN 位置 1 可以使能 DMA 模式进行接收。接收数据字节时,数据会从 I2C_DR 寄存器加载到使用 DMA 外设配置的存储区中(参见 DMA 规范)。要映射一个 DMA 通道以便进行 I 2 C 接收,请按以下步骤操作:其中的 x 表示通道编号。
1. 设置 DMA_CPARx 寄存器中的 I2C_DR 寄存器地址。每次发生 RxNE 事件后,数据都会从此地址移动到存储器。
2. 设置 DMA_CMARx 寄存器中的存储器地址。每次发生 RXNE 事件后,数据都会从 I2C_DR寄存器加载到此存储区。
3. 在 MA_CNDTRx 寄存器中配置要传输的总字节数。在每次 RxNE 事件后,此值都会递减。
4. 使用 DMA_CCRx 寄存器中的 PL[0:1] 位来配置通道优先级。
5. 在完成半数传输或全部传输(取决于应用的需求)之后,将 DMA_CCRx 寄存器中的 DIR位重新置 1 并配置中断。
6. 将 DMA_CCRx 寄存器中的 EN 位置 1 以激活通道。
当传输的数据量达到 DMA 控制器中编程设定的值时,DMA 控制器会发送一个结束传输EOT/EOT_1 信号给 I 2 C 接口,而 DMA 会在 DMA 通道中断向量上生成一个中断(如果已使能):
注意: 如果使用 DMA 进行接收,请勿使能 I2C_CR2 寄存器中的 ITBUFEN 位。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:55 | 显示全部楼层
IIC驱动设计
终于到了最重要的环节,(鼓掌)。在这里我们需要坐下来静静地分析一下该怎样设计这个驱动。设计驱动的目的是尽量使得调用者方便调用。所以应该尽量封装一些不常更改的变量和方法,如在这里就忽略了stm32作为从机,并且默认是使能中断的。暴露给调用者经常要变更的变量和方法,如对于stm32经常要根据实际情况更改使用的iic外设编号(IIC1、IIC2、IIC3)、复用引脚、引脚时钟、DMA通道等。所以为了代码的复用率高这里以结构体来组合一些经常需要更改的变量。这些变量主要用来初始化IIC。又因为要设计的功能是数据传输,所以这里定义一个Message结构体,里面可以包含这个Message包含的数据长度、传输方向、传输状态等。
以上是一些数据结构的分析,然后是方法的设计。因为要传输数据,所以这里需要一个数据传输的方法,该方法根据Message结构体里的数据得出要发送或者接收的数据信息,根据初始化结构体来判断要进行通信的iic编号。最后根据该方法生成读取或者发送信息的API。以下是具体实现。It’s time for Code!


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:56 | 显示全部楼层
IIC初始化

首先定义iic初始化结构体,如下:


///i2c定义

typedef struct

{

  I2C_TypeDef*        i2cPort;                      ///i2cx

  uint32_t            i2cPerif;                     ///i2c时钟

  uint32_t            i2cEVIRQn;                    ///i2c事件中断

  uint32_t            i2cERIRQn;                    ///i2c错误处理中断

  uint32_t            i2cClockSpeed;                ///通信速率

  uint32_t            gpioSCLPerif;                 ///scl时钟

  GPIO_TypeDef*       gpioSCLPort;                  ///scl端口

  uint32_t            gpioSCLPin;                   ///scl引脚

  uint32_t            gpioSCLPinSource;             ///scl引脚source

  uint32_t            gpioSDAPerif;                 ///sda时钟

  GPIO_TypeDef*       gpioSDAPort;                  ///sda端口

  uint32_t            gpioSDAPin;                   ///sda引脚

  uint32_t            gpioSDAPinSource;             ///sda端口source


  uint32_t            gpioAF;                       ///复用pack


  uint32_t            dmaPerif;                     ///dma时钟

  uint32_t            dmaChannel;                   ///dma通道

  DMA_Stream_TypeDef* dmaRxStream;                  ///dma数据流

  uint32_t            dmaRxIRQ;                     ///dma中断

  uint32_t            dmaRxTCFlag;                  ///dma接收完成中断

  uint32_t            dmaRxTEFlag;                  ///dma接收错误中断


} I2cDef;


这里包含了stm32 的iic初始化需要的所有信息,这个结构体最终包含在I2cDrv上,如下:


///i2c驱动结构体

typedef struct

{

  const I2cDef *def;                    //< i2c定义

  I2cMessage txMessage;                 //< i2c通信message

  uint32_t messageIndex;                //< 发送或者接收字节的索引

  SemaphoreHandle_t isBusFreeSemaphore; //< 信号量用来同步传输

  SemaphoreHandle_t isBusFreeMutex;     //< 互斥信号量来保护传输数据

  DMA_InitTypeDef DMAStruct;            //< DMA 配置

} I2cDrv;

注意const I2cDef *def; 这表明I2cDrv包含了I2cDef的指针。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:56 | 显示全部楼层
然后根据I2cDrv里的数据进行初始化工作,如下:


static void i2cdrvInitBus(I2cDrv* i2c)
{
  I2C_InitTypeDef  I2C_InitStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
  GPIO_InitTypeDef GPIO_InitStructure;


  // 使能IIC端口时钟
  RCC_AHB1PeriphClockCmd(i2c->def->gpioSDAPerif, ENABLE);
  RCC_AHB1PeriphClockCmd(i2c->def->gpioSCLPerif, ENABLE);
  // 使能IIC总线时钟
  RCC_APB1PeriphClockCmd(i2c->def->i2cPerif, ENABLE);


  // 配置引脚属性
  GPIO_StructInit(&GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_InitStructure.GPIO_OType = GPIO_OType_OD;
  GPIO_InitStructure.GPIO_Pin = i2c->def->gpioSCLPin; // SCL
  GPIO_Init(i2c->def->gpioSCLPort, &GPIO_InitStructure);


  GPIO_InitStructure.GPIO_Pin =  i2c->def->gpioSDAPin; // SDA
  GPIO_Init(i2c->def->gpioSDAPort, &GPIO_InitStructure);
    /*解锁总线*/
  i2cdrvdevUnlockBus(i2c->def->gpioSCLPort, i2c->def->gpioSDAPort, i2c->def->gpioSCLPin, i2c->def->gpioSDAPin);


  //配置iic端口复用
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
  GPIO_InitStructure.GPIO_Pin = i2c->def->gpioSCLPin; // SCL
  GPIO_Init(i2c->def->gpioSCLPort, &GPIO_InitStructure);
  GPIO_InitStructure.GPIO_Pin =  i2c->def->gpioSDAPin; // SDA
  GPIO_Init(i2c->def->gpioSDAPort, &GPIO_InitStructure);


  //端口重映射
  GPIO_PinAFConfig(i2c->def->gpioSCLPort, i2c->def->gpioSCLPinSource, i2c->def->gpioAF);
  GPIO_PinAFConfig(i2c->def->gpioSDAPort, i2c->def->gpioSDAPinSource, i2c->def->gpioAF);


  // I2C配置
  I2C_DeInit(i2c->def->i2cPort);
  I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
  I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
  I2C_InitStructure.I2C_OwnAddress1 = I2C_SLAVE_ADDRESS7;
  I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
  I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
  I2C_InitStructure.I2C_ClockSpeed = i2c->def->i2cClockSpeed;
  I2C_Init(i2c->def->i2cPort, &I2C_InitStructure);


  // 使能IIC错误处理中断
  I2C_ITConfig(i2c->def->i2cPort, I2C_IT_ERR, ENABLE);


  NVIC_InitStructure.NVIC_IRQChannel = i2c->def->i2cEVIRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 7;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
  NVIC_InitStructure.NVIC_IRQChannel = i2c->def->i2cERIRQn;
  NVIC_Init(&NVIC_InitStructure);


  i2cdrvDmaSetupBus(i2c);//IIC DMA使能


    /*创建信号量*/
  i2c->isBusFreeSemaphore = xSemaphoreCreateBinary();
  i2c->isBusFreeMutex = xSemaphoreCreateMutex();
}



最后两句 i2c->isBusFreeSemaphore = xSemaphoreCreateBinary();和 i2c->isBusFreeMutex = xSemaphoreCreateMutex();创建信号量和互斥信号量,是freeRTOS里的函数,用来同步发送/接收信号,互斥信号量来保护传输数据,这两种信号的用法需要读者自己查阅资料,这里不深入分析。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:57 | 显示全部楼层
通过以上两个步骤便可以自定义初始化任何stm32的iic了,只需要定义一个I2cDef用来包含板级iic设置了,如可以这样定义:

static const I2cDef sensorBusDef =
{
  .i2cPort            = I2C1,
  .i2cPerif           = RCC_APB1Periph_I2C1,
  .i2cEVIRQn          = I2C1_EV_IRQn,
  .i2cERIRQn          = I2C1_ER_IRQn,
  .i2cClockSpeed      = I2C_DECK_CLOCK_SPEED,
  .gpioSCLPerif       = RCC_AHB1Periph_GPIOB,
  .gpioSCLPort        = GPIOB,
  .gpioSCLPin         = GPIO_Pin_6,
  .gpioSCLPinSource   = GPIO_PinSource6,
  .gpioSDAPerif       = RCC_AHB1Periph_GPIOB,
  .gpioSDAPort        = GPIOB,
  .gpioSDAPin         = GPIO_Pin_7,
  .gpioSDAPinSource   = GPIO_PinSource7,
  .gpioAF             = GPIO_AF_I2C1,
  .dmaPerif           = RCC_AHB1Periph_DMA1,
  .dmaChannel         = DMA_Channel_1,
  .dmaRxStream        = DMA1_Stream0,
  .dmaRxIRQ           = DMA1_Stream0_IRQn,
  .dmaRxTCFlag        = DMA_FLAG_TCIF0,
  .dmaRxTEFlag        = DMA_FLAG_TEIF0,
};


不过以上很多函数这里都不能一一给出原型,因为太多了,比如解锁总线这个函数还有DMA配置这个函数需要读者自己实现了,这里主要讲设计的思路和方法。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:58 | 显示全部楼层
数据的传输

数据传输的函数如下:


bool i2cdrvMessageTransfer(I2cDrv* i2c, I2cMessage* message)

{

  bool status = false;

    //取互斥信号会使得该互斥信号变成无效状态,直到再给一次信号

  xSemaphoreTake(i2c->isBusFreeMutex, portMAX_DELAY); // Protect message data

  // Copy message

  memcpy((char*)&i2c->txMessage, (char*)message, sizeof(I2cMessage));

  // 开始通过ISR方式发送信号.

  i2cdrvStartTransfer(i2c);

  // 等待传输完成

  if (xSemaphoreTake(i2c->isBusFreeSemaphore, I2C_MESSAGE_TIMEOUT) == pdTRUE)

  {

    if (i2c->txMessage.status == i2cAck)

    {

      status = true;

    }

  }

  else

  {

    i2cdrvClearDMA(i2c);

    i2cdrvTryToRestartBus(i2c);

    //TODO: If bus is really hanged... fail safe

  }

  xSemaphoreGive(i2c->isBusFreeMutex);//发送信号


  return status;

}



其中主要是调用i2cdrvStartTransfer(i2c);这个函数,其作用是发送起始信号以及开启对应中断,原型如下:


static void i2cdrvStartTransfer(I2cDrv *i2c)

{

  if (i2c->txMessage.direction == i2cRead)///DMA读取

  {

    i2c->DMAStruct.DMA_BufferSize = i2c->txMessage.messageLength;

    i2c->DMAStruct.DMA_Memory0BaseAddr = (uint32_t)i2c->txMessage.buffer;

    DMA_Init(i2c->def->dmaRxStream, &i2c->DMAStruct);

    DMA_Cmd(i2c->def->dmaRxStream, ENABLE);

  }


  I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);//失能buff中断

  I2C_ITConfig(i2c->def->i2cPort, I2C_IT_EVT, ENABLE);//使能事件中断

  i2c->def->i2cPort->CR1 = (I2C_CR1_START | I2C_CR1_PE);//发送起始信号

}


可以看出在进行发送时只需要发送一个起始信号以及打开事件中断即可,进行接收时还需要打开DMA对应通道,并且设置内存地址即可。最后的一系列处理均在中断里执行。那么接下来就开始中断处理了。


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:58 | 显示全部楼层
中断处理

在这里一共需要处理三个中断函数

事件中断
错误处理中断
DMA完成及错误中断




使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:59 | 显示全部楼层
事件中断
事件中断主要处理一系列事件,主要是上面定义的EV5、EV6等事件,原型如下,该函数被iic事件中断函数调用:

static void i2cdrvEventIsrHandler(I2cDrv* i2c)
{
  uint16_t SR1;
  uint16_t SR2;

  // 首先读取状态寄存器
  SR1 = i2c->def->i2cPort->SR1;



  // 起始事件EV5
  if (SR1 & I2C_SR1_SB)
  {
    i2c->messageIndex = 0;

    if(i2c->txMessage.direction == i2cWrite ||
       i2c->txMessage.internalAddress != I2C_NO_INTERNAL_ADDRESS)
    {
      I2C_Send7bitAddress(i2c->def->i2cPort, i2c->txMessage.slaveAddress << 1, I2C_Direction_Transmitter);
    }
    else
    {
      I2C_AcknowledgeConfig(i2c->def->i2cPort, ENABLE);
      I2C_Send7bitAddress(i2c->def->i2cPort, i2c->txMessage.slaveAddress << 1, I2C_Direction_Receiver);
    }
  }
  // 地址事件,代表从机响应地址的发生EV6
  else if (SR1 & I2C_SR1_ADDR)
  {
    if(i2c->txMessage.direction == i2cWrite ||
       i2c->txMessage.internalAddress != I2C_NO_INTERNAL_ADDRESS)
    {
      SR2 = i2c->def->i2cPort->SR2;                               // 清除addr位
      // 判断内部地址是有的
      if (i2c->txMessage.internalAddress != I2C_NO_INTERNAL_ADDRESS)
      {
        if (i2c->txMessage.isInternal16bit)
        {
          I2C_SendData(i2c->def->i2cPort, (i2c->txMessage.internalAddress & 0xFF00) >> 8);
          I2C_SendData(i2c->def->i2cPort, (i2c->txMessage.internalAddress & 0x00FF));
        }
        else
        {
          I2C_SendData(i2c->def->i2cPort, (i2c->txMessage.internalAddress & 0x00FF));
        }
        i2c->txMessage.internalAddress = I2C_NO_INTERNAL_ADDRESS;
      }
      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, ENABLE);        // 使能EV7
    }
    //为读取,开启DMA
    else
    {
      if(i2c->txMessage.messageLength == 1)
      {
        I2C_AcknowledgeConfig(i2c->def->i2cPort, DISABLE);
      }
      else
      {
        I2C_DMALastTransferCmd(i2c->def->i2cPort, ENABLE);
      }
      // 进制iic buff中断
      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_EVT | I2C_IT_BUF, DISABLE);
      // 使能DMA传输完成中断
      DMA_ITConfig(i2c->def->dmaRxStream, DMA_IT_TC | DMA_IT_TE, ENABLE);
      I2C_DMACmd(i2c->def->i2cPort, ENABLE); // Enable before ADDR clear

      __DMB();                        
      SR2 = i2c->def->i2cPort->SR2;    // 读取SR2来清除addr
    }
  }
  // 传输完成EV8-2
  else if (SR1 & I2C_SR1_BTF)
  {
    SR2 = i2c->def->i2cPort->SR2;
    if (SR2 & I2C_SR2_TRA) // 是在写的模式?
    {
      if (i2c->txMessage.direction == i2cRead) // read
      {

        i2c->def->i2cPort->CR1 = (I2C_CR1_START | I2C_CR1_PE); // 生成起始信号
      }
      else
      {
        i2cNotifyClient(i2c);

        i2cTryNextMessage(i2c);
      }
    }
    else // 读取模式,在DMA接收下不会发生
    {
      i2c->txMessage.buffer[i2c->messageIndex++] = I2C_ReceiveData(i2c->def->i2cPort);
      if(i2c->messageIndex == i2c->txMessage.messageLength)
      {
        i2cNotifyClient(i2c);

        i2cTryNextMessage(i2c);
      }
    }

    while (i2c->def->i2cPort->CR1 & 0x0100) { ; }
  }
  // 字节接收
  else if (SR1 & I2C_SR1_RXNE) // 读取模式,在DMA接收下不会发生
  {
    i2c->txMessage.buffer[i2c->messageIndex++] = I2C_ReceiveData(i2c->def->i2cPort);
    if(i2c->messageIndex == i2c->txMessage.messageLength)
    {
      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);   
    }
  }

  else if (SR1 & I2C_SR1_TXE)
  {
    if (i2c->txMessage.direction == i2cRead)
    {

      I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);
    }
    else
    {
      I2C_SendData(i2c->def->i2cPort, i2c->txMessage.buffer[i2c->messageIndex++]);
      if(i2c->messageIndex == i2c->txMessage.messageLength)
      {

        I2C_ITConfig(i2c->def->i2cPort, I2C_IT_BUF, DISABLE);
      }
    }
  }
}



使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 12:59 | 显示全部楼层
错误处理
在错误处理中断里主要需要处理应答失败,其余的错误直接清除标志位,原型如下

static void i2cdrvErrorIsrHandler(I2cDrv* i2c)
{
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_AF))
  {
    if(i2c->txMessage.nbrOfRetries-- > 0)
    {
      // 重新生成开始信号
      i2c->def->i2cPort->CR1 = (I2C_CR1_START | I2C_CR1_PE);
    }
    else
    {
      // 重试几次后还未成功则尝试下一个
      i2c->txMessage.status = i2cNack;
      i2cNotifyClient(i2c);
      i2cTryNextMessage(i2c);
    }
    I2C_ClearFlag(i2c->def->i2cPort, I2C_FLAG_AF);
  }
  ///剩下几个错误直接清除就行了
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_BERR))
  {
      I2C_ClearFlag(i2c->def->i2cPort, I2C_FLAG_BERR);
  }
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_OVR))
  {
      I2C_ClearFlag(i2c->def->i2cPort, I2C_FLAG_OVR);
  }
  if (I2C_GetFlagStatus(i2c->def->i2cPort, I2C_FLAG_ARLO))
  {
      I2C_ClearFlag(i2c->def->i2cPort,I2C_FLAG_ARLO);
  }
}


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 13:00 | 显示全部楼层
DMA中断
DMA中断里分完成和错误中断,原型如下:

static void i2cdrvDmaIsrHandler(I2cDrv* i2c)
{
  if (DMA_GetFlagStatus(i2c->def->dmaRxStream, i2c->def->dmaRxTCFlag)) // 传输完成
  {
    i2cdrvClearDMA(i2c);
    i2cNotifyClient(i2c);

    i2cTryNextMessage(i2c);
  }
  if (DMA_GetFlagStatus(i2c->def->dmaRxStream, i2c->def->dmaRxTEFlag)) //传输错误
  {
    DMA_ClearITPendingBit(i2c->def->dmaRxStream, i2c->def->dmaRxTEFlag);
    i2c->txMessage.status = i2cNack;
    i2cNotifyClient(i2c);
    i2cTryNextMessage(i2c);
  }
}



使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 13:01 | 显示全部楼层
函数封装
最后是对函数进行封装,因为我们最终要使用iic进行读取或者发送数据,所以需要对函数封装,这里举个例子,如读取函数封装如下:

/**
* 从i2c外设中读取数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param len  要读取的字节长度
* @param data[OUT]  读取数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevRead(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
               uint16_t len, uint8_t *data);


函数原型如下:

bool i2cdevRead(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
               uint16_t len, uint8_t *data)
{
  I2cMessage message;

    i2cdrvCreateMessageIntAddr(&message, devAddress, false, memAddress,
                            i2cRead, len, data);

  return i2cdrvMessageTransfer(dev, &message);
}


这里首先创建message然后调用i2cdrvMessageTransfer进行数据传输。i2cdrvCreateMessageIntAddr原型如下:

void i2cdrvCreateMessageIntAddr(I2cMessage *message,
                             uint8_t  slaveAddress,
                             bool IsInternal16,
                             uint16_t intAddress,
                             uint8_t  direction,
                             uint32_t length,
                             uint8_t  *buffer)
{
  message->slaveAddress = slaveAddress;
  message->direction = direction;
  message->isInternal16bit = IsInternal16;
  message->internalAddress = intAddress;
  message->messageLength = length;
  message->status = i2cAck;
  message->buffer = buffer;
  message->nbrOfRetries = I2C_MAX_RETRIES;
}


使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 13:02 | 显示全部楼层
以上只是一个读取的example,最终的封装如下:(原型就不写了太多)

/**
* 从i2c外设中读取数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param len  要读取的字节长度
* @param data[OUT]  读取数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevRead(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
               uint16_t len, uint8_t *data);

/**
* 从16位内部地址的设备中读取数据
* @param dev  指向i2c外设的指针
* @param devAddress  从设备地址
* @param memAddress  内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param len  要读取的长度.
* @param data[OUT]  读取数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevRead16(I2C_Dev *dev, uint8_t devAddress, uint16_t memAddress,
               uint16_t len, uint8_t *data);

/**
* 初始化i2c外设
* @param dev  指向i2c外设指针
*
* @return TRUE =成功 FALSE=失败.
*/
int i2cdevInit(I2C_Dev *dev);

/**
* 从i2c外设中读取一个字节数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param data[OUT]  读取数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevReadByte(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                    uint8_t *data);

/**
* 从i2c外设中读取一个bit数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param bitNum      要读取bit的位置(0-7)
* @param data[OUT]  读取数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevReadBit(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                     uint8_t bitNum, uint8_t *data);
/**
* 从i2c外设中读取多个bit数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param bitNum      要读取bit的起始位置(0-7)
* @param length      要读取的长度
* @param data[OUT]  读取数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevReadBits(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                    uint8_t bitStart, uint8_t length, uint8_t *data);

/**
* 向i2c外设中写入数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param len  要写入的字节长度
* @param data[OUT]  写入数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevWrite(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                 uint16_t len, uint8_t *data);

/**
* 向16位内部地址的设备中写入数据
* @param dev  指向i2c外设的指针
* @param devAddress  从设备地址
* @param memAddress  内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param len  要写入的长度.
* @param data[OUT]  写入数据的缓存区指针
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevWrite16(I2C_Dev *dev, uint8_t devAddress, uint16_t memAddress,
                   uint16_t len, uint8_t *data);

/**
* 向i2c外设中写入1byte数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要读取数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param data  写入数据
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevWriteByte(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                     uint8_t data);

/**
* 向i2c外设中写入一个bit数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要写入数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param bitNum      要写入bit的位置(0-7)
* @param data  写入的数据
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevWriteBit(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                    uint8_t bitNum, uint8_t data);

/**
* 向i2c外设中写入多个bit数据
* @param dev 指向i2c外设的指针
* @param devAddress  从机地址
* @param memAddress  要写入数据的内部地址, I2CDEV_NO_MEM_ADDR 代表没有地址.
* @param bitStart      要写入bit的起始位置(0-7)
* @param length      要写入bit的长度
* @param data  写入的数据
*
* @return TRUE =成功 FALSE=失败.
*/
bool i2cdevWriteBits(I2C_Dev *dev, uint8_t devAddress, uint8_t memAddress,
                     uint8_t bitStart, uint8_t length, uint8_t data);



使用特权

评论回复
keaibukelian|  楼主 | 2018-12-5 13:02 | 显示全部楼层
尾言
经过以上步骤一个stm32 的iic驱动便完成了,我主要使用它读取mpu9250,经过验证没有问题,读取mpu9250上的AK8963(pass by模式)也毫无压力。再也没有出现锁死在while(1)的尴尬了(因为这里根本没有while(1))。。。也使用它读取过ak8975.仍无压力。最后声明一下不要找我要源码,这是我接下来自己开发飞控程序里的一部分,等我完成这一工程后兴许会开放出来。。。在这之前保密!
祝你早日摆脱硬件IIC的烦恼 ∩_∩


使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

63

主题

3825

帖子

5

粉丝