实战经验 | 启用“实时观察窗口”导致通讯出错的案例分享

cathy的头像
cathy 发布于:周六, 09/14/2024 - 14:47 ,关键词:

01、前言 

通常我们使用的 IDE 在调试时都支持在程序运行过程中实时观察窗口内容的功能,当启用这个功能后,实时观察窗口中包含的寄存器或变量的值会被周期性或重复性的进行采 样,进而实现窗口内容的实时更新。但是这个功能使用不当的话可能会导致一些问题,下面我们介绍这样一个外设通讯出错的案例。 

02、问题描述 

客户在使用 STM32H723 的 SPI 外设进行通讯时,通过逻辑分析仪抓取到的总线数据是正确的,但是实际接收到的数据却为 0。这种情况每隔一段时间会出现一次,这个间隔时间不是固定的。客户的测试是使用 Keil MDK 在调试状态下进行的。 

03、SPI 相关知识 

2.png

▲ 图1. RM0468 的 SPI_SR_RXP 寄存器位 

图 1 是 SPI_SR 寄存器中 RXP 位的描述。SPI_SR_RXP 寄存器位由硬件进行管理。当 RxFIFO 包含至少一个完整的数据帧时,SPI_SR_RXP 位为 1,否则为 0。

3.png

▲ 图2. RM0468 的 SPI_RXDR 寄存器 

图 2 是 SPI_RXDR 寄存器的描述,对 RxFIFO 的访问是通过读取 SPI_RXDR 寄存器进行的。并且不建议在 RxFIFO 中的数据少于一个数据帧时读取 SPI_RXDR 寄存器。

4.png

▲ 图3. RM0468 的55.4.11 SPI data transmission and reception procedures 章节 

图 3 是 SPI 数据收发章节的相关描述。在进行数据读取时,当 RxFIFO 中数据不足一个完整的数据帧时,读取的值为 0;当 RxFIFO 中的数据包含至少一个数据帧时,则每次读取都会读取一个完整的数据帧,并将此数据帧从 RxFIFO 中弹出。

综上所述,当 SPI_SR_RXP 为 1 时,表示 RxFIFO 中包含至少一个完整的数据帧,可以通过 SPI_RXDR 寄存器读取接收到的数据,每次读取一个完整的数据帧,并且在读取后 会将此数据帧从 RxFIFO 中弹出,也就是此数据帧只能通过 SPI_RXDR 读取一次。当 SPI_SR_RXP 位为 0 时,表示 RxFIFO 中不足一个完整的数据帧,如果此时通过 SPI_RXDR 寄存器进行读取的话,则读取得到的数据为 0,并且不建议这样做。

04、问题分析与测试 

客户抓取 SPI 总线上的数据是正确的,但是读取到的数据为 0 的情况,这很可能是由 于在 SPI_SR_RXP 为 0 的时候读取了 SPI_RXDR 寄存器。下面就这个推测对 SPI_SR_RXP 进行测试。

4.1. 测试思路 

使用 NUCLEO-H723 作为硬件平台,对 SPI1 进行自收发测试,也就是将 SPI1 的 MISO 与 MOSI 引脚连接在一起。再使用 MCU 的其它两个引脚进行辅助测试,分别用来 显示 SPI_SR_RXP 位的状态和定位程序执行的位置。将 PF0 设置为 GPIO_Output 模式来显示 SPI_SR_RXP 位的状态,因为 RXP 为 SPI_SR 的第 0 位,所以在代码中只要将 SPI_SR 的值赋值给“GPIOF->ODR”,即可将 SPI_SR_RXP 位的状态值设置到 PF0 中, 设置好之后即可通过 PF0 的 GPIO 电平来反映设置时的 SPI_SR_RXP 状态。将 PD0 设置为 EVENTOUT 模式来指示当前程序执行的位置,只要在代码中执行"SEV"的汇编指令,即 可在 EVENTOUT 引脚上立即输出一个脉冲,速度很快。

4.2. 使用 CubeMX 进行配置 

1. 设置数据长度为 8-bit,FIFO 阈值设为 1 个数据。5.png

▲ 图4. 配置 SPI1 的参数

