Qurak

Make Code Talk

0%

STM32单片机HAL库学习MacOS(5)——I2C通信

——Feb,21,2021

目录

本次项目采用正点原子STM32F407探索者开发版和STM32cubeIDE软件开发

  • I2C协议介绍
  • STM32上I2C的实现
  • STM32F4探索者I2C实验

一、I2C协议介绍

1、I2C通信原理

  • I2C通讯协议(Inter-Integrated Circuit)引脚少,硬件实现简单,可扩展性强,不需要USART、CAN等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。支持双向传输,但是由于公用数据线只能实现半双工通信,大部分I2C设备支持100KHz400KHz模式,一些设备还支持3.4MbHz高速模式。

  • I2C通信硬件十分简单,最少仅需要2条数据线:

    • SDA (Serial Data, 串行数据 )

    • SCL (Serial Clock, 串行时钟 )

    • 在一些情况下需要额外的数据线寻址。

    • 物理层示意图:

    I2C总线连接图
  • 设备连接和寻找:每个连接到总线的设备都有一个独立的地址(7位或10位),主机可以利用这个地址进行不同设备之间的访问。其中地址若是7位二进制数表示的话,高四位为设备固有地址,后三位地址由用户设置,因此同一个设备最多可以连接8个。在通信开始后,主机发送的第一个字节为寻址信号,当从机的地址与寻址信号相匹配时才会与主机建立通信。

  • 通信进程S—>Slave Address—>R/W—>A—>Data->…->NA—>P

    (S:开始信号,A:应答信号,保证通信过程没有断开,NA:非应答信号,P:停止信号)I2C通信流程图

2、I2C时序图

  • 起始信号(S):当时钟线 (SCL线)为高时,数据线(SDA 线)从高到低;停止信号§:当时钟线 (SCL线)是高时,数据线(SDA 线)由低到高切换。起止信号时序图

  • 传输:I2C使用SDA信号线来传输数据,使用SCL信号线进行数据同步。SDA数据线在SCL的每个时钟周期传输一位数据。传输时,SCL为高时SDA数据有效,即此时的SDA为高电平时表示数据“1”,为低电平时表示数据“0”。当SCL为低时,SDA数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。

  • 应答:I2C的数据和地址传输都带响应。响应包括“应答(ACK)”和“非应答(NACK)”两种信号。一般数据都是以字节(8bits)的形式发送在第九个CLK周期时,主机为接收模式等待从机的应答信号,若收到低电平为A信号,高电平为NA信号

    应答信号时序图

二、STM32上I2C的实现

1、利用IO口模拟I2C实现通信

  • 优点:可以用在任意引脚,方便外接外设;流程清楚,稳定;

  • 缺点:占用CPU,效率不如;用IO口模拟,速度较慢;

  • 实现方法:根据时序图软件编程

    • 初始化两个IO口分别作为SDA和SCL
    • S:主机发送模式,SCL为高时,SDA由高到低;P:主机发送模式,SCL为高时,SDA由低到高;
    • A:主机切换为接收模式,传输一字节后SDA先拉低,在拉高SCL,高电平在持续时间中重新拉高SDA。NA则SDA不拉低。
    • 数据传输:SCL为时钟,在SCL为高时数据有效。

    (软件实现要注意高低电平的建立时间和持续时间保证传输正确)

    • 代码可以参考正点原子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      //代码由正点原子提供
      #include "myiic.h"
      #include "delay.h"
      //IO方向设置
      #define SDA_IN() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=0<<9*2;} //PB9输入模式
      #define SDA_OUT() {GPIOB->MODER&=~(3<<(9*2));GPIOB->MODER|=1<<9*2;} //PB9输出模式
      //IO操作
      #define IIC_SCL PBout(8) //SCL
      #define IIC_SDA PBout(9) //SDA
      #define READ_SDA PBin(9) //输入SDA
      void IIC_Init(void) //IIC初始化
      {
      GPIO_InitTypeDef GPIO_Initure;
      __HAL_RCC_GPIOB_CLK_ENABLE(); //使能GPIOB时钟
      //PH4,5初始化设置
      GPIO_Initure.Pin=GPIO_PIN_8|GPIO_PIN_9;
      GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出
      GPIO_Initure.Pull=GPIO_PULLUP; //上拉
      GPIO_Initure.Speed=GPIO_SPEED_FAST; //快速
      HAL_GPIO_Init(GPIOB,&GPIO_Initure);
      IIC_SDA=1; //SDA输出1
      IIC_SCL=1; //SCL输出1
      }
      void IIC_Start(void) //产生IIC起始信号
      {
      SDA_OUT(); //sda线输出
      IIC_SDA=1;
      IIC_SCL=1;
      delay_us(4);
      IIC_SDA=0;//START:when CLK is high,DATA change form high to low
      delay_us(4);
      IIC_SCL=0;//钳住I2C总线,准备发送或接收数据
      }
      void IIC_Stop(void) //产生IIC停止信号
      {
      SDA_OUT();//sda线输出
      IIC_SCL=0;
      IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
      delay_us(4);
      IIC_SCL=1;
      IIC_SDA=1;//发送I2C总线结束信号
      delay_us(4);
      }
      //等待应答信号到来
      //返回值:1,接收应答失败;0,接收应答成功
      u8 IIC_Wait_Ack(void)
      {
      u8 ucErrTime=0;
      SDA_IN(); //SDA设置为输入
      IIC_SDA=1;delay_us(1);
      IIC_SCL=1;delay_us(1);
      while(READ_SDA){
      ucErrTime++;
      if(ucErrTime>250){
      IIC_Stop();
      return 1;
      }
      }
      IIC_SCL=0;//时钟输出0
      return 0;
      }
      //产生ACK应答
      void IIC_Ack(void)
      {
      IIC_SCL=0;
      SDA_OUT();
      IIC_SDA=0;
      delay_us(2);
      IIC_SCL=1;
      delay_us(2);
      IIC_SCL=0;
      }
      //不产生ACK应答
      void IIC_NAck(void)
      {
      IIC_SCL=0;
      SDA_OUT();
      IIC_SDA=1;
      delay_us(2);
      IIC_SCL=1;
      delay_us(2);
      IIC_SCL=0;
      }
      //IIC发送一个字节
      //返回从机有无应答
      //1,有应答
      //0,无应答
      void IIC_Send_Byte(u8 txd)
      {
      u8 t;
      SDA_OUT();
      IIC_SCL=0;//拉低时钟开始数据传输
      for(t=0;t<8;t++)
      {
      IIC_SDA=(txd&0x80)>>7;
      txd<<=1;
      delay_us(2); //对TEA5767这三个延时都是必须的
      IIC_SCL=1;
      delay_us(2);
      IIC_SCL=0;
      delay_us(2);
      }
      }
      //读1个字节,ack=1时,发送ACK,ack=0,发送nACK
      u8 IIC_Read_Byte(unsigned char ack)
      {
      unsigned char i,receive=0;
      SDA_IN();//SDA设置为输入
      for(i=0;i<8;i++ )
      {
      IIC_SCL=0;
      delay_us(2);
      IIC_SCL=1;
      receive<<=1;
      if(READ_SDA)receive++;
      delay_us(1);
      }
      if (!ack)
      IIC_NAck();//发送nACK
      else
      IIC_Ack(); //发送ACK
      return receive;
      }

