
SPI是飞控中IMU的首选通信总线——高速、全双工、适合高频数据采集。FMT基于RT-Thread的SPI框架实现了总线管理、设备挂载、互斥锁保护、自动重配置等机制,支持多个传感器共享同一条SPI总线。本文从SPI协议出发,讲解FMT的SPI框架设计和驱动开发流程。
1. SPI协议基础
1.1 物理层
SPI使用四根线:
- SCLK:时钟,由主机控制
- MOSI:主机出从机入
- MISO:主机入从机出
- CS:片选,每个从机一根
┌──────┐
MCU ─────┤ SCLK ├───── Sensor1
MCU ─────┤ MOSI ├───── Sensor1
MCU ─────┤ MISO ├───── Sensor1
MCU ─────┤ CS1 ├───── Sensor1
MCU ─────┤ CS2 ├───── Sensor2
└──────┘
1.2 四种模式
| 模式 | CPOL | CPHA | 含义 |
|---|---|---|---|
| Mode 0 | 0 | 0 | 空闲低电平,第一个边沿采样 |
| Mode 1 | 0 | 1 | 空闲低电平,第二个边沿采样 |
| Mode 2 | 1 | 0 | 空闲高电平,第一个边沿采样 |
| Mode 3 | 1 | 1 | 空闲高电平,第二个边沿采样 |
// spi.h
#define RT_SPI_CPHA (1 << 0) // 时钟相位
#define RT_SPI_CPOL (1 << 1) // 时钟极性
#define RT_SPI_MODE_0 (0 | 0) // CPOL=0, CPHA=0
#define RT_SPI_MODE_1 (0 | RT_SPI_CPHA) // CPOL=0, CPHA=1
#define RT_SPI_MODE_2 (RT_SPI_CPOL | 0) // CPOL=1, CPHA=0
#define RT_SPI_MODE_3 (RT_SPI_CPOL | RT_SPI_CPHA) // CPOL=1, CPHA=1
1.3 FMT中的SPI传感器
注意:以下"芯片支持的模式"指芯片数据手册标注的SPI兼容模式,"FMT实际配置"指FMT驱动代码中实际设置的模式。FMT统一将所有SPI传感器配置为 Mode 3,时钟 7MHz(除TF卡使用Mode 0/10MHz外)。
| 传感器 | 芯片 | 芯片支持的模式 | FMT实际配置 |
|---|---|---|---|
| IMU | BMI088 | Mode 0/3 | Mode 3, 7MHz |
| IMU | ICM42688P | Mode 0/3 | Mode 3, 7MHz |
| IMU | ICM20689 | Mode 0/3 | Mode 3 |
| IMU | ICM20600 | Mode 0/3 | Mode 3 |
| 气压计 | MS5611 | Mode 0/3 | Mode 3, 7MHz |
| 气压计 | SPL06 | Mode 0/3 | Mode 3, 7MHz |
| 气压计 | BMP581 | Mode 0/3 | Mode 3, 7MHz |
| 磁力计 | BMM150 | Mode 0/3 | Mode 3, 7MHz |
| 磁力计 | RM3100 | Mode 0/3 | Mode 3, 1MHz |
| Flash | GD25QXX | Mode 0/3 | Mode 3, 7MHz |
| FRAM | RAMTRON | Mode 0/3 | Mode 3, 7MHz |
| TF卡 | SPI TF Card | Mode 0/3 | Mode 0, 10MHz |
2. 框架架构
┌─────────────────────────────────────────────┐
│ 传感器驱动层 │
│ BMI088 / ICM42688P / MS5611 / GD25QXX │
├─────────────────────────────────────────────┤
│ SPI便捷函数 │
│ spi_read_reg8 / spi_write_reg8 │
│ spi_read_multi_reg8 / spi_read_bank_reg8 │
├─────────────────────────────────────────────┤
│ SPI核心层 (spi_core) │
│ rt_spi_transfer / rt_spi_send_then_recv │
│ rt_spi_take_bus / rt_spi_release_bus │
│ 互斥锁 + 自动重配置 │
├─────────────────────────────────────────────┤
│ SPI总线驱动 (BSP) │
│ configure / xfer 操作函数表 │
└─────────────────────────────────────────────┘
3. 总线结构
3.1 总线与设备
// spi.h
struct rt_spi_bus {
struct rt_device parent; // 设备基类
const struct rt_spi_ops* ops; // 操作函数表
struct rt_mutex lock; // 互斥锁(多设备共享保护)
struct rt_spi_device* owner; // 当前总线所有者
};
struct rt_spi_device {
struct rt_device parent; // 设备基类
struct rt_spi_bus* bus; // 所属总线
struct rt_spi_configuration config; // 设备配置
};
struct rt_spi_configuration {
rt_uint8_t mode; // SPI模式
rt_uint8_t data_width; // 数据宽度
rt_uint16_t reserved;
rt_uint32_t max_hz; // 最大时钟频率
};
3.2 操作函数表
// spi.h
struct rt_spi_ops {
rt_err_t (*configure)(struct rt_spi_device* dev, struct rt_spi_configuration* config);
rt_uint32_t (*xfer)(struct rt_spi_device* dev, struct rt_spi_message* message);
};
BSP层只需实现 configure 和 xfer 两个函数,即可完成SPI总线驱动。
4. 总线注册与设备挂载
4.1 注册SPI总线
在BSP初始化中注册SPI总线:
// drv_spi.c - SIEON S1
static struct rt_spi_ops stm32_spi_ops = { configure, transfer };
rt_err_t drv_spi_init(void)
{
// 注册SPI总线
static struct stm32_spi_bus stm32_spi1;
RT_TRY(stm32_spi_register(SPI1, &stm32_spi1, "spi1"));
static struct stm32_spi_bus stm32_spi2;
RT_TRY(stm32_spi_register(SPI2, &stm32_spi2, "spi2"));
static struct stm32_spi_bus stm32_spi4;
RT_TRY(stm32_spi_register(SPI4, &stm32_spi4, "spi4"));
static struct stm32_spi_bus stm32_spi5;
RT_TRY(stm32_spi_register(SPI5, &stm32_spi5, "spi5"));
// ... 挂载SPI设备 ...
return RT_EOK;
}
stm32_spi_register 内部调用 rt_spi_bus_register,完成总线注册、互斥锁初始化和GPIO配置。
4.2 挂载SPI设备
每个SPI从机需要挂载到总线上,通过CS引脚区分。FMT使用 stm32_spi_cs 结构体封装CS的GPIO端口和引脚:
// drv_spi.c - SIEON S1 BSP层SPI设备挂载
struct stm32_spi_cs {
GPIO_TypeDef* GPIOx;
uint16_t GPIO_Pin;
};
// SPI1总线上挂载1个设备(气压计)
static struct rt_spi_device rt_spi1_device_1;
static struct stm32_spi_cs stm32_spi1_cs_1; // PB1
rt_spi_bus_attach_device(&rt_spi1_device_1, "spi1_dev1", "spi1", (void*)&stm32_spi1_cs_1);
// SPI4总线上挂载4个设备(IMU + 磁力计)
static struct rt_spi_device rt_spi4_device_1, rt_spi4_device_2, rt_spi4_device_3, rt_spi4_device_4;
static struct stm32_spi_cs stm32_spi4_cs_1; // PC13 - BMI088 Gyro
static struct stm32_spi_cs stm32_spi4_cs_2; // PI8 - BMI088 Accel
static struct stm32_spi_cs stm32_spi4_cs_3; // PE3 - ICM42688P
static struct stm32_spi_cs stm32_spi4_cs_4; // PI4 - BMM150
rt_spi_bus_attach_device(&rt_spi4_device_1, "spi4_dev1", "spi4", (void*)&stm32_spi4_cs_1);
rt_spi_bus_attach_device(&rt_spi4_device_2, "spi4_dev2", "spi4", (void*)&stm32_spi4_cs_2);
rt_spi_bus_attach_device(&rt_spi4_device_3, "spi4_dev3", "spi4", (void*)&stm32_spi4_cs_3);
rt_spi_bus_attach_device(&rt_spi4_device_4, "spi4_dev4", "spi4", (void*)&stm32_spi4_cs_4);
// SPI5总线上挂载1个设备(Flash)
static struct rt_spi_device rt_spi5_device_1;
static struct stm32_spi_cs stm32_spi5_cs_1; // PF10
rt_spi_bus_attach_device(&rt_spi5_device_1, "spi5_dev1", "spi5", (void*)&stm32_spi5_cs_1);
BSP层的 transfer 函数通过 user_data 获取CS引脚,执行GPIO操作:
// drv_spi.c - transfer函数中的CS控制
struct stm32_spi_cs* stm32_spi_cs = device->parent.user_data;
if (message->cs_take) {
LL_GPIO_ResetOutputPin(stm32_spi_cs->GPIOx, stm32_spi_cs->GPIO_Pin); // 拉低CS
}
// ... SPI数据传输 ...
if (message->cs_release) {
LL_GPIO_SetOutputPin(stm32_spi_cs->GPIOx, stm32_spi_cs->GPIO_Pin); // 拉高CS
}
4.3 配置SPI设备
驱动初始化时配置SPI参数。注意FMT中所有SPI传感器统一使用 Mode 3:
// icm42688p.c
rt_err_t drv_icm42688_init(const char* spi_dev_name, const char* gyro_dev_name,
const char* accel_dev_name, uint32_t dev_id)
{
spi_dev = rt_device_find(spi_dev_name);
RT_ASSERT(spi_dev != NULL);
/* 配置SPI参数 */
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MODE_3 | RT_SPI_MSB; // FMT统一使用Mode 3
cfg.max_hz = 7000000; // 7MHz
struct rt_spi_device* spi_device_t = (struct rt_spi_device*)spi_dev;
// 直接写入设备配置(不等待下次传输时才生效)
spi_device_t->config.data_width = cfg.data_width;
spi_device_t->config.mode = cfg.mode & RT_SPI_MODE_MASK;
spi_device_t->config.max_hz = cfg.max_hz;
RT_TRY(rt_spi_configure(spi_device_t, &cfg));
/* 底层初始化(寄存器配置) */
RT_TRY(low_level_init());
/* 注册陀螺仪和加速度计HAL设备 */
RT_TRY(hal_gyro_register(&gyro_dev, gyro_dev_name, RT_DEVICE_FLAG_RDWR, (void*)dev_id));
RT_TRY(hal_accel_register(&accel_dev, accel_dev_name, RT_DEVICE_FLAG_RDWR, (void*)dev_id));
return RT_EOK;
}
5. 互斥锁与自动重配置
5.1 多设备共享保护
当多个设备共享同一条SPI总线时,需要互斥访问:
// spi_core.c - rt_spi_transfer()
rt_size_t rt_spi_transfer(struct rt_spi_device* device,
const void* send_buf,
void* recv_buf,
rt_size_t length)
{
// 1. 获取总线锁
result = rt_mutex_take(&(device->bus->lock), RT_WAITING_FOREVER);
if (result == RT_EOK) {
// 2. 检查是否需要重配置
if (device->bus->owner != device) {
result = device->bus->ops->configure(device, &device->config);
if (result == RT_EOK) {
device->bus->owner = device; // 更新总线所有者
}
}
// 3. 执行传输
message.send_buf = send_buf;
message.recv_buf = recv_buf;
message.length = length;
message.cs_take = 1;
message.cs_release = 1;
result = device->bus->ops->xfer(device, &message);
// 4. 释放总线锁
rt_mutex_release(&(device->bus->lock));
}
return result;
}
5.2 自动重配置
为什么需要自动重配置? 不同传感器可能使用不同的SPI模式或时钟频率。当总线从设备A切换到设备B时,需要重新配置总线参数。
设备A (Mode 0, 10MHz) 使用总线
│
▼
设备B (Mode 3, 1MHz) 请求总线
│
├── 检测到 owner != deviceB
├── 重新调用 configure(deviceB, ...)
├── owner = deviceB
▼
设备B 正常传输
owner 字段记录当前总线配置对应的设备,避免不必要的重配置。
6. SPI消息结构
// spi.h
struct rt_spi_message {
const void* send_buf; // 发送缓冲区
void* recv_buf; // 接收缓冲区
rt_size_t length; // 数据长度
struct rt_spi_message* next; // 下一条消息(链表)
unsigned cs_take : 1; // 是否拉低CS(开始传输)
unsigned cs_release : 1; // 是否拉高CS(结束传输)
};
链表消息:多条消息可以串联,实现一次CS周期内的复杂传输。
7. 便捷读写函数
7.1 寄存器读写
SPI传感器通常使用"寄存器地址+数据"的通信格式,地址最高位表示读/写方向:
// spi.h
#define SPI_DIR_READ 0x80 // 读方向位
#define SPI_DIR_WRITE 0x00 // 写方向位
// 写寄存器
rt_inline rt_err_t spi_write_reg8(rt_device_t spi_device, uint8_t reg, uint8_t val)
{
uint8_t buffer[2];
buffer[0] = SPI_DIR_WRITE | reg; // 寄存器地址(写方向)
buffer[1] = val; // 数据
rt_size_t w_byte = rt_spi_transfer((struct rt_spi_device*)spi_device, buffer, NULL, 2);
return (w_byte == 2) ? RT_EOK : RT_ERROR;
}
// 读寄存器
rt_inline rt_err_t spi_read_reg8(rt_device_t spi_device, uint8_t reg, uint8_t* buffer)
{
uint8_t reg_addr;
reg_addr = SPI_DIR_READ | reg; // 寄存器地址(读方向)
return rt_spi_send_then_recv((struct rt_spi_device*)spi_device,
®_addr, 1, buffer, 1);
}
// 读多个连续寄存器
rt_inline rt_err_t spi_read_multi_reg8(rt_device_t spi_device, uint8_t reg,
uint8_t* buffer, uint8_t len)
{
uint8_t reg_addr;
reg_addr = SPI_DIR_READ | reg;
return rt_spi_send_then_recv((struct rt_spi_device*)spi_device,
®_addr, 1, buffer, len);
}
7.2 Bank寄存器读写
ICM42688P等传感器使用Bank选择寄存器,需要先切换Bank再读写:
// spi.h
rt_inline rt_err_t spi_read_bank_reg8(rt_device_t spi_device,
uint8_t bank_reg, uint8_t bank,
uint8_t reg, uint8_t* buffer)
{
spi_write_reg8(spi_device, bank_reg, bank); // 切换Bank
return spi_read_reg8(spi_device, reg, buffer); // 读取寄存器
}
8. 驱动开发流程
以ICM42688P为例,完整的SPI驱动开发流程:
8.1 查找设备
spi_dev = rt_device_find("spi4_dev3");
8.2 配置SPI参数
struct rt_spi_configuration cfg;
cfg.data_width = 8;
cfg.mode = RT_SPI_MODE_3 | RT_SPI_MSB; // FMT统一使用Mode 3
cfg.max_hz = 7000000; // 7MHz
rt_spi_configure((struct rt_spi_device*)spi_dev, &cfg);
8.3 读取设备ID
uint8_t dev_id;
RT_TRY(spi_read_reg8(spi_dev, REG_WHO_AM_I, &dev_id));
if (dev_id != 0x47) {
DRV_DBG("ICM42688P wrong device id:0x%x\n", dev_id);
return RT_ENOSYS;
}
8.4 配置寄存器
// 切换到Bank0
spi_write_reg8(spi_dev, REG_REG_BANK_SEL, 0);
// 配置陀螺仪量程和采样率
spi_write_reg8(spi_dev, REG_PWR_MGMT0, 0x0F); // 使能陀螺仪+加速度计
spi_write_reg8(spi_dev, REG_GYRO_CONFIG0, ...);
spi_write_reg8(spi_dev, REG_ACCEL_CONFIG0, ...);
8.5 读取数据
uint8_t raw[6];
float gyro[3];
// 读取陀螺仪6字节原始数据
RT_TRY(spi_read_multi_reg8(spi_dev, REG_GYRO_DATA_X1, raw, 6));
// 使用int16_t_from_bytes辅助函数拼接为16位有符号整数
// 定义在 conversion.h: int16_t int16_t_from_bytes(uint8_t bytes[]);
gyro[0] = int16_t_from_bytes(&raw[0]);
gyro[1] = int16_t_from_bytes(&raw[2]);
gyro[2] = int16_t_from_bytes(&raw[4]);
8.6 写后读校验
FMT的IMU驱动使用"写后读校验"确保寄存器配置正确。不同传感器的实现略有差异:
ICM42688P版本(读一次校验):
// icm42688p.c
static rt_err_t __write_checked_reg(rt_device_t spi_device, rt_uint8_t reg, rt_uint8_t val)
{
rt_uint8_t r_val;
RT_TRY(spi_write_reg8(spi_device, reg, val));
RT_TRY(spi_read_reg8(spi_device, reg, &r_val));
return (r_val == val) ? RT_EOK : RT_ERROR;
}
BMI088版本(读两次校验,因为BMI088加速度计部分的SPI读操作会先返回一个dummy byte):
// bmi088.c
static rt_err_t __write_checked_reg(rt_device_t spi_device, rt_uint8_t reg, rt_uint8_t val)
{
rt_uint8_t r_val;
RT_TRY(spi_write_reg8(spi_device, reg, val));
/* BMI088加速度计读操作会先发送一个dummy字节,
之后才是实际的寄存器内容 */
RT_TRY(spi_read_reg8(spi_device, reg, &r_val));
RT_TRY(spi_read_reg8(spi_device, reg, &r_val));
return (r_val == val) ? RT_EOK : RT_ERROR;
}
9. SIEON S1的SPI总线布局
SPI1 ─── SPL06 (气压计)
└── spi1_dev1 (PB1)
SPI2 ─── EXT_CS1 (外部扩展)
├── spi2_dev1 (PG10)
├── EXT_CS2 (外部扩展)
├── spi2_dev2 (PG11)
└── EXT_CS3 (外部扩展)
└── spi2_dev3 (PG12)
SPI4 ─── BMI088 Gyro (IMU)
├── spi4_dev1 (PC13)
├── BMI088 Accel (IMU)
├── spi4_dev2 (PI8)
├── ICM42688P (冗余IMU)
├── spi4_dev3 (PE3)
└── BMM150 (磁力计)
└── spi4_dev4 (PI4)
SPI5 ─── GD25QXX (Flash)
└── spi5_dev1 (PF10)
SPI4总线上挂载了4个设备(2个IMU芯片共3个传感器 + 1个磁力计),通过互斥锁和自动重配置机制实现安全共享。SPI2提供3个外部扩展CS引脚,可用于外接SPI传感器。
10. 与I2C对比
| 特性 | SPI | I2C |
|---|---|---|
| 速度 | 高(MHz级) | 低(KHz级) |
| 线数 | 4根(SCLK/MOSI/MISO/CS) | 2根(SCL/SDA) |
| 全双工 | 是 | 否 |
| 多设备 | CS片选 | 地址寻址 |
| 适用场景 | IMU、Flash、高频传感器 | 低速外设、导出接口 |
在SIEON S1中,板载IMU(BMI088/ICM42688P)、磁力计(BMM150)、气压计(SPL06)和Flash(GD25QXX)全部使用SPI。I2C总线主要用于外部扩展接口(如注释中提到的QMC5883L磁力计 i2c1_dev2)。
总结
FMT SPI总线框架的核心设计要点:
| 设计 | 原因 | 实现 |
|---|---|---|
| 总线/设备分离 | 多个传感器共享SPI总线 | rt_spi_bus + rt_spi_device |
| 互斥锁保护 | 防止并发访问冲突 | rt_mutex lock |
| 自动重配置 | 不同设备使用不同模式/频率 | owner 字段检测 |
| 操作函数表 | BSP层只需实现底层驱动 | configure + xfer |
| 便捷读写函数 | 简化寄存器操作 | spi_read_reg8/write_reg8 |
| Bank寄存器支持 | ICM系列多Bank访问 | spi_read_bank_reg8 |


被折叠的 条评论
为什么被折叠?



