GD32 SPI通信协议详解与W25Q64 Flash驱动实战
1. 项目概述:从“点灯”到“通信”,SPI是嵌入式开发的必经之路
搞嵌入式开发的朋友,从单片机入门到进阶,通常会经历几个标志性的阶段。第一个阶段是“点灯”,用GPIO控制LED闪烁,宣告硬件世界的大门已为你打开。第二个阶段往往是“打印”,通过串口(UART)在电脑上看到“Hello World”,打通了软件与硬件的对话通道。而当你开始需要驱动屏幕、读取传感器、连接存储芯片时,第三个关键阶段就来了——掌握通信总线。SPI(Serial Peripheral Interface),正是这个阶段你绕不开的核心技能之一。
我接触过很多项目,从简单的温湿度传感器数据采集,到复杂的TFT液晶屏驱动,再到高速的Flash存储器读写,SPI的身影无处不在。它不像I2C那样需要上拉电阻和复杂的寻址协议,也不像UART那样是异步通信需要双方严格匹配波特率。SPI是一种高速、全双工、同步的串行通信总线,以其简单、高效、可靠的特点,在芯片间短距离通信中占据了绝对主流的地位。这次,我们就以国民技术的GD32系列MCU为平台,彻底拆解SPI从原理到实战的每一个细节。无论你是刚刚搞定串口的新手,还是正在为某个外设驱动不起来而头疼的开发者,相信这篇指南都能帮你把SPI“吃透”。
2. SPI通信协议深度解析:四根线背后的精密时钟舞蹈
很多人初学SPI,觉得就四根线:SCK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、NSS(片选),比I2C还少一根,应该更简单。但实际上,SPI的灵活性就藏在这简单的四线制中,理解其时钟极性和相位是精准通信的基石。
2.1 核心四线功能与主从架构
SPI通信严格区分主(Master)和从(Slave)。在整个通信过程中,时钟信号SCK完全由主机产生并控制,从机只是被动地跟随这个时钟来收发数据。这就是“同步”的含义——数据位的采样和移出,都严格以时钟边沿为基准。
- SCK (Serial Clock):时钟线。主机输出的脉冲信号,每一个脉冲周期对应一个数据位的传输。通信速率(波特率)就是由这个时钟的频率决定的。
- MOSI (Master Out Slave In):主机输出,从机输入。当主机要向从机发送数据时,数据就通过这根线一位一位地移出。
- MISO (Master In Slave Out):主机输入,从机输出。当主机要从从机读取数据时,数据通过这根线一位一位地移入主机。
- NSS (Slave Select):从机选择线(也常称为CS、SS)。这是SPI总线实现“一主多从”的关键。主机通过控制不同的NSS线(每个从机独占一根)输出低电平,来选中想要通信的特定从机。被选中的从机才会响应SCK时钟并激活其MISO输出,未被选中的从机则必须将其MISO引脚置于高阻态,以避免总线冲突。
这里有一个非常重要的实操细节:NSS信号的管理模式。GD32的SPI模块支持硬件NSS和软件NSS两种模式。硬件模式下,MCU的某个特定硬件引脚(与SPI外设绑定)会自动产生NSS信号;软件模式下,则需要你手动控制一个普通的GPIO引脚来模拟NSS片选信号。在绝大多数实际项目中,尤其是驱动单个SPI设备或需要更灵活时序控制时,我强烈推荐使用软件NSS。因为你可以完全掌控片选信号拉低(开始通信)和拉高(结束通信)的时机,方便在通信前后插入必要的延时,或者实现更复杂的多字节通信帧。
2.2 时钟极性(CPOL)与相位(CPHA):理解数据采样的“节奏”
这是SPI协议中最容易让人混淆,但也最必须搞清楚的概念。CPOL和CPHA共同定义了数据位相对于时钟边沿的关系,共有四种组合模式(模式0-3)。不同的SPI从设备(如传感器、Flash芯片)可能工作在不同的模式下,主机必须配置成与之匹配的模式,否则读到的全是乱码。
时钟极性 CPOL (Clock Polarity):定义SCK时钟线在空闲状态(即两次传输之间,NSS为高时)的电平。
- CPOL = 0:SCK空闲时为低电平。
- CPOL = 1:SCK空闲时为高电平。 你可以简单地把它理解为时钟的“初始状态”。
时钟相位 CPHA (Clock Phase):定义数据在时钟的哪个边沿被采样(捕获),以及在哪个边沿被改变(移出)。
- CPHA = 0:数据在时钟的第一个边沿(即SCK从空闲状态跳变到第一个有效状态的边沿)被采样,在下一个边沿被改变。
- CPHA = 1:数据在时钟的第二个边沿被采样,在第一个边沿被改变。
光看定义很抽象,我们结合波形图和生活化的比喻来理解。把一次数据传输(1个位)想象成两个人(主机和从机)在时钟口令下同时递出纸条(数据)和接收纸条。
模式0 (CPOL=0, CPHA=0):
- SCK空闲时为低。
- 第一个边沿是上升沿。在这个上升沿时刻,双方采样(读取)对方递出的数据位。
- 第二个边沿是下降沿。在这个下降沿时刻,双方改变(准备)下一个要递出的数据位。
- 特点:数据在上升沿被捕获,在下降沿更新。这是最常用的模式之一,很多传感器(如BMP280)、Flash(如W25Qxx)都使用此模式。
模式3 (CPOL=1, CPHA=1):
- SCK空闲时为高。
- 第一个边沿是下降沿。此时数据更新。
- 第二个边沿是上升沿。此时数据被采样。
- 特点:数据在上升沿被捕获,在下降沿更新。注意,虽然捕获和更新的边沿与模式0相同(都是上升沿采、下降沿变),但由于空闲电平不同,整个波形是反相的。一些射频芯片(如NRF24L01)就使用模式3。
模式1和模式2则是另一种组合,数据在时钟的下降沿被采样。具体使用哪种模式,唯一且必须的依据就是从设备的数据手册。通常在时序图章节会明确标注。在GD32的库函数中,通过设置spi_init_struct.clock_polarity_phase这个参数来选择这四种模式。
注意:CPOL和CPHA的设置必须绝对保证主机与从机一致。这是SPI通信能正常工作的首要条件。在调试时如果发现数据异常,第一个要检查的就是这两个参数。
2.3 数据帧格式、传输顺序与速率配置
除了时钟模式,还有几个配置项决定了数据是如何被打包和传送的。
- 数据帧格式:GD32的SPI支持8位和16位数据帧。绝大部分外设使用8位帧(1个字节)。16位帧在某些特定场景下,比如传输音频数据或某些ADC的数据时,可能用于提高效率。
- 数据传输顺序:即MSB(最高有效位)先行还是LSB(最低有效位)先行。绝大多数SPI设备都是MSB先行,即数据字节的最高位(bit7)最先被移出。这也是GD32的默认配置。但在驱动某些特定器件(如一些老式的移位寄存器)时,可能需要设置为LSB先行。同样,需要查阅从设备手册确认。
- 波特率预分频:这是设置SPI通信速度的地方。SCK的频率由APB总线时钟经过预分频器得到。GD32提供了丰富的分频系数(如2, 4, 8, 16, 32, 64, 128, 256等)。选择速率时需遵循两个原则:一是不能超过从设备支持的最大SCK频率(见其数据手册);二是在长距离或干扰环境下的通信中,适当降低速率可以提高稳定性。通常,对于板级芯片间通信(几厘米内),几MHz到十几MHz都是安全的。
3. GD32 SPI外设驱动层详解:从寄存器到HAL库
理解了协议,我们就要在GD32这片“土地”上实现它。GD32的SPI外设功能完善,支持全双工、半双工、只收、只发等多种模式。我们通常不会直接操作寄存器,而是使用官方提供的标准外设库或HAL库,这能极大提高开发效率和代码可移植性。
3.1 SPI外设初始化配置清单
初始化一个SPI外设,就像给一个多功能工具箱设定工作模式。以下是使用GD32标准库进行SPI主机初始化的一个典型步骤和参数解析:
void spi_config(void) { spi_parameter_struct spi_init_struct; /* 第一步:开启相关时钟 */ rcu_periph_clock_enable(RCU_GPIOA); // SPI引脚所在的GPIO端口时钟 rcu_periph_clock_enable(RCU_SPI0); // SPI0外设时钟 /* 第二步:配置SPI引脚复用 */ // 假设SPI0: PA5-SCK, PA6-MISO, PA7-MOSI, PA4作为软件NSS 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; // 初始将NSS引脚置高(不选中从机) /* 第三步:配置SPI外设参数 */ spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 全双工模式 spi_init_struct.device_mode = SPI_MASTER; // 主机模式 spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; // 8位数据帧 spi_init_struct.clock_polarity_phase = SPI_CK_PL_LOW_PH_1EDGE; // 模式0: CPOL=0, CPHA=0 spi_init_struct.nss = SPI_NSS_SOFT; // 软件管理NSS spi_init_struct.prescale = SPI_PSC_8; // 预分频8,若系统时钟108MHz,则SCK=13.5MHz spi_init_struct.endian = SPI_ENDIAN_MSB; // MSB先行 spi_init(SPI0, &spi_init_struct); /* 第四步:使能SPI外设 */ spi_enable(SPI0); }关键参数解读与避坑指南:
spi_init_struct.nss:如前所述,除非你的硬件电路设计恰好需要,否则建议始终选择SPI_NSS_SOFT(软件NSS)。硬件NSS(SPI_NSS_HARD)在某些多主机模式下有用,但对初学者来说容易产生意料之外的行为。spi_init_struct.prescale:预分频系数。计算实际SCK频率的公式是:SCK频率 = APB时钟频率 / 预分频系数。APB时钟频率可以在system_gd32xxxx.c文件或通过rcu_clock_freq_get()函数查询。务必确保计算结果小于从设备的最大SCK频率,并留有一定余量(比如80%)。spi_init_struct.clock_polarity_phase:这是模式选择的关键。库中提供了四个宏:SPI_CK_PL_LOW_PH_1EDGE: 模式0 (CPOL=0, CPHA=0)SPI_CK_PL_LOW_PH_2EDGE: 模式1 (CPOL=0, CPHA=1)SPI_CK_PL_HIGH_PH_1EDGE: 模式2 (CPOL=1, CPHA=0)SPI_CK_PL_HIGH_PH_2EDGE: 模式3 (CPOL=1, CPHA=1)
3.2 软件NSS的精细控制与通信时序
使用软件NSS时,我们需要手动控制一个GPIO引脚。这带来了一个巨大的优势:可以精确控制通信帧的边界。很多SPI设备不仅仅是以单字节为单位通信,而是以“命令+地址+数据”构成的多字节帧。正确的NSS时序是帧正确识别的关键。
一个健壮的SPI字节收发函数应该包含NSS控制:
uint8_t spi_master_byte_exchange(uint8_t tx_data) { uint8_t rx_data = 0; // 1. 拉低NSS,选中从设备 GPIO_BC(GPIOA) = GPIO_PIN_4; // 假设PA4是NSS // 2. 等待发送缓冲区为空,然后写入要发送的数据 while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_TBE)); spi_i2s_data_transmit(SPI0, tx_data); // 3. 等待接收缓冲区非空,然后读取接收到的数据 while(RESET == spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE)); rx_data = spi_i2s_data_receive(SPI0); // 4. 拉高NSS,释放从设备 GPIO_BOP(GPIOA) = GPIO_PIN_4; // 5. 可选:短暂延时,满足某些器件对片选无效时间的需求 // delay_us(1); return rx_data; }这里有一个至关重要的细节:SPI是全双工,每次主机发送一个字节的同时,也会从从机那里接收一个字节。即使你只想发送命令(比如0x90),你也会收到一个字节(可能是从机的状态寄存器值,也可能是无意义的0xFF或0x00)。因此,上面的函数总是“交换”一个字节。如果你只想发送,可以忽略返回值;如果只想接收,可以发送一个哑元数据(Dummy Byte,通常是0xFF或0x00)。
实操心得:对于多字节连续传输(比如向Flash写一页数据),不要在每一个字节传输后都拉高NSS。正确的做法是:在传输整个帧开始时拉低NSS,在发送完帧的最后一个字节并读取了最后一个响应字节后,再拉高NSS。这保证了从设备将整个字节序列识别为一个完整的命令帧。
4. 实战演练:驱动SPI Flash存储器(W25Q64)
理论说得再多,不如真刀真枪干一场。我们以市面上最常见的SPI Flash芯片华邦W25Q64(8MB容量)为例,完成一个完整的驱动实现。这个实战涵盖了SPI初始化的所有细节、命令序列的构建、以及如何应对实际器件中的特殊要求。
4.1 硬件连接与器件识别
首先,确保硬件连接正确:
- GD32 SPI0_MOSI (PA7) ---> W25Q64 DI (数据输入)
- GD32 SPI0_MISO (PA6) ---> W25Q64 DO (数据输出)
- GD32 SPI0_SCK (PA5) ---> W25Q64 CLK
- GD32 GPIO_PA4 (软件NSS) ---> W25Q64 CS#
- 电源和地接好,注意W25Q64的VCC是3.3V。
W25Q64工作在SPI模式0(CPOL=0, CPHA=0),支持最高时钟频率104MHz(在Fast Read指令下),我们的初始化配置(13.5MHz)完全在其能力范围内。
上电后,第一件事是读取器件的ID,确保通信链路和基本驱动是正确的。W25Q64的ID读取命令是0x90,后面需要跟3个地址字节(0x00, 0x00, 0x00)和2个哑元字节,然后才会返回制造商ID和存储器ID。
uint16_t w25q64_read_id(void) { uint16_t id = 0; uint8_t manufacturer_id, device_id; // 拉低片选 W25QXX_CS_LOW(); // 发送读取ID命令 0x90 spi_master_byte_exchange(0x90); // 发送3字节地址 0x000000 spi_master_byte_exchange(0x00); spi_master_byte_exchange(0x00); spi_master_byte_exchange(0x00); // 发送2个哑元字节,用于产生时钟让Flash输出ID spi_master_byte_exchange(0xFF); // 哑元 spi_master_byte_exchange(0xFF); // 哑元 // 读取制造商ID(华邦是0xEF) manufacturer_id = spi_master_byte_exchange(0xFF); // 读取设备ID(W25Q64JV是0x4017,这里先读高字节,实际只读一个字节是0x17) device_id = spi_master_byte_exchange(0xFF); // 拉高片选 W25QXX_CS_HIGH(); id = (manufacturer_id << 8) | device_id; return id; // 预期返回 0xEF17 }这个函数清晰地展示了一个多字节SPI命令帧的构成:命令码 + 地址 + 哑元 + 数据。注意哑元字节的作用:它们不携带有效信息,只是为了让主机产生足够数量的时钟脉冲,从而将从机内部的数据移到MISO线上。很多SPI设备的读操作都需要哑元字节。
4.2 实现扇区擦除与数据写入
SPI Flash在写入数据前,必须确保目标存储单元是已擦除状态(全为0xFF)。擦除的最小单位通常是扇区(Sector, 4KB)。擦除和写入是相对耗时的操作,芯片在执行这些内部操作时,会置位状态寄存器中的“忙”标志位。因此,我们的驱动必须包含等待忙状态结束的函数。
// 等待Flash空闲 void w25q64_wait_busy(void) { uint8_t status; W25QXX_CS_LOW(); spi_master_byte_exchange(0x05); // 读状态寄存器1命令 do { status = spi_master_byte_exchange(0xFF); // 持续读取状态字节 } while(status & 0x01); // 检查BUSY位(bit0)是否为1 W25QXX_CS_HIGH(); } // 扇区擦除(4KB) void w25q64_sector_erase(uint32_t sector_addr) { // 1. 写使能 W25QXX_CS_LOW(); spi_master_byte_exchange(0x06); // WREN 命令 W25QXX_CS_HIGH(); delay_us(5); // 小延时确保命令生效 // 2. 发送扇区擦除命令 W25QXX_CS_LOW(); spi_master_byte_exchange(0x20); // Sector Erase 命令 // 发送24位地址(扇区地址,需左移12位,因为扇区地址是以4KB为单位的) spi_master_byte_exchange((sector_addr >> 16) & 0xFF); spi_master_byte_exchange((sector_addr >> 8) & 0xFF); spi_master_byte_exchange(sector_addr & 0xFF); W25QXX_CS_HIGH(); // 3. 等待擦除完成 w25q64_wait_busy(); } // 页编程(写入,最多256字节) void w25q64_page_program(uint32_t addr, uint8_t *data, uint16_t len) { uint16_t i; if (len > 256) len = 256; // 页编程不能跨页 // 1. 写使能 W25QXX_CS_LOW(); spi_master_byte_exchange(0x06); W25QXX_CS_HIGH(); delay_us(5); // 2. 发送页编程命令和数据 W25QXX_CS_LOW(); spi_master_byte_exchange(0x02); // Page Program 命令 spi_master_byte_exchange((addr >> 16) & 0xFF); spi_master_byte_exchange((addr >> 8) & 0xFF); spi_master_byte_exchange(addr & 0xFF); for(i=0; i<len; i++) { spi_master_byte_exchange(data[i]); } W25QXX_CS_HIGH(); // 3. 等待写入完成 w25q64_wait_busy(); }关键点解析:
- 写使能(WREN, 0x06):这是一个安全特性。Flash芯片上电后默认处于写保护状态,任何改变存储内容的操作(编程、擦除)之前,都必须先发送写使能命令。该命令在一次操作后自动失效。
- 地址对齐:扇区擦除命令
0x20要求传入的地址是4KB对齐的。页编程命令0x02要求写入不能跨页(256字节边界)。在驱动中做好长度检查是避免错误的关键。 - 等待机制:
w25q64_wait_busy()函数通过轮询状态寄存器的方式等待芯片内部操作完成。这是阻塞式等待,在实际产品中,如果系统有实时性要求,可以考虑结合中断或超时机制来优化。
4.3 实现数据读取与驱动整合
读取操作相对简单,最常用的是“快速读”命令0x0B,它比普通的读命令0x03允许更高的时钟频率,并且需要在地址字节后跟一个哑元字节。
void w25q64_read_data(uint32_t addr, uint8_t *buffer, uint32_t len) { uint32_t i; W25QXX_CS_LOW(); spi_master_byte_exchange(0x0B); // Fast Read 命令 spi_master_byte_exchange((addr >> 16) & 0xFF); spi_master_byte_exchange((addr >> 8) & 0xFF); spi_master_byte_exchange(addr & 0xFF); spi_master_byte_exchange(0xFF); // 快速读必需的哑元字节 for(i=0; i<len; i++) { buffer[i] = spi_master_byte_exchange(0xFF); // 持续发送时钟并读取数据 } W25QXX_CS_HIGH(); }将以上所有函数整合起来,并加上一些宏定义(如W25QXX_CS_LOW/HIGH, 容量定义等),就构成了一个完整的、可用的W25Q64驱动。你可以用它来存储系统日志、配置文件、字库或者任何需要掉电保存的数据。
5. 高级应用与多从机系统设计
掌握了单个SPI设备的驱动后,我们来看看更复杂的场景。在实际项目中,一个SPI主机连接多个从机是非常普遍的需求,比如一个主控同时连接一个显示屏、一个Flash和一个传感器。
5.1 一主多从的硬件连接方案
主要有两种硬件连接方式:
独立片选(最常用、最推荐):
- 每个从设备独占一根主机的GPIO作为其片选(NSS/CS)信号。
- 所有从设备的SCK、MOSI、MISO分别并联到主机的对应引脚上。
- 优点:逻辑清晰,各个从设备完全独立,互不干扰。通信时序可以针对每个设备单独优化。
- 缺点:需要占用较多的主机GPIO资源。
菊花链(Daisy-Chain):
- 所有从设备的SPI接口串联起来。主机的MOSI连接到第一个从机的MOSI,第一个从机的MISO连接到第二个从机的MOSI,以此类推,最后一个从机的MISO连接回主机。所有从机共享SCK和片选信号。
- 优点:节省GPIO,只需要一个片选信号。
- 缺点:所有设备必须支持菊花链模式(很多常见传感器不支持);数据传输是广播式的,主机发送的数据会经过链上所有设备,难以进行单独寻址和通信;软件驱动复杂。
- 这种模式在某些特定的移位寄存器或LED驱动芯片阵列中有所应用。
对于绝大多数应用,独立片选方案是首选。在GD32上实现起来非常简单:为每个从设备定义一个片选GPIO引脚,在与其通信前,拉低对应的片选引脚,通信结束后拉高即可。确保在任一时刻,只有一个片选信号是低电平。
5.2 多从机系统中的软件架构与资源管理
当有多个SPI外设时,软件设计需要一些考量:
- SPI外设实例:如果GD32有多个SPI外设(如SPI0, SPI1),可以将不同速率的设备分到不同的SPI总线上,避免频繁重新配置波特率。
- 统一的底层收发函数:可以抽象一个更通用的SPI收发函数,将片选引脚作为参数传入。
void spi_device_transfer(GPIO_TypeDef* cs_port, uint16_t cs_pin, uint8_t *tx_buf, uint8_t *rx_buf, uint32_t len) { GPIO_BC(cs_port) = cs_pin; // 拉低片选 for(uint32_t i=0; i<len; i++) { if(tx_buf) { rx_buf[i] = spi_master_byte_exchange(tx_buf[i]); } else { rx_buf[i] = spi_master_byte_exchange(0xFF); // 只读模式 } } GPIO_BOP(cs_port) = cs_pin; // 拉高片选 }- 中断与DMA:对于高速或大数据量传输(如刷新屏幕),轮询方式会长时间占用CPU。此时应启用SPI的传输完成中断(TXE/RXNE),甚至使用DMA进行内存到外设的数据搬运,从而解放CPU去处理其他任务。GD32的SPI支持DMA,配置稍复杂,但能极大提升系统效率。
- 互斥访问:在多任务(RTOS)环境中,SPI总线是一个需要被保护的共享资源。必须使用信号量(Semaphore)或互斥锁(Mutex)来确保同一时间只有一个任务访问SPI总线,防止数据错乱。
6. 调试技巧与常见问题排查实录
SPI通信调试,逻辑分析仪是比示波器更得力的工具,因为它能直接解析出SPI协议的数据包。如果没有硬件工具,就要靠“软件灯”和逻辑推理了。
6.1 典型问题排查流程
当你发现SPI通信失败时,可以按照以下清单逐步排查:
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无数据/波形 | 1. 电源或地未接好。 2. 时钟或数据线连接错误/断开。 3. SPI外设时钟未开启。 4. GPIO模式配置错误(如输出配成输入)。 | 1. 检查硬件连接,用万用表测通断和电压。 2. 检查 rcu_periph_clock_enable是否已调用。3. 检查GPIO初始化代码,确认SCK、MOSI为复用推挽输出,MISO为浮空/上拉输入。 |
| 有时钟但数据全0或全1 | 1. 片选(NSS)信号未正确拉低。 2. 主从设备模式(CPOL/CPHA)不匹配。 3. 从设备未上电或损坏。 | 1. 用逻辑分析仪或示波器观察NSS引脚时序,确保在通信期间为低。 2.重点检查:核对主机与从设备数据手册中的SPI模式是否完全一致。 3. 测量从设备VCC电压。 |
| 能发送但接收不到正确数据 | 1. MISO线连接错误或接触不良。 2. 从设备输出使能问题(需特定命令激活)。 3. 数据传输顺序(MSB/LSB)不匹配。 4. 读操作时序错误(缺少哑元字节)。 | 1. 检查MISO线路。 2. 确认从设备是否需要先发送“读使能”类命令。 3. 尝试更改 spi_init_struct.endian设置。4. 仔细对照从设备数据手册的读时序图,检查命令、地址、哑元字节的数量和顺序。 |
| 通信速度慢或不稳定 | 1. 上拉电阻缺失(某些开漏输出的MISO需要上拉)。 2. 通信速率过高,线路过长引起信号畸变。 3. 电源噪声大。 | 1. 在SCK/MOSI/MISO线上增加合适阻值的上拉电阻(如4.7kΩ)。 2. 降低SPI波特率(减小 prescale分频系数)。3. 检查电源滤波,在芯片VCC附近加退耦电容(0.1uF)。 |
6.2 软件调试的“土办法”
在没有逻辑分析仪的情况下,可以借助GPIO翻转来辅助调试:
- “数字灯”法:在SPI收发函数的开始和结束位置,控制一个空闲的GPIO引脚翻转。用示波器观察这个引脚,可以知道SPI函数是否被调用以及执行时间。
- “模拟逻辑分析仪”法:在SPI的SCK、MOSI、MISO、NSS信号线上,并联一个电阻(如1kΩ)再连接到另一个GPIO引脚,并将这些GPIO配置为输入。然后在主循环中快速采样这些引脚的电平并打印出来(通过串口),虽然精度很低,但有时能看出信号的大致变化。
- 简化测试:先不接从设备,将主机的MOSI和MISO短接,实现“自发自收”。编写一个测试程序,发送固定的数据(如
0xAA,0x55),然后接收并比较。如果回环测试成功,说明主机的SPI配置和底层驱动基本正确,问题可能出在从设备端或硬件连接上。
6.3 关于SPI中断与DMA的注意事项
当你进阶使用中断或DMA时,会碰到新的问题:
- 中断服务程序(ISR)要短平快:SPI的TXE(发送缓冲区空)和RXNE(接收缓冲区非空)中断频率可能很高。在ISR里只做最必要的操作(如填充数据到DR寄存器,或从DR寄存器读取数据),清除中断标志后立即退出。复杂的处理(如校验、存储)应放到主循环或任务中。
- DMA传输的配置陷阱:
- 数据宽度对齐:确保SPI的数据帧宽度(8/16位)与DMA通道配置的数据宽度一致。
- 存储器与外设地址:DMA的存储器地址是数组的地址(
&buffer),外设地址是SPI数据寄存器的地址((uint32_t)&SPI_DATA(SPIx))。 - 传输完成中断:使能DMA传输完成中断,在中断里进行后续处理(如拉高片选),并禁用DMA通道,防止重复传输。
- 缓存一致性:如果使用了CPU的缓存(在某些高端MCU中),在启动DMA传输前,可能需要手动清理缓存数据,确保DMA读到的是内存中最新的数据。
SPI作为嵌入式系统中最稳定、最高效的芯片间通信方式之一,其核心在于对时序的精确理解和控制。从搞懂CPOL/CPHA开始,到能稳健地驱动各类SPI从设备,再到设计多从机系统和应用高级特性,每一步都需要动手实践和细心调试。我个人的体会是,把W25Q64这类Flash芯片的驱动从头到尾写一遍并调通,SPI的功力就算基本过关了。以后遇到任何SPI设备,无非就是查手册、看时序、调模式、写驱动这个流程,万变不离其宗。最后一个小建议,建立一个自己的“外设驱动库”,把调试好的SPI Flash、OLED屏、IMU传感器等驱动代码模块化保存好,下次新项目直接移植,能省下大量重复劳动的时间。
