16-CubeMx+Keil+Proteus仿真STM32 - I2C

本文例子参考《STM32单片机开发实例——基于Proteus虚拟仿真与HAL/LL库》
源代码:https://github.com/LanLinnet/STM32F103R6

项目要求

掌握\(I^2C\)的通讯方法和时序,通过串口发送数据,单片机接收并存入AT24C02首地址中。按下按键BTN,单片机将存放在AT24C02首地址中的数据取出并通过串口发送。串口通信参数:波特率为19200bits/s;无校验。

硬件设计

  1. 第一节的基础上,在Proteus中添加电路如下图所示。其中我们添加了一个I2C通信的外设:EEPROM芯片AT24C02(在Proteus中为FM24C02)。

    此外,还添加了\(I^2C\)总线调试工具I2C DEBUGGER,用于读取\(I^2C\)输入输出的数据。

    串口和按键的相关电路可以参考第13节。COMPIM设置如下图所示。

  2. \(I^2C\)
    1)简介:\(I^2C\)(Inter-Integrated Circuit)总线是由Philips公司提出的一种两线式串行总线。\(I^2C\)总线属于多主总线,每个节点都可以设置唯一的地址,向总线发送数据的设备作为发送器,从总线接收数据的设备作为接收器
    2)\(I^2C\)总线:由时钟信号线SCL和双向数据线SDA组成。
    3)通信时序:\(I^2C\)总线的通信时序分为发送器启动/停止通信、数据位传送、接收器返回响应信号三种。

    • 发送器启动/停止通信:SCL保持高电平期间,SDA产生下降沿,即通信启动信号;SCL保持高电平期间,SDA产生上升沿,即通信停止信号。

    • 数据位传送:数据发送器在启动通信之后,便向\(I^2C\)总线发送数据,发送数据字节长度为1字节,发送顺序高位在前,低位在后,逐位发送。如下图所示,在SCL处于高电平期间,SDA必须保持稳定,SDA低电平代表数据0,高电平代表数据1;只有在SCL处于低电平期间,SDA才能改变电平状态。

    • 接收器返回响应信号:数据发送器每发送1个字节,数据接收器都必须返回1位响应信号,响应信号若为低电平则规定为应答响应位(ACK),表示数据接收器接收该字节数据成功;反之,则称为非应答响应位(NACK),表示数据接收器接收该字节数据失败。

      如果数据接收器是主机,则在它收到最后一字节数据后,返回一个非应答位,通知数据发送器结束数据发送,接着主机向\(I^2C\)总线发送一个停止通信信号,结束通信过程。

  3. AT24C02
    1)简介:AT24Cxx是美国Atmel公司出品的单行\(EEPROM\)系列芯片,xx表示不同的容量。如02表示该芯片的总容量为2kbits(256字节)。
    2)引脚:AT24C02芯片引脚如下图所示,引脚功能如下表所示。


    其中,1-3引脚参与构成AT24C02在\(I^2C\)总线上的地址。如图1K/2K的地址所示,地址高4位固定为1010B,低4位的最低位在总线“写”指令中固定为0,在总线“读”指令中固定为1,其余3位就由1-3引脚决定。

    3)读写时序:AT24C02的读写方式有写入字节、写入页、读当前地址、随机读取和连续读取5种方式,下面我们介绍本项目中使用的两种。

    • 写入字节时序(Byte Write):写入字节即向AT24C02写入1字节,由下面8步组成。
      ①主机发送启动通信(Start)信号
      ②发送器件(芯片)地址(Device Address)
      ③产生应答响应(ACK)
      ④发送字地址(Word Address)
      ⑤产生应答响应(ACK)
      ⑥发送数据(Data)
      ⑦产生应答响应(ACK)
      ⑧发送停止通信(Stop)信号

    • 随机读取时序(Random Read):随机读取即从AT24C02读取1字节,由下面11步组成。
      ①主机发送启动通信(Start)信号
      ②发送器件(芯片)地址(Device Address)
      ③产生应答响应(ACK)
      ④发送字地址(Word Address)
      ⑤产生应答响应(ACK)
      ⑥再次发送启动通信(Start)信号
      ⑦发送器件(芯片)地址(Device Address)
      ⑧产生应答响应(ACK)
      ⑨读取数据(Data)
      ⑩发送非应答响应(No ACK)
      ⑪发送停止通信(Stop)信号

  4. 打开CubeMX,建立工程。设置PB6、PB7为GPIO_Output,PC0为GPIO_Input,点击“Categories”中的“GPIO”的“User Label”设置如下图所示。

    这里要注意,STM32F103R6自带一个\(I^2C\)总线通信模块,但是为了便于移植,我们这里采用GPIO引脚PB6、PB7模拟\(I^2C\)总线的时序。
    随后进行串口设置,如下图所示,这里就不赘述了,具体可以参考第13节

  5. 点击“Generator Code”生成Keil工程。

