C语言SPI虚拟总线中间层设计详解
针对嵌入式开发中GPIO模拟SPI协议的复杂性,本文解析了一个轻量级C语言中间层架构的设计思路。通过区分共享的SPI总线与独立的SPI设备结构体,引入互斥锁机制解决并发抢占问题,并利用统一的消息传递接口屏蔽底层硬件细节。该设计实现了从物理接线到数据全双工传输的完整逻辑抽象,为MCU平台的通用SPI通信提供了标准化的实现方案。
SPI协议与基础特性
SPI(Serial Peripheral Interface,串行外设接口)通常采用一主多从的拓扑结构,由四根核心信号线组成:CLK(时钟)、MISO(主机输入/从机输出)、MOSI(主机输出/从机输入)以及CS/NSS(片选)。其基本接线规则为主从设备的一一对应连接:
- 主机 CLK 连接从机 CLK
- 主机 MISO 连接从机 MISO
- 主机 MOSI 连接从机 MOSI
- 主机 CS/NSS 连接从机 CS/NSS
与传统I2C协议依赖地址码不同,SPI通过CS/NSS电平切换来选中特定设备,这使得多个从机可以挂载在同一组CLK、MISO和MOSI总线上。理论上只要拥有足够的IO口,即可无限扩展设备数量;但在实际应用中,IO资源往往稀缺,因此常结合菊花链(Daisy Chain)设计以优化引脚占用。
工作本质:SPI通信的核心在于移位寄存器与环形缓冲区。主机将待发送数据写入发送缓冲器后,在时钟脉冲驱动下,数据逐位移出至MOSI;与此同时,MISO线上的数据也会同步移入主机的接收缓冲器。这种机制天然支持全双工通信,即在发送的同时完成接收。
SPI工作模式配置
为了确保主机与从机能够正确识别数据,双方必须配置相同的通信参数,主要由时钟极性(CPOL)和时钟相位(CPHA)决定,共形成4种标准工作模式:
| 参数 | 说明 |
|---|---|
| CPOL (Clock Polarity) | 0:空闲时CLK为低电平1:空闲时CLK为高电平 |
| CPHA (Clock Phase) | 0:第一个跳变沿采样数据1:第二个跳变沿采样数据 |
组合后的四种模式如下:
| MODE | CPOL | CPHA |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 2 | 1 | 0 |
| 3 | 1 | 1 |
虚拟总线中间层架构设计
在软件实现中,直接操作底层寄存器较为繁琐且难以移植。通过定义虚拟总线中间层,可以将共享的信号线与独立的设备控制分离。设计中包含两个核心结构体与三个关键步骤:挂载、获取总线权限、释放总线。
第一步:数据结构定义
SPI总线(spi-bus):负责管理公共信号线及并发访问。由于CLK、MISO、MOSI是共享资源,必须引入互斥锁来防止数据传输时的抢占。
struct mr_soft_spi_bus
{
void (*set_clk)(mr_uint8_t level); // 操作 CLK 的函数指针
void (*set_mosi)(mr_uint8_t level); // 操作 MOSI 的函数指针
mr_uint8_t (*get_miso)(void); // 读取 MISO 的函数指针
struct mr_soft_spi *owner; // 当前该总线的所有者设备指针
mr_uint8_t lock; // 互斥锁状态标志
};
SPI设备(spi-device):负责管理特有的片选信号与工作模式。每个从机仅需绑定自己的CS/NSS引脚。
struct mr_soft_spi
{
mr_uint8_t mode :2; // SPI 工作模式 (Mode 0~3)
mr_uint8_t cs_active :1; // CS/NSS 有效电平 (0或1)
struct mr_soft_spi_bus *bus; // 指向所属的SPI总线结构体
void (*set_cs)(mr_uint8_t level); // 操作 CS/NSS 的函数指针
};
第二步:挂载与总线控制
当物理接线完成后,需先建立关联:mr_soft_spi_attach(spi, spi_bus),将设备绑定到总线上。
在使用传输功能前,必须调用mr_soft_spi_bus_take()获取总线控制权。该函数执行以下逻辑:
- 检查所有权:若当前设备已是所有者,则仅加锁并拉低片选(视电平有效情况而定)。
- 竞争获取:若非所有者,进入循环等待互斥锁变为解锁状态(
MR_UNLOCK)。 - 状态切换:一旦获得锁,将原设备(若有)的片选置为无效,关闭旧连接;随后更新
owner指针为本设备,并拉低本设备的片选信号,开始通信。
使用完毕后,调用mr_soft_spi_bus_release(spi)释放总线。它会将片选恢复为非有效状态,并将互斥锁重置为解锁,允许其他设备接入。
第三步:统一消息传输接口
为了简化上层应用逻辑,设计了标准的消息结构体mr_soft_spi_msg及传输函数mr_soft_spi_transfer:
struct mr_soft_spi_msg
{
mr_uint8_t read_write; // 读写模式:SPI_WR / SPI_RD / SPI_RDWR / SPI_WR_THEN_RD
mr_uint8_t *send_buffer; // 发送数据地址
mr_size_t send_size; // 发送数据长度
mr_uint8_t *recv_buffer; // 接收数据地址
mr_size_t recv_size; // 接收数据长度
};
transfer内部封装了具体的移位逻辑。根据read_write的类型,循环调用底层的mr_soft_spi_bus_transmit函数(用于单次字节的全双工交互),自动处理收发缓冲区指针的移动,并在所有数据传输结束后自动释放总线,确保线程安全。
C语言实战应用示例
以下代码演示如何在STM32等平台上使用该中间层进行SPI写操作:
/* 1. 配置GPIO中断接回调 */
void set_clk(mr_uint8_t level) {
GPIO_WriteBit(GPIOA, GPIO_Pin_0, level);
}
mr_uint8_t get_miso(void) {
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2);
}
void set_cs(mr_uint8_t level) {
GPIO_WriteBit(GPIOA, GPIO_Pin_3, level);
}
int main(void) {
/* 初始化系统时钟与GPIO模式(省略部分硬件初始化细节) */
/* 2. 实例化并配置SPI总线 */
struct mr_soft_spi_bus spi_bus;
spi_bus.set_clk = set_clk;
spi_bus.set_mosi = set_clk; /* 实际应为 set_mosi 函数 */
spi_bus.get_miso = get_miso;
spi_bus.lock = MR_UNLOCK;
spi_bus.owner = MR_NULL;
/* 3. 实例化并配置SPI设备 */
struct mr_soft_spi spi_device;
spi_device.mode = SPI_MODE_0;
spi_device.cs_active = LEVEL_LOW; /* 假设低电平片选有效 */
spi_device.set_cs = set_cs;
/* 4. 建立连接 */
mr_soft_spi_attach(&spi_device, &spi_bus);
/* 5. 准备发送数据 */
mr_uint8_t buffer[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
struct mr_soft_spi_msg msg;
msg.send_buffer = buffer;
msg.send_size = sizeof(buffer);
msg.recv_buffer = NULL;
msg.recv_size = 0;
msg.read_write = SPI_WR; /* 仅发送模式 */
/* 6. 执行通信 (内部自动处理取锁、传输、释放) */
mr_soft_spi_transfer(&spi_device, msg);
}
通过上述分层设计,底层只需关注单个字节的移位发送,而业务层无需关心复杂的时序与时钟翻转,极大地降低了代码维护成本。开源项目的详细实现请参考 gitee.com/chen-fanyi/mr-library/tree/master/mr-library/device/mr_soft_spi 。
以上就是C语言软件spi虚拟总线中间层设计的详细内容,更多关于C语言软件spi虚拟总线中间层的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









