STM32 SPI 中断模式收发数据
全局流程总览(先建立地图)
主循环 中断(ISR) 结果 │ ├─ W25Qxx_ReadID_IT() │ ├─ CS_Enable()(拉低PE6) │ └─ HAL_SPI_TransmitReceive_IT() │ ├─ 登记缓冲区/计数 → 挂RxISR/TxISR函数指针 │ └─ 使能 TXEIE|RXNEIE|ERRIE 中断 │ │ │ ▼ 硬件: 发送缓冲空 → TXE=1 → 中断请求 │ ┌─ NVIC 裁决 → SPI1_IRQHandler │ │ └─ HAL_SPI_IRQHandler 按标志分发 │ │ ├─ TXE置位 → hspi->TxISR() = SPI_2linesTxISR_8BIT │ │ │ 写第1字节到DR → 产生时钟 → 移出1位/移入1位 │ │ └─ RXNE置位 → hspi->RxISR() = SPI_2linesRxISR_8BIT │ │ 从DR读回1字节 → 存进rx_buffer │ │ (收发交替,直到 XferCount 双双归零) │ ▼ │ SPI_CloseRxTx_ISR: 关中断 → 等BSY清0 → State=READY │ └─ 调用 HAL_SPI_TxRxCpltCallback() │ └─ 拼ID、CS_Disable、jedec_id_ready = true ▼ while(1) 轮询 jedec_id_ready → printf 打印结果一句话:
启动函数只"登记+开中断",之后全靠"中断标志交替置位→ISR 交替搬数",搬完由收尾函数调回调通知主循环。
一、SPI 协议深入(扩展)
1.1 四线定义与数据交换本质
信号 全称 方向(主机视角) 作用 SCK Serial Clock 主机→从机 时钟,由主机产生,决定传输节奏 MOSI Master Out Slave In 主机→从机 主机发数据 MISO Master In Slave Out 从机→主机 从机发数据 NSS/CS Slave Select 主机→从机 片选,低有效,选中唯一从机 1.2 全双工交换的物理本质(重点理解)
SPI 主从双方各有一个移位寄存器,SCK 每个上升/下降沿:
- 主机把移位寄存器最高位移到 MOSI 线上(发出 1 位);
- 同时从 MISO 线上采入从机移出的 1 位;
- 发 1 位 = 收 1 位,永远同时发生。
所以:
- 主机想读 N 个字节,必须先"发" N 个字节(哪怕都是哑元
0x00),因为时钟靠写数据产生;- 主机想发 N 个字节,同时也会收回来 N 个字节垃圾(忽略即可)。
这就是"只接收也要先发哑元""只发送时忽略交换回来的数据"两条细节的根因。
1.3 四种工作模式(CPOL / CPHA)
模式 CPOL CPHA 时钟空闲电平 采样边沿 Mode 0 0 0 低电平 第一个边沿(上升沿)采样 Mode 1 0 1 低电平 第二个边沿(下降沿)采样 Mode 2 1 0 高电平 第一个边沿(下降沿)采样 Mode 3 1 1 高电平 第二个边沿(上升沿)采样 W25Q64 支持 Mode 0 和 Mode 3,CubeMX 一般配 Mode 0(CPOL=0, CPHA=0)。
1.4 数据帧格式
- 默认 8 位数据(DFF=0);F1 系列也可配 16 位(DFF=1)
- 默认 MSB first(LSBFIRST=0)
- 每字节:先发高位 → 再发低位
二、STM32 SPI 寄存器详解(F1 系列)
2.1 SPI_CR1(控制寄存器1,偏移 0x00)
位 名称 含义 15 BIDIMODE 单线双向模式使能 14 BIDIOE 单线双向模式下输出使能 13 CRCEN 硬件 CRC 使能 12 CRCNEXT 下一个发送帧为 CRC 11 DFF 数据帧格式:0=8位,1=16位 10 RXONLY 只接收模式(配合 BIDIMODE) 9 SSM 软件从设备管理(配合 SSI) 8 SSI 内部从设备选择(软件管理时用) 7 LSBFIRST 帧格式:0=MSB先,1=LSB先 6 SPE SPI 使能(必须最后置位) 5:3 BR[2:0] 波特率分频:000=fPCLK/2 … 111=fPCLK/256 2 MSTR 主从选择:1=主机 1 CPOL 时钟极性 0 CPHA 时钟相位 2.2 SPI_CR2(控制寄存器2,偏移 0x04)—— 中断模式核心
位 名称 含义 7 TXEIE 发送缓冲区空中断使能:TXE=1 时产生中断 6 RXNEIE 接收缓冲区非空中断使能:RXNE=1 时产生中断 5 ERRIE 错误中断使能:CRCERR/OVR/MODF 时产生中断 2 SSOE SS 输出使能 1 TXDMAEN 发送 DMA 使能 0 RXDMAEN 接收 DMA 使能 中断模式下 HAL 会一次性打开 TXEIE | RXNEIE | ERRIE(只发时只开 TXEIE)。
2.3 SPI_SR(状态寄存器,偏移 0x08)
位 名称 含义 清除方式 7 BSY 忙标志:正在通信/发送缓冲非空 硬件自动,等通信结束 6 OVR 溢出错误:接收缓冲没被读走,新数据又来了 读 DR 后再读 SR 5 MODF 模式错误:多主机冲突 读 SR 后写 CR1 4 CRCERR CRC 校验错误 软件写 0 1 TXE 发送缓冲区空:可以写 DR 了 写 DR 自动清 0 RXNE 接收缓冲区非空:DR 里有数据可读 读 DR 自动清 核心两个标志(务必分清):
- TXE=1 → 可以"发货"(往 DR 写);
- RXNE=1 → 收到货了(从 DR 读)。
2.4 SPI_DR(数据寄存器,偏移 0x0C)双缓冲
- 写 DR:数据进入发送缓冲区(移位寄存器空闲后移入),同时清除 TXE;
- 读 DR:读出接收缓冲区内容,同时清除 RXNE;
- 读写操作实际都作用于同一物理移位链路,所以收发同步。
三、NVIC 深入:SPI 中断怎么到达 CPU
3.1 中断链路
SPI1 外设事件(TXE/RXNE/ERR 置位 + CR2 对应使能位=1) → 中断请求线 IRQn(SPI1_IRQn=35) 挂起 → NVIC(中断控制器)检查:该中断是否 ENABLE + 优先级是否足够 → 通过后 Cortex-M3 自动压栈(PC、xPSR、寄存器)并跳转 → 执行中断服务函数 SPI1_IRQHandler → 函数返回后自动出栈,恢复现场3.2 中断号与优先级
- 中断号在
stm32f103xe.h的IRQn_Type枚举中定义(SPI1_IRQn = 35,SPI2 = 36,SPI3 = 51);- 优先级由 NVIC 寄存器控制,F1 共 4 位,可拆为"抢占优先级 + 子优先级"(分组方式由
SCB->AIRCR决定);- 抢占优先级高 → 可打断;同抢占优先级 → 子优先级高先执行;都相同 → 按中断号小者先执行。
3.3 CubeMX 配置步骤("现场配"部分还原)
- SPI1:Mode 选择
Full-Duplex Master;参数:Prescaler 按需(如 2 分频)、CPOL=Low、CPHA=1 Edge、8 Bits、MSB First;- NVIC 设置页:勾选
SPI1 global interrupt,设置 Preemption Priority / Sub Priority(如 2/0);- 生成代码后,CubeMX 自动生成
SPI1_IRQHandler:void SPI1_IRQHandler(void) { HAL_SPI_IRQHandler(&hspi1); // 统一入口,内部按标志分发 }- 业务代码全部写在
USER CODE区(中断版1/2 就是这么组织的)。
四、HAL 库中断传输全流程追踪(重点·逐层拆解)
第 1 层:HAL_SPI_TransmitReceive_IT(启动函数)
调用它时,HAL 内部依次做(伪代码还原):
HAL_StatusTypeDef HAL_SPI_TransmitReceive_IT(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size) { /* ① 参数检查:指针非空、Size > 0,否则 HAL_ERROR */ /* ② 状态检查:State 必须 == HAL_SPI_STATE_READY,否则 HAL_BUSY */ /* ③ 状态置忙:State = HAL_SPI_STATE_BUSY */ /* ④ 错误码清零:ErrorCode = HAL_SPI_ERROR_NONE */ /* ⑤ 登记收发缓冲区与计数 */ hspi->pTxBuffPtr = pTxData; hspi->pRxBuffPtr = pRxData; hspi->TxXferSize = Size; hspi->TxXferCount = Size; /* 剩余待发 = 总长 */ hspi->RxXferSize = Size; hspi->RxXferCount = Size; /* 剩余待收 = 总长 */ /* ⑥ 挂函数指针(8位双线模式) */ hspi->RxISR = SPI_2linesRxISR_8BIT; hspi->TxISR = SPI_2linesTxISR_8BIT; /* ⑦ 使能 TXE / RXNE / ERR 三个中断源 */ __HAL_SPI_ENABLE_IT(hspi, (SPI_IT_TXE | SPI_IT_RXNE | SPI_IT_ERR)); /* ⑧ 返回 HAL_OK(此时只是"启动成功",数据还没传) */ return HAL_OK; }关键点:⑦ 使能后第一个立刻触发的中断是 TXE(发送缓冲初始为空),于是马上进 TxISR 发第一个字节——这就是"总是先发"的代码级原因。
第 2 层:SPI1_IRQHandler → HAL_SPI_IRQHandler(分发)
void HAL_SPI_IRQHandler(SPI_HandleTypeDef *hspi) { uint16_t itflag = hspi->Instance->SR; /* 读状态寄存器 */ uint16_t itsource = hspi->Instance->CR2; /* 读中断使能寄存器 */ /* ① 错误通道:OVR/MODF/CRCERR 且 ERRIE 使能 → 进错误处理并调 ErrorCallback */ /* ② 接收通道:RXNE=1 且 RXNEIE=1 → 调接收 ISR */ if ((itflag & SPI_FLAG_RXNE) && (itsource & SPI_IT_RXNE)) { hspi->RxISR(hspi); return; } /* ③ 发送通道:TXE=1 且 TXEIE=1 → 调发送 ISR */ if ((itflag & SPI_FLAG_TXE) && (itsource & SPI_IT_TXE)) { hspi->TxISR(hspi); return; } }由于
return,一次中断只处理一个通道——这就是"读写中断交替执行"的分发实现。第 3 层:SPI_2linesTxISR_8BIT(发送 ISR,逐行)
static void SPI_2linesTxISR_8BIT(struct __SPI_HandleTypeDef *hspi) { /* ① 把发送缓冲区当前字节写入 DR(写 DR 自动清 TXE、产生时钟) */ *(__IO uint8_t *)&hspi->Instance->DR = *((const uint8_t *)hspi->pTxBuffPtr); hspi->pTxBuffPtr++; /* ② 指针后移,指向下一个待发字节 */ hspi->TxXferCount--; /* ③ 剩余待发数 -1 */ /* ④ 判断是否全部发完 */ if (hspi->TxXferCount == 0U) { __HAL_SPI_DISABLE_IT(hspi, SPI_IT_TXE); /* ⑤ 发完了,关 TXE 中断 */ if (hspi->RxXferCount == 0U) /* ⑥ 检查对方(接收)是否也完成 */ { SPI_CloseRxTx_ISR(hspi); /* ⑦ 双方都完成 → 收尾 */ } } }第 4 层:SPI_2linesRxISR_8BIT(接收 ISR,逐行)
static void SPI_2linesRxISR_8BIT(struct __SPI_HandleTypeDef *hspi) { /* ① 从 DR 读出刚交换回来的字节(读 DR 自动清 RXNE) */ *hspi->pRxBuffPtr = *((__IO uint8_t *)&hspi->Instance->DR); hspi->pRxBuffPtr++; /* ② 接收指针后移 */ hspi->RxXferCount--; /* ③ 剩余待收数 -1 */ /* ④ 判断是否全部收完 */ if (hspi->RxXferCount == 0U) { __HAL_SPI_DISABLE_IT(hspi, (SPI_IT_RXNE | SPI_IT_ERR)); /* ⑤ 关 RXNE/ERR 中断 */ if (hspi->TxXferCount == 0U) /* ⑥ 检查对方(发送)是否也完成 */ { SPI_CloseRxTx_ISR(hspi); /* ⑦ 双方都完成 → 收尾 */ } } }第 5 层:SPI_CloseRxTx_ISR(收尾函数,干什么)
static void SPI_CloseRxTx_ISR(SPI_HandleTypeDef *hspi) { /* ① 关闭全部相关中断(TXE/RXNE/ERR) */ __HAL_SPI_DISABLE_IT(hspi, (SPI_IT_RXNE | SPI_IT_TXE | SPI_IT_ERR)); /* ② 等待 BSY 标志复位(保证最后一个字节真的在线上发完) */ while ((hspi->Instance->SR & SPI_FLAG_BSY) != RESET) { /* 带超时保护,防止死等 */ } /* ③ 状态复位为 READY,解除外设占用锁 */ hspi->State = HAL_SPI_STATE_READY; /* ④ 触发完成回调 */ if (hspi->ErrorCode == HAL_SPI_ERROR_NONE) { hspi->TxRxCpltCallback(hspi); /* 无错误 → HAL_SPI_TxRxCpltCallback */ } else { hspi->ErrorCallback(hspi); /* 有错误 → HAL_SPI_ErrorCallback */ } }收尾为什么恰好执行一次(再深入)
以 4 字节收发为例,设发送(Tx)和接收(Rx)计数都从 4 递减:
时刻 事件 TxXferCount RxXferCount 谁可能收尾 1 TxISR 发第1字节 3 3 — 2 RxISR 收第1字节 3 2 — 3 TxISR 发第2字节 2 2 — … (交替) 7 TxISR 发第4字节 0 1 检查 RxXferCount≠0,不收尾 8 RxISR 收第4字节 0 0 检查 TxXferCount=0,收尾! 规则:
收尾条件是" 自己计数=0 且 对方计数=0 "。
先完成的一方(第 7 步的发送)发现对方没完成,只能放弃收尾;
最后完成的一方(第 8 步的接收)看到双方都归零,执行收尾。 → 收发串行交替 + 交叉检查对方计数 = 收尾恰好一次,绝无两次。
五、代码逐行注释(中断版为重点)
5.1 中断版1 逐行
/* ========== 全局数据区(主循环和中断共享,必须 volatile) ========== */ uint8_t tx_buffer[4] = {0x9F, 0x00, 0x00, 0x00}; /* 第1字节=JEDEC指令,后3字节=哑元(产生时钟) */ uint8_t rx_buffer[4] = {0x00, 0x00, 0x00, 0x00}; /* 收4字节:字节0=指令交换回的哑元,1~3=厂商/类型/容量 */ volatile uint32_t id = 0; /* volatile:中断里写、主循环读,禁止编译器缓存 */ volatile bool jedec_id_ready = false; /* 就绪标志:中断置1,主循环轮询清零 */ int main(void) { HAL_Init(); /* 复位所有外设、初始化Flash接口和SysTick */ SystemClock_Config(); /* 配置系统时钟(PLL倍频等) */ MX_GPIO_Init(); /* GPIO 初始化(PA5/6/7、PE6/PE4) */ MX_SPI1_Init(); /* SPI1 初始化:模式0、主机、8位、分频 */ MX_USART1_UART_Init(); /* 串口1 初始化(printf 输出用) */ BSP_Usart_Init(&huart1); /* 重定向 printf 到串口1 */ W25Qxx_ReadID_IT(); /* 发起中断读:只登记+开中断,立刻返回 */ while (1) /* 主循环:不阻塞,干别的事也行 */ { if (jedec_id_ready) /* 轮询标志:中断搬完数据后才为 true */ { printf("jedec_id: 0x%x\n", id); /* 打印拼接好的 ID */ jedec_id_ready = false; /* 清标志,允许下一次传输 */ } } } void W25Qxx_ReadID_IT(void) { W25Qxx_CS_Enable(); /* 拉低 PE6:选中 W25Q64,之后才能通信 */ HAL_StatusTypeDef state = HAL_SPI_TransmitReceive_IT(&hspi1, tx_buffer, rx_buffer, 4); /* 启动 4 字节中断收发(登记+开中断) */ if (state == HAL_OK) { printf("SPI Enable Interrupt Success\n"); /* 启动成功 ≠ 传输完成 */ } else { W25Qxx_CS_Disable(); /* 启动失败:立刻释放片选,防止卡住从机 */ printf("SPI Enable Interrupt Failed\n"); } } /* 收发完成回调(收尾函数里被调用,运行在中断上下文) */ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) /* 只处理 SPI1 */ { W25Qxx_CS_Disable(); /* 传输结束,拉高片选 */ id = (rx_buffer[1] << 16) | (rx_buffer[2] << 8) | rx_buffer[3]; /* 拼接:厂商ID<<16 | 类型<<8 | 容量 */ jedec_id_ready = true; /* 置标志,主循环会打印 */ } } /* 异常回调:任何错误都会走到这里 */ void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) { W25Qxx_CS_Disable(); /* 出错也要释放片选 */ id = 0; /* 结果清零 */ jedec_id_ready = false; /* 标志复位 */ printf("SPI Error Occurred\n"); } }5.2 中断版2
uint8_t cmd = 0x9F; uint8_t rx_id[3] = {0x00, 0x00, 0x00}; void W25Qxx_ReadID_IT_version2(void); int main(void) { MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); /* USER CODE BEGIN 2 */ BSP_Usart_Init(&huart1); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ W25Qxx_ReadID_IT_version2(); while (1) { if (jedec_id_ready) { printf("jedec_id: 0x%x\n", id); jedec_id_ready = false; } } /* USER CODE END 3 */ } void W25Qxx_CS_Enable(void) { HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_RESET); } void W25Qxx_CS_Disable(void) { HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_SET); } void W25Qxx_ReadID_IT_version2(void) { W25Qxx_CS_Enable(); // 拉低⽚选 HAL_StatusTypeDef state = HAL_SPI_Transmit_IT(&hspi1, &cmd, sizeof(cmd)); // 使能SPI中断 if (state == HAL_OK) { printf("SPI Enable Interrupt Success\n"); } else { W25Qxx_CS_Disable(); // 如果开启失败,就关闭⽚选 printf("SPI Enable Interrupt Failed\n"); } } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) { // 指令发送完成,现在启动获取3字节ID HAL_StatusTypeDef state = HAL_SPI_Receive_IT(hspi, rx_id, sizeof(rx_id) / sizeof(rx_id[0])); if (state != HAL_OK) { W25Qxx_CS_Disable(); // 关闭⽚选 } } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi == &hspi1) { // ⾛到这⾥,就读完了,直接构建id W25Qxx_CS_Disable(); // 关闭⽚选 id = ((rx_id[0] << 16) | (rx_id[1] << 8) | rx_id[2]); jedec_id_ready = true; } }5.3中断版2 与中断版1 的差异(逐点)
环节 中断版1 中断版2 第1次启动 TransmitReceive_IT(4字节同时收发)Transmit_IT(只发 1 字节命令)完成回调1 无中间回调 HAL_SPI_TxCpltCallback:命令发完,接着启动Receive_IT收 3 字节完成回调2 HAL_SPI_TxRxCpltCallback直接收尾HAL_SPI_RxCpltCallback:收完 3 字节后收尾缓冲区分工 一个 4 字节 tx + 一个 4 字节 rx cmd(1字节)+rx_id(3字节)中断接力 单次传输内 TxISR/RxISR 交替 两次独立传输,靠回调"接力" 中断版2 的本质:
把"发命令"和"收数据"拆成两次中断传输,用回调把它们串成一条链:
Transmit_IT → [TxCpltCallback 启动 Receive_IT] → [RxCpltCallback 拼ID收尾]。适用场景:命令与数据长度不同、中间需要处理时,用分步模式更灵活。
六、W25Q64 深入
6.1 常用指令集(节选)
指令码 名称 说明 0x06 Write Enable 写使能(写操作前必须) 0x9F JEDEC ID 读 3 字节 ID:厂商 / 类型 / 容量 0x90 Device ID 读设备 ID(旧式) 0x05 Read Status 1 读状态寄存器1(含 BUSY 位) 0x02 Page Program 页编程(写数据) 0x03 Read Data 读数据 0x20 Sector Erase 扇区擦除 6.2 JEDEC ID 应答结构
发送
0x9F后,W25Q64 在 MOSI 继续输入哑元的同时,在 MISO 依次移出 3 字节:
字节序 内容 W25Q64 典型值 第1字节 厂商 ID(Manufacturer) 0xEF(Winbond)第2字节 存储器类型(Memory Type) 0x40第3字节 容量代码(Capacity) 0x17(=64Mbit)拼接:
0xEF << 16 | 0x40 << 8 | 0x17= 0xEF4017(不同批次/型号可能不同,以实际读回为准)。
容量代码规律:W25Q16=0x15、W25Q32=0x16、W25Q64=0x17、W25Q128=0x18。
6.3 0x9F 读取时序拆解(对照代码)
CS 拉低(PE6=0) 发 0x9F ─┐ (字节0 交换时,从机正在解码指令,回的是哑元) 发 0x00 ─┼─→ 字节1 = 厂商 ID 0xEF 发 0x00 ─┼─→ 字节2 = 类型 0x40 发 0x00 ─┘ (字节3 = 容量 0x17) CS 拉高(PE6=1)所以代码里 ID 从
rx_buffer[1]开始拼,rx_buffer[0]丢弃——这正是阻塞版和两个中断版都这么做拼接的原因。
七、阻塞版与中断版对比总结
维度 阻塞版 中断版 调用后是否立即返回 否(死等 HAL_MAX_DELAY)是(登记完就返回) CPU 利用率 传输期间 100% 空转 可同时跑主循环业务 代码结构 一个函数内串行完成 启动函数 + 回调函数分家 数据同步 函数返回即数据可用 靠 volatile标志位通知错误处理 检查返回值 HAL_SPI_ErrorCallback片选管理 函数内闭合(使能→收发→释放) 启动时拉低,回调里拉高 适用 简单初始化、单任务 实时性要求高、多任务
八、常见坑与面试题
8.1 易踩的坑
- 忘记
volatile:主循环永远看不到jedec_id_ready变 true(编译器优化导致)。- 在回调里做耗时操作:回调运行在 ISR 上下文,
printf/延时会拖垮实时性;正式工程只置标志。- 启动失败不放片选:CS 一直拉低,从机被"永久占用",后续通信全乱。
- 错误回调不复位状态:出错后
State可能是 BUSY,下次启动返回HAL_BUSY。- 误以为返回值=传输完成:
HAL_OK只是"中断启动成功"。- 发送哑元数量不够:收 N 字节必须发 N 字节(时钟数不够就读不全)。
- 回调里再次启动传输:注意上次传输收尾完成(State 回到 READY)再启动,否则
HAL_BUSY。8.2 面试/考试题(带答案)
Q1:SPI 中断模式为什么"只接收"也要先发送?
A:SPI 是同步全双工,主机模式时钟由主机产生,而时钟依赖写 DR 触发;
必须发送 N 个哑元(如 0x00)才能产生 N 个时钟、收回 N 字节。
Q2:TXE 和 RXNE 分别代表什么?怎么清除?
A:TXE=发送缓冲区空(可写 DR),写 DR 自动清除;
RXNE=接收缓冲区非空(DR 有数据),读 DR 自动清除。
Q3:中断模式下
HAL_SPI_TransmitReceive_IT返回 HAL_OK 代表什么?A:只代表中断传输成功启动(登记缓冲区、挂函数指针、开中断),不代表数据传完;完成与否看回调。
Q4:为什么
SPI_CloseRxTx_ISR不会被两个 ISR 重复调用?A:两个 ISR 都只有在"自己 XferCount=0 且 对方 XferCount=0"时才收尾;
先完成的一方检查到对方未完成会放弃,只有最后完成的那一方满足双零条件,所以恰好一次。
Q5:HAL 库如何把中断分发给收发处理函数?
A:
SPI_HandleTypeDef里保存函数指针RxISR/TxISR,HAL_SPI_IRQHandler读 SR/CR2,按 RXNE/TXE 标志调用对应指针指向的SPI_2linesRxISR_8BIT/SPI_2linesTxISR_8BIT。
Q6:为什么共享变量要加
volatile?A:中断和主循环两个执行流共享变量,
volatile禁止编译器把变量缓存进寄存器,保证每次访问都读内存真实值。
Q7:SPI 传输完成后 BSY 标志要等多久?
A:最后一个字节的发送在线上仍有位在移,需等 BSY 清零才算真正传完;HAL 收尾函数里会带超时地等待 BSY。
Q8:阻塞、中断、DMA 怎么选?
A:数据少且不要求实时 → 阻塞;
需要非阻塞、数据量中 → 中断;
数据量大/高频连续 → DMA(CPU 零参与)。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2602_95649725/article/details/166897045