软件编写

  1. 考虑到代码的可移植性,这里将\(I^2C\)总线时序模拟和AT24C02操作代码分别写入头文件“vI2C.h”“AT24C02.h”中。我们可以先在...\Core\Src文件夹中建立这两个头文件,此时Keil可能找不到对应文件,可以直接将文件拽入Keil中进行编辑,然后再在“main.c”文件中进行include。

  2. 点击“Open Project”在Keil中打开工程,打开“vI2C.h”,添加代码如下。

    //I2C总线时序模拟
    #ifndef VI2C_H_
    #define VI2C_H_
    #include "main.h"
    
    //延时1μs
    void delay_us(uint16_t n)
    {
      uint16_t i = n*8;  //8MHz,周期为1/8μs
      while(i--);
    }
    
    //设置数据线模式: I-输入 O-输出
    void Pin_vSDA_Mode(char status)
    {
      GPIO_InitTypeDef GPIO_InitStruct = {0};
      HAL_GPIO_WritePin(GPIOB, vSDA_Pin, GPIO_PIN_SET);
      GPIO_InitStruct.Pin = vSDA_Pin;
      GPIO_InitStruct.Pull = GPIO_PULLUP;
      if(status == 'I')  //输入
      {
        GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
      }
      else if(status == 'O')  //输出
      {
        GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
      }
      HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
    
    //时钟线输出
    void vSCL_Out(uint8_t dat)
    {
      switch(dat)
      {
        case 0: HAL_GPIO_WritePin(GPIOB, vSCL_Pin, GPIO_PIN_RESET); break;
        default: HAL_GPIO_WritePin(GPIOB, vSCL_Pin, GPIO_PIN_SET); break;
      }
    }
    
    //写数据线
    void vSDA_Out(uint8_t dat)
    {
      switch(dat)
      {
        case 0: HAL_GPIO_WritePin(GPIOB, vSDA_Pin, GPIO_PIN_RESET); break;
        default: HAL_GPIO_WritePin(GPIOB, vSDA_Pin, GPIO_PIN_SET); break;
      }
    }
    
    //读数据线
    uint8_t vSDA_In()
    {
      GPIO_PinState PinState;
      uint8_t rt;
      PinState = HAL_GPIO_ReadPin(GPIOB, vSDA_Pin);
      switch(PinState)
      {
        case GPIO_PIN_RESET: rt = 0; break;
        default: rt = 1; break;
      }
      return rt;
    }
    
    //启动I2C通信
    void I2C_Start()
    {
      Pin_vSDA_Mode('O');
      vSDA_Out(1);
      delay_us(6);  //至少延时4.7μs
      vSCL_Out(1);
      delay_us(6);  //至少延时4.7μs
      vSDA_Out(0);  //下降沿
      delay_us(6);  //至少延时4.7μs
      vSCL_Out(0);
    }
    
    //停止I2C通信
    void I2C_Stop()
    {
      Pin_vSDA_Mode('O');
      vSDA_Out(0);
      delay_us(6);  //至少延时4.7μs
      vSCL_Out(1);
      delay_us(6);  //至少延时4.7μs
      vSDA_Out(1);	//上升沿
      delay_us(6);  //至少延时4.7μs
    }
    
    //发送应答-低电平
    void I2C_Ack()
    {
      Pin_vSDA_Mode('O');
      vSDA_Out(0);
      delay_us(6);  //至少延时4.7μs
      vSCL_Out(1);
      delay_us(6);  //至少延时4.7μs
      vSCL_Out(0);
      delay_us(6);  //至少延时4.7μs
      vSDA_Out(1);
      delay_us(6);  //至少延时4.7μs
    }
    
    //写1字节数据
    void I2C_WtByte(uint8_t Dat)
    {
      uint8_t i, tmp;
      Pin_vSDA_Mode('O');
      for(i = 0; i < 8; i++)
      {
        tmp = Dat & (0x80>>i);  //高位在前,低位在后,逐位发送
        vSCL_Out(0);
        delay_us(6);
        (tmp == 0) ? (vSDA_Out(0)) : (vSDA_Out(1));
        delay_us(6);
        vSCL_Out(1);
        delay_us(6);
      }
      vSCL_Out(0);
      delay_us(6);
      vSDA_Out(1);
      delay_us(6);
    }
    
    //读1字节数据
    uint8_t I2C_RdByte()
    {
      uint8_t Dat = 0, tmp, i;
      Pin_vSDA_Mode('I');
      vSCL_Out(0);
      delay_us(6);
      for(i = 0; i < 8; i++)
      {
        vSCL_Out(1);
        delay_us(6);
        tmp = vSDA_In();
        Dat = Dat << 1;  //读1位左移1位
        Dat = Dat | tmp;
        delay_us(6);
        vSCL_Out(0);
        delay_us(6);
      }
      return Dat;
    }
    #endif /* VI2C_H_ */
    

    打开“AT24C02.h”,添加代码如下。

    //AT24C02操作
    #ifndef AT24C02_H_
    #define AT24C02_H_
    #define AT24C02_ADDR 0xa0
    #include "main.h"
    #include "vI2C.h"
    
    //写入1字节
    void AT24C02_Write(uint8_t DatAddr, uint8_t Dat)
    {
      I2C_Start();		//主机发送启动通信信号
      I2C_WtByte(AT24C02_ADDR + 0);		//发送器件(芯片)地址
      I2C_Ack(); 		//产生应答响应
      I2C_WtByte(DatAddr);  //发送字地址
      I2C_Ack();		//产生应答响应
      I2C_WtByte(Dat);		//发送数据
      I2C_Ack();		//产生应答响应
      I2C_Stop();		//发送停止通信信号
    }
    
    //读取1字节
    uint8_t AT24C02_Read(uint8_t DatAddr)
    {
      uint8_t Dat;
      I2C_Start();		//主机发送启动通信信号
      I2C_WtByte(AT24C02_ADDR + 0);		//发送器件地址
      I2C_Ack();		//产生应答响应
      I2C_WtByte(DatAddr);		//发送字地址
      I2C_Ack();		//产生应答响应
      I2C_Start();		//再次发送启动通信信号
      I2C_WtByte(AT24C02_ADDR + 1);		//发送器件地址
      I2C_Ack();		//产生应答响应
      Dat = I2C_RdByte();		//读取数据
      I2C_Stop();		//产生非应答信号,发送停止通信信号
      return Dat;
    }
    #endif /* AT24C02_H_ */
    
  3. 随后我们需要在main.c文件中的最前面引入我们自定义的头文件

    /* USER CODE BEGIN Includes */
    #include "vI2C.h"		//引用I2C总线时序模拟头文件
    #include "AT24C02.h"		//引用AT24C02操作头文件
    /* USER CODE END Includes */
    

    在main函数中定义一些全局变量

    /* USER CODE BEGIN PV */
    uint8_t RcvDat[1];		//存放接收数据数组
    uint8_t SndDat[1];		//存放发送数据数组
    uint8_t rf = 0;		//接收完成标志位
    /* USER CODE END PV */
    

    进行串口相关操作

    /* USER CODE BEGIN 2 */
    HAL_UART_Receive_IT(&huart1, RcvDat, 1);		//串口1接收中断
    /* USER CODE END 2 */
    
    /* USER CODE BEGIN WHILE */
    while (1)
    {
      if(rf==1)  //若接收完成
      {
        rf = 0;  //清0标志位
        AT24C02_Write(0, RcvDat[0]);		//写入1字节
        HAL_UART_Receive_IT(&huart1, RcvDat, 1);		//每次接收前需调用一次
      }
      else if(HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET)		//若按下按键
      {
        SndDat[0] = AT24C02_Read(0);		//读1字节数据,并存入数组
        HAL_UART_Transmit(&huart1, SndDat, 1, 0xffff);		//串口1发送1字节,超时65535ms
        while(HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin) == GPIO_PIN_RESET);		//直到按键松开
      }
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    
    /* USER CODE BEGIN 4 */
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)		//串口接收完毕回调函数
    {
      if(huart == &huart1)
      {
        rf = 1;		//若接收完成,则标志位置1
      }
    }
    /* USER CODE END 4 */
    

联合调试

  1. 点击运行,生成HEX文件。

  2. 在Proteus中加载相应HEX文件,点击运行。

  3. 打开串口调试助手“XCOM”,选择COM4,设置相应的波特率、停止位、数据位、奇偶校验等,勾选“16进制显示”和“16进制发送”,点击“打开串口”。在发送框输入“CD”,点击“发送”。在Proteus中我们可以看到“I2C Debug”接收到数据“CD”。按下按键,同时再观察串口调试助手“XCOM”,可以看到接收窗口收到数据“CD”。

posted @ 2022-05-27 10:11  Sheepeach  阅读(3332)  评论(0编辑  收藏  举报