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

原理了解

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实验就完成了,也希望大家可以指出有不对地方共同学习,谢谢!

  • 8
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
以下是基于STM32F103的IIC控制三个AT24C02的程序设计,其中每个AT24C02都有一个唯一的IIC地址。 ```c #include "stm32f10x.h" #include "stm32f10x_i2c.h" #define I2C1_SLAVE_ADDRESS_1 0xA0 #define I2C1_SLAVE_ADDRESS_2 0xA2 #define I2C1_SLAVE_ADDRESS_3 0xA4 void I2C1_Init(void); int main(void) { uint8_t data_write[2] = {0x00, 0x55}; uint8_t data_read[2]; I2C1_Init(); // 写入第一个AT24C02 I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, I2C1_SLAVE_ADDRESS_1, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, data_write[0]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, data_write[1]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); // 读取第二个AT24C02 I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, I2C1_SLAVE_ADDRESS_2, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, data_write[0]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, I2C1_SLAVE_ADDRESS_2, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); data_read[0] = I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); // 写入第三个AT24C02 I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, I2C1_SLAVE_ADDRESS_3, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, data_write[0]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, data_write[1]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); while (1); } void I2C1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress1 = 0x00; I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed = 400000; I2C_Init(I2C1, &I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); } ``` 注意,这里的程序是演示如何写入读取数据,实际应用中需要根据具体的需求进行修改。同时,需要在AT24C02上设置唯一的IIC地址,以便STM32F103可以正确地控制它们。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

葛叶灬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值