云计算百科
云计算领域专业知识百科平台

【STM32】 I2C_EEPROM_test 工程(实战篇)


 工程剖析:串口配置 → AT24C02 持久化 → LED 闪灯  


1. 工程概览

1.1 功能一句话

        通过串口交互配置"选哪几颗灯、闪几次、间隔多久、延时多久启动",把配置写入 AT24C02(I2C EEPROM)固化,上电后按保存的配置自动闪灯——典型的"参数掉电持久化"应用。

实际运行效果(串口控制台):

        下一次上电选"2.使用现在的配置",读回 EEPROM 数据、校验通过后按同一套参数闪灯。


1.2 芯片与目录结构

项目内容
芯片 STM32F103ZET6(LQFP144,512KB Flash / 64KB SRAM)
主频 8MHz HSE × PLL9 = 72MHz
外设 I2C2(400kHz)、USART1(115200)、GPIO PF8~PF11
EEPROM AT24C02(256 字节,8 字节/页,写周期最长 5ms)
工具链 CubeMX 生成 + Keil MDK-ARM(VSCode + Keil Assistant)

I2C_EEPROM_test/ ← 工程根目录
├── APP/ # 应用层(用户代码)
│ ├── App.c / App.h # 配置交互 + 执行逻辑
├── Core/ # CubeMX 生成:HAL 初始化骨架
│ ├── Inc/ # main.h / stm32f1xx_it.h …
│ └── Src/ # main.c / stm32f1xx_hal_msp.c / stm32f1xx_it.c
├── Drivers/ # CubeMX 生成:HAL 库 + CMSIS
├── MDK-ARM/ # Keil 工程(.uvprojx + .vscode/)
├── I2C_EEFORM_test.ioc # CubeMX 工程文件(引脚/时钟配置)
└── .mxproject

D:\\code_project\\BSP_drivers/ ← 工程外的共享 BSP 驱动库
├── inc/ BSP_AT24C02.h BSP_LED.h BSP_Usart_Redir.h
└── src/ BSP_AT24C02.c BSP_LED.c BSP_Usart_Redir.c

注意:

        BSP 驱动不在工程目录里,Keil 通过 ../../BSP_drivers/inc 引用头文件,Usart_Redir_test 等其他测试工程共用同一份驱动。这就是"驱动独立成库、多工程复用"的工程化做法。


2. 硬件设计

2.1 引脚总表

外设引脚GPIO 模式速度/细节
I2C2_SCL PB10 复用开漏 AF_OD High speed,400kHz Fast 模式
I2C2_SDA PB11 复用开漏 AF_OD 同上(需外部上拉电阻)
USART1_TX PA9 复用推挽 AF_PP 115200
USART1_RX PA10 复用推挽 AF_PP 输入
LED1~LED4 PF8~PF11 通用开漏输出 OD 低电平点亮

2.2 LED 为什么是开漏输出

CubeMX 里 PF8~PF11 配的是 GPIO_MODE_OUTPUT_OD(开漏),代码里"点亮 = 写低电平":

GPIO_PinState state = flag ? GPIO_PIN_RESET : GPIO_PIN_SET; // ON → RESET(低)

开漏输出的本质:

        引脚只能拉低(导通到地),不能主动输出高。对应硬件电路通常是:

VCC ── LED ── 限流电阻 ── GPIO(PF8)

        GPIO 输出低电平 → 电流灌进引脚 → LED 亮;

        输出高阻/高电平时 LED 灭。

这样做的两个好处:

        一是可以驱动高于 MCU 电平的负载(比如 5V 供电的 LED 灯条,开漏 + 外部上拉就实现了电平转换);

        二是多个开漏输出可以直接并联(线与逻辑)。

        缺点是必须靠外部上拉电阻,所以配置里 Pull = GPIO_NOPULL,上拉在板子上。


2.3 AT24C02 器件速览

        AT24C02 是 Atmel/Microchip 的 2Kbit(256 字节)I2C EEPROM,本工程用到的关键特性:

特性对代码的影响
容量 256 字节 地址 0x00~0xFF,8 位存储地址
页大小 8 字节 一次写周期最多写 8 字节且不能跨页
器件地址 1010 A2 A1 A0(0x50 基址) A2A1A0=000 时 7 位地址 0x50
写周期 tWR 最长 5ms 写完必须轮询等 ACK,否则读到旧数据
擦写寿命 100 万次 频繁写要留意磨损(本工程只在配置时写一次,无压力)

        引脚上除了 SCL/SDA,还有 A0/A1/A2(地址选择,接地为 000 → 0x50)、WP(写保护,接地允许写)。同一 I2C 总线上最多可以挂 8 片 AT24C02,靠 A0~A2 区分地址。


