☰
大型量产固件的工程实践(九):GPIO 模拟 SPI 驱动设计
2026/9/25 17:38:31 网站建设 项目流程

大型量产固件的工程实践(九):GPIO 模拟 SPI 驱动设计

本文是《大型量产固件的工程实践》专栏第 9 篇。
上一篇:第 8 篇《多款同类型芯片驱动的统一抽象》 | 下一篇:第 9 篇实战《GPIO 模拟 SPI 实现源码与使用范例》


一、为什么需要 GPIO 模拟 SPI

SPI 是嵌入式最常用的外设总线之一。但很多场景下,硬件 SPI 不够用:

  1. 硬件 SPI 通道被占用,或干脆没有多余通道;
  2. 非字节对齐地址:某些芯片的寄存器地址是 9-bit、10-bit,而硬件 SPI 通常只支持 8-bit 的倍数;
  3. 时序特殊:需要自定义 CPOL/CPHA、时钟极慢、或插入特定延时。

这时就要用GPIO 位绑(bit-bang)模拟 SPI——用普通 GPIO 引脚,按 SPI 时序手动翻转时钟线、读写数据线。

本文给出一个通用、可复用的 bit-bang SPI 驱动设计,核心亮点是:任意位宽传输 + CPOL/CPHA 四套时序 + 驱动/协议分层。


二、设计总览:驱动层与协议层分离

一个关键设计原则:驱动层只做"纯 SPI 位绑收发",不掺任何具体芯片的协议。

协议层(每个芯片一套) 驱动层(通用,只此一套) ┌──────────────────────┐ ┌──────────────────────────┐ │ 组装芯片特定命令格式 │ │ 只负责时钟线 + 数据线翻转 │ │ 如:R/W 标志 + 地址编码│ ──▶ │ 不关心地址是 9-bit 还是 │ │ 决定先发哪些 bit │ │ 16-bit,也不关心 R/W 语义 │ └──────────────────────┘ └──────────────────────────┘

这样换一颗芯片,只需改协议层的"命令组装",驱动层零改动。


三、通用 transfer 接口:任意位宽

驱动层对外只暴露一个传输接口,发送和接收的位宽可以不同:

/* 发送 txBitLen 位,接收 rxBitLen 位(可只发不收,或只收不发) */voidhwspi_transfer(constuint8_t*tx_buf,uint8_ttx_bit_len,uint8_t*rx_buf,uint8_trx_bit_len);
  • tx_bit_len/rx_bit_len支持1~32 bit 任意值,不受字节对齐限制;
  • 内部把"位"拆成"完整字节 + 剩余 bit"两段处理。
/* 发送:先发完整字节,再发剩余 bit */full_bytes=tx_bit_len>>3;for(i=0;i<full_bytes;i++)for(bit=0;bit<8;bit++)send_one_bit(...);remain_bits=tx_bit_len&0x07;for(i=0;i<remain_bits;i++)send_one_bit(...);

这就是"非字节对齐地址"能直接传输的关键。


四、CPOL / CPHA 四套时序

SPI 有 4 种模式(CPOL×CPHA),它们的时钟相位和采样边沿不同。设计上用4 个static inline函数分别实现,传输前用函数指针分派选定,避免在每个 bit 里做分支判断:

/* 输出 bit 的函数指针类型 */typedefvoid(*out_bit_fn)(constuint8_t*buf,uint8_tbyte,uint8_tbit,uint8_tlsb,bool sck_active,bool sck_idle,uint8_tdly);

4.1 CPHA=0(数据在第一个边沿变化,在第二个边沿采样)

