AT24C02C-SSHM-T用法
下面的方案结合了提供的 PLC 程序、硬件原理图和 AT24C02C 数据手册,给出了在动平衡机项目中 EEPROM 的存储规划以及完整的驱动示例。AT24C02C 总容量 256 字节,分成 32 个 8 字节的页。因此写入时必须遵守 8 字节页边界,写操作完成后需要等待芯片内部自写周期完成。
需要存储的数据
动平衡机上采用 Delta A3 伺服控制转子,PLC 程序中有大量运行参数和调节量。对于 MCU 版本,为了断电后能够恢复这些参数,可将以下内容持久化到 AT24C02C 中:
类别 | 参数 | 说明 |
---|---|---|
伺服通信参数 | stationID、baudRate、timeout | 从站地址、Modbus 波特率和超时。便于在上电时重新初始化 RS‑485。 |
伺服机械参数 | encoderResolution、gearRatio、maxSpeed、maxAccel、maxDecel | 编码器分辨率、减速比和速度/加减速度上限。决定电机运动范围。 |
动平衡特有参数 | balanceSpeed、searchSpeed | 平衡测试速度和寻找基准速度(rpm)。 |
positionAccuracy | 定位精度(度),用于 OK/NG 判断。 | |
angleOffset | 安装基准与传感器基准的角度偏移(度)。 | |
secondPosAngle、secondPosSpeed | “二次定位”时的角度和速度,适用于某些转子需要二次修正。 | |
photoSensorAlarmTime | 光电异常报警时的等待时间,单位 100 ms。 | |
传感器选择 | sensorType | 0 为光电传感器,1 为接近开关。 |
验证字段 | magic、version、crc | 用于标识有效数据和校验完整性。 |
根据这些字段定义了一个 EEPROM_Settings_t
结构体(见下),约 55 字节,可一次性写入 EEPROM 的前一段地址。若还要持久化其他整定参数,只需在结构体末尾继续添加即可。
驱动和存取实现
下面给出一个完整的 AT24C02C 驱动和配置存取函数,基于 HAL I2C,已适配 MCU F103 的 hi2c1
:
头文件 at24c02c.h
#ifndef __AT24C02C_H
#define __AT24C02C_H#ifdef __cplusplus
extern "C" {
#endif#include "main.h"
#include <stdint.h>
#include <stdbool.h>/* A0~A2 接地时从机地址 0x50,HAL 需左移一位 */
#ifndef AT24C02C_I2C_ADDRESS
#define AT24C02C_I2C_ADDRESS (0x50u << 1)
#endif#define AT24C02C_PAGE_SIZE 8u
#define AT24C02C_TOTAL_SIZE 256uHAL_StatusTypeDef AT24C02C_Write(uint8_t mem_addr,const uint8_t *data,uint16_t len);
HAL_StatusTypeDef AT24C02C_Read(uint8_t mem_addr,uint8_t *data,uint16_t len);/* 单字节封装 */
static inline HAL_StatusTypeDef AT24C02C_WriteByte(uint8_t mem_addr, uint8_t value) {return AT24C02C_Write(mem_addr, &value, 1u);
}
static inline HAL_StatusTypeDef AT24C02C_ReadByte(uint8_t mem_addr, uint8_t *value) {return AT24C02C_Read(mem_addr, value, 1u);
}/* 配置结构:包含伺服和动平衡所有参数 */
typedef struct __attribute__((packed)) {uint16_t magic; // 0xA3A3 表示有效数据uint8_t version; // 结构版本uint8_t reserved0;/* 伺服通信参数 */uint8_t stationID;uint32_t baudRate;uint16_t timeout;/* 伺服机械参数 */uint32_t encoderResolution;float gearRatio;float maxSpeed;float maxAccel;float maxDecel;/* 动平衡参数 */float balanceSpeed;float searchSpeed;float positionAccuracy;float angleOffset;float secondPosAngle;float secondPosSpeed;uint16_t photoSensorAlarmTime;uint8_t sensorType; // 0:光电,1:接近开关uint8_t reserved1[3];uint16_t crc; // CRC16 校验
} EEPROM_Settings_t;/* CRC16/CCITT 计算 */
uint16_t EEPROM_CalcCRC16(const uint8_t *data, uint16_t len);/* 保存/读取整个配置 */
HAL_StatusTypeDef EEPROM_SaveSettings(const EEPROM_Settings_t *settings);
HAL_StatusTypeDef EEPROM_LoadSettings(EEPROM_Settings_t *settings);#ifdef __cplusplus
}
#endif
#endif /* __AT24C02C_H */
源文件 at24c02c.c
#include "at24c02c.h"
#include <string.h>extern I2C_HandleTypeDef hi2c1;/* 等待 EEPROM 自写完成 */
static HAL_StatusTypeDef AT24C02C_WaitReady(void) {for (uint32_t i = 0; i < 50u; i++) {if (HAL_I2C_IsDeviceReady(&hi2c1, AT24C02C_I2C_ADDRESS, 1, 5) == HAL_OK) {return HAL_OK;}HAL_Delay(1);}return HAL_ERROR;
}/* 分页写 */
HAL_StatusTypeDef AT24C02C_Write(uint8_t mem_addr,const uint8_t *data,uint16_t len) {if (!data || len == 0) return HAL_ERROR;if ((uint16_t)mem_addr + len > AT24C02C_TOTAL_SIZE) return HAL_ERROR;HAL_StatusTypeDef status = HAL_OK;while (len > 0 && status == HAL_OK) {uint8_t page_offset = mem_addr & (AT24C02C_PAGE_SIZE - 1u);uint8_t space_in_page = AT24C02C_PAGE_SIZE - page_offset;uint16_t chunk = (len < space_in_page) ? len : space_in_page;status = HAL_I2C_Mem_Write(&hi2c1, AT24C02C_I2C_ADDRESS,mem_addr, I2C_MEMADD_SIZE_8BIT,(uint8_t*)data, chunk, HAL_MAX_DELAY);if (status != HAL_OK) return status;status = AT24C02C_WaitReady();if (status != HAL_OK) return status;mem_addr += chunk;data += chunk;len -= chunk;}return status;
}/* 连续读 */
HAL_StatusTypeDef AT24C02C_Read(uint8_t mem_addr,uint8_t *data,uint16_t len) {if (!data || len == 0) return HAL_ERROR;if ((uint16_t)mem_addr + len > AT24C02C_TOTAL_SIZE) return HAL_ERROR;return HAL_I2C_Mem_Read(&hi2c1, AT24C02C_I2C_ADDRESS,mem_addr, I2C_MEMADD_SIZE_8BIT,data, len, HAL_MAX_DELAY);
}/* CRC16/CCITT */
uint16_t EEPROM_CalcCRC16(const uint8_t *data, uint16_t len) {uint16_t crc = 0xFFFFu;for (uint16_t i = 0; i < len; i++) {crc ^= ((uint16_t)data[i] << 8);for (uint8_t bit = 0; bit < 8u; bit++) {crc = (crc & 0x8000u) ? (crc << 1) ^ 0x1021u : (crc << 1);}}return crc;
}/* 保存配置到地址 0 */
HAL_StatusTypeDef EEPROM_SaveSettings(const EEPROM_Settings_t *settings) {if (!settings) return HAL_ERROR;EEPROM_Settings_t temp;memcpy(&temp, settings, sizeof(temp));temp.magic = 0xA3A3u;/* 计算 CRC,覆盖除 magic 外所有字段 */temp.crc = EEPROM_CalcCRC16(((const uint8_t *)&temp) + offsetof(EEPROM_Settings_t, version),sizeof(EEPROM_Settings_t)- offsetof(EEPROM_Settings_t, version) - sizeof(uint16_t));return AT24C02C_Write(0u, (const uint8_t *)&temp, sizeof(EEPROM_Settings_t));
}/* 读取并验证配置 */
HAL_StatusTypeDef EEPROM_LoadSettings(EEPROM_Settings_t *settings) {if (!settings) return HAL_ERROR;EEPROM_Settings_t temp;if (AT24C02C_Read(0u, (uint8_t*)&temp, sizeof(temp)) != HAL_OK) {return HAL_ERROR;}if (temp.magic != 0xA3A3u) {return HAL_ERROR; // 没有有效数据}uint16_t calc_crc = EEPROM_CalcCRC16(((const uint8_t *)&temp) + offsetof(EEPROM_Settings_t, version),sizeof(EEPROM_Settings_t)- offsetof(EEPROM_Settings_t, version) - sizeof(uint16_t));if (calc_crc != temp.crc) {return HAL_ERROR; // CRC 校验失败}memcpy(settings, &temp, sizeof(EEPROM_Settings_t));return HAL_OK;
}
使用示例
#include "at24c02c.h"
#include "tigerservo.h"// 启动时读取配置
void LoadConfig(void) {EEPROM_Settings_t cfg;if (EEPROM_LoadSettings(&cfg) == HAL_OK) {// 用 EEPROM 的参数覆盖默认值g_servoController.params.stationID = cfg.stationID;g_servoController.params.baudRate = cfg.baudRate;g_servoController.params.timeout = cfg.timeout;g_servoController.params.encoderResolution = cfg.encoderResolution;g_servoController.params.gearRatio = cfg.gearRatio;g_servoController.params.maxSpeed = cfg.maxSpeed;g_servoController.params.maxAccel = cfg.maxAccel;g_servoController.params.maxDecel = cfg.maxDecel;g_servoController.balanceSpeed = cfg.balanceSpeed;g_servoController.searchSpeed = cfg.searchSpeed;g_servoController.positionAccuracy = cfg.positionAccuracy;g_servoController.angleOffset = cfg.angleOffset;// 其它参数同理...g_tigerServoController.referenceSel = (cfg.sensorType == 0) ? PHOTOSENSOR : PROXIMITYSWITCH;} else {// EEPROM 没有有效数据,使用默认参数}
}// 参数修改后保存
void SaveConfig(void) {EEPROM_Settings_t cfg = {0};cfg.version = 1;cfg.stationID = g_servoController.params.stationID;cfg.baudRate = g_servoController.params.baudRate;cfg.timeout = g_servoController.params.timeout;cfg.encoderResolution = g_servoController.params.encoderResolution;cfg.gearRatio = g_servoController.params.gearRatio;cfg.maxSpeed = g_servoController.params.maxSpeed;cfg.maxAccel = g_servoController.params.maxAccel;cfg.maxDecel = g_servoController.params.maxDecel;cfg.balanceSpeed = g_servoController.balanceSpeed;cfg.searchSpeed = g_servoController.searchSpeed;cfg.positionAccuracy = g_servoController.positionAccuracy;cfg.angleOffset = g_servoController.angleOffset;cfg.secondPosAngle = /* 单位度 */;cfg.secondPosSpeed = /* 单位 rpm */;cfg.photoSensorAlarmTime = /* 单位 100 ms */;cfg.sensorType = (g_tigerServoController.referenceSel == PHOTOSENSOR) ? 0 : 1;EEPROM_SaveSettings(&cfg);
}
在主程序初始化时调用 LoadConfig()
可从 EEPROM 恢复参数;用户通过串口/HMI 调整参数后再调用 SaveConfig()
存入 EEPROM。这样就能在 F103 + RS‑485 项目中完全替代 PLC 方案,实现伺服上电参数、动平衡调节量的长期保存。
好,我用“图解 + 要点”把 AT24C02C 的容量、分页、地址与读写流程一次讲清。可以对照那张原理图一起看。
存储组织(2 Kbit = 256 B)
总体:256 字节
分页:32 页 × 8 字节/页
地址:8 位字节地址 0x00 ~ 0xFF
0x00 ┌──────── Page 0 ────────┐ 0x07
0x08 ├──────── Page 1 ────────┤ 0x0F
0x10 ├──────── Page 2 ────────┤ 0x17
0x18 ├──────── Page 3 ────────┤ 0x1F... │ ... │ ...
0xE8 ├──────── Page 29 ────────┤ 0xEF
0xF0 ├──────── Page 30 ────────┤ 0xF7
0xF8 └──────── Page 31 ────────┘ 0xFF
- 页号 =
addr >> 3
(因为每页 8 字节) - 页内偏移 =
addr & 0x07
关键规则:一次“页写(Page Write)”最多 8 字节,不能跨页。若写入跨过页尾,芯片会在该页内回卷覆盖(wrap-around)。
I²C 从地址(7 位)
AT24C02C 的 7 位地址格式:1010 A2 A1 A0
- 原理图里 A0/A1/A2 接地 ⇒ 7 位地址 =
0b1010 000
= 0x50 - 在 STM32 HAL 里要左移一位(加上 R/W 位),所以用
0x50 << 1
。
基本时序(简图)
写(Page Write / Byte Write)
START
[0x50|W] ACK
[word_addr] ACK ← 0x00~0xFF
[data0] ACK
[data1] ACK
...(同页内最多 8 字节)
STOP
- 之后芯片进入自写周期(~5 ms 典型)。
- 需做 ACK 轮询:反复发
[0x50|W]
,直到应答,表示写完可继续下一次操作。
读(Random/Sequential Read)
随机读常见做法:
START
[0x50|W] ACK
[word_addr] ACK
REPEATED START
[0x50|R] ACK
[data0] 主机回 ACK(若还要继续读)
[data1] 主机回 ACK
... 最后一个字节 主机回 NACK
STOP
顺序读(Sequential)可跨页连续读;只有写才受 8 字节页限制。
与原理图的核对
-
上拉:SCL/SDA 各有 4.7 kΩ 上拉到 3.3 V ✅
-
去耦:VCC 旁并 100 nF ✅
-
A0/A1/A2 接地 ⇒ 地址 0x50 ✅
-
WP(写保护,脚 7):建议固定接 GND(允许写)。
- 图里看起来 WP 的连线靠近 I²C 总线,请确认不要接到 SCL/SDA。
- 若接到 VCC = 永久写保护;若漂浮或跟随时钟,会出现随机写失败。
代码里怎么用这个组织(要点)
-
计算一次能写的字节数,避免跨页:
uint8_t page_off = addr & 0x07; uint8_t space = 8 - page_off; uint16_t chunk = MIN(len, space); // 本次写 chunk
-
每次页写后做 ACK 轮询,直到
HAL_I2C_IsDeviceReady()
返回 OK。 -
连续读不受页限制,直接
HAL_I2C_Mem_Read()
就行。
我给驱动里已经按这个规则做了:
AT24C02C_Write()
会自动分页写和等待写完成;读写接口分别是AT24C02C_Read/Write
,总容量用AT24C02C_TOTAL_SIZE = 256
,页大小AT24C02C_PAGE_SIZE = 8
。
小例子(写 10 字节到 0x1C)
-
地址 0x18
0x1F 是同一页,0x200x27 是下一页。 -
从 0x1C 开始写 10 字节:
- 第 1 次写:
0x1C ~ 0x1F
(4 字节,填满当前页) - 轮询就绪
- 第 2 次写:
0x20 ~ 0x25
(剩余 6 字节,下一页)
- 第 1 次写:
如果一次把 10 字节直接丢给芯片,它会 在 0x18~0x1F 内回卷覆盖,导致后 6 字节写到 0x18~0x1D
——这就是“跨页回卷”的坑。
放什么数据最合适(结合项目)
建议把“会改、且要掉电保存”的参数集中做成一个结构体(几十字节),比如:
- 通信:stationID、baudRate、timeout
- 伺服:maxSpeed/maxAccel/maxDecel、gearRatio、encoderResolution
- 动平衡:balanceSpeed、searchSpeed、positionAccuracy、angleOffset、secondPosAngle/secondPosSpeed、photoSensorAlarmTime、sensorType
- 末尾加 CRC16 做校验,前面加 magic 和 version 做兼容
一次 EEPROM_SaveSettings()
全写;上电 EEPROM_LoadSettings()
校验通过就覆盖默认参数。
如果愿意,我也可以把当前参数和默认值直接按“起始地址 + 字段长度”再画一张地址分配表,或者帮把结构体映射到每个 EEPROM 地址段的“工程图”(类似内存地图)。更喜欢哪种展示方式?
太好了!我给你把 **AT24C02C 的“内存地图”**画清楚,并把每个参数放到具体地址。这样你在代码里能一眼对照、也方便联调。
存储组织快速图
容量:256 B = 32 页 × 8 B/页 地址:0x00 ~ 0xFF
页号 = addr >> 3 页内偏移 = addr & 0x070x00 ┌──────── Page 0 ────────┐ 0x07
0x08 ├──────── Page 1 ────────┤ 0x0F
0x10 ├──────── Page 2 ────────┤ 0x17... │ … │ ...
0xF0 ├──────── Page 30 ───────┤ 0xF7
0xF8 └──────── Page 31 ───────┘ 0xFF
地址分配表(主副本各一份,便于容错)
主副本 A:0x00–0x3B;CRC:0x3A–0x3B
备份 B:0x40–0x7B;CRC:0x7A–0x7B(可选;不开启就留空)
单位说明:Speed=rpm;Angle=°;
photoSensorAlarmTime
=100 ms 步进。
起始 | 结束 | 长度 | 名称 | 类型 | 备注 / 默认 |
---|---|---|---|---|---|
0x00 | 0x01 | 2 | magic | u16 | 0xA3A3 |
0x02 | 0x02 | 1 | version | u8 | 1 |
0x03 | 0x03 | 1 | stationID | u8 | 1 |
0x04 | 0x07 | 4 | baudRate | u32 | 38400 |
0x08 | 0x09 | 2 | timeout | u16 | ms,500 |
0x0A | 0x0D | 4 | encoderResolution | u32 | 10000(例) |
0x0E | 0x11 | 4 | gearRatio | float | 1.0 |
0x12 | 0x15 | 4 | maxSpeed | float | 3000 |
0x16 | 0x19 | 4 | maxAccel | float | 1000 |
0x1A | 0x1D | 4 | maxDecel | float | 1000 |
0x1E | 0x21 | 4 | balanceSpeed | float | 600 |
0x22 | 0x25 | 4 | searchSpeed | float | 200 |
0x26 | 0x29 | 4 | positionAccuracy | float | 1.0 |
0x2A | 0x2D | 4 | angleOffset | float | 0.0 |
0x2E | 0x31 | 4 | secondPosAngle | float | 0.0 |
0x32 | 0x35 | 4 | secondPosSpeed | float | 200 |
0x36 | 0x37 | 2 | photoSensorAlarmTime | u16 | ×100 ms,10 |
0x38 | 0x38 | 1 | sensorType | u8 | 0=光电/1=接近 |
0x39 | 0x39 | 1 | reserved | u8 | 保留 |
0x3A | 0x3B | 2 | crc16 | u16 | CCITT |
备份 B 完全同布局,起始地址换为 0x40。
落在各页的边界(便于页写):
- Page0: 0x00–0x07(magic/version/stationID/baudRate)
- Page1: 0x08–0x0F(timeout/encoderRes/gearRatio 前 2B)
- Page2: 0x10–0x17(gearRatio 尾/maxSpeed 前 2B)
- …(驱动里已自动分页,无需手动切分,只是给你对齐参考)
读/写流程(建议)
-
加载(开机)
- 先读 A(0x00–0x3B),校验 magic 与 CRC;
- 若 A 失败,再读 B(0x40–0x7B);
- 都失败 ⇒ 加载默认值。
-
保存(用户改参后)
- 先写 A:按页写(8 B/次)→ ACK 轮询 → 全部写完 → 最后写 CRC 页;
- 需要容错时再写 B(同样流程)。
- 建议:参数未变化不写;写前先比较旧值。
-
注意
- 写保护 WP 脚接 GND;
- 禁止跨页写(驱动已处理);
- 浮点
float
用 小端 存(STM32 默认),与 MCU 内存一致。
你已有驱动如何对应
AT24C02C_Write()
:已按 8 B 页自动切块并做 ACK 轮询;EEPROM_SaveSettings()
:一次性把上表的数据结构写到 0x00;EEPROM_LoadSettings()
:从 0x00 读并校验magic + CRC
,失败再尝试 0x40(如果你启用备份)。
最小调用
void LoadConfig(void) {EEPROM_Settings_t cfg;if (EEPROM_LoadSettings(&cfg) == HAL_OK) {// 用 cfg 覆盖 g_servoController / g_tigerServoController} else {// 载入默认,并可立即 EEPROM_SaveSettings(&defaults);}
}void SaveConfig(void) {EEPROM_Settings_t cfg = {0};// 从当前运行参数填充 cfg ...EEPROM_SaveSettings(&cfg);
}
如果你希望强一致 + 断电保护,我可以把保存流程改成:
写 B → 校验 B → 写 A → 校验 A(带 1 字节“计数器”用于选择较新副本)。
需要的话告诉我,我直接把你现有 EEPROM_SaveSettings/LoadSettings
改成“主备容错版”。