1. SPI通信基础解析
SPI(Serial Peripheral Interface)作为一种高速串行通信协议,在嵌入式系统中扮演着重要角色。我第一次接触SPI是在调试一个温湿度传感器项目时,当时被它简洁高效的特性所吸引。与I2C相比,SPI不需要复杂的地址分配和应答机制,仅需四根线就能实现高速数据传输。
1.1 SPI核心特性
SPI最显著的特点是它的全双工通信能力。这意味着主从设备可以同时发送和接收数据,就像两个人在打电话时可以同时说话和听对方讲话一样。这种特性使得SPI特别适合需要快速数据交换的应用场景。
在实际项目中,我发现SPI的同步特性带来了很大优势。由于数据传输由主设备提供的时钟信号同步,不需要像UART那样精确匹配波特率。记得有一次调试,我只需要调整时钟分频系数就能轻松适配不同速度的外设。
SPI的主从架构设计也很巧妙。一个主设备可以管理多个从设备,通过独立的片选信号进行选择。这就像老师(主设备)在课堂上点名提问学生(从设备)一样,每次只与一个学生交流,但可以管理整个班级。
1.2 硬件接口详解
标准的SPI接口使用四线制:
- SCLK(Serial Clock):时钟信号,由主设备产生
- MOSI(Master Out Slave In):主设备发送,从设备接收
- MISO(Master In Slave Out):从设备发送,主设备接收
- SS/CS(Slave Select/Chip Select):片选信号,低电平有效
在我的项目经验中,引脚配置是最容易出错的地方。曾经因为把MOSI和MISO接反,调试了一整天。后来养成了习惯:每次连接SPI设备前,都要再三确认引脚定义。
特别提醒:不同厂商对MOSI/MISO的命名可能不同。比如有些Flash芯片使用DI(Data In)和DO(Data Out)。实际连接时要遵循"主出从入,主入从出"的原则。
2. SPI工作模式深度剖析
2.1 四种传输模式
SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)两个参数决定:
| 模式 | CPOL | CPHA | 时钟空闲状态 | 数据采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 低电平 | 上升沿 |
| 1 | 0 | 1 | 低电平 | 下降沿 |
| 2 | 1 | 0 | 高电平 | 下降沿 |
| 3 | 1 | 1 | 高电平 | 上升沿 |
选择哪种模式完全取决于从设备的要求。以常见的Flash芯片为例:
- W25Q系列通常使用模式0或模式3
- ADXL345加速度计使用模式3
- RFM69无线模块使用模式0
重要提示:务必查阅从设备的数据手册确定正确的SPI模式。模式不匹配是SPI通信失败的常见原因。
2.2 多从机配置方案
在实际系统中,经常需要连接多个SPI从设备。根据应用场景不同,有两种主流配置方式:
常规模式:
- 每个从设备需要独立的CS线
- 主设备通过拉低对应的CS线选择通信对象
- 优势:通信效率高,每个从设备可以独立工作
- 缺点:占用较多IO口资源
菊花链模式:
- 所有从设备共享一个CS线
- 数据从第一个从设备传递到最后一个
- 优势:节省IO口资源
- 缺点:通信效率低,且需要从设备支持
在我的一个工业控制器项目中,使用了常规模式连接了6个SPI设备。为了节省IO口,采用了74HC138译码器来扩展CS信号,这种方法在从设备数量较多时特别实用。
3. GD32F470的SPI实现
3.1 硬件配置要点
GD32F470的SPI外设功能强大,支持多种配置选项。以下是关键配置步骤:
- 时钟配置:
c复制rcu_periph_clock_enable(RCU_SPI4); // 使能SPI4时钟
rcu_periph_clock_enable(RCU_GPIOE); // 使能GPIOE时钟
- GPIO初始化:
c复制gpio_init(GPIOE, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_2 | GPIO_PIN_6); // SPI4_SCK/SPI4_MOSI
gpio_init(GPIOE, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_5); // SPI4_MISO
gpio_init(GPIOE, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_3); // CS引脚
- SPI参数配置:
c复制spi_parameter_struct spi_init_struct;
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;
spi_init_struct.device_mode = SPI_MASTER;
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;
spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE; // 模式3
spi_init_struct.nss = SPI_NSS_SOFT;
spi_init_struct.prescale = SPI_PSC_2; // 时钟2分频
spi_init_struct.endian = SPI_ENDIAN_MSB;
spi_init(SPI4, &spi_init_struct);
spi_enable(SPI4);
经验分享:在初始化阶段建议使用较低的时钟频率(如1MHz),待通信稳定后再提高速度。这样可以避免因信号完整性问题导致的通信失败。
3.2 软件实现关键点
字节收发函数:
c复制uint8_t SPI_TransferByte(uint8_t byte)
{
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_TBE)); // 等待发送缓冲区空
spi_i2s_data_transmit(SPI4, byte); // 发送数据
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_RBNE)); // 等待接收数据
return spi_i2s_data_receive(SPI4); // 返回接收数据
}
这个函数体现了SPI全双工的特性:每次发送一个字节的同时也会接收一个字节。即使不需要接收数据,也必须发送数据来产生时钟信号。
Flash操作流程:
- 读取ID:
c复制void Flash_ReadID(uint8_t *id)
{
FLASH_CS_LOW();
SPI_TransferByte(0x9F); // 读ID命令
id[0] = SPI_TransferByte(0xFF); // 制造商ID
id[1] = SPI_TransferByte(0xFF); // 设备ID高字节
id[2] = SPI_TransferByte(0xFF); // 设备ID低字节
FLASH_CS_HIGH();
}
- 扇区擦除:
c复制void Flash_SectorErase(uint32_t addr)
{
Flash_WriteEnable();
FLASH_CS_LOW();
SPI_TransferByte(0x20); // 扇区擦除命令
SPI_TransferByte((addr >> 16) & 0xFF); // 地址高字节
SPI_TransferByte((addr >> 8) & 0xFF); // 地址中字节
SPI_TransferByte(addr & 0xFF); // 地址低字节
FLASH_CS_HIGH();
Flash_WaitBusy();
}
- 页编程:
c复制void Flash_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
Flash_WriteEnable();
FLASH_CS_LOW();
SPI_TransferByte(0x02); // 页编程命令
SPI_TransferByte((addr >> 16) & 0xFF);
SPI_TransferByte((addr >> 8) & 0xFF);
SPI_TransferByte(addr & 0xFF);
for(uint16_t i=0; i<len; i++) {
SPI_TransferByte(data[i]);
}
FLASH_CS_HIGH();
Flash_WaitBusy();
}
4. 调试与验证技巧
4.1 硬件调试方法
逻辑分析仪是最强大的SPI调试工具。我习惯使用Saleae Logic Analyzer,它能直观显示SPI波形并自动解析协议。关键检查点:
- CS信号是否在传输期间保持低电平
- 时钟频率是否符合预期
- 数据在正确的时钟边沿采样
- MOSI/MISO数据是否符合预期
示波器可以用来检查信号质量:
- 时钟信号是否干净无振铃
- 数据线是否有过冲或下冲
- 信号上升/下降时间是否满足要求
4.2 软件调试技巧
- 回环测试:
c复制void SPI_LoopbackTest(void)
{
spi_i2s_data_transmit(SPI4, 0x55);
while(RESET == spi_i2s_flag_get(SPI4, SPI_FLAG_RBNE));
uint8_t received = spi_i2s_data_receive(SPI4);
if(received != 0x55) {
printf("SPI loopback test failed! Received: 0x%02X\r\n", received);
} else {
printf("SPI loopback test passed!\r\n");
}
}
- 信号完整性优化:
- 在高速SPI通信中(>10MHz),建议:
- 使用短接线(<10cm)
- 添加适当的端接电阻(通常33-100Ω)
- 避免信号线平行走线过长
5. 常见问题与解决方案
5.1 典型问题排查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无法读取ID | 1. 硬件连接错误 2. SPI模式不匹配 3. CS信号问题 |
1. 检查MOSI/MISO连接 2. 确认从设备SPI模式 3. 用逻辑分析仪检查CS信号 |
| 数据错误 | 1. 时钟频率过高 2. 信号完整性问题 3. 时序不满足 |
1. 降低时钟频率测试 2. 检查PCB走线 3. 调整SPI相位 |
| 通信不稳定 | 1. 电源噪声 2. 地线问题 3. 电磁干扰 |
1. 增加电源滤波电容 2. 检查地回路 3. 使用屏蔽线 |
5.2 实战经验分享
- CS信号管理:
- 确保CS信号在传输间隙保持高电平
- 对于不支持连续传输的设备,每个字节传输后都要拉高CS
- CS信号切换时要留出足够的时间(通常>100ns)
- DMA使用技巧:
c复制void SPI_DMA_Init(void)
{
dma_parameter_struct dma_init_struct;
// 发送DMA配置
dma_deinit(DMA0, DMA_CH0);
dma_init_struct.direction = DMA_MEMORY_TO_PERIPHERAL;
dma_init_struct.memory_addr = (uint32_t)tx_buffer;
dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
dma_init_struct.memory_width = DMA_MEMORY_WIDTH_8BIT;
dma_init_struct.number = BUFFER_SIZE;
dma_init_struct.periph_addr = (uint32_t)&SPI_DATA(SPI4);
dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
dma_init_struct.periph_width = DMA_PERIPH_WIDTH_8BIT;
dma_init_struct.priority = DMA_PRIORITY_HIGH;
dma_init(DMA0, DMA_CH0, &dma_init_struct);
// 接收DMA配置类似
// ...
spi_dma_enable(SPI4, SPI_DMA_TRANSMIT);
dma_channel_enable(DMA0, DMA_CH0);
}
使用DMA可以大幅提高传输效率,特别是在需要传输大量数据时(如LCD刷新、Flash读写等)。
- 中断处理优化:
c复制void SPI4_IRQHandler(void)
{
if(spi_i2s_flag_get(SPI4, SPI_FLAG_RBNE) != RESET) {
rx_buffer[rx_index++] = spi_i2s_data_receive(SPI4);
if(rx_index >= BUFFER_SIZE) {
// 处理完整帧数据
}
}
}
中断方式适合处理不定长数据或需要快速响应的场景。要注意避免在中断服务程序中执行耗时操作。