/* 输出:SCK 变 active 时换 MOSI,变 idle 前数据保持 */staticinlinevoidout_bit_cpha0(...){gpio_write(SCK,sck_active);// 时钟翻转gpio_write(MOSI,bit_value(buf,...));// 同时送出数据delay_us(dly);gpio_write(SCK,sck_idle);// 时钟回落delay_us(dly);}
/* 输入:SCK 变 active 后采样 MISO */staticinlinevoidin_bit_cpha0(...){gpio_write(SCK,sck_active);if(gpio_read(MISO))bit_set(buf,...);// 采样delay_us(dly);gpio_write(SCK,sck_idle);delay_us(dly);}

4.2 CPHA=1(数据在第一个边沿采样,在第二个边沿变化)

/* 输出:先换 MOSI,再翻转 SCK */staticinlinevoidout_bit_cpha1(...){gpio_write(MOSI,bit_value(buf,...));// 先送数据gpio_write(SCK,sck_active);delay_us(dly);gpio_write(SCK,sck_idle);// 采样发生在回落沿delay_us(dly);}
/* 输入:SCK 回落沿采样 */staticinlinevoidin_bit_cpha1(...){gpio_write(SCK,sck_active);delay_us(dly);gpio_write(SCK,sck_idle);// 回落沿if(gpio_read(MISO))bit_set(buf,...);// 此时采样delay_us(dly);}

4.3 函数指针分派

传输开始时,根据 CPHA 选一次函数指针,之后每个 bit 都走同一个函数:

out_bit_fn out_bit=cpha?out_bit_cpha1:out_bit_cpha0;

收益:static inline保证零函数调用开销,函数指针分派保证循环体里没有 per-bit 的 if 分支——这对 bit-bang 这种"每个 bit 都要快"的场景很重要。


五、关键细节

5.1 CPOL 只影响"空闲电平"

CPOL 决定时钟空闲时是高还是低,但活动电平始终是空闲电平的取反,可以从空闲电平推导:

sck_active=sck_idle^1;// 活动电平 = 空闲电平取反

所以 4 个时序函数只需关心"active/idle 两个电平",CPOL 通过"把哪一档当成空闲"来体现。

5.2 LSB/MSB 可配置

bit 的提取/写入方向可配:

/* lsb_first=1 从最低位开始,=0 从最高位开始 */uint8_tbit_value(constuint8_t*buf,uint8_tbyte,uint8_tbit,uint8_tlsb_first){return(buf[byte]>>(lsb_first?bit:(7-bit)))&0x01;}

5.3 传输流程

voidhwspi_transfer(constuint8_t*tx_buf,uint8_ttx_bit_len,uint8_t*rx_buf,uint8_trx_bit_len){gpio_write(CS,active);// 拉低片选,进入传输gpio_write(SCK,sck_idle);// 确保时钟在空闲电平if(tx_bit_len)tx_bits(tx_buf,tx_bit_len,...);// 发送阶段if(rx_bit_len){// 接收阶段memset(rx_buf,0,(rx_bit_len+7)>>3);rx_bits(rx_buf,rx_bit_len,...);}gpio_write(CS,idle);// 拉高片选,结束传输gpio_write(SCK,sck_idle);// 时钟恢复空闲}

六、使用范例:协议层组装

驱动层只管收发 bit,协议层负责"把芯片命令组装成要发的 bit"。以一颗"16-bit 地址 + 8-bit 数据、读操作最高位置 1"的芯片为例:

/* 协议层:读寄存器 —— 组装命令,然后调通用 transfer */uint16_tsensor_read_reg(uint16_taddr){uint8_ttx[3];uint8_trx[1]={0};/* 16-bit 地址,读标志(R/W=1)放在最高位 */uint16_tcmd=addr|0x8000;tx[0]=(uint8_t)(cmd>>8);// 地址高字节tx[1]=(uint8_t)(cmd);// 地址低字节/* 发 16-bit 命令,收 8-bit 数据 */hwspi_transfer(tx,16,rx,8);returnrx[0];}

注意:R/W 标志、地址位宽、字节序这些"芯片特有的东西",全在协议层处理;驱动层始终是那个通用的hwspi_transfer。


七、小结

一个通用、可复用的 bit-bang SPI 驱动,四个设计要点:

要点说明
驱动/协议分离驱动只做位绑,协议组装命令,换芯片零改驱动
任意位宽transfer支持 1~32 bit,非字节对齐地址直接传
四套时序CPOL/CPHA 用 4 个static inline+ 函数指针分派,循环体无分支
电平推导CPOL 只定空闲电平,活动电平取反推导

这套设计最大的价值在于通用性——它不绑定任何具体芯片,任何"要按位精确控制时序"的外设都能复用。下一篇,我们把视角从"位级驱动"上升到"状态级设计",聊聊嵌入式里无处不在的状态机。


本文仅介绍 GPIO 模拟 SPI 的通用驱动设计方法,不涉及任何具体项目或产品的私有实现。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询