2、直接利用STM32自带的硬件I2C

  • 优点:传输时候为硬件传输,不占用CPU;传输稳定;速度快;支持DMA传输;
  • 缺点:只能采用固定IO口;配置寄存器比较复杂;据说旧版标准库中I2C的调用存在bug,HAL库目前没有遇到问题;
  • 实现方法:配置好I2C后直接调用HAL库函数即可

三、STM32F4探索者I2C实验

实验目的:利用硬件I2C实现对24c02存储芯片进行读写

1、配置相关设置

  • 外部时钟开启——>时钟树配置
  • LCD显示(FSMC,PB15)配置
  • 按键控制配置
  • 硬件I2C配置

(1)外部晶振打开,时钟树配置如下:

配置时钟树

(2)LCD显示相关配置

详情见 STM32单片机HAL库学习MacOS(4)——LCD显示

(3)按键我们只需要用到key1与key0

Key配置

(4)硬件I2C配置与硬件连接(connectivity -> I2C1)

如下图配置即可。注意:由于I2C硬件接口有多个,当我们打开I2C1接口时,默认的引脚可能不是我们所要用到的(第一次没注意还以为程序除了bug啥都没显示)根据开发版所给的资料显示,I2C接口处于PB8和PB9的IO口,因此我们需要在STM32cubeMX配置过程点击PB8和PB9手动调整。

24C02硬件连接

I2C配置

配置好后生成代码~

2、24c02芯片连接以及读写

  • 地址:24c02芯片为I2C接口,地址为7-bit格式,在开发板上A2-A0为0,最低位为读写控制。因此写的时候地址为0xA0,读地址为0xA1。

    24c02地址
  • 存储方式:查阅24c02的芯片手册可以知道24c02中数据由32页存储,每页存储8字节,寻址格式为8bit。同时24c02最多支持页写入,而读取是没有限制的。

    24c02存储方式

  • 读写:在芯片手册中有提到,写入最多一口气写8字节(页写入),且写入后需要等待10ms才会把数据存储好。

3、HAL库函数介绍

  • HAL库中自带I2C调用函数,可以在“stm32f4xx_hal_i2c.c”中查看,在不用DMA和中断情况下,主要有以下函数:

    •   HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
        HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
        HAL_StatusTypeDef HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
        HAL_StatusTypeDef HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
        HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
        HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
        HAL_StatusTypeDef HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);
        <!--code1-->
      
      

改写正点原子24c02的用处在于以后用到正点原子相关外设(例如触摸模块)涉及到24c02时不用额外编写新代码,可以直接兼容使用。

(2)HAL库实现

如果仔细发现上面改写的代码可能就会已经明白HAL库的直接写法,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//读取全部存储空间
//读取没有限制,可一口气读取全部
HAL_I2C_Mem_Read(&hi2c1,0xA1,0,I2C_MEMADD_SIZE_8BIT,I2C_Buffer_Read,256,1000);
//写入全部存储空间
uint8_t I2C_Buffer_Write[256]={"STM32 EXPORER I2C TEST..."};
uint8_t I2C_Buffer_Read[256]={0};
uint16_t i;
for(i=0;i<256;i=i+8)//页写入,一次最多8字节
{
if(HAL_I2C_Mem_Write(&hi2c1,0xA0,i,I2C_MEMADD_SIZE_8BIT,&(I2C_Buffer_Write[i]),8,1000)==HAL_OK)//若传输完成
{
HAL_Delay(10);//等待10ms
}
else{
break;}//传输失败,结束写入
}

(3)加上按键控制和显示完善例程

最后加上按键的操作就可以实现key1写入,key0读出,最后再用LCD显示。以下是完整代码。

Here~ 点击下载


END