STM32软件模拟实现IIC写入和读取AT24C02(附STM32CubeMx配置步骤)

模拟IIC

  • 原理了解
  • IIC总线协议
  • AT24C02器件
  • STM32CubeMx配置
  • 工程生成及代码编写
  • 工程生成
  • 代码编写
  • 延时函数
  • delay.c编写
  • delay.h编写
  • IIC函数实现
  • IIC起始信号
  • IIC停止信号
  • 应答信号
  • 数据的发送及读取
  • iic函数头文件
  • AT24C02的写/读函数
  • AT24C02写函数
  • AT24C02读函数
  • main函数编写
  • 实现效果
  • 原理了解

    IIC总线协议

    IIC:Inter Integrated Circuit,集成电路总线,是一种同步 串行 半双工通信总线。
    在使用IIC时分为硬件IIC以及软件IIC,下图为两者的区别:

    在使用IIC前先来了解一下IIC总线结构图,即下图:

    从图中可以看出IIC有两个双向信号线,一根是数据线SDA,一根是时钟线SCL,并且都接上拉电阻,保证总线空闲状态为高电平;同时上面可以挂在多个设备,允许多主机存在,每个设备都有一个唯一的地址。
    在使用IIC过程中可以归纳为以下几个比较重要的部分:
    三个信号:起始信号、停止信号、应答信号
    两个注意:数据有效性、数据传输顺序
    一个状态:空闲状态
    后续会将这几个部分拆开来看并使用代码进行实现

    AT24C02器件

    EEPROM是一种掉电后数据不丢失的储存器,常用来存储一些配置信息,在系统重新上电时就可以加载,而AT24C02是一个2K bit的EEPROM存储器,使用IIC通信方式。所以在本实验中将AT24C02作为我们的EEPROM。首先我们先来了解一下如何对AT24C02进行写入和读取,AT24C02的通讯地址具有8位,如下图:
    前四位是固定死的为1010,后面的A0-A2三个位则为可编程部分,这七个位则构成了我们的设备地址,而最后一位为方向位,当你置1时则为读数据,置0时则为写入数据,那么八位构成我们的通讯地址,此时将A0-A2置0,最后一位也置0,则生成八位数字10100000即0xA0,这个就是我们的写操作地址,同理读操作地址则为0xA1。
    完成这两个之后,根据硬件手册的写时序及读时序设置我们的函数就可以实现对EEPROM的操作啦,后续将会结合代码讲解。

    STM32CubeMx配置

    选好自己的板子型号,根据硬件原理图:

    选用PB6以及PB7两个IO口进行软件模拟IIC,配置如下:

    PB6设置为推挽输出,用作SCL线,方便输出高低电平对时钟线进行控制,PB7设置为开漏输出,用作SDA线,这里将PB7设置为开漏输出是因为SDA线既要用作输出,也要用作输入(从机应答信号),使用开漏模式则可以解决这个问题,剩下的PE3/4/5则是按键和LED方便验证程序,可根据自身需要进行设置。使用串口通信验证写入和读取,接下来就可以生成代码了。

    工程生成及代码编写

    工程生成

    打开工程后可以看到左边已经生成了我们需要的代码:

    打开gpio.c文件可以看到里面已经初始化好我们刚刚设置IO口:

    代码编写

    延时函数

    在操作IIC的过程中需要对时间进行一定的掌控,包括高电平的稳定,电平跳变的产生,等待EEPROM写入数据,所以第一步我们先实现延时函数方便控制。

    delay.c编写
    #include "delay.h"
    
    static uint16_t  g_fac_us = 0;      /* us延时倍乘数 */
    
    /**
     * @brief       初始化延迟函数
     * @param       sysclk: 系统时钟频率, 即CPU频率(HCLK)
     * @retval      无
     */
    void delay_init(uint16_t sysclk)
    {
        SysTick->CTRL = 0;                                          /* 清Systick状态,以便下一步重设,如果这里开了中断会关闭其中断 */
        HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK_DIV8);   /* SYSTICK使用内核时钟源8分频,因systick的计数器最大值只有2^24 */
        g_fac_us = sysclk / 8;                                      /* g_fac_us作为1us的基础时基 */
    }
    
    /**
     * @brief       延时nus
     * @param       nus: 要延时的us数.
     * @note        注意: nus的值,不要大于1864135us(最大值即2^24 / g_fac_us @g_fac_us = 9)
     * @retval      无
     */
    void delay_us(uint32_t nus)
    {
        uint32_t temp;
        SysTick->LOAD = nus * g_fac_us; /* 时间加载 */
        SysTick->VAL = 0x00;            /* 清空计数器 */
        SysTick->CTRL |= 1 << 0 ;       /* 开始倒数 */
    
        do
        {
            temp = SysTick->CTRL;
        } while ((temp & 0x01) && !(temp & (1 << 16))); /* CTRL.ENABLE位必须为1, 并等待时间到达 */
    
        SysTick->CTRL &= ~(1 << 0) ;    /* 关闭SYSTICK */
        SysTick->VAL = 0X00;            /* 清空计数器 */
    }
    
    /**
     * @brief       延时nms
     * @param       nms: 要延时的ms数 (0< nms <= 65535)
     * @retval      无
     */
    void delay_ms(uint16_t nms)
    {
        uint32_t repeat = nms / 1000;   /* 记录超出1000ms的值,即1s */
        uint32_t remain = nms % 1000;   /* 记录未超出1000ms的值 */
    
        while (repeat)
        {
            delay_us(1000 * 1000);      /* 利用delay_us 实现 1000ms 延时 */
            repeat--;
        }
    
        if (remain)
        {
            delay_us(remain * 1000);    /* 利用delay_us, 把尾数延时(remain ms)给做了 */
        }
    }
    
    
    delay.h编写
    #ifndef __DELAY_H
    #define __DELAY_H
    
    #include "main.h"
    
    void delay_init(uint16_t sysclk);
    void delay_us(uint32_t nus);
    void delay_ms(uint16_t nms);
    
    #endif
    
    

    IIC函数实现

    完成了延时函数,首先再定义一个IIC中独立使用到的delay函数:

    /* iic delay函数 */
    static void iic_delay(void)
    {
        delay_us(2);
    }
    

    接下来根据上面所讲的三个信号来看看对应的时序是怎么完成的。

    IIC起始信号

    首先是起始信号:

    从图中可以看出,起始信号在代码中要做的就三件事,1.保持SCL高电平;2.SDA产生一个下降沿;3.将SCL拉低,代码如下:

    /* iic起始信号 */
    void iic_start(void)
    {
        /* 保持时钟线高电平,数据线产生下降沿 */
        IIC_SDA(1);
        IIC_SCL(1);
        iic_delay();
        IIC_SDA(0);
        iic_delay();
    
        /* 拉低时钟线,准备发送/接收数据 */
        IIC_SCL(0);
        iic_delay();
    }
    
    IIC停止信号

    再来看看停止信号:

    从图中可以看出,停止信号操作跟起始信号操作大致一样,只不过SDA的下降沿改为了上升沿,代码如下:

    /* iic停止信号 */
    void iic_stop(void)
    {
        /* 保持时钟线高电平,数据线产生上升沿 */
        IIC_SDA(0);
        IIC_SCL(1);
        iic_delay();
        IIC_SDA(1);
        iic_delay();
    }
    
    应答信号

    最后再来看看应答信号:

    从图中可以看出,在保持SCL高电平的状态下,通过读取SDA线的电平状态来判断从机是否应答,由于SDA默认是为高信号,所以通过从机操作SDA线,将SDA线拉低则视为应答,而不动则视为非应答。实现代码如下:

    /* 等待应答信号 */
    /* return 0:fail 1:succeed*/
    uint8_t iic_wait_ack (void)
    {
        IIC_SDA(1);    /* 主机释放SDA线 */
        iic_delay();
        IIC_SCL(1);    /* 拉高SCL等待读取从机应答信号 */
        iic_delay();
        if (IIC_READ_SDA) /* SCL高电平读取SDA状态 */
        {
            iic_stop();     /* SDA高电平表示从机非应答 */
            return 0;
        }
        IIC_SCL(0);         /* SCL低电平表示结束应答检查 */
        iic_delay();
        return 1;
    }
    

    同样主机在读取从机发送数据时,也要通知从机是应答还是非应答来选择是否继续接受数据,实现函数如下:

    /* 应答信号 */
    void iic_ack(void)
    {
        IIC_SCL(0);
        iic_delay();
        IIC_SDA (0);  /* 数据线为低电平,表示应答 */
        iic_delay();
        IIC_SCL(1);
        iic_delay();
    }
    
    /* 非应答信号 */
    void iic_nack(void)
    {
        IIC_SCL(0);
        iic_delay();
        IIC_SDA(1);  /* 数据线为高电平,表示非应答 */
        iic_delay();
        IIC_SCL(1);
        iic_delay();
    }
    

    到这里,三个信号我们就解决了。

    数据的发送及读取

    接下来看一下两个注意,即实现IIC发送函数及读取函数,首先根据前文所说IIC的特性之一是串行,即数据的发送是一位一位进行发送,在这里选择数据的发送顺序为高位在先,实现代码如下:

    /* 发送一个字节数据 */
    void iic_send_byte(uint8_t data)
    {
        for (uint8_t t=0; t<8; t++)
        {
            /* 高位先发 */
            IIC_SDA((data & 0x80) >> 7);
            iic_delay();
            /* 拉高时钟线,稳定数据接收 */
            IIC_SCL (1);
            iic_delay();
            
            IIC_SCL (0);
            data <<= 1;     /* 左移1位, 用于下一次发送 */
        }
        IIC_SDA (1);      /* 发送完成,主机释放SDA线 */ 
    }
    
    /* 读取1字节数据 */
    /* ack:通知从机是否继续发送。
       0,停止发送;1,继续发送
    */
    uint8_t iic_read_byte(uint8_t ack)
    { 
        uint8_t receive = 0 ;
        for (uint8_t t=0; t<8; t++)
        {
            /* 高位先输出,先收到的数据位要左移 */ 
            receive <<= 1;
            IIC_SCL(1);
            iic_delay();
            if (IIC_READ_SDA) 
            {
                receive++;
            }
            IIC_SCL(0);
            iic_delay();
        }
        /* 判断是否继续读取从机数据 */
        if ( !ack ) 
        {
            iic_nack();
        }
        else 
        {
            iic_ack();
        }
        return receive;
    }
    

    还有一个状态需要注意的是,空闲状态为高电平,所以在我们操作完SCL以及SDA两条线之后都需要将其置为高电平,代码中已实现。

    iic函数头文件

    #ifndef __MYIIC_H
    #define __MYIIC_H
    
    #include "main.h"
    #include "delay.h"
    
    /* 引脚定义 */
    #define IIC_SCL_GPIO_PORT GPIOB
    #define SCL GPIO_PIN_6
    
    #define IIC_SDA_GPIO_PORT GPIOB
    #define SDA GPIO_PIN_7
    
    /* SCL引脚设置宏定义 */
    #define IIC_SCL(x) do{x ? \
                          HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, SCL, GPIO_PIN_SET) : \
                          HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, SCL, GPIO_PIN_RESET); \
                         }while(0)
    
    /* SDA引脚设置宏定义 */
    #define IIC_SDA(x) do{x ? \
                          HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, SDA, GPIO_PIN_SET) : \
                          HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, SDA, GPIO_PIN_RESET); \
                         }while(0)
    
    #define IIC_READ_SDA HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, GPIO_PIN_7)
    
    
    void at24c02_write_one_byte(uint8_t addr, uint8_t data);
    uint8_t at24c02_read_one_byte(uint8_t addr);
    
    #endif
    
    

    这样我们的IIC函数就大功告成啦,但是我们只是完成了IIC的基础功能,接下来则是添加写入/读取AT24C02函数啦,即头文件中的:

    void at24c02_write_one_byte(uint8_t addr, uint8_t data);
    uint8_t at24c02_read_one_byte(uint8_t addr);
    

    AT24C02的写/读函数

    AT24C02写函数

    先来看看AT24C02支持的写入操作有哪些:

    可以看出,若使用页写模式可能会因为操作不当导致原先的数据被覆盖,所以这里选择字节写模式。
    选定好字节写模式后,接来就是了解字节写的时序并进行代码编写:

    从图中我们就可以清晰的看出我们需要操作的步骤,结合IIC实现的函数即可实现对AT24C02的写操作,代码如下:

    /* 写入一个字节到从机AT24C02中 */
    void at24c02_write_one_byte(uint8_t addr, uint8_t data)
    {
        /* 1、发送起始信号 */
        iic_start();
        
        /* 2、发送通讯地址(写操作地址) */
        iic_send_byte(0xA0);
        
        /* 3、等待应答信号 */
        iic_wait_ack();
        
        /* 4、发送内存地址:0~255 */
        iic_send_byte(addr);
        
        /* 5、等待应答信号 */
        iic_wait_ack();
        
        /* 6、发送写入数据 */
        iic_send_byte(data);
        
        /* 7、等待应答信号 */
        iic_wait_ack();
        
        /* 8、发送停止信号 */
        iic_stop();
        
        /* 等待EEPROM写入完成 */
        delay_ms(10);
    }
    

    在这里需要注意,AT24C02的容量为256byte,所以在传输地址参数时范围应当选择0~255,以及EEPROM的写入时间在器件手册中有标明最长是需要5ms时间,所以这里延时10ms确保其写入完成。

    AT24C02读函数

    接下来来看看读函数的实现,首先了解一下支持的读取操作

    这里从图中可以看出随机地址读模式是比较自由的,因为可以读取指定地址的数据,所以这里选定随机地址读模式。
    接下来同样先查看对应的时序图:

    可以看出读时序相比较写时序则多了很多步骤,但是实质上也就是将先前编写好的IIC函数按照时序一个一个放进去就好了,代码如下:

    /* 往从机AT24C02中读取一个字节 */
    uint8_t at24c02_read_one_byte(uint8_t addr)
    {
        uint8_t read = 0;
        
        /* 1、发送起始信号 */
        iic_start();
        
        /* 2、发送通讯地址(写操作地址) */
        iic_send_byte(0xA0);
        
        /* 3、等待应答信号 */
        iic_wait_ack();
        
        /* 4、发送内存地址:0~255 */
        iic_send_byte(addr);
        
        /* 5、等待应答信号 */
        iic_wait_ack();
        
        /* 6、发送起始信号 */
        iic_start();
        
        /* 7、发送通讯地址(读操作地址) */
        iic_send_byte(0xA1);
        
        /* 8、等待应答信号 */
        iic_wait_ack();
        
        /* 9、接收数据并发送非应答(获取该地址即可) */
        read = iic_read_byte(0);
        
        /* 10、发送停止信号 */
        iic_stop();
        
        return read;
    }
    

    至此AT24C02的写入和读取函数就完成了,接下来就是验证程序是否能实现所需的功能了。

    main函数编写

    /* USER CODE BEGIN Includes */
    #include "key.h"
    #include "myiic.h"
    #include "delay.h"
    /* USER CODE END Includes */
    

    首先将需要的头文件包含进去,接下来就是功能验证:

    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART1_UART_Init();
      /* USER CODE BEGIN 2 */
        delay_init(72);
        uint8_t t = 0;
        uint8_t key = 0;
        uint8_t data = 0;
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
          key = key_scan();
          if(key == KEY0_PRESS)
          {
            at24c02_write_one_byte(0, 100);
            printf("write success\r\n");
          }
          
          if(key == KEY1_PRESS)
          {
            data = at24c02_read_one_byte(0);
            printf("data:%d\r\n",data);
          }
          
          /* LED闪烁证明程序正常运行 */
          t++;
          if(t == 20)
          {
              t = 0;
              HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_5);
          }
          delay_ms(10);
      }
      /* USER CODE END 3 */
    }
    

    这里通过板子上的两个按键,一个进行写入,一个进行读取,通过串口打印到电脑上观看数据是否成功写入,以及LED灯闪烁证明程序正常运行。

    实现效果


    可以看到已经能够成功的写入及读取,这里将数据改为50再来看看

    修改为50也成功的写入了,证明代码是没有问题的。
    至此软件模拟IIC实验就完成了,也希望大家可以指出有不对地方共同学习,谢谢!

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32软件模拟实现IIC写入和读取AT24C02(附STM32CubeMx配置步骤)

    发表评论