C语言实现无依赖软件SPI:普通GPIO也能驱动高速ADC芯片
没有硬件SPI外设、或者SPI总线被LCD/Flash占满时,用普通GPIO模拟SPI是兜底方案。只要按CPOL/CPHA把时钟沿和采样点写对,SAR ADC在几十kHz~几百kHz SCK下能稳定出数;再配合寄存器直写和批量读,可进一步逼近芯片上限。
一、先抽象底层IO,做到无库可移植
不依赖HAL/RTOS,只用“写寄存器/置位宏”。以通用寄存器映射为例:
typedef struct {
volatile uint32_t *dir; // 方向寄存器
volatile uint32_t *set; // 输出置1
volatile uint32_t *clr; // 输出清0
volatile uint32_t *in; // 输入电平
uint8_t sck, mosi, miso, cs;
uint8_t cpol, cpha; // 0/0或1/1常用
} soft_spi_t;
static inline void pin_set(volatile uint32_t *s, uint8_t b){ *s |= (1u<<b); }
static inline void pin_clr(volatile uint32_t *s, uint8_t b){ *s &= ~(1u<<b); }
static inline uint8_t pin_get(volatile uint32_t *i, uint8_t b){ return (*i>>b)&1u; }
void sspi_init(soft_spi_t *s){
*s->dir |= (1u<<s->sck)|(1u<<s->mosi)|(1u<<s->cs);
*s->dir &= ~(1u<<s->miso);
pin_set(s->set, s->cs); // CS空闲高
pin_set(s->set, s->sck); if(!s->cpol) pin_clr(s->clr, s->sck);
}
不同MCU把dir/set/clr/in换成对应端口寄存器即可,应用层不动。
二、模式0/模式3单字节交换
SPI四模式由CPOL(空闲时钟)和CPHA(采样边沿)决定:模式0空闲低、上升沿采样;模式3空闲高、上升沿采样。 ADC常用模式0,少数高速片用模式3。统一写一个交换函数:
static inline void delay_ns(uint32_t n){ // 按主频折算空循环
for(volatile uint32_t i=0;i<n;i++);
}
uint8_t sspi_xchg(soft_spi_t *s, uint8_t tx){
uint8_t rx=0;
uint8_t idle = s->cpol ? 1:0;
uint8_t lead = s->cpha ? 1:0; // 先翻时钟再采样=CPHA1
for(uint8_t bit=0; bit<8; bit++){
if(!lead) { // 采样前沿前先放数据
(tx&0x80)?pin_set(s->set,s->mosi):pin_clr(s->clr,s->mosi);
}
idle? pin_clr(s->clr,s->sck) : pin_set(s->set,s->sck); // 产生采样沿
delay_ns(2);
rx<<=1; if(pin_get(s->in,s->miso)) rx|=1;
idle? pin_set(s->set,s->sck) : pin_clr(s->clr,s->sck); // 释放沿
delay_ns(2);
tx<<=1;
}
return rx;
}
模式0传cpol=0,cpha=0、模式3传cpol=1,cpha=1,其余由lead/idle自动切换。若只跑一种ADC,把if判断展开成专用函数,能少走分支、提速率。
三、驱动SAR ADC:以单/多通道为例
很多SPI SAR(如MCP3201单通道、MCP3204/3208多通道、TLC2543多通道)都是“发命令字节+读12/10/16位”。多通道示例:
uint16_t adc_read_ch(soft_spi_t *s, uint8_t ch, uint8_t bits){
uint16_t val=0;
pin_clr(s->clr, s->cs);
if(bits==12){ // 类MCP3204/3208帧
sspi_xchg(s, 0x06|(ch>>2)); // start=1,diff=0,高1位地址
val = (sspi_xchg(s, ch<<6) & 0x0F)<<8; // 取低4位高半
val |= sspi_xchg(s, 0x00); // 低8位
} else { // 类TLC2543帧
sspi_xchg(s, (ch<<4)|0x0C); // 16clk,12bit,msb
sspi_xchg(s, 0x00);
val = sspi_xchg(s, 0x00); // 依芯片调整返回帧
}
pin_set(s->set, s->cs);
return val;
}
只读单通道、无地址的ADC可省命令字节,CS拉低后直接时钟出数据,进一步省时间。
四、提速四招
直写寄存器不用库函数:HAL_GPIO_WritePin类有函数调用和判断开销,BSRR/set-clr寄存器直写更快;关键路径用static inline。
最短合法延时:SCK高低脉宽按ADC手册最小t{SCK,H}/t{SCK,L}取整,空循环用主频标定;不盲目加for(i<100)。
批量连续读:多通道轮询时,全部读完再拉高CS,避免每通道重新片选;换通道只发地址字节,不重初始化。
中断临界:高速采样时在xchg外层关全局中断或提高BASEPRI,防止ISR插入导致时钟拉伸;若ADC有DRDY/IDLE,用GPIO中断触发批量搬数,再交给主循环处理。
五、边界与替代
软件SPI速率受CPU主频、GPIO翻转和延时精度限制,一般适用于几十k~几百k SCK;若ADC要求1MHz以上、或多通道持续百kSPS,软件SPI会吃满CPU且抖动大,应改硬件SPI+DMA,或用FPGA做串并转换。 调试先用逻辑分析仪抓CS/SCK/MOSI/MISO,确认模式、MSB、建立保持时间,再谈“高速”。
把上述soft_spi_t做成全局句柄,ADC驱动只调用xchg和cs控制,换芯片只改adc_read_ch的命令帧,换MCU只改pin宏,基本能做到无依赖、可复用。