2.4 时钟树

HSE 8MHz ──×9(PLL)──→ SYSCLK 72MHz
├─ AHB /1 → HCLK 72MHz(SysTick、GPIO 时钟域)
├─ APB1 /2 → PCLK1 36MHz(I2C2、USART2/3、TIM 等)
└─ APB2 /1 → PCLK2 72MHz(USART1、GPIO、TIM1 等)

   SystemClock_Config 里 FLASH_LATENCY_2 对应 72MHz 需要 2 个 Flash 等待周期——这是 72MHz 超频/正常满频运行的必要配置。


3. I2C 协议与 AT24C02 基础(把底层讲透)

3.1 7 位地址 vs 8 位地址:为什么 0x50 << 1 = 0xA0

        I2C 协议里,从机地址在总线上传输时是一个完整的 8 位字节:高 7 位是器件地址,最低位是读写标志位(0=写,1=读)

        AT24C02 的 7 位地址由"固定部分 1010" + "A2A1A0"组成:

7 位地址: 1 0 1 0 A2 A1 A0 ← A2A1A0=000 → 0b1010000 = 0x50
8 位写地址: 1010 000 0 = 0xA0 ← 最低位 R/W=0
8 位读地址: 1010 000 1 = 0xA1 ← 最低位 R/W=1

        所以 DEV_ADDRESS 0x50 是 7 位地址,at24c02.dev_address = 0x50 << 1 = 0xA0 是带上写位的 8 位地址——HAL 的 HAL_I2C_Mem_Read/Write 需要的正是这个 8 位值。

        这是个经典易混点:数据手册上写的"0xA0"通常已经是 8 位写地址,而代码里习惯存 7 位再移位,两种写法都对,但要分清楚。


3.2 一次写事务/读事务在总线上的样子

写一字节到地址 0x00(HAL_I2C_Mem_Write 做的事):

S | 0xA0(W) | ACK | 0x00(存储地址) | ACK | D0 | ACK | P
起始 从机地址 应答 内存地址 应答 数据 应答 停止

从地址 0x00 读 8 字节(HAL_I2C_Mem_Read 做的事,注意中间有 Repeated START):

S | 0xA0(W) | ACK | 0x00 | ACK | Sr | 0xA1(R) | ACK | D0..D6 | ACK | D7 | NACK | P
起始 先写地址 应答 重复起始 切到读模式 读8字节 末字节不答 停止

为什么要"先写地址再切换读模式"?

        因为 EEPROM 是内存映射器件,必须先告诉它"从哪个地址开始",然后才能连续读出。 最后一个字节主机回 NACK 告诉从机"不要再发了",然后发停止位——这是 I2C 读操作的收尾规范。


3.3 为什么 I2C_MEMADD_SIZE_8BIT

        AT24C02 只有 256 字节,存储地址 0255 用一个字节就能表示,所以 I2C_MEMADD_SIZE_8BIT

        换大容量型号要改这里AT24C04/08/16 地址超过 8 位(9~11 位),一部分地址位被并进器件地址字节里,I2C_MEMADD_SIZE_8BIT 就不够用了——这是移植时最容易漏的地方。


3.4 写周期 tWR:为什么写完还要轮询

        EEPROM 收到数据后,内部要用电荷泵擦除+编程,这个过程需要时间(最长 5ms),期间器件不响应任何 ACK。所以驱动写完必须轮询探测:

static bool BSP_AT24C02_Ready(void)
{
return HAL_OK == HAL_I2C_IsDeviceReady(hi2c, dev_address, POLL_RETYIES, POLL_TIME_MS);
// 重试10次 × 每次间隔2ms,器件恢复ACK即认为写周期结束
}

  HAL_I2C_IsDeviceReady 发"起始+地址+等ACK+停止",EEPROM 没写完就不 ACK,函数返回超时。

       漏掉这一步的典型症状:写完立刻读,读到的是旧数据


4. 分层架构

