1.EEPROM
EEPROM是一种电可擦除和重新编程、数据不丢失、可按字节修改的非易失性存储器,非常适合存储那些需要频繁小量更新但又要长期保存的配置或数据。
虽然 Flash 存储器( 是 EEPROM 技术的⼀种衍⽣ )在⼤容量、低成本存储上占据主导,但传统
EEPROM 在需要精确字节级修改、中等容量、⾼可靠性的场景中仍有不可替代的优势。
如下图就是我使用的AT24C02 EEPROM模块和管脚信号图
2.AT24C02
AT24C02是Atmel公司出品的I2C两线串行EEPROM,嵌入式入门最经典的掉电存储芯片,广泛应用与单片机存储少量配置参数
2.2 核心参数
- 容量:2Kbit = 256Byte,地址范围0x00 ~ 0xFF
- 存储组织:32页,没页8个字节(类似于一个二维表)
- 通信接口:I2C,支持100KHz标准模式、400KHz快速模式
2.3 引脚定义
| 引脚 | 名称 | 作用 |
|---|---|---|
| 1 | A0 | 设备地址引脚 |
| 2 | A1 | 设备地址引脚 |
| 3 | A2 | 设备地址引脚 |
| 4 | GND | 电源地 |
| 5 | SDA | I2C 双向数据线,必须外部上拉 |
| 6 | SCL | I2C 时钟线,必须外部上拉 |
| 7 | WP | 写保护:接 GND 允许读写;接 VCC 只读,禁止写入 |
| 8 | VCC | 电源 |
3.AT24C02 读写介绍
3.1AT24C02 读写特点
AT24C02支持两种读写:随机读写、顺序读写。
- 随机读取:可以指定任意地址读取单个字节
- 顺序读取:读取一个字节后,AT24C02芯片的内部地址指针自动递增,可以连续读取后续字节
关于AT24C02地址:
- 把AT24C02想象成uint8_t array[256] 的数组,所谓的地址,就是数组下标
- 也可以当作uint8_t array[32][8]的二维数组
写入规则:
- 字节写入:可以写入单个字节到任意地址
- 页写入:AT24C02内部把8个字节叫做一页,所以AT24C02一共有:256字节/8 = 32个页。24C02芯片允许一次性最多写入一页。写⼊的起始地址必须是⻚的起始地址,因为有个bug就是在该芯片中,写入的数据超过一页的末尾地址,那么会覆盖前面的数据进行写入
3.2 AT24C02 写保护
- 芯片有一个WP(Write Protect)引脚
- 当WP引脚连接到GND时,允许正常的读、写操作。
- 当WP引脚连接到VCC时,整个存储器被写保护,禁止写入操作,但可以读取。
3.3 AT24C02自身设备地址
- AT24C02 芯片三个地址引脚A2、A1、A0.这些引脚可以链接到VCC或GND来设置该芯片在I2C总线上的低3位地址。
4.I2C
4.1.I2C的配置
如图所示,时I2C2接口配置
主要配置信息:
- I2C2为快速模式(速率400KHz)
- 7-bit地址模式(需要与从机保持一致,AT24Cxx芯片是7位地址)
- 时钟占空比为Tlow/Thigh = 2
- SCL和SDA复用的GPIO针脚是PB10,PB11,而且默认是开漏
4.1.1 问题一:I2C配置为快速模式这个是为什么?
该问题的衍生问题是我该如何选择I2C的通信速度。
- 首先,你的MCU内部的I2C接口本身是支持两种模式的(快速和标准)
- 速度的选择主要是取决于充当Slave的AT24C02的速度要求
我们如何选择I2C的通信速度呢?
- I2C属于通信,在可靠情况下,越快越好
- 参与通信的设备,大家支持的速度频率不同,就要去所有参与通信的I2C设备所支持的速度中大家都支持的,最快的方式
- 在可靠的前提下,取所有设备支持速度的最小的交集(最小值),选其中最快的那个。
4.1.2 问题二:I2C配置中,占空比是什么?
什么是占空比?
- 是指在一个完整的SCL时钟周期内,SCL信号处于低电平的时间与高电平的时间之间的额分配的时间比例
- 也就是,在⼀个周期内,占空⽐ = 低电平时⻓ / ⾼电平时⻓的⽐重
注意:
- 传统意义上,占空⽐是: (有效电平时间 / 总周期时间) × 100%
- 不过在 I2C 上,我们不采⽤这样的理解,⽽采⽤“占空⽐ = 低电平时⻓ / ⾼电平时⻓的
⽐重”,⼀⽅⾯适配后续在 STM32CubeMX 上配置,⼀⽅⾯我们还要对“有效电平“中
的“有效“进⾏解释,太⿇烦。
占空比只有SCL要考虑,SDA不考虑,因为占空比前提是需要输出方要有稳定比率的输出方波。
为什么要有占空⽐?- 为什么
- 占空⽐是⾼低电平⽠分周期时⻓的⼀种分配⽅案
- 占空⽐设置成特定⽐率的⽬标是:让⾼低电平要满⾜ I2C 的电⽓特性要求
5.项目的建立
主要是模块化,我将模块分为5层,第一层就是我们的硬件(MCU,Flash,I2C....);第二层就是Hal库,工程已经有的;第三层就是板级驱动(LED,Usart,AT24C02...),也就是驱动程序,对硬件完成读写;第四层就是控制逻辑,如何在MCU当中执行我们的代码,去读取EEPROM里的数据,初始化相关内容,完成电点内容;第五层就是调用逻辑,main函数调用控制逻辑,告诉我们代码怎么写,控制逻辑再调用扳级驱动,完成基于HAL库对所对应硬件进行读写的逻辑,本文章主要是对上面三层进行配置
5.1 控制逻辑代码
APP.h的代码
#ifndef __APP_H #define __APP_H #ifdef __cplusplus extern "C" { #endif #include<stdint.h> #include<stdio.h> #include "BSP_LED.h" #include "BSP_Usart_Redir.h" #include "BSP_AT24C02.h" #define APP_MAGIC 0x35 #define APP_VERSION 0x01 //8字节的配置结构体类型 typedef struct _config_t { uint8_t magic; //魔数,保证配置有效 uint8_t version; //版本 uint8_t led_id; //哪一颗led uint8_t reapet_cnt; //闪几次,重复次数 uint16_t delay_ms; //闪灯的频率,ms uint16_t start_time; //开始闪灯时间,ms }config_t; void App_Init(void); void App_Run(void); #ifdef __cplusplus } #endif #endif我的头文件是这样定义的,这一段是头文件保护宏,是C语言头文件最基础的语法。作用就是:防止同一个头文件被重复包含
1.#ifdef __APP_H
#ifndef = if not defined,翻译:如果__APP_H这个宏没有被定义
2.#define __APP_H
如果上面条件成立(没有定义过),那就是定义宏 __APP_H.
3.直到文件末尾#endif结束这个判断。
#ifndef __APP_H #define __APP_H // 头文件里面所有内容 #endif举个例子:
main.c里面#include "app.h"
bsp.c里面也#include "app.h"那么预处理过程里第一次引入头文件,会展开里面的内容,但是第二次就不会展开了,因为上一步已经定义好了~
然后后面的#ifdef __cplusplus和extern "C"
#ifdef __cplusplus:是 C++ 编译器内置自动定义的宏,C 编译器不会定义这个宏。
extern "C":告诉 C++ 编译器,里面这些函数按照 C 语言规则编译,不要用 C++ 的名字修饰
#ifdef __cplusplus extern "C" { #endif // 函数声明、结构体... #ifdef __cplusplus } #endifAPP.c的代码
#include"App.h" //全局配置变量 config_t gconfig; static void Config_Display(void) { printf(">>------------------配置清单---------------------<<"); printf("\nconfig.magic:%x\n",gconfig.magic); printf("config.version:%x\n",gconfig.version); printf("config.led_id:%d\n",gconfig.led_id); printf("config.reapet_cnt:%d\n",gconfig.reapet_cnt); printf("config.start_time:%d\n",gconfig.start_time); printf("config.delay_ms:%d\n",gconfig.delay_ms); } // ret == EOF; 读取失败 static int Request_Input(const char *message) //static修饰函数指当前函数在本模块有效 { int value = 0; printf(">>%s\n", message); if (scanf("%d", &value) == 1) { return value; } // =========新增:清空缓冲区所有残留字符,直到换行========= printf("[!] 输入无效, 请重新尝试!\n"); return EOF; } void App_Init(void) { // 获取用户的选择 1.重新配置参数 2.使用现有的配置 int choice = Request_Input("1.重新配置参数 2.使用现有的配置 "); if (choice == 1) { gconfig.magic = APP_MAGIC; gconfig.version = APP_VERSION; gconfig.led_id = (uint8_t)Request_Input("step1: 选择LED[1,4],0表示全选>>"); gconfig.reapet_cnt = (uint8_t)Request_Input("step2: 设置闪灯次数, [1,255]>> "); gconfig.delay_ms = (uint16_t)Request_Input("step3: 设置闪灯间隔,单位ms>> "); gconfig.start_time = (uint16_t)Request_Input("step4: 设置开始时间,单位ms>> "); Config_Display(); //写入到EEPROM,写入之前还要确定一下是否是正确的 BSP_AT24C02_Write(&gconfig,sizeof(gconfig)); //AT24C02写入数据 } else if(choice == 2) { //读取对应的EEPROM数据,读取之前还要确定一下是否是正确的 BSP_AT24C02_Read(&gconfig,sizeof(gconfig)); //AT24C02读取数据 } else { printf(">>输入无效,选项只有1 || 2!\n"); } } void App_Run(void) { if(gconfig.magic == APP_MAGIC && gconfig.version == APP_VERSION) { printf(">> %d ms之后,开始闪灯\n",(int)gconfig.start_time); HAL_Delay(gconfig.start_time); printf(">> 开始闪灯..... \n"); LED_Blink_Version2((int)gconfig.led_id,gconfig.reapet_cnt,gconfig.delay_ms); } }我这里控制逻辑里,函数主要是通过串口对配置结构体进行配置,选择是1.重新配置还是2.通过AT24C02进行读取配置
APP_Init()里,通过Request_Input()这个函数进行提示并且通过该函数里的输入输出重定向,对串口进行写入数据,配置要进行的项目(指通过串口设置结构体电灯,但是究竟是重新配置还是通过I2C进行设置)
5.2 .板级驱动代码
BSP_AT24C02.h
#ifndef __BSP_AT24C02_H #define __BSP_AT24C02_H #ifdef __cplusplus extern "C" { #endif #include<stdbool.h> #include"stm32f1xx_hal.h" typedef struct { I2C_HandleTypeDef *hi2c; uint16_t dev_address; //从设备地址 uint8_t page_size; //页大小 bool is_init; //是否被初始化 }eeprom_t; #define DEV_ADDRESS 0x50 #define PAGE_SIZE 8 #define SCAN_RETYIES 2 #define SCAN_TIME_MS 5 #define DEV_TOTAL_SIZE 256 #define READ_TIMEOUT_MS 50 #define WRITE_TIMEOUT_MS 50 #define POLL_RETYIES 10 #define POLL_TIME_MS 2 bool BSP_AT24C02_Init(I2C_HandleTypeDef *hi2c); bool BSP_AT24C02_Read(uint16_t start_address, void *data_out, uint8_t date_len); bool BSP_AT24C02_Write(void *data_in,uint8_t date_len); #ifdef __cplusplus } #endif #endif扳机外设AT24C02头文件
- 该文件首先是AT24C02这个EEPROM的配置结构体,里面包括哪个I2C,从设备地址,页大小,是否配置成功
- 然后就是一些重要的函数声明,关于AT24C02的初始化,读取,写入之类的
BSP_AT24C02.c
#include "BSP_AT24C02.h" eeprom_t at24c02; bool BSP_AT24C02_Init(I2C_HandleTypeDef *hi2c) { if (!hi2c) { return false; } at24c02.hi2c = hi2c; at24c02.dev_address = DEV_ADDRESS << 1; // 左移一位 at24c02.page_size = PAGE_SIZE; at24c02.is_init = false; // 初始化以后还要进行等待,检测从设备就绪 // 1.产生起始条件;2.发送从设备地址 +w(0) = 8字节数据;3.ACK -> 从设备活着呢 4.stop if (HAL_OK == HAL_I2C_IsDeviceReady(at24c02.hi2c, at24c02.dev_address, SCAN_RETYIES, SCAN_TIME_MS)) { at24c02.is_init = true; } return at24c02.is_init; } // 可以进行按页读,字节读,任意读 bool BSP_AT24C02_Read(uint16_t start_address, void *data_out, uint8_t data_len) { if (!at24c02.is_init || !data_out || data_len == 0) return false; if (start_address + data_len > DEV_TOTAL_SIZE) return false; HAL_StatusTypeDef state = HAL_I2C_Mem_Read( at24c02.hi2c, at24c02.dev_address, start_address, I2C_MEMADD_SIZE_8BIT, data_out, data_len, READ_TIMEOUT_MS); return state == HAL_OK; } static bool BSP_AT24C02_Ready() { if(!at24c02.is_init) return false; return HAL_OK == (at24c02.hi2c,at24c02.dev_address,POLL_RETYIES,POLL_TIME_MS); } // 单页,页内写入 static bool BSP_AT24C02_Write_Page(uint16_t start_address, void *data_in, uint8_t data_len) { if (!at24c02.is_init || !data_len == 0 || !data_in) return false; // 检查写入是否跨页 // 得知道当前写入哪一页 // uint16_t page_num = start_address / at24c02.page_size; //不重要 uint16_t page_offset = start_address % at24c02.page_size; // 代表你在这一页的哪个位置写入 if (page_offset + data_len > at24c02.page_size) return false; HAL_StatusTypeDef state = HAL_I2C_Mem_Write( at24c02.hi2c, at24c02.dev_address, start_address, I2C_MEMADD_SIZE_8BIT, data_in, data_len, WRITE_TIMEOUT_MS); if(state != HAL_OK) return false; return BSP_AT24C02_Ready(); } // 支持任意长度写入,自动跨页 bool BSP_AT24C02_Write(uint16_t start_address,void *data_in, uint8_t data_len) { if(!at24c02.is_init || data_in || data_len == 0) return false; if(start_address + data_len > DEV_TOTAL_SIZE) return false; uint16_t Bytes_written = 0; //已经写入了多少字节 while(Bytes_written < data_len) { //数据还有多少没有发送 uint16_t data_left = data_len - Bytes_written; //必须按照页写,计算一下当前页还剩多少空间 } }主要是.C文件
首先是对AT24C02的初始化,1.先判断是否是空指针,要不然无效;2.其次就是对该I2C从设备的配置;3.最重要,通过HAL_I2C_IsDeviceReady函数对目标I2C设备进行检测,是否准备好通信
HAL_StatusTypeDef HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout)hi2c:指向I2C句柄结构体指针
DevAddress:7位目标设备地址,调用前必须左移1位
Trials:尝试次数,建议设置1-10
Timeout:超时时间,单次检测操作的超时时间ms