[其他ST产品] STM32实战之深入理解I²C通信协议

[复制链接]
1342|15
 楼主| 欢乐家园 发表于 2023-12-29 00:11 | 显示全部楼层 |阅读模式
IIC(Inter-Integrated Circuit),也称为I²C或TWI(Two-Wire Interface),是一种广泛使用的串行总线接口,用于连接低速度的集成电路。这种通信协议非常适合在单个主设备和多个从设备之间进行短距离通信。

I²C的物理层
IIC通信只需要两根线:一个是串行数据线(SDA),另一个是串行时钟线(SCL)。这两根线都需要通过上拉电阻连接到正电源,以确保在没有信号驱动时,线路能够保持在高电平状态。
52027658d9e06e8d52.png
I²C的协议层

IIC协议定义了一系列的信号,包括开始信号、停止信号、数据有效性和应答信号。开始信号和停止信号用于标识一次通信的开始和结束,而数据有效性确保数据在时钟信号稳定时被读取。应答信号则是从设备对接收数据的确认。


 楼主| 欢乐家园 发表于 2023-12-29 00:11 | 显示全部楼层
I²C特点
两线制接口:I2C通信只需要两根线,一根是串行数据线(SDA),另一根是串行时钟线(SCL),所以I2C为半双工通信。
多主设备:I2C允许多个主设备(master)和多个从设备(slave)在同一总线上通信。
地址识别:每个从设备都有一个唯一的地址,主设备通过这个地址与特定的从设备通信。
同步通信:I2C是一种同步通信协议,数据传输是由时钟信号(SCL)来同步的。
支持多速率:I2C支持多种不同的数据传输速率,包括标准模式(100kbps)、快速模式(400kbps)、快速模式加(1Mbps)和高速模式(3.4Mbps)。
软件可配置:I2C设备的地址和一些功能可以通过软件进行配置。
简单易用:I2C接口的硬件实现相对简单,易于集成到各种微控制器和其他集成电路中。
应用广泛:I2C广泛应用于各种电子产品中,如手机、电视、医疗设备和嵌入式系统等。
支持热插拔:I2C设备支持在系统运行时添加或移除,即热插拔。
总线仲裁:在多主设备的情况下,I2C协议提供了一种仲裁机制,以决定哪个主设备可以控制总线。
时钟拉伸:从设备可以通过拉低时钟线来暂停通信(称为时钟拉伸),以便有足够的时间处理接收到的数据或完成数据发送。
应答机制:I2C通信中包含应答(ACK)和非应答(NACK)信号,用于指示数据是否成功接收。
I2C由于其简单和灵活的特性,成为了连接低速外围设备,如传感器、EEPROM、显示器等的理想选择。
 楼主| 欢乐家园 发表于 2023-12-29 00:11 | 显示全部楼层
I²C 总线时序图
总线时序图是理解IIC通信的关键。它展示了开始信号、数据位的传输、应答位以及停止信号的顺序。在IIC通信中,数据位在SCL线为高电平时被认为是稳定的,因此数据应该在SCL的高电平期间被读取。

起始条件: SCL高电平期间,SDA从高电平切换到低电平

终止条件: SCL高电平期间,SDA从低电平切换到高电平
 楼主| 欢乐家园 发表于 2023-12-29 00:11 | 显示全部楼层
起始和终止条件都是由主机产生

8796658d9e3a3582a.png
 楼主| 欢乐家园 发表于 2023-12-29 00:11 | 显示全部楼层
发送一个字节: SCL低电平期间,主机将数据位依次放到SDA线上,(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许由数据变化,依次循环上述过程8次即可发送一个字节

2382658d9e4609342.png
 楼主| 欢乐家园 发表于 2023-12-29 00:12 | 显示全部楼层
接收一个字节: SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所哟一SCL高电平期间SDA不允许有数据变换,依次循环上述过程8次,即可接收一个字节(主机在接收数据前需要先释放SDA) 45256658d9e53589bb.png
 楼主| 欢乐家园 发表于 2023-12-29 00:12 | 显示全部楼层
发送应答:  主机在接收完一个字节后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

接收应答:  主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前需要释放SDA) 53222658d9e61cce0a.png
 楼主| 欢乐家园 发表于 2023-12-29 00:12 | 显示全部楼层
