3、SPI通信协议详解:SPI四线制原理、LoRa芯片SPI时序要求、寄存器读写操作流程、SPI速率与稳定性权衡
各位好,我是老张。今天咱们聊聊SPI通信协议,这是LoRa芯片打交道的基础。说实话,我见过不少工程师在SPI上栽跟头——不是速率上不去,就是数据偶尔错位。其实搞懂了原理,这些问题都能避免。
3.1 SPI四线制原理:SCK/MOSI/MISO/NSS
SPI,全称Serial Peripheral Interface,说白了就是同步串行通信。它用四根线搞定一切:
- SCK(Serial Clock):时钟线,由主机产生。我习惯叫它“心跳线”,因为所有数据都跟着它跳。
- MOSI(Master Out Slave In):主机输出,从机输入。数据从主机流向从机。
- MISO(Master In Slave Out):主机输入,从机输出。数据从从机流回主机。
- NSS(Negative Slave Select):片选线,低电平有效。用来选中你要通信的从机。
你想想看,这四根线其实就干了一件事:主机通过SCK产生时钟,在时钟的上升沿或下降沿,同时发送和接收数据。嗯,这里要注意,SPI是全双工的——发送和接收是同时进行的。
核心要点:SPI没有应答机制,主机发一个字节的同时,从机也会回一个字节。如果你只关心发送,那收到的数据直接丢弃就行。
我在项目中遇到过一个问题:有次调试LoRa模块,发现MISO上始终没数据。查了半天,原来是NSS引脚没拉低。你看,片选信号虽然简单,但忘了它,整个通信就废了。
3.2 LoRa芯片SPI时序要求
LoRa芯片(比如SX1278)对SPI时序有明确要求。我直接说重点:
- 时钟极性(CPOL)和相位(CPHA):LoRa芯片要求CPOL=0,CPHA=0,也就是模式0。时钟空闲时为低电平,数据在上升沿采样。
- 最大时钟频率:通常支持到10MHz。但我个人建议,实际使用时控制在5MHz以内,尤其是走线较长时。
- 片选建立时间:NSS拉低后,至少等待100ns再开始发送时钟。这个时间很短,但如果你用GPIO模拟SPI,很容易忽略。
我的经验:如果你用硬件SPI,记得检查MCU的SPI配置是否匹配模式0。我曾经用STM32的SPI1,默认是模式0,但换了另一款MCU,默认是模式3,结果LoRa芯片死活不响应。
为什么会这样?因为模式不匹配,数据采样点就错了。你发出去的指令,芯片根本看不懂。
3.3 寄存器读写操作流程
LoRa芯片的寄存器读写,遵循一个固定流程。我画了个图,你一看就明白:
具体流程是这样的:
- 拉低NSS:选中LoRa芯片。注意,NSS必须保持低电平直到整个操作结束。
- 发送地址字节:最高位表示读写方向(1=读,0=写),低7位是寄存器地址。
- 发送/接收数据:如果是写操作,继续发送数据字节;如果是读操作,发送一个任意字节(通常发0x00),同时从MISO读取数据。
- 拉高NSS:结束本次操作。
避坑指南:我曾经在连续读写时犯过一个错误——两次操作之间没有拉高NSS。结果LoRa芯片把第二次的地址当成了数据,寄存器值全乱了。记住:每次操作必须完整执行拉低→通信→拉高。
3.4 SPI速率与稳定性权衡
SPI速率越高,通信越快,但稳定性会下降。这个道理谁都懂,但实际怎么选?我分享几个经验:
| 速率范围 | 适用场景 | 注意事项 |
|---|---|---|
| 1MHz以下 | 长线通信(>10cm)、抗干扰要求高 | 几乎不会出错,但速度慢 |
| 1MHz ~ 5MHz | 大多数LoRa应用场景 | 我推荐的折中方案,稳定性和速度都OK |
| 5MHz ~ 10MHz | 短距离、低噪声环境 | 需要留意PCB走线,避免串扰 |
| 10MHz以上 | 不推荐用于LoRa芯片 | 容易丢数据,得不偿失 |
你可能会问:为什么速率高了就不稳定?说白了,SPI是同步通信,时钟和数据必须严格对齐。速率高了,信号上升沿变陡,反射和串扰的影响就大了。尤其是MISO和MOSI两条线挨着走,互相干扰是常有的事。
我的建议:如果你不确定选多少速率,直接上2MHz。这个速率在大多数MCU上都能稳定工作,LoRa芯片也完全吃得消。等系统调通了,再考虑要不要提速率。
嗯,这里还要提一句:SPI速率不是越高越好。LoRa芯片本身处理数据的速度有限,你发太快了,它可能来不及响应。我见过有人把SPI调到8MHz,结果读回来的数据全是0xFF——因为芯片根本没准备好。
3.5 实际代码示例
最后,给一段我常用的SPI读写代码(基于STM32 HAL库)。你直接拿去用,注意改一下SPI句柄:
// SPI读写一个字节
uint8_t SPI_ReadWriteByte(uint8_t data)
{
uint8_t rx_data;
HAL_SPI_TransmitReceive(&hspi1, &data, &rx_data, 1, 100);
return rx_data;
}
// 写寄存器
void LoRa_WriteReg(uint8_t addr, uint8_t data)
{
HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_RESET); // 拉低NSS
SPI_ReadWriteByte(addr & 0x7F); // 写操作,地址最高位为0
SPI_ReadWriteByte(data); // 写入数据
HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_SET); // 拉高NSS
}
// 读寄存器
uint8_t LoRa_ReadReg(uint8_t addr)
{
uint8_t data;
HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_RESET); // 拉低NSS
SPI_ReadWriteByte(addr | 0x80); // 读操作,地址最高位为1
data = SPI_ReadWriteByte(0x00); // 发送任意数据,读取返回值
HAL_GPIO_WritePin(NSS_GPIO_Port, NSS_Pin, GPIO_PIN_SET); // 拉高NSS
return data;
}
这段代码我用了好几年,从来没出过问题。你唯一要确认的是:NSS引脚在初始化时一定要拉高,否则芯片一开始就被选中了,后续操作会乱套。