嵌入式实战 BSP驱动
软件 I2C + AT24C02 EEPROM:BSP 驱动的分层设计与时序实现
EEPROM 驱动是展示 BSP 层设计原则的绝佳案例。它包含三个层次:比特级时序(软件 I2C)、芯片级协议(AT24C02)、模块级接口(bsp_eeprom)。上层只看到一个读/写/初始化三个函数的统一接口。
第一层:软件 I2C Bit-Bang
PA11(SDA) / PA12(SCL),开漏输出,100kHz 软件时序。核心是一个 5μs 的延时循环:
// 72 MHz → 360 个空转 ≈ 5 μs → ~100 kHz SCL
#define SOFT_I2C_HALF_PERIOD_TICKS 360U
static void soft_i2c_delay(void) {
volatile uint32_t i = SOFT_I2C_HALF_PERIOD_TICKS;
while (i > 0U) { i--; }
}
| 原语 | 代码 | 说明 |
|---|---|---|
| START | SDA 下降沿(SCL 高时) | i2c_start() |
| STOP | SDA 上升沿(SCL 高时) | i2c_stop() |
| 写字节 | MSB first,每位移完后拉高 SCL | i2c_write_byte() |
| 读字节 | 释放 SDA,每位移入,SCL 脉冲 | i2c_read_byte(ack) |
| ACK 检测 | 写完后释放 SDA,读第 9 个 SCL 脉冲的电平 | i2c_write_byte 返回值 |
// START 条件:SDA 在 SCL 高时拉低
static void i2c_start(void) {
sda_set(); // SDA 拉高
scl_set(); // SCL 拉高
soft_i2c_delay();
sda_clr(); // SDA 拉低(START)
soft_i2c_delay();
scl_clr(); // SCL 拉低(准备传输)
}
// 写一个字节,返回 ACK(0=应答,1=无应答)
static uint8_t i2c_write_byte(uint8_t byte) {
for (int i = 0; i < 8; i++) {
if ((byte & 0x80U) != 0U) sda_set();
else sda_clr();
scl_set(); // SCL 高 → 从机采样
soft_i2c_delay();
scl_clr(); // SCL 低 → 准备下一位
byte = (uint8_t)(byte << 1U);
}
sda_set(); // 释放 SDA,从机发 ACK
scl_set();
uint8_t ack = (sda_read() == GPIO_PIN_RESET) ? 0U : 1U;
scl_clr();
return ack;
}
第二层:AT24C02 协议接入
AT24C02 的设备地址固定为 0xA0(写)/ 0xA1(读)。256 字节容量,地址用 8 位,页写入粒度 8 字节:
// 随机读取:START + 写地址 + 寄存器地址 + START + 读地址 + 读数据 + STOP
static uint8_t soft_iic_read(uint8_t addr, uint8_t reg,
uint8_t *buf, uint16_t len) {
i2c_start();
if (i2c_write_byte(addr) != 0U) { i2c_stop(); return 1U; } // 设备地址
if (i2c_write_byte(reg) != 0U) { i2c_stop(); return 1U; } // 寄存器地址
i2c_start(); // 重复 START
if (i2c_write_byte(addr | 0x01U) != 0U) { i2c_stop(); return 1U; } // 读地址
for (uint16_t i = 0; i < len; i++) {
buf[i] = i2c_read_byte((i < len - 1) ? 1U : 0U); // 最后一字节 NACK
}
i2c_stop();
return 0U;
}
第三层:bsp_eeprom 统一接口
typedef enum {
BSP_EEPROM_STATUS_OK = 0,
BSP_EEPROM_STATUS_ERROR = 1,
BSP_EEPROM_STATUS_INVALID_ARG = 2,
BSP_EEPROM_STATUS_NOT_READY = 3,
BSP_EEPROM_STATUS_OUT_OF_RANGE = 4
} bsp_eeprom_status_t;
bsp_eeprom_status_t bsp_eeprom_init(void);
bsp_eeprom_status_t bsp_eeprom_deinit(void);
bsp_eeprom_status_t bsp_eeprom_read(uint16_t address, uint8_t *buf, uint16_t len);
bsp_eeprom_status_t bsp_eeprom_write(uint16_t address, const uint8_t *buf, uint16_t len);
BSP 接口设计原则(从本例看通用规则)
这个 EEPROM 驱动展示了 BSP 层设计的四个核心原则:
原则一:用枚举取代魔法数字
// ❌ 坏味道
if (eeprom_write(addr, buf, len) != 0) { error }
// ✅ 可读的错误码
if (bsp_eeprom_write(addr, buf, len) != BSP_EEPROM_STATUS_OK) { error }
原则二:参数边界检查在接口层
bsp_eeprom_status_t bsp_eeprom_write(uint16_t address, const uint8_t *buf, uint16_t len) {
// 入参检查
if ((buf == NULL) || (len == 0U)) return BSP_EEPROM_STATUS_INVALID_ARG;
// 范围检查
if ((address + len) > BSP_EEPROM_SIZE_BYTES) return BSP_EEPROM_STATUS_OUT_OF_RANGE;
// 就绪检查
if (!s_initialized) return BSP_EEPROM_STATUS_NOT_READY;
// 实际写入...
}
原则三:软件 I2C 用开漏输出
PA11/PA12 配置为开漏输出(GPIO_MODE_OUTPUT_OD),不需要切换输入输出方向——读 SDA 时直接读输入寄存器,写 SDA 时写输出寄存器。这是 GPIO 模拟 I2C 的正确做法。
原则四:分三层隔离
| 层 | 文件 | 职责 |
|---|---|---|
| 硬件时序 | bsp_eeprom.c(前 100 行) | GPIO 电平、延时、START/STOP/字节 |
| 芯片协议 | (bsp_at24cxx.c) | AT24C02 读写命令序列、页写拆分 |
| 模块接口 | bsp_eeprom.h | init/read/write/deinit + 错误码 |
换芯片(AT24C02→AT24C04/AT24C128)只需要改芯片协议层和容量常量,时序层和接口层都不动。
Comments NOTHING