2. 这里将 PA6 设置为 SPI1_MISO,PB5 设置为 SPI1_MOSI。

6.png

▲ 图5. SPI1 引脚配置

3. 设置 PF0 为 GPIO_Output 模式,用来显示 SPI_SR_RXP 的状态。

7.png

▲ 图6. PF0 的配置 

4. 设置 PD0 为 EVENTOUT 模式,通过发出脉冲来指示当前程序执行的位置。

8.png

▲ 图6. PD0 的配置 

4.3. 硬件连接将 PA6 与 PB5 通过杜邦线等方式连接在一起即可。

4.4. 代码修改

使用 CubeMX 生成代码后,对代码进行下面修改来进行测试。

 添加两个 buffer 分别用来作为 SPI 收发的 buffer

#define BUFFER_SIZE 100
uint8_t txBuffer[BUFFER_SIZE] = {0};
uint8_t rxBuffer[BUFFER_SIZE] = {0};

2. 添加函数,用来比较收发 buffer 中数据是否完全相同。相同则返回 1,不相同则返回 0。

uint8_t compareBuffer(void)
{
    for(uint8_t i=0; i<BUFFER_SIZE; i++)
    {
        if(txBuffer[i] != rxBuffer[i])
        {
            return 0;
        }
    }
    return 1;
}

3. 在 main 函 数 中添 加下 面 的 操 作 进 行收 发测 试 , 清空 rxBuffer, 然 后使 用HAL_SPI_TransmitReceive()函数进行收据收发。

int main(void)
{ 
    /* USER CODE BEGIN 1 */ 
    volatile uint32_t error_count = 0; 
    for(uint8_t i=0; i<BUFFER_SIZE; i++) 
    { 
        txBuffer[i] = i+1; 
    }
    /* USER CODE END 1 */ 
    ...... 
    /* USER CODE BEGIN WHILE */ 
    while (1) 
    {
        /* USER CODE END WHILE */
        /* USER CODE BEGIN 3 */
        memset(rxBuffer, 0, BUFFER_SIZE);
        if(HAL_SPI_TransmitReceive(&hspi1, txBuffer, rxBuffer,BUFFER_SIZE,10) == HAL_OK)
        {
            if(compareBuffer() == 0)
            {
              error_count++;
            }
        } 
    }
    /* USER CODE END 3 */
}

4. 在函数外添加了宏定义 SPI_TEST,在 HAL_SPI_TransmitReceive()函数内使用 “#ifdef SPI_TEST”和“#endif”围起来的内容是新添加的内容。每次执行 __SEV()的时候,都会在 PD0 中输出一个脉冲信号。每次执行“GPIOF->ODR =  hspi->Instance->SR”的时候,PF0 的输出就会被更新为 SPI_SR_RXP 当前的状 态。这部分主要是测试在 MCU 检测到 SPI_SR_RXP 为 1 到读取 SPI_RXDR 这一 段时间内,SPI_SR_RXP 的值是否被改变(也就是 SPI_RXDR 中的值是否被读取)。

#define SPI_TEST
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, const uint8_t *pTxData, uint8_t *pRxData, uint16_t Size, uint32_t Timeout)
{ 
    ...... 
    /* Transmit and Receive data in 8 Bit mode */ 
    else 
    { 
        while ((initial_TxXferCount > 0UL) || (initial_RxXferCount > 0UL)) 
        { 
            /* Check the TXP flag */ 
            if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_TXP)) && (initial_TxXferCount > 0UL)) 
            {
                ...... 
            }
            #ifdef SPI_TEST
            /* STEP 1 */
            __SEV();
            GPIOF->ODR = hspi->Instance->SR;
            /* STEP 2 */
            __SEV();
            #endif 
            /* Check the RXP flag */ 
            if ((__HAL_SPI_GET_FLAG(hspi, SPI_FLAG_RXP)) && (initial_RxXferCount > 0UL)) 
            {
                #ifdef SPI_TEST
                /* STEP 3 */
                __SEV();
                GPIOF->ODR = hspi->Instance->SR;
                #endif
                *((uint8_t *)hspi->pRxBuffPtr) = *((__IO uint8_t *)&hspi->Instance->RXDR);
                #ifdef SPI_TEST
                /* STEP 4 */
                __SEV();
                GPIOF->ODR = hspi->Instance->SR;
                /* STEP 5 */
                __SEV();
                #endif 
                hspi->pRxBuffPtr += sizeof(uint8_t); 
                hspi->RxXferCount--; 
                initial_RxXferCount = hspi->RxXferCount; 
        } 
        ......
}

 4.5. 测试与分析 

