1. GD32 SPI通信基础解析
SPI(Serial Peripheral Interface)作为嵌入式领域最常用的同步串行通信协议之一,在GD32系列MCU中有着广泛的应用实现。与UART和I2C相比,SPI以其全双工、高速率(GD32F303系列SPI时钟最高可达36MHz)和简单的硬件连接特性,成为传感器、存储芯片、显示屏等外设的首选接口。
在实际项目中,我经常遇到开发者对SPI的四种工作模式选择存在困惑。这里需要特别强调的是,模式选择本质上由CPOL(Clock Polarity)和CPHA(Clock Phase)两个参数决定:
- CPOL=0表示时钟空闲时为低电平
- CPOL=1表示时钟空闲时为高电平
- CPHA=0表示在时钟的第一个边沿采样数据
- CPHA=1表示在时钟的第二个边沿采样数据
以常见的SPI Flash芯片为例,W25Q64JV通常需要Mode 0(CPOL=0, CPHA=0)或Mode 3(CPOL=1, CPHA=1),而某些传感器如BME280则可能要求Mode 1。若模式配置错误,轻则通信失败,重则可能损坏器件。
重要提示:GD32的SPI时钟分频系数计算方式为fPCLK/(2*(SPI_PSC+1))。例如当APB2时钟为72MHz,SPI_PSC设为3时,实际SCK频率为72/(2*(3+1))=9MHz
2. 硬件设计与接口配置
2.1 物理连接规范
标准的SPI四线制连接包含:
- SCK(Serial Clock):同步时钟线
- MOSI(Master Output Slave Input):主机输出从机输入
- MISO(Master Input Slave Output):主机输入从机输出
- NSS(Slave Select):片选信号(低电平有效)
在GD32开发板上,以GD32F303ZET6为例,其SPI0接口对应引脚为:
- PA4 - NSS
- PA5 - SCK
- PA6 - MISO
- PA7 - MOSI
实际布线时需注意:
- 高频信号线(SCK)长度应尽量短,必要时可串联22Ω电阻抑制振铃
- MISO和MOSI建议等长布线,长度差控制在5mm以内
- 对于多个从机的情况,可采用菊花链连接或独立片选方式
2.2 GPIO初始化代码实现
c复制void SPI_GPIO_Config(void)
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_SPI0);
/* SPI0引脚配置 */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_5 | GPIO_PIN_7); // SCK, MOSI
gpio_init(GPIOA, GPIO_MODE_IN_FLOATING, GPIO_OSPEED_50MHZ, GPIO_PIN_6); // MISO
gpio_init(GPIOA, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4); // NSS
/* 初始片选置高 */
GPIO_BOP(GPIOA) = GPIO_PIN_4;
}
3. SPI外设深度配置
3.1 参数化配置解析
GD32的SPI初始化结构体包含以下关键参数:
c复制typedef struct
{
uint32_t device_mode; /* 主从模式选择 */
uint32_t trans_mode; /* 全双工/半双工/只收/只发 */
uint32_t frame_size; /* 8位或16位数据帧 */
uint32_t nss; /* 硬件/软件NSS管理 */
uint32_t endian; /* MSB/LSB优先 */
uint32_t clock_polarity_phase; /* CPOL和CPHA设置 */
uint32_t prescale; /* 时钟预分频 */
} spi_parameter_struct;
典型的主机模式配置示例:
c复制void SPI_Config(void)
{
spi_parameter_struct spi_init_struct;
rcu_periph_clock_enable(RCU_SPI0);
spi_init_struct.device_mode = SPI_MASTER;
spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;
spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;
spi_init_struct.nss = SPI_NSS_SOFT;
spi_init_struct.endian = SPI_ENDIAN_MSB;
spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE;
spi_init_struct.prescale = SPI_PSC_8; // 72MHz/8=9MHz
spi_init(SPI0, &spi_init_struct);
spi_enable(SPI0);
}
3.2 时钟配置要点
GD32的SPI时钟源来自APB总线,不同型号的时钟树略有差异:
- GD32F10x系列:SPI0在APB2,SPI1/2在APB1
- GD32F30x系列:所有SPI均在APB2
时钟配置不当会导致通信速率异常。建议在system_gd32f30x.c中确认系统时钟配置,典型配置如下:
c复制#define __SYSTEM_CLOCK_108M_PLL_HXTAL (uint32_t)(108000000)
4. 数据收发实战
4.1 基础通信函数实现
SPI全双工通信的核心函数:
c复制uint8_t SPI_TransmitReceive(uint8_t tx_data)
{
while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_TBE));
spi_i2s_data_transmit(SPI0, tx_data);
while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE));
return spi_i2s_data_receive(SPI0);
}
多字节传输优化版本:
c复制void SPI_TransmitReceiveBuf(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len)
{
for(uint16_t i=0; i<len; i++) {
rx_buf[i] = SPI_TransmitReceive(tx_buf[i]);
}
}
4.2 典型外设通信时序
以W25Q64JV Flash芯片为例,读取设备ID的标准流程:
- 拉低片选(NSS)
- 发送0x9F指令码
- 连续读取3个字节的制造商ID、存储器类型和容量
- 拉高片选
具体实现:
c复制void W25Q_ReadID(uint8_t *id)
{
GPIO_BC(GPIOA) = GPIO_PIN_4; // NSS低
SPI_TransmitReceive(0x9F); // 发送读ID命令
id[0] = SPI_TransmitReceive(0xFF); // 制造商ID
id[1] = SPI_TransmitReceive(0xFF); // 存储器类型
id[2] = SPI_TransmitReceive(0xFF); // 容量
GPIO_BOP(GPIOA) = GPIO_PIN_4; // NSS高
}
5. 性能优化与异常处理
5.1 DMA加速实现
对于高速数据传输(如LCD刷新),建议启用DMA:
c复制void SPI_DMA_Config(void)
{
dma_parameter_struct dma_init_struct;
rcu_periph_clock_enable(RCU_DMA0);
/* TX DMA配置 */
dma_deinit(DMA0, DMA_CH2);
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(SPI0);
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_CH2, &dma_init_struct);
/* 类似配置RX DMA... */
spi_dma_enable(SPI0, SPI_DMA_TRANSMIT);
dma_channel_enable(DMA0, DMA_CH2);
}
5.2 常见故障排查
-
无数据收发
- 检查SPI是否使能(spi_enable())
- 确认NSS信号是否正常拉低
- 用逻辑分析仪抓取SCK波形
-
数据错位
- 检查CPOL/CPHA模式是否匹配从设备
- 确认MSB/LSB设置
- 检查时钟分频是否过高
-
DMA传输不完整
- 确认DMA缓冲区地址对齐
- 检查DMA中断是否使能
- 验证DMA传输长度寄存器
调试技巧:在SPI初始化后,可先尝试发送0xAA或0x55这类交替位模式,用示波器观察波形是否正常
6. 进阶应用实例
6.1 多从机管理系统
当需要控制多个SPI设备时,可采用以下方案:
c复制typedef enum {
DEV_FLASH = 0,
DEV_SENSOR,
DEV_LCD
} SPI_Device;
void SPI_SelectDevice(SPI_Device dev)
{
// 根据不同的设备选择不同的片选线
switch(dev) {
case DEV_FLASH:
GPIO_BC(GPIOB) = GPIO_PIN_12; // Flash片选
GPIO_BOP(GPIOC) = GPIO_PIN_7; // 其他设备取消选中
GPIO_BOP(GPIOD) = GPIO_PIN_2;
break;
case DEV_SENSOR:
GPIO_BC(GPIOC) = GPIO_PIN_7;
GPIO_BOP(GPIOB) = GPIO_PIN_12;
GPIO_BOP(GPIOD) = GPIO_PIN_2;
break;
// 其他设备类似...
}
}
6.2 软件模拟SPI实现
对于引脚资源紧张的情况,可用GPIO模拟SPI:
c复制void SoftSPI_WriteByte(uint8_t data)
{
for(uint8_t i=0; i<8; i++) {
GPIO_BOP(GPIOA) = GPIO_PIN_5; // SCK低
if(data & 0x80) {
GPIO_BOP(GPIOA) = GPIO_PIN_7; // MOSI高
} else {
GPIO_BC(GPIOA) = GPIO_PIN_7; // MOSI低
}
data <<= 1;
delay_us(1);
GPIO_BC(GPIOA) = GPIO_PIN_5; // SCK高
delay_us(1);
}
}
在实际项目中,我发现GD32的硬件SPI稳定性相当出色,但在某些特殊场景(如需要动态切换极性和相位的场合),软件SPI反而更具灵活性。两者各有优劣,需要根据具体需求选择。