架构要点:

  • 生成层只做初始化:main.c 全部是 CubeMX 生成的初始化代码,用户代码只出现在 USER CODE 注释区间——CubeMX 重新生成不会覆盖业务代码。
  • BSP 独立共享:驱动库在工程外,被多个工程复用,业务层只依赖 BSP 接口,不直接碰寄存器。
  • 单向依赖:APP → BSP → HAL → 硬件,每一层只调下一层的接口,没有反向依赖。这种结构让换硬件(比如换 I2C1)、换芯片都只动生成层。

  • 5. 代码逐模块深读

    5.1 main.c:启动流程逐行

    int main(void)
    {
    HAL_Init(); // ① 复位所有外设、初始化 Flash 接口和 SysTick(1ms 节拍)
    SystemClock_Config(); // ② 72MHz 时钟树
    MX_GPIO_Init(); // ③ PF8~11 开漏输出,初始电平全部拉低(RESET)
    MX_I2C2_Init(); // ④ I2C2 400kHz,7 位地址模式
    MX_USART1_UART_Init(); // ⑤ USART1 115200

    /* USER CODE BEGIN 2 */ // ← 用户代码从这里开始
    BSP_Usart_Init(&huart1); // ⑥ 把 UART 句柄交给 BSP(printf/scanf 立即可用)
    BSP_AT24C02_Init(&hi2c2); // ⑦ 探测 EEPROM 是否在线
    /* USER CODE END 2 */

    while (1) {
    App_Init(); // ⑧ 串口交互:重新配置 or 读回现有配置
    App_Run(); // ⑨ 校验魔数 → 延时 → 按配置闪灯
    }
    }

    几个值得注意的点:

    • 句柄是"交出去"不是"包起来":BSP_Usart_Init(&huart1) 把 huart1 指针存到 BSP 层。以后换串口(比如改用 USART2),只需要改 main.c 里这一行传参,BSP 代码零改动——这就是"依赖注入"思想在嵌入式里的朴素形态。
    • I2C2 初始化参数:400kHz、I2C_DUTYCYCLE_2(Fast 模式下高低电平 2:1 占空比)。HAL 会根据 PCLK1=36MHz 自动计算 CCR 分频和 TRISE 上升时间寄存器,不需要手算。
    • USART1 的特殊配置(重点):

    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_9B; // 9 位字长(M=1)
    huart1.Init.Parity = UART_PARITY_ODD; // 奇校验使能

      UART_WORDLENGTH_9B + 奇校验的组合,在线上实际是 1 起始位 + 8 数据位 + 1 奇校验位 + 1 停止位(校验位占据 9 位帧的最高位)。

            也就是说:串口助手要配成 8 位数据 + 奇校验(8-O-1)才能对上,配成常见的 8N1 会整个帧错位、乱码。如果你上位机协议是标准 8N1,这里要改回 UART_WORDLENGTH_8B + UART_PARITY_NONE。

    • Error_Handler:任何初始化失败都会进来,__disable_irq() 后死循环——嵌入式里最朴素的"卡死报错"方式,方便调试时一眼看出初始化没过。

    5.2 BSP_Usart_Redir:printf/scanf 重定向的原理

    这是整个工程最"妙"的一层,只有 40 行:

    UART_HandleTypeDef *ghusart = NULL; // 全局串口句柄(单例)

    void BSP_Usart_Init(UART_HandleTypeDef *husart) { ghusart = husart; }

    int fputc(int ch, FILE *fp) // printf("%d", x) 最终会调它
    {
    UNUSED(fp);
    if (ghusart && HAL_UART_Transmit(ghusart, (uint8_t *)&ch, 1, HAL_MAX_DELAY) == HAL_OK)
    return ch;
    return EOF;
    }

    int fgetc(FILE *fp) // scanf("%d", &x) 最终会调它
    {
    UNUSED(fp);
    uint8_t ch;
    while (1) {
    if (HAL_UART_Receive(ghusart, &ch, 1, HAL_MAX_DELAY) != HAL_OK)
    return EOF;
    if (ch != '\\r') // 过滤回车符 \\r,只留有效字符
    return (int)ch;
    }
    }

    原理拆解:

            C 标准库的 printf 内部把格式化结果逐字符写到 stdout 流,流的底层写函数就是 fputc;scanf 反过来从 stdin 流逐字符读,底层是 fgetc。

            只要实现了这两个函数,把字节导向 UART,整个应用层就能用 printf/scanf 做串口交互——应用层完全不知道 UART 的存在。这正是"标准库重定向(retarget)"的经典做法(Keil 下通常配合 MicroLIB 使用)。

    三个细节:

  • HAL_MAX_DELAY = 阻塞等待发送完成,printf 是同步阻塞的——本工程串口只是调试交互,没问题;将来接实时任务调度(FreeRTOS)就要改成中断/DMA + 互斥锁。
  • fgetc 里 while(1) 阻塞等一个字节——scanf 没输入时程序就停在那等,这就是菜单能"卡住等用户"的原因。
  • 过滤 '\\r':串口助手发送时通常带 \\r\\n,如果不过滤,scanf("%d") 之后的残留 \\r 会被当成下一个输入字符。这里只过滤了 \\r,\\n 会留在缓冲区——但 scanf 的 %d 会自动跳过空白字符(空格、\\n、\\t),所以不影响。
  • 请求函数的小细节:

    static int Request_Input(const char *message)
    {
    int choice = 0;
    printf(">>%s\\n", message);
    if (scanf("%d", &choice) == 1) // scanf 返回值 = 成功转换的参数个数
    return choice;
    printf("选择错误!请重新输入!");
    return -1; // 输入了非数字 → 返回 -1
    }

      scanf 返回 0 表示"一个都没转出来"(比如输入了字母 abc),此时返回 -1。

            而调用处 uint8_t choice = Request_Input(…) 把 -1 塞进 uint8_t 变成 255,255 != 1 && 255 != 2,走 else 分支打印错误——错误输入被"无意中"兜住了,但这属于隐式类型转换的巧合,见第 7 节。


    5.3 BSP_LED:控制与闪灯

    void BSP_LED_Control(LED_t led, bool flag)
    {
    GPIO_PinState state = flag ? GPIO_PIN_RESET : GPIO_PIN_SET; // ON = 低电平
    switch (led) {
    case LED1: HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8, state); break;
    case LED2: HAL_GPIO_WritePin(GPIOF, GPIO_PIN_9, state); break;
    case LED3: HAL_GPIO_WritePin(GPIOF, GPIO_PIN_10, state); break;
    case LED4: HAL_GPIO_WritePin(GPIOF, GPIO_PIN_11, state); break;
    default: break;
    }
    }

    flag ? GPIO_PIN_RESET : GPIO_PIN_SET 这行是极性反转:

            逻辑层说"ON",物理层写低电平——把"低电平点亮"这个硬件事实封装在 BSP 内部,上层永远只说"亮/灭",不懂电学也能用。

            以后电路改成高电平点亮,只改这一个函数。


    闪灯函数 LED_Blink_Version2(led, cnt, delay) 分两路:

    if (led == 0) { // 全选:4 颗灯一起闪
    LED_t leds[] = {LED1, LED2, LED3, LED4};
    int leds_num = sizeof(leds) / sizeof(leds[0]); // = 4,数组元素个数
    do {
    // 全部点亮 → HAL_Delay(delay) → 全部熄灭 → HAL_Delay(delay)
    } while (cnt–);
    } else { // 单选:闪 cnt 次
    for (int i = 0; i < cnt; i++) { /* 亮 delay → 灭 delay */ }
    }


    5.4 BSP_AT24C02:EEPROM 驱动全解析(工程核心)

    数据结构:一个"设备对象"

    typedef struct {
    I2C_HandleTypeDef *hi2c; // 指向 I2C 句柄(哪条 I2C 总线)
    uint16_t dev_address; // 8 位设备地址(0xA0)
    uint8_t page_size; // 页大小 8
    bool is_init; // 初始化是否成功(EEPROM 是否在线)
    } eeprom_t;

    eeprom_t at24c02; // 全局单例实例

            把"设备属性"打包成一个结构体、用一个全局实例持有,是嵌入式驱动最常见的形式。局限是单例不支持挂多片 EEPROM(改进见第 8 节)。


    初始化:探测 + 记录属性

    bool BSP_AT24C02_Init(I2C_HandleTypeDef *hi2c)
    {
    if (!hi2c) return false;
    at24c02.hi2c = hi2c;
    at24c02.dev_address = DEV_ADDRESS << 1; // 0x50 → 0xA0
    at24c02.page_size = PAGE_SIZE; // 8
    at24c02.is_init = false;
    if (HAL_OK == HAL_I2C_IsDeviceReady(at24c02.hi2c, at24c02.dev_address,
    SCAN_RETYIES, SCAN_TIME_MS)) {
    at24c02.is_init = true; // 收到 ACK = 器件在线
    }
    return at24c02.is_init;
    }

            所有接口开头都检查 is_init——没初始化成功就拒绝读写,避免对不存在的器件空转超时。


    读:边界检查 + Mem_Read

    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;
    }

            读操作没有页限制(EEPROM 可以跨页连续读),所以只有一个总容量越界检查。


    写:单页写 + 自动翻页(重点算法)

    为什么不能一把梭全写进去:

            AT24C02 的写周期限制是"一次最多写一页(8 字节)且不能跨页"。

            从地址 5 开始写 6 字节,会跨过地址 7 进入下一页——这种跨页写在器件内部会出错(要么被忽略,要么数据错乱)。所以驱动必须自己把大块数据拆成"每段不超过当前页剩余空间"的小段。


    单页写:

    // 单⻚,⻚内写⼊
    static bool BSP_AT24C02_Write_Page(uint16_t start_address, void *data_in,
    uint8_t data_len)
    {
    if (!at24c02.is_init || !data_in || data_len == 0)
    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;
    // 必须按照⻚来写,当前⻚剩余多少空间
    uint16_t new_start_address = start_address + bytes_written;//翻页逻辑
    // uint16_t current_page_num = new_start_address / at24c02.page_size;
    uint16_t page_offset = new_start_address % at24c02.page_size;//已经占用的空间
    uint16_t page_space = at24c02.page_size – page_offset;//剩余空间
    // 数据还有多少没有发送 -> 当前⻚剩余多少空间
    // ⼀定能写⼊当前⻚的剩余空间吗?不⼀定!!
    // 计算本次写⼊量
    // 1. 数据还有多少没有发送 <= 当前⻚剩余多少空间 = 数据还有多少
    // 2. 数据还有多少没有发送 > 当前⻚剩余多少空间 = 剩余多少空间
    uint16_t write_bytes = data_left;
    if (write_bytes > page_space)
    {
    write_bytes = page_space;//可以写且不跨页
    }
    if (!BSP_AT24C02_Write_Page(new_start_address,(uint8_t *)data_in + bytes_written, write_bytes))
    {
    return false;//单页写没成功
    }
    bytes_written += write_bytes;//把当页能写的加入,再次循环,翻页写
    }
    return true;
    }

            算法核心一句话:每次循环只写"当前页剩余空间"这么长,写完推进指针进入下一页,直到数据全部写完。调用方不需要知道页边界,驱动内部透明处理——这是 EEPROM 驱动设计的关键封装。


    具体过程(以"从地址 5 写 10 字节"为例)见下图:

    再给一张迭代跟踪表(start=5, len=10),把循环跑一遍:

    迭代bytes_writtennew_addrpage_offsetpage_spacedata_leftwrite_bytes写入范围
    1 0 5 5 3 10 3 地址 5~7
    2 3 8 0 8 7 7 地址 8~14
    10 = data_len → 循环结束

            而本工程实际调用 SAVE_CONFIG 时,CONFIG_ADDRESS=0x00、len=8,恰好 8 字节正好一页——page_offset=0、page_space=8、write_bytes=8,循环一次就完成,连翻页都不需要。这也是把配置放在 0x00 地址的巧妙之处:刚好对齐页边界。


    5.5 APP 层:配置对象、交互、魔数校验

    config_t 的内存布局

    typedef struct _config_t {
    uint8_t magic; // 偏移 0:魔数 0x35
    uint8_t version; // 偏移 1:版本 0x01
    uint8_t led_id; // 偏移 2:灯序号(0=全选)
    uint8_t repeat_cnt; // 偏移 3:闪灯次数
    uint16_t delay_ms; // 偏移 4:闪烁间隔
    uint16_t start_time; // 偏移 6:启动延时
    } config_t; // sizeof = 8 字节,无填充字节

       uint16_t 需要 2 字节对齐,编译器把它排到偏移 4 和 6,前 4 个 uint8_t 占 0~3——天然紧凑,正好 8 字节,恰好填满 AT24C02 的一页。

            这个"结构体直接落盘"的方式依赖编译器不插填充字节,如果将来在中间加一个 uint8_t 字段,sizeof 可能变成 10,跨页逻辑就会触发——这是结构体直接持久化的一个隐患。


    读写宏

    #define SAVE_CONFIG(conf_addr, conf_len) BSP_AT24C02_Write(CONFIG_ADDRESS, conf_addr, conf_len)
    #define LOAD_CONFIG(conf_addr, conf_len) BSP_AT24C02_Read(CONFIG_ADDRESS, conf_addr, conf_len)
    // BSP_AT24C02_Write(0x00, &gconfig, 8)
    // BSP_AT24C02_Read(0x00, &gconfig, 8)

            宏把"配置存储在地址 0x00"这个约定统一收口,业务代码里只写 SAVE_CONFIG(&gconfig, sizeof(gconfig)),改存储位置只动一处。


    App_Init:交互式配置

    void App_Init(void)
    {
    uint8_t choice = Request_Input("1.重新配置参数 2.使用现在的配置");
    if (choice == 1) {
    gconfig.magic = APP_MAGIC; // 0x35,写前就打上标签
    gconfig.version = APP_VERSION; // 0x01
    gconfig.led_id = (uint8_t)Request_Input("step1:选择LED【1-4】,0表示全选>> ");
    gconfig.repeat_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();
    SAVE_CONFIG(&gconfig, sizeof(gconfig)); // 写入 EEPROM
    printf(">> write config done <<\\n");
    } else if (choice == 2) {
    LOAD_CONFIG(&gconfig, sizeof(gconfig)); // 读回 EEPROM
    printf(">> load config done <<\\n");
    } else {
    printf("选择错误!请重新输入!");
    }
    }

            注意 magic 和 version 是写前先填好的——它不是用户输入项,是驱动层打的"数据标签"。用户只输入 4 个业务参数(灯、次数、间隔、延时)。


    App_Run:魔数校验 + 执行

    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((LED_t)gconfig.led_id, gconfig.repeat_cnt, gconfig.delay_ms);
    }
    // 校验不过 → 什么都不做(安全降级)
    }

            魔数校验是 APP 的精华:全新 EEPROM 内容全是 0xFF,如果直接拿 0xFF 当 repeat_cnt(255 次)和 delay_ms(255ms)用,会出现"幽灵闪灯"。

      magic == 0x35 校验把"存过有效配置"和"空片/数据损坏"区分开,校验不过就静默不闪——宁可什么都不做,也不要用错误数据驱动硬件。

      version 是给将来格式升级留的兼容位。


    6. 完整运行时序:把两条流程走一遍

    场景 A:首次上电 + 配置并保存

    上电 → HAL/时钟/外设初始化 → EEPROM 探测成功
    → App_Init: 打印菜单 → 用户选 1
    → 依次 scanf 4 个参数 → 填 gconfig(magic=0x35)
    → SAVE_CONFIG: I2C2 写 8 字节到 0x00 → 轮询等 tWR 结束
    → App_Run: magic 校验通过 → 延时 start_time ms → 闪灯

    场景 B:断电重启,使用保存的配置

    上电 → 初始化 → EEPROM 探测成功
    → App_Init: 打印菜单 → 用户选 2
    → LOAD_CONFIG: I2C2 读 8 字节 → gconfig
    → App_Run: magic==0x35 && version==0x01 → 延时 → 按参数闪灯

    场景 C:EEPROM 是空片 / 从没写过

    选 2 → 读回全 0xFF → App_Run 校验失败 → 不闪灯(安全)

    7. 完整工程代码

    BSP_AT24C02.h/c

    #ifndef __BSP_AT24C02_H
    #define __BSP_AT24C02_H
    #ifdef __cplusplus
    extern "C"
    {
    #endif

    #include <stdbool.h>
    #include "stm32f1xx_hal.h"

    bool Read_Page_Data(void *data_out, uint8_t data_len);
    bool Write_Page_Data(void *data_in, uint8_t data_len);

    // 表⽰从设备
    typedef struct
    {
    I2C_HandleTypeDef *hi2c; // I2C2句柄指针
    uint16_t dev_address; // 从设备地址
    uint8_t page_size; // ⻚⼤⼩
    bool is_init; // 是否被初始化
    } eeprom_t;

    #define DEV_ADDRESS 0x50 // I2C从机设备地址
    #define PAGE_SIZE 8 // EEPROM单页写入字节数
    #define DEV_TOTAL_SIZE 256 // EEPROM总存储字节大小
    #define SCAN_RETYIES 2 // I2C设备扫描重试次数
    #define SCAN_TIME_MS 5 // 扫描重试间隔(ms)
    #define POLL_RETYIES 10 // 写等待轮询重试次数
    #define POLL_TIME_MS 2 // 轮询等待间隔(ms)
    #define READ_TIMEOUT_MS 50 // I2C读取超时时间(ms)
    #define WRITE_TIMEOUT_MS 50 // I2C写入超时时间(ms)

    bool BSP_AT24C02_Init(I2C_HandleTypeDef *hi2c);
    bool BSP_AT24C02_Read(uint16_t start_address, void *data_out, uint8_t data_len);
    bool BSP_AT24C02_Write(uint16_t start_address, void *data_in, uint8_t data_len);

    #ifdef __cplusplus
    }
    #endif

    #endif
    ——————————————————————
    #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; // 左移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;
    }

    // 按字节,按page,按任意读
    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 == HAL_I2C_IsDeviceReady(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_in || data_len == 0)
    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;
    // 必须按照⻚来写,当前⻚剩余多少空间
    uint16_t new_start_address = start_address + bytes_written;//翻页逻辑
    // uint16_t current_page_num = new_start_address / at24c02.page_size;
    uint16_t page_offset = new_start_address % at24c02.page_size;//已经占用的空间
    uint16_t page_space = at24c02.page_size – page_offset;//剩余空间
    // 数据还有多少没有发送 -> 当前⻚剩余多少空间
    // ⼀定能写⼊当前⻚的剩余空间吗?不⼀定!!
    // 计算本次写⼊量
    // 1. 数据还有多少没有发送 <= 当前⻚剩余多少空间 = 数据还有多少
    // 2. 数据还有多少没有发送 > 当前⻚剩余多少空间 = 剩余多少空间
    uint16_t write_bytes = data_left;
    if (write_bytes > page_space)
    {
    write_bytes = page_space;//可以写且不跨页
    }
    if (!BSP_AT24C02_Write_Page(new_start_address,(uint8_t *)data_in + bytes_written, write_bytes))
    {
    return false;//单页写没成功
    }
    bytes_written += write_bytes;//把当页能写的加入,再次循环,翻页写
    }
    return true;
    }


    App.h\\c

    #ifndef __APP_H
    #define __APP_H

    #ifdef __cplusplus
    extern "C"
    {
    #endif

    #include <stdint.h>
    #include "BSP_AT24C02.h"
    #include "BSP_Usart_Redir.h"
    #include "BSP_LED.h"
    #define APP_MAGIC 0x35
    #define APP_VERSION 0x01

    typedef struct _config_t
    {
    uint8_t magic; // 魔数
    uint8_t version; // 版本
    uint8_t led_id; // 灯序号
    uint8_t repeat_cnt; // 重复次数
    uint16_t delay_ms; // 延迟间隔
    uint16_t start_time; // 开始时间
    } config_t;

    #define CONFIG_ADDRESS 0x0 // 配置数据在AT24C02中的存储起始地址
    #define SAVE_CONFIG(conf_addr, conf_len) BSP_AT24C02_Write(CONFIG_ADDRESS,conf_addr, conf_len)
    #define LOAD_CONFIG(conf_addr, conf_len) BSP_AT24C02_Read(CONFIG_ADDRESS,conf_addr, conf_len)

    void App_Init(void);
    void App_Run(void);

    #ifdef __cplusplus
    }
    #endif

    #endif
    —————————————————————-
    #include "App.h"

    config_t gconfig;
    static void Config_Display(void)
    {
    printf("———–配置清单———-\\n");
    printf("config.magic:0x%x\\n", gconfig.magic);
    printf("config.version:0x%x\\n", gconfig.version);
    printf("config.led_id:%d\\n", gconfig.led_id);
    printf("config.repeat_cnt:%d\\n", gconfig.repeat_cnt);
    printf("config.delay_ms:%d\\n", gconfig.delay_ms);
    printf("config.start_time:%d\\n", gconfig.start_time);
    }
    static int Request_Input(const char *message)
    {
    int choice = 0;
    printf(">>%s\\n", message);
    if (scanf("%d", &choice) == 1)
    {
    return choice;
    }

    printf("选择错误!请重新输入!");
    return -1;
    }
    void App_Init(void)
    {
    uint8_t 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.repeat_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
    SAVE_CONFIG(&gconfig, sizeof(gconfig)); // AT24C02写⼊
    printf(">> write config done <<\\n");
    }
    else if (choice == 2)
    {
    // 读取EEPROM
    LOAD_CONFIG(&gconfig, sizeof(gconfig));
    printf(">> load config done <<\\n");
    }
    else
    {
    printf("选择错误!请重新输入!");
    }
    }
    void App_Run()
    {
    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((LED_t)(gconfig.led_id), gconfig.repeat_cnt,
    gconfig.delay_ms);
    }
    }


    main.h\\c

    #ifndef __MAIN_H
    #define __MAIN_H

    #ifdef __cplusplus
    extern "C" {
    #endif

    /* Includes ——————————————————————*/
    #include "stm32f1xx_hal.h"

    /* Private includes ———————————————————-*/
    /* USER CODE BEGIN Includes */
    #include <stdbool.h>
    #include "BSP_AT24C02.h"
    #include "BSP_Usart_Redir.h"
    #include "BSP_LED.h"
    #include "App.h"

    /* Exported functions prototypes ———————————————*/
    void Error_Handler(void);

    #ifdef __cplusplus
    }
    #endif

    #endif /* __MAIN_H */

    ——————————————————–

    #include "main.h"

    /* Private variables ———————————————————*/
    I2C_HandleTypeDef hi2c2;

    UART_HandleTypeDef huart1;

    /* Private function prototypes ———————————————–*/
    void SystemClock_Config(void);
    static void MX_GPIO_Init(void);
    static void MX_I2C2_Init(void);
    static void MX_USART1_UART_Init(void);

    int main(void)
    {

    MX_GPIO_Init();
    MX_I2C2_Init();
    MX_USART1_UART_Init();

    /* USER CODE BEGIN 2 */
    BSP_Usart_Init(&huart1);
    BSP_AT24C02_Init(&hi2c2);
    /* USER CODE END 2 */

    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
    App_Init();
    App_Run();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
    }

    /**
    * @brief System Clock Configuration
    * @retval None
    */
    void SystemClock_Config(void)
    {
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

    /** Initializes the RCC Oscillators according to the specified parameters
    * in the RCC_OscInitTypeDef structure.
    */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.HSIState = RCC_HSI_ON;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
    {
    Error_Handler();
    }

    /** Initializes the CPU, AHB and APB buses clocks
    */
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
    |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
    {
    Error_Handler();
    }
    }

    /**
    * @brief I2C2 Initialization Function
    * @param None
    * @retval None
    */
    static void MX_I2C2_Init(void)
    {

    /* USER CODE BEGIN I2C2_Init 0 */

    /* USER CODE END I2C2_Init 0 */

    /* USER CODE BEGIN I2C2_Init 1 */

    /* USER CODE END I2C2_Init 1 */
    hi2c2.Instance = I2C2;
    hi2c2.Init.ClockSpeed = 400000;
    hi2c2.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c2.Init.OwnAddress1 = 0;
    hi2c2.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c2.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c2.Init.OwnAddress2 = 0;
    hi2c2.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c2.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    if (HAL_I2C_Init(&hi2c2) != HAL_OK)
    {
    Error_Handler();
    }
    /* USER CODE BEGIN I2C2_Init 2 */

    /* USER CODE END I2C2_Init 2 */

    }

    /**
    * @brief USART1 Initialization Function
    * @param None
    * @retval None
    */
    static void MX_USART1_UART_Init(void)
    {

    /* USER CODE BEGIN USART1_Init 0 */

    /* USER CODE END USART1_Init 0 */

    /* USER CODE BEGIN USART1_Init 1 */

    /* USER CODE END USART1_Init 1 */
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_9B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_ODD;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
    Error_Handler();
    }
    /* USER CODE BEGIN USART1_Init 2 */

    /* USER CODE END USART1_Init 2 */

    }

    /**
    * @brief GPIO Initialization Function
    * @param None
    * @retval None
    */
    static void MX_GPIO_Init(void)
    {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    /* USER CODE BEGIN MX_GPIO_Init_1 */

    /* USER CODE END MX_GPIO_Init_1 */

    /* GPIO Ports Clock Enable */
    __HAL_RCC_GPIOF_CLK_ENABLE();
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /*Configure GPIO pin Output Level */
    HAL_GPIO_WritePin(GPIOF, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11, GPIO_PIN_RESET);

    /*Configure GPIO pins : PF8 PF9 PF10 PF11 */
    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOF, &GPIO_InitStruct);

    /* USER CODE BEGIN MX_GPIO_Init_2 */

    /* USER CODE END MX_GPIO_Init_2 */
    }


    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【STM32】 I2C_EEPROM_test 工程(实战篇)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!