Brilliantwxx头像
关注
【STM32】 SPI 中断模式收发数据实战:从阻塞到中断的架构构建封面图

【STM32】 SPI 中断模式收发数据实战:从阻塞到中断的架构构建


  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 四线定义与数据交换本质

信号全称方向(主机视角)作用
SCKSerial Clock主机→从机时钟,由主机产生,决定传输节奏
MOSIMaster Out Slave In主机→从机主机发数据
MISOMaster In Slave Out从机→主机从机发数据
NSS/CSSlave Select主机→从机片选,低有效,选中唯一从机

1.2 全双工交换的物理本质(重点理解)

SPI 主从双方各有一个移位寄存器,SCK 每个上升/下降沿:

  1. 主机把移位寄存器最高位移到 MOSI 线上(发出 1 位);
  2. 同时从 MISO 线上采入从机移出的 1 位;
  3. 发 1 位 = 收 1 位,永远同时发生。

所以:

  • 主机想读 N 个字节,必须先"发" N 个字节(哪怕都是哑元 0x00),因为时钟靠写数据产生;
  • 主机想发 N 个字节,同时也会收回来 N 个字节垃圾(忽略即可)。

这就是"只接收也要先发哑元""只发送时忽略交换回来的数据"两条细节的根因。

1.3 四种工作模式(CPOL / CPHA)

模式CPOLCPHA时钟空闲电平采样边沿
Mode 000低电平第一个边沿(上升沿)采样
Mode 101低电平第二个边沿(下降沿)采样
Mode 210高电平第一个边沿(下降沿)采样
Mode 311高电平第二个边沿(上升沿)采样

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)

位名称含义
15BIDIMODE单线双向模式使能
14BIDIOE单线双向模式下输出使能
13CRCEN硬件 CRC 使能
12CRCNEXT下一个发送帧为 CRC
11DFF数据帧格式:0=8位,1=16位
10RXONLY只接收模式(配合 BIDIMODE)
9SSM软件从设备管理(配合 SSI)
8SSI内部从设备选择(软件管理时用)
7LSBFIRST帧格式:0=MSB先,1=LSB先
6SPESPI 使能(必须最后置位)
5:3BR[2:0]波特率分频:000=fPCLK/2 … 111=fPCLK/256
2MSTR主从选择:1=主机
1CPOL时钟极性
0CPHA时钟相位

2.2 SPI_CR2(控制寄存器2,偏移 0x04)—— 中断模式核心

位名称含义
7TXEIE发送缓冲区空中断使能:TXE=1 时产生中断
6RXNEIE接收缓冲区非空中断使能:RXNE=1 时产生中断
5ERRIE错误中断使能:CRCERR/OVR/MODF 时产生中断
2SSOESS 输出使能
1TXDMAEN发送 DMA 使能
0RXDMAEN接收 DMA 使能

中断模式下 HAL 会一次性打开 TXEIE | RXNEIE | ERRIE(只发时只开 TXEIE)。

2.3 SPI_SR(状态寄存器,偏移 0x08)

位名称含义清除方式
7BSY忙标志:正在通信/发送缓冲非空硬件自动,等通信结束
6OVR溢出错误:接收缓冲没被读走,新数据又来了读 DR 后再读 SR
5MODF模式错误:多主机冲突读 SR 后写 CR1
4CRCERRCRC 校验错误软件写 0
1TXE发送缓冲区空:可以写 DR 了写 DR 自动清
0RXNE接收缓冲区非空: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 配置步骤("现场配"部分还原)

  1. SPI1:Mode 选择 Full-Duplex Master;参数:Prescaler 按需(如 2 分频)、CPOL=Low、CPHA=1 Edge、8 Bits、MSB First;
  2. NVIC 设置页:勾选 SPI1 global interrupt,设置 Preemption Priority / Sub Priority(如 2/0);
  3. 生成代码后,CubeMX 自动生成 SPI1_IRQHandler:
    void SPI1_IRQHandler(void)
    {
        HAL_SPI_IRQHandler(&hspi1);   // 统一入口,内部按标志分发
    }
    
  4. 业务代码全部写在 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 递减:

时刻事件TxXferCountRxXferCount谁可能收尾
1TxISR 发第1字节33—
2RxISR 收第1字节32—
3TxISR 发第2字节22—
…(交替)
7TxISR 发第4字节01检查 RxXferCount≠0,不收尾
8RxISR 收第4字节00检查 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 字节
完成回调2HAL_SPI_TxRxCpltCallback 直接收尾HAL_SPI_RxCpltCallback:收完 3 字节后收尾
缓冲区分工一个 4 字节 tx + 一个 4 字节 rxcmd(1字节)+ rx_id(3字节)
中断接力单次传输内 TxISR/RxISR 交替两次独立传输,靠回调"接力"

中断版2 的本质:

        把"发命令"和"收数据"拆成两次中断传输,用回调把它们串成一条链: Transmit_IT → [TxCpltCallback 启动 Receive_IT] → [RxCpltCallback 拼ID收尾]。

适用场景:命令与数据长度不同、中间需要处理时,用分步模式更灵活。


六、W25Q64 深入

6.1 常用指令集(节选)

指令码名称说明
0x06Write Enable写使能(写操作前必须)
0x9FJEDEC ID读 3 字节 ID:厂商 / 类型 / 容量
0x90Device ID读设备 ID(旧式)
0x05Read Status 1读状态寄存器1(含 BUSY 位)
0x02Page Program页编程(写数据)
0x03Read Data读数据
0x20Sector 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 易踩的坑

  1. 忘记 volatile:主循环永远看不到 jedec_id_ready 变 true(编译器优化导致)。
  2. 在回调里做耗时操作:回调运行在 ISR 上下文,printf/延时会拖垮实时性;正式工程只置标志。
  3. 启动失败不放片选:CS 一直拉低,从机被"永久占用",后续通信全乱。
  4. 错误回调不复位状态:出错后 State 可能是 BUSY,下次启动返回 HAL_BUSY。
  5. 误以为返回值=传输完成:HAL_OK 只是"中断启动成功"。
  6. 发送哑元数量不够:收 N 字节必须发 N 字节(时钟数不够就读不全)。
  7. 回调里再次启动传输:注意上次传输收尾完成(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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--