大型量产固件的工程实践(九):GPIO 模拟 SPI 驱动设计
大型量产固件的工程实践九GPIO 模拟 SPI 驱动设计本文是《大型量产固件的工程实践》专栏第 9 篇。上一篇第 8 篇《多款同类型芯片驱动的统一抽象》 下一篇第 9 篇实战《GPIO 模拟 SPI 实现源码与使用范例》一、为什么需要 GPIO 模拟 SPISPI 是嵌入式最常用的外设总线之一。但很多场景下硬件 SPI 不够用硬件 SPI 通道被占用或干脆没有多余通道非字节对齐地址某些芯片的寄存器地址是 9-bit、10-bit而硬件 SPI 通常只支持 8-bit 的倍数时序特殊需要自定义 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_bytestx_bit_len3;for(i0;ifull_bytes;i)for(bit0;bit8;bit)send_one_bit(...);remain_bitstx_bit_len0x07;for(i0;iremain_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 CPHA0数据在第一个边沿变化在第二个边沿采样/* 输出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 CPHA1数据在第一个边沿采样在第二个边沿变化/* 输出先换 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_bitcpha?out_bit_cpha1:out_bit_cpha0;收益static inline保证零函数调用开销函数指针分派保证循环体里没有 per-bit 的 if 分支——这对 bit-bang 这种每个 bit 都要快的场景很重要。五、关键细节5.1 CPOL 只影响空闲电平CPOL 决定时钟空闲时是高还是低但活动电平始终是空闲电平的取反可以从空闲电平推导sck_activesck_idle^1;// 活动电平 空闲电平取反所以 4 个时序函数只需关心active/idle 两个电平CPOL 通过把哪一档当成空闲来体现。5.2 LSB/MSB 可配置bit 的提取/写入方向可配/* lsb_first1 从最低位开始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_len7)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/W1)放在最高位 */uint16_tcmdaddr|0x8000;tx[0](uint8_t)(cmd8);// 地址高字节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 的通用驱动设计方法不涉及任何具体项目或产品的私有实现。

相关新闻