将代码编译下载后进行调试。在调试状态下运行一段时间后复现了客户情况,可以看 出 rxBuffer[19]的值,本来应该是 0x14,但是实际获取到的值却是 0x00。

9.png

▲ 图8. 使用 KEIL 调试时 SPI 接收到 0x00 数据 

下图是在 rxBuffer[19]处抓取到的波形,可以看到在总线上的数据是正确的,解析出来 是 0x14。为了更方便的进行说明,下面使用图中标签来说明执行的操作。

下面开始执行流程的分析: 

  • 在图中波形的 RXP_1 处看到 PF0 的状态变高,也就是当前 SPI_SR_RXP 的值为 1,也即 RxFIFO 中包含至少一个数据帧。

  • 然后执行程序的分支判断语句(用来判断 SPI_SR_RXP 是否为 1),由 STEP2 之 后的波形可以看出,MCU 执行了程序 STEP_3 及之后的操作,也即这里 MCU 读 取到的 SPI_SR_RXP 的值也为 1。

  • 在 STEP_3 和 STEP_4 之间执行了两个操作,一个是将 SPI_SR_RXP 的状态更新到 PF0 上(RXP_2 位置处),另一个是读取 SPI_RXDR 的值。从图中波形可以看出,在 RXP_2 位置处,波形由高变低,也即当前 SPI_SR_RXP 的值为 0。也就是说,在 STEP_3 和 RXP_2 之间(包含这两个操作),SPI_SR_RXP 的值发生了从 1 变为 0 的 变化,并且在 MCU 读取 SPI_RXDR 的时候 SPI_SR_RXP 的状态为 0。

10.png

▲ 图9. 在 rxBuffer[19]处抓取的波形与对应 SPI 进行接收的代码 

但是在 STEP_3 和 RXP_2 之间(包含这两个操作),工程代码中并没有读取 SPI_RXDR 寄存器的操作,所以到底什么操作读取了 SPI_RxDR 呢?通过查询《Getting started  with MDK Version 5》文档发现,KEIL 在调试时有一个“Periodic Window Update” 选项,当这个选项 Enable 时,调试窗口中的值会周期性的进行更新。如下图所示。

11.png

▲ 图10. Getting started with MDK Version 5 文档的 System Viewer 章节 

至此,问题原因比较明显了。因为在 KEIL 调试时默认打开了"Periodic Window  Update"这个选项,并且又由于在窗口中显示了 SPI_RXDR,导致调试器周期性的读取 SPI_RXDR 的值,如果调试器恰好在 STEP_3 和 RXP_2 之间(包含这两个操作)读取了 SPI_RXDR 寄存器的话,则会导致 MCU 读取 SPI_RXDR 的值为 0。下面对这个猜想进行验 证,方法很简单,关闭"Periodic Window Update"这个选项做对比测试,如下图所示: 

12.png

▲ 图11. 关闭 KEIL 的"Periodic Window Update"选项 

关闭"Periodic Window Update"选项后运行了很长一段时间,并未复现读取数据为 0 的情况。客户在关闭这个调试选项后,后续也并未再复现读取数据为 0 的情况。

05、小结

这个案例提醒我们,如果使用了可以通过读操作进行更新的寄存器时,最好在调试时 慎用“实时观察窗口”来观察其值,因为这可能会影响程序的正常执行。这个“Periodic  Window Update”的调试功能不是 Keil 独有的,在 IAR 中的"Live Watch"窗口以及 CubeIDE 中的"Live Expressions"窗口都是“实时观察窗口”的功能,所以在使用时也要注意下。

来源:STM32

免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:cathy@eetrend.com)。

围观 16