软件模拟I²C时序分享
/**
  * @brief  定义SCL写函数
  * @param  None
  * @retval None
  */
    void myi2c_w_scl(uint8_t bitval){
        GPIO_WriteBit(GPIOA, GPIO_Pin_1, (BitAction)bitval); //将bitval的值写入GPIOA的Pin_1,也就是SCL线
        Delay_us(10); //延迟10微秒
    }
   
    /**
  * @brief  定义SDA写函数
  * @param  None
  * @retval None
  */
    void myi2c_w_sda(uint8_t bitval){
        GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)bitval); //将bitval的值写入GPIOA的Pin_0,也就是SDA线
        Delay_us(10); //延迟10微秒
    }
   
    /**
    * @brief  读取SDA数据
    * @param  None
    * @retval None
    */
    uint8_t myi2c_r_sda(void){
        return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0); //读取GPIOA的Pin_0,也就是SDA线的值
    }

/**
  * @brief  软件模拟I2C初始化
    *        SDA        PA0        推挽输出
    *        SCL        PA1        推挽输出
  * @param  None
  * @retval None
  */
void myi2c_init(void){
    //初始化GPIO口
        RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
      GPIO_InitTypeDef GPIO_InitStructure; //定义GPIO初始化结构体
      GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //设置GPIO模式为开漏输出
      GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; //设置GPIO的Pin_0和Pin_1
      GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //设置GPIO速度为50MHz
      GPIO_Init(GPIOA, &GPIO_InitStructure); //初始化GPIOA
   
    //释放总线
    GPIO_SetBits(GPIOA, GPIO_Pin_0 | GPIO_Pin_1); //将GPIOA的Pin_0和Pin_1设置为高电平,释放总线
}

/**
  * @brief  I2C起始条件
  * @param  None
  * @retval None
  */
void i2c_start(void){
    //输出起始条件
    myi2c_w_sda(1); //将SDA线设置为高电平
    myi2c_w_scl(1); //将SCL线设置为高电平
   
    myi2c_w_sda(0); //将SDA线设置为低电平,生成起始条件
    myi2c_w_scl(0); //将SCL线设置为低电平
}

/**
  * @brief  I2C结束条件
  * @param  None
  * @retval None
  */
void i2c_stop(void){
    //输出起始条件
    myi2c_w_sda(0); //将SDA线设置为低电平
    myi2c_w_scl(1); //将SCL线设置为高电平
    myi2c_w_sda(1); //将SDA线设置为高电平,生成结束条件
}

/**
* @brief  I2C发送一个字节
  * @param  None
  * @retval None
  */
void myi2c_sendbyte(uint8_t byte){
    for(uint8_t i = 0; i < 8; i++){ //循环8次,发送一个字节
        myi2c_w_sda(byte & 0x80 >> i);    //每发送一次向右偏移一个字节
        myi2c_w_scl(1); //将SCL线设置为高电平
        myi2c_w_scl(0); //将SCL线设置为低电平
    }
}

/**
  * @brief  I2C接收一个字节
  * @param  None
  * @retval None
  */
uint8_t myi2c_recv_byte(void){
    uint8_t byte = 0; //定义一个字节变量
    for(uint8_t i = 0; i < 8; i++){ //循环8次,接收一个字节
        myi2c_w_scl(1); //将SCL线设置为高电平
        if(myi2c_r_sda() == 1){byte |= (0x80 >> i);} //如果SDA线为高电平,将byte的相应位设置为1
            myi2c_w_scl(0); //将SCL线设置为低电平
    }
    return byte; //返回接收到的字节
}

/**
  * @brief  I2C接收应答
  * @param  None
  * @retval None
  */
uint8_t myi2c_recv_ack(void){
    uint8_t ackbit = 0; //定义一个应答位变量
    myi2c_w_sda(1); //将SDA线设置为高电平
    myi2c_w_scl(1); //将SCL线设置为高电平
    ackbit = myi2c_r_sda(); //读取SDA线的值,也就是应答位
    myi2c_w_scl(0); //将SCL线设置为低电平
    return ackbit; //返回应答位
}

/**
* @brief  I2C发送应答
  * @param  None
  * @retval None
  */
void myi2c_send_ack(uint8_t ackbit){
    myi2c_w_sda(ackbit); //将应答位的值写入SDA线
    myi2c_w_scl(1); //将SCL线设置为高电平
    myi2c_w_scl(0); //将SCL线设置为低电平
}
 楼主| 欢乐家园 发表于 2023-12-29 00:12 | 显示全部楼层
软件模拟IIC驱动AT24C02分享
例程简介
通过I2C协议与AT24C04 EEPROM芯片进行交互的函数。EEPROM代表电可擦除可编程只读存储器,这是一种非易失性存储器,用于计算机和其他电子设备中存储断电后必须保存的少量数据。

以下是每个函数的简要概述:

​​AT24_init​​:此函数初始化与AT24C04芯片通信的I2C接口。
​​AT24_write_byte​​:此函数将单个字节的数据写入AT24C04芯片的指定地址。
​​AT24_read_byte​​:此函数从AT24C04芯片的指定地址读取单个字节的数据。
​​AT24_write_page​​:此函数将多个字节的数据写入AT24C04芯片的指定地址。AT24C04的内存被划分为多个页面,每个页面可以容纳多个字节的数据。
​​AT24_WriteBuffer​​:此函数将数据缓冲区写入AT24C04芯片。它考虑到芯片内存的页面结构,并在必要时跨多个页面写入数据。
​​AT24_readBuffer​​​:此函数从AT24C04芯片读取数据缓冲区。与​​AT24_WriteBuffer​​一样,它也考虑到芯片内存的页面结构。
 楼主| 欢乐家园 发表于 2023-12-29 00:13 | 显示全部楼层
例程分享
  1. /*源代码*/
  2. #include "AT24.h"

  3. uint8_t AT24_ADDR_W1        = 0XA0;
  4. uint8_t AT24_ADDR_W2        = 0XA2;
  5. uint8_t AT24_ADDR_R1        = 0xA1;
  6. uint8_t AT24_ADDR_R2        = 0xA3;


  7. /**
  8.   * @brief  AT24C04初始化
  9.   * @param  None
  10.   * @retval None
  11.   */
  12. void AT24_init(void){
  13.        
  14.         myi2c_init();
  15.        
  16. }
  17.        
  18. /**
  19.   * @brief  指定地址写入一个字节数据(0 ---- 255)
  20.   * @param  uint16_t addr        写入数据地址
  21.   * @param        uint8_t data        写入字节
  22.   * @retval 写入成功返回4
  23.   */
  24. uint8_t AT24_write_byte(uint16_t addr, uint8_t data){
  25.         i2c_start();        //发送起始信号
  26.         myi2c_sendbyte(AT24_ADDR_W1);        //发送从机地址
  27.         if(myi2c_recv_ack() == 1){
  28.                 i2c_stop();                //发送停止位
  29.                 printf("AT24寻址未应答\r\n");
  30.                 return 1;
  31.         }
  32.        
  33.         myi2c_sendbyte(addr);        //发送要写入的地址
  34.         if(myi2c_recv_ack() == 1){
  35.                 i2c_stop();                //发送停止位
  36.                 printf("AT24内部寻址未应答\r\n");
  37.                 return 2;
  38.         }
  39.        
  40.         myi2c_sendbyte(data);        //发送要写入的数据
  41.         if(myi2c_recv_ack() == 1){
  42.                 i2c_stop();                //发送停止位
  43.                 printf("AT24写入数据未应答\r\n");
  44.                 return 3;
  45.         }
  46.         i2c_stop();                //发送停止位
  47.         printf("AT24写入数据成功\r\n");
  48.         return  4;
  49. }


  50. /**
  51.   * @brief  指定地址读出一个字节数据(0 ---- 255)
  52.   * @param  uint16_t addr        读数据地址
  53.   * @retval 成功返回读出数据
  54.   */
  55. uint8_t AT24_read_byte(uint16_t addr){
  56.         uint8_t read_data = 0;
  57.         i2c_start();        //发送起始信号
  58.         myi2c_sendbyte(AT24_ADDR_W1);        //发送从机地址
  59.         if(myi2c_recv_ack() == 1){
  60.                 i2c_stop();                //发送停止位
  61.                 printf("AT24寻址未应答\r\n");
  62.                 return 1;
  63.         }
  64.        
  65.         myi2c_sendbyte(addr);        //发送要写入的地址
  66.         if(myi2c_recv_ack() == 1){
  67.                 i2c_stop();                //发送停止位
  68.                 printf("AT24内部寻址未应答\r\n");
  69.                 return 2;
  70.         }
  71.         i2c_stop();                //发送停止位
  72.         i2c_start();        //发送起始信号
  73.         myi2c_sendbyte(AT24_ADDR_R1);        //发送从机地址
  74.         if(myi2c_recv_ack() == 1){
  75.                 i2c_stop();                //发送停止位
  76.                 printf("AT24寻址未应答\r\n");
  77.                 return 1;
  78.         }
  79.         read_data = myi2c_recv_byte();
  80.         myi2c_send_ack(1);
  81.         i2c_stop();                //发送停止位
  82.         return read_data;
  83. }


  84. /**
  85.   * @brief  指定地址页写入数据(0 ---- 255)
  86.   * @param  uint16_t addr        写入数据地址
  87.   * @param        uint8_t data        写入字节首地址
  88.   * @param        uint8_t num 写入字节个数
  89.   * @retval 写入成功返回4
  90.   */
  91. uint8_t AT24_write_page(uint16_t addr, uint8_t num, uint8_t *data){
  92.         i2c_start();        //发送起始信号
  93.         myi2c_sendbyte(AT24_ADDR_W1);        //发送从机地址
  94.         if(myi2c_recv_ack() == 1){
  95.                 i2c_stop();                //发送停止位
  96.                 printf("AT24寻址未应答\r\n");
  97.                 return 1;
  98.         }
  99.        
  100.         myi2c_sendbyte(addr);        //发送要写入的地址
  101.         if(myi2c_recv_ack() == 1){
  102.                 i2c_stop();                //发送停止位
  103.                 printf("AT24内部寻址未应答\r\n");
  104.                 return 2;
  105.         }
  106.         while(num--){
  107.                 myi2c_sendbyte(*data);        //发送要写入的数据
  108.                 if(myi2c_recv_ack() == 1){
  109.                         i2c_stop();                //发送停止位
  110.                         printf("AT24写入数据未应答\r\n");
  111.                         return 3;
  112.                 }
  113.                 data++;
  114.         }
  115.        
  116.         i2c_stop();                //发送停止位
  117.         printf("AT24写入数据成功\r\n");
  118.         return  4;
  119. }


  120. /**
  121.   * @brief  随机写
  122.   * @param  uint8_t *pBuffer        写入数据的首地址
  123.   * @param  uint32_t WriteAddr        写入地址
  124.   * @param  uint16_t NumByteToWrite        数据长度
  125.   * @retval None
  126.   */
  127. void AT24_WriteBuffer(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite){
  128.         uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
  129.         Addr = WriteAddr % 16;        //判断地址是否为整页
  130.         count = 16 - Addr;                        //当前页剩余字节数
  131.         NumOfPage =  NumByteToWrite / 16;        //需要的整页数
  132.         NumOfSingle = NumByteToWrite % 16;        //除整页剩余的字节数
  133.        
  134.         if (Addr == 0) /*整页开始  */
  135.   {
  136.     if (NumOfPage == 0) /*所写数据不够一整页,直接调用页编程函数 */
  137.     {
  138.       AT24_write_page(WriteAddr, NumByteToWrite, pBuffer);
  139.     }
  140.     else /*所写数据超过一页*/
  141.     {
  142.       while (NumOfPage--)        //整页写
  143.       {
  144.         AT24_write_page(WriteAddr, 16, pBuffer);
  145.         WriteAddr +=  16;
  146.         pBuffer += 16;
  147.       }

  148.       AT24_write_page(WriteAddr, NumOfSingle, pBuffer);        //除整页之外剩余的
  149.     }
  150.   }
  151.   else /*不是整页开始写  */
  152.   {
  153.     if (NumOfPage == 0) /*所写不到一页 */
  154.     {
  155.       if (NumOfSingle > count) /*所需空间大于当前页所剩空间*/
  156.       {
  157.         temp = NumOfSingle - count;        //当前页写完之后剩余量

  158.         AT24_write_page(WriteAddr, count, pBuffer);        //在当前页写,写满
  159.         WriteAddr +=  count;
  160.         pBuffer += count;

  161.         AT24_write_page(WriteAddr, temp, pBuffer);        //剩余写入下一页
  162.       }
  163.       else
  164.       {
  165.         AT24_write_page(WriteAddr, NumByteToWrite, pBuffer);        //直接写当前页
  166.       }
  167.     }
  168.     else /*写入数据量大于一页 */
  169.     {
  170.       NumByteToWrite -= count;        //写满当前页所剩数据
  171.       NumOfPage =  NumByteToWrite / 16;        //要写入的整页
  172.       NumOfSingle = NumByteToWrite % 16;        //写完整页剩余的字节

  173.       AT24_write_page(WriteAddr, count, pBuffer);//把当前页写满
  174.       WriteAddr +=  count;
  175.       pBuffer += count;

  176.       while (NumOfPage--)        //写整页
  177.       {
  178.         AT24_write_page(WriteAddr, 16, pBuffer);
  179.         WriteAddr +=  16;
  180.         pBuffer += 16;
  181.       }

  182.       if (NumOfSingle != 0)        //写剩余不满一页的字节
  183.       {
  184.         AT24_write_page(WriteAddr, NumOfSingle, pBuffer);
  185.       }
  186.     }
  187.   }
  188. }

  189. /**
  190.   * @brief  随便读
  191.   * @param  None
  192.   * @retval None
  193.   */
  194. uint8_t AT24_readBuffer(uint16_t addr, uint16_t num, uint8_t *recvdata){
  195.         i2c_start();        //发送起始信号
  196.         myi2c_sendbyte(AT24_ADDR_W1);        //发送从机地址
  197.                 Delay_us(10);
  198.         if(myi2c_recv_ack() == 1){
  199.                 i2c_stop();                //发送停止位
  200.                 printf("AT24器件寻址未应答\r\n");
  201.                 return 1;
  202.         }

  203.         myi2c_sendbyte(addr);        //发送要写入的地址
  204.         if(myi2c_recv_ack() == 1){
  205.                 i2c_stop();                //发送停止位
  206.                 printf("AT24内部寻址未应答\r\n");
  207.                 return 2;
  208.         }
  209.         i2c_stop();                //发送停止位
  210.         i2c_start();        //发送起始信号
  211.         myi2c_sendbyte(AT24_ADDR_R1);        //发送从机地址
  212.         if(myi2c_recv_ack() == 1){
  213.                 i2c_stop();                //发送停止位
  214.                 printf("AT24器件2寻址未应答\r\n");
  215.                 return 1;
  216.         }
  217.         while(num--){
  218.                 *recvdata = myi2c_recv_byte();
  219.                 myi2c_send_ack(0);
  220.                 recvdata++;
  221.                 Delay_us(5);
  222.         }
  223.         myi2c_send_ack(1);
  224.         i2c_stop();                //发送停止位
  225.         return num;
  226. }
 楼主| 欢乐家园 发表于 2023-12-29 00:13 | 显示全部楼层
/*头文件*/
#ifndef __AT24_H_
#define __AT24_H_

#include "stm32f4xx.h"                  // Device header
#include "myi2c.h"
#include "usart.h"
#include "delay.h"

void AT24_init(void);
uint8_t AT24_write_byte(uint16_t addr, uint8_t data);
uint8_t AT24_read_byte(uint16_t addr);
uint8_t AT24_write_page(uint16_t addr, uint8_t num, uint8_t *data);
void AT24_WriteBuffer(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
uint8_t AT24_readBuffer(uint16_t addr, uint16_t num, uint8_t *recvdata);
#endif
 楼主| 欢乐家园 发表于 2023-12-29 00:13 | 显示全部楼层
STM32的I²C外设
STM32内部集成了硬件I²C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据发送等功能,减轻CPU的负担。STM32的I²C外设支持多主机模式、7位或10位地址模式、不同的通信速度(标准速度高达100KHZ,快速400KHZ)、DMA,以及兼容SMBus协议。

硬件自动执行:STM32内部的硬件I2C模块可以自动执行时钟生成、起始终止条件生成、应答位收发、数据发送等功能,减轻了CPU的负担,使通信更高效。
多主机模式:STM32的硬件I2C模块支持多主机模式,可以实现多个主机设备在同一总线上进行通信。
7位或10位地址模式:STM32的硬件I2C模块支持7位或10位地址模式,可以适应不同设备的寻址需求。
不同通信速度:STM32的硬件I2C模块支持不同的通信速度,标准速度可达100KHz,快速模式可达400KHz,可以根据具体需求选择合适的通信速率。
支持DMA:STM32的硬件I2C模块支持DMA(直接内存访问)功能,可以通过DMA传输数据,提高数据传输效率,减少CPU的负载。
兼容SMBus协议:STM32的硬件I2C模块与SMBus(系统管理总线)协议兼容,SMBus是一种基于I2C的通信协议,用于管理和控制电子设备。
 楼主| 欢乐家园 发表于 2023-12-29 00:13 | 显示全部楼层
 楼主| 欢乐家园 发表于 2023-12-29 00:13 | 显示全部楼层
 楼主| 欢乐家园 发表于 2023-12-29 00:13 | 显示全部楼层
这些特点使得STM32的硬件I2C模块成为在嵌入式系统中实现I2C通信的理想选择,提供了方便、高效和可靠的通信功能。
sfd123 发表于 2023-12-29 08:29 | 显示全部楼层
谢谢分享!
您需要登录后才可以回帖 登录 | 注册

本版积分规则

114

主题

1067

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部