解决rt-thread上使用SPI+DMA

本文介绍了如何通过SPI+DMA的方式优化STM32F407上的数据读取,以降低CPU占用率和避免线程时序问题。作者首先遇到70%的CPU占用率,原因是SPI速率设置错误。纠正后,通过添加DMA,实现了CPU零阻塞的SPI传输。详细步骤包括开启RTT设备驱动、配置HAL库、添加中断回调函数,并修改官方SPI驱动,利用信号量在DMA传输期间释放CPU。最后,作者分享了具体的代码实现和优化后的效果。
摘要由CSDN通过智能技术生成

首先阐述下遇到的问题

使用SPI读取icm20602数据,读取频率为1000hz,使用stm32f407主控,发现CPU占用率达到了70%,且扰乱了线程的时序,将此线程注释掉后CPU占用率掉到25%,线程频率恢复正常,看来这里要着重优化,便萌生了使用DMA的想法。

2023.5.7更新

  • 当时CPU占用率高是因为错误设置了spi速率,不过即使如此,使用DMA还是大幅降低了CPU占用率。
  • 经多次在stm32f4上跑RTT,使用SPI+DMA,有了更高效的实现方法,故优化文章。本次优化仅对官方drv_spi.c添加一句代码,即可在实现SPI+DMA的同时CPU零阻塞。

使用SPI+DMA要进行的配置

RTT部分
  1. 开启RTT设备驱动。点击自己的工程 ->RT-Thread Setting,开启SPI设备驱动。
    在这里插入图片描述

  2. 在board.h中添加开启宏
    在这里插入图片描述

开启后设备驱动会自动调用HAL库进行底层硬件的初始化默认配置,并将spi注册到设备容器。
在这里插入图片描述

HAL库部分
  1. 在board.c文件里加入以下函数,此函数被初始化SPI的HAL库调用以进行底层硬件初始化。
void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    if(hspi->Instance == SPI2)
    {
        /* Peripheral clock enable */
        __HAL_RCC_SPI2_CLK_ENABLE();
        __HAL_RCC_GPIOB_CLK_ENABLE();
        /**SPI2 GPIO Configuration,这里配置自己的
        PB13     ------> SPI3_MISO
        PB14     ------> SPI3_SCK
        PB15     ------> SPI3_MOSI
        */
        GPIO_InitStruct.Pin = GPIO_PIN_13| GPIO_PIN_14 | GPIO_PIN_15;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
        GPIO_InitStruct.Pull = GPIO_PULLUP;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
        GPIO_InitStruct.Alternate = GPIO_AF5_SPI2;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
}

类似于stm32的中断函数一样,此函数在hal库中弱定义,用户定义则执行用户的函数。
在这里插入图片描述

  1. 使能HAL库-SPI,即可与RTT设备驱动对接
    在这里插入图片描述
至此基础配置已完毕,接下来是用户配置,配置片选,并挂载到具体的SPI设备

这里给出我的配置源文件,作为参考1
(思路:在DMA传输时挂起当前线程等待信号量(这里需更改官方spi驱动,后文有具体说明),在SPI中断回调函数中释放信号量恢复线程)

/*
 * Copyright (c) 2006-2021, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2023-02-24     86188       the first version
 */
#include "head_rtthread.h"
#include "head_user.h"

SPI_HandleTypedef SPI_Handle;

/* 注册到spi设备总线的设备名(可任取,这里以总线+0 1 2命名直观,方便) */
#define ICM42688_SPI_DEVICE_NAME    "spi10"
#define MS5611_SPI_DEVICE_NAME      "spi11"
#define ICM20602_SPI_DEVICE_NAME    "spi12"
#define FM25V05_SPI_DEVICE_NAME     "spi20"

/* 数字序号对应的引脚可在drv_gpio.c中查看 */
#define ICM42688_SPI_DEVICE_CS    4  //PA4
#define MS5611_SPI_DEVICE_CS      40 //PC8
#define ICM20602_SPI_DEVICE_CS    46 //PC14
#define FM25V05_SPI_DEVICE_CS     28 //PB12

/* 传输完成回调,通知被阻塞的线程恢复执行 */
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
    rt_sem_release(((struct stm32_spi *)hspi)->spi_bus.owner->user_data);
}
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
    rt_sem_release(((struct stm32_spi *)hspi)->spi_bus.owner->user_data);
}
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
    rt_sem_release(((struct stm32_spi *)hspi)->spi_bus.owner->user_data);
}
static int spi_device_init(void)
{
    /*-------------------------------------------------------------------*
     * attach device cs pin
     *-------------------------------------------------------------------*/
    /* cs_pin clock enable */
    __GPIOA_CLK_ENABLE();
    __GPIOB_CLK_ENABLE();
    __GPIOC_CLK_ENABLE();
    /* attach cs pin */
    rt_hw_spi_device_attach("spi1", ICM42688_SPI_DEVICE_NAME,     GPIOA, GPIO_PIN_4);
    rt_hw_spi_device_attach("spi1", MS5611_SPI_DEVICE_NAME,       GPIOC, GPIO_PIN_8);
    rt_hw_spi_device_attach("spi1", ICM20602_SPI_DEVICE_NAME,     GPIOC, GPIO_PIN_14);
    rt_hw_spi_device_attach("spi2", FM25V05_SPI_DEVICE_NAME,      GPIOB, GPIO_PIN_12);

    /*-------------------------------------------------------------------*
     * init specific spi device
     *-------------------------------------------------------------------*/
    /* SPI1_device*/
    SPI_Handle.SPI1dev.ICM42688 = (struct rt_spi_device *)rt_device_find(ICM42688_SPI_DEVICE_NAME);
    if (SPI_Handle.SPI1dev.ICM42688 == RT_NULL)
    {
        rt_kprintf("failed to create spi1_device:ICM42688!\r\n");
    }
    SPI_Handle.SPI1dev.MS5611   = (struct rt_spi_device *)rt_device_find(MS5611_SPI_DEVICE_NAME);
    if (SPI_Handle.SPI1dev.MS5611 == RT_NULL)
    {
        rt_kprintf("failed to create spi1_device:MS5611!\r\n");
    }
    //SPI1dev_Handle.ICM20602 = (struct rt_spi_device *)rt_device_find(ICM20602_SPI_DEVICE_NAME);
    /* SPI2_device*/
    SPI_Handle.SPI2dev.FM25V05   = (struct rt_spi_device *)rt_device_find(FM25V05_SPI_DEVICE_NAME);
    if (SPI_Handle.SPI2dev.FM25V05 == RT_NULL)
    {
        rt_kprintf("failed to create spi2_device:FM25V05!\r\n");
    }

	/* init spi */
    struct rt_spi_configuration spi_cfg;
    spi_cfg.data_width = 8;
    spi_cfg.mode       = RT_SPI_MASTER | RT_SPI_MODE_0 | RT_SPI_MSB;
    spi_cfg.max_hz     = 10000000;  //10M,这里并不是10M,结果选最近的时钟源的2^n分频。官方驱动有点小bug,文章末尾会提到。
    rt_spi_configure(SPI_Handle.SPI1dev.ICM42688,  &spi_cfg);
    rt_spi_configure(SPI_Handle.SPI1dev.MS5611,    &spi_cfg);
    //rt_spi_configure(SPI1dev_Handle.ICM20602,  &spi_cfg);
    rt_spi_configure(SPI_Handle.SPI2dev.FM25V05,   &spi_cfg);

    /* semaphore be used to relax CPU when SPI is working */
    SPI_Handle.SPI1dev.sem = rt_sem_create("spidma_sem", 0, RT_IPC_FLAG_PRIO);
    if (SPI_Handle.SPI1dev.sem == RT_NULL)
    {
        rt_kprintf("failed to create spidma_sem!\r\n");
    }
    SPI_Handle.SPI2dev.sem = rt_sem_create("spi2dma_sem", 0, RT_IPC_FLAG_PRIO);
    if (SPI_Handle.SPI2dev.sem == RT_NULL)
    {
        rt_kprintf("failed to create spi2dma_sem!\r\n");
    }

    /* 同一总线,公用一个信号量 */
    /* SPI1_bus semaphore*/
    SPI_Handle.SPI1dev.ICM42688->user_data = (void *)SPI_Handle.SPI1dev.sem;
    SPI_Handle.SPI1dev.MS5611->user_data   = (void *)SPI_Handle.SPI1dev.sem;
    /* SPI2_bus semaphore*/
    SPI_Handle.SPI2dev.FM25V05->user_data  = (void *)SPI_Handle.SPI2dev.sem;


    SPI_Handle.RWBytes = rt_spi_transfer;
    SPI_Handle.WthenWs = rt_spi_send_then_send;
    SPI_Handle.WthenRs = rt_spi_send_then_recv;

    return RT_EOK;
}
/* 导出到自动初始化 */
INIT_PREV_EXPORT(spi_device_init);

头文件:

/*
 * Copyright (c) 2006-2021, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2023-02-24     86188       the first version
 */
#ifndef APPLICATIONS_HEADER_FILE_SPI_DRV_H_
#define APPLICATIONS_HEADER_FILE_SPI_DRV_H_
#include "drv_spi.h"
#include "head_rtthread.h"

typedef struct
{
    struct rt_spi_device* ICM42688;
    struct rt_spi_device* MS5611;
    struct rt_spi_device* ICM20602;
    rt_sem_t sem;
}SPI1dev_HandleTypedef;
typedef struct
{
    struct rt_spi_device* FM25V05;
    rt_sem_t sem;
}SPI2dev_HandleTypedef;

typedef struct
{
    SPI1dev_HandleTypedef SPI1dev;
    SPI2dev_HandleTypedef SPI2dev;
    rt_size_t (*RWBytes)(struct rt_spi_device *device, const void *send_buf, void *recv_buf, rt_size_t length);
    rt_err_t  (*WthenRs)(struct rt_spi_device *device, const void *send_buf,  rt_size_t send_length,  void *recv_buf,        rt_size_t recv_length);
    rt_err_t  (*WthenWs)(struct rt_spi_device *device, const void *send_buf1, rt_size_t send_length1, const void *send_buf2, rt_size_t send_length2);
}SPI_HandleTypedef;
extern SPI_HandleTypedef SPI_Handle;

#endif /* APPLICATIONS_HEADER_FILE_SPI_DRV_H_ */

配置完毕,开始使用

比如实现一个IMU的读写驱动函数:

static void Icm42688_R_reg(uint8_t addr, uint8_t *res, uint32_t num)
{
    uint8_t dat;

    dat =  addr | ICM42688_SPI_R;
    SPI_Handle.WthenRs(SPI_Handle.SPI1dev.ICM42688, &dat, 1, res, num);
}
static void Icm42688_W_reg(uint8_t addr, uint8_t val)
{
    uint8_t dat[2];
    dat[0] = addr | ICM42688_SPI_W;
    dat[1] = val;
    SPI_Handle.RWBytes(SPI_Handle.SPI1dev.ICM42688, dat, RT_NULL, 2);
}

解决RTT的SPI设备驱动(drv_spi.c)缺陷(使用DMA),仅新增一行代码!

上文源文件末尾引用了3个RTT-SPI设备驱动中的数据传输函数:

    SPI_Handle.RWBytes = rt_spi_transfer;
    SPI_Handle.WthenWs = rt_spi_send_then_send;
    SPI_Handle.WthenRs = rt_spi_send_then_recv;

这3个数据传输函数最终都会调用 spixfer 来进行spi数据读写,就是这个:

static rt_uint32_t spixfer(struct rt_spi_device *device, struct rt_spi_message *message)
{
    HAL_StatusTypeDef state;
    rt_size_t message_length, already_send_length;
    rt_uint16_t send_length;
    rt_uint8_t *recv_buf;
    const rt_uint8_t *send_buf;

    RT_ASSERT(device != RT_NULL);
    RT_ASSERT(device->bus != RT_NULL);
    RT_ASSERT(device->bus->parent.user_data != RT_NULL);
    RT_ASSERT(message != RT_NULL);

    struct stm32_spi *spi_drv =  rt_container_of(device->bus, struct stm32_spi, spi_bus);
    SPI_HandleTypeDef *spi_handle = &spi_drv->handle;
    struct stm32_hw_spi_cs *cs = device->parent.user_data;

    if (message->cs_take)
    {
        HAL_GPIO_WritePin(cs->GPIOx, cs->GPIO_Pin, GPIO_PIN_RESET);
    }

    LOG_D("%s transfer prepare and start", spi_drv->config->bus_name);
    LOG_D("%s sendbuf: %X, recvbuf: %X, length: %d",
          spi_drv->config->bus_name,
          (uint32_t)message->send_buf,
          (uint32_t)message->recv_buf, message->length);

    message_length = message->length;
    recv_buf = message->recv_buf;
    send_buf = message->send_buf;
    while (message_length)
    {
        /* the HAL library use uint16 to save the data length */
        if (message_length > 65535)
        {
            send_length = 65535;
            message_length = message_length - 65535;
        }
        else
        {
            send_length = message_length;
            message_length = 0;
        }

        /* calculate the start address */
        already_send_length = message->length - send_length - message_length;
        send_buf = (rt_uint8_t *)message->send_buf + already_send_length;
        recv_buf = (rt_uint8_t *)message->recv_buf + already_send_length;
        
        /* start once data exchange in DMA mode */
        if (message->send_buf && message->recv_buf)
        {
            if ((spi_drv->spi_dma_flag & SPI_USING_TX_DMA_FLAG) && (spi_drv->spi_dma_flag & SPI_USING_RX_DMA_FLAG))
            {
                state = HAL_SPI_TransmitReceive_DMA(spi_handle, (uint8_t *)send_buf, (uint8_t *)recv_buf, send_length);
            }
            else
            {
                state = HAL_SPI_TransmitReceive(spi_handle, (uint8_t *)send_buf, (uint8_t *)recv_buf, send_length, 1000);
            }
        }
        else if (message->send_buf)
        {
            if (spi_drv->spi_dma_flag & SPI_USING_TX_DMA_FLAG)
            {
                state = HAL_SPI_Transmit_DMA(spi_handle, (uint8_t *)send_buf, send_length);
            }
            else
            {
                state = HAL_SPI_Transmit(spi_handle, (uint8_t *)send_buf, send_length, 1000);
            }
        }
        else
        {
            memset((uint8_t *)recv_buf, 0xff, send_length);
            if (spi_drv->spi_dma_flag & SPI_USING_RX_DMA_FLAG)
            {
                state = HAL_SPI_Receive_DMA(spi_handle, (uint8_t *)recv_buf, send_length);
            }
            else
            {
                state = HAL_SPI_Receive(spi_handle, (uint8_t *)recv_buf, send_length, 1000);
            }
        }

        if (state != HAL_OK)
        {
            LOG_I("spi transfer error : %d", state);
            message->length = 0;
            spi_handle->State = HAL_SPI_STATE_READY;
        }
        else
        {
            LOG_D("%s transfer done", spi_drv->config->bus_name);
        }

        /* For simplicity reasons, this example is just waiting till the end of the
           transfer, but application may perform other tasks while transfer operation
           is ongoing. */
        while (HAL_SPI_GetState(spi_handle) != HAL_SPI_STATE_READY);
    }

    if (message->cs_release)
    {
        HAL_GPIO_WritePin(cs->GPIOx, cs->GPIO_Pin, GPIO_PIN_SET);
    }

    return message->length;
}
        /* For simplicity reasons, this example is just waiting till the end of the
           transfer, but application may perform other tasks while transfer operation
           is ongoing. */
        while (HAL_SPI_GetState(spi_handle) != HAL_SPI_STATE_READY);

使用DMA时也是在这里死等,这就失去了我们使用DMA的初衷,现在优化这段代码,在DMA传输数据时释放CPU去干其它事。

更改刚刚提到的代码段为如下代码,这里的方法依然是用信号量释放CPU
        /* For simplicity reasons, this example is just waiting till the end of the
           transfer, but application may perform other tasks while transfer operation
           is ongoing. */
        while (HAL_SPI_GetState(spi_handle) != HAL_SPI_STATE_READY)
        {
            rt_sem_take((rt_sem_t)(device->user_data), RT_WAITING_FOREVER);
        }

完成,可以愉快的使用spi+dma干活了
CPU,DMA搭配干活不累^ _ ^

最后附上一张导图

旧导图
请添加图片描述
2023.5.7更新(文章已更新,留旧导图做对比)
请添加图片描述

第一次写这么长的文章,有不当之处还请批评指正~


  1. 官方驱动取的是APB2时钟频率进行的分频,配置所有SPI都用的这个频率,SPI1挂在APB2总线上,配置上没问题,但SPI2挂在APB1总线上,APB1总线的频率是APB2的两倍,故在SPI2设置时钟频率时会造成 实际频率值设置频率值 的二分之一 ↩︎

  • 23
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
使用ADC DMA,您需要执行以下步骤: 1. 在RT-Thread Studio中创建一个新的RT-Thread项目,并在末尾添加ADC和DMA设备驱动程序。 2. 初始化ADC和DMA设备并设置其参数。 3. 在DMA传输完成后,将数据从DMA缓冲区传输到用户缓冲区。 4. 启动DMA传输并等待传输完成。 以下是一个使用ADC DMA的示例代码: ```c #include <rtthread.h> #include <rtdevice.h> #define ADC_DEV_NAME "adc1" #define DMA_DEV_NAME "dma1" #define SAMPLE_NUM 1024 static rt_uint16_t adc_buffer[SAMPLE_NUM]; static void adc_dma_callback(struct rt_completion *comp) { rt_completion_done(comp); } void adc_dma_test(void) { rt_device_t adc_dev, dma_dev; struct rt_completion adc_dma_complete; rt_err_t result; adc_dev = rt_device_find(ADC_DEV_NAME); if (adc_dev == RT_NULL) { rt_kprintf("Failed to find ADC device!\n"); return; } dma_dev = rt_device_find(DMA_DEV_NAME); if (dma_dev == RT_NULL) { rt_kprintf("Failed to find DMA device!\n"); return; } result = rt_device_open(adc_dev, RT_DEVICE_OFLAG_RDWR); if (result != RT_EOK) { rt_kprintf("Failed to open ADC device!\n"); return; } result = rt_device_open(dma_dev, RT_DEVICE_OFLAG_RDWR); if (result != RT_EOK) { rt_kprintf("Failed to open DMA device!\n"); rt_device_close(adc_dev); return; } /* Configure ADC */ rt_adc_enable(adc_dev, RT_TRUE); rt_adc_set_sample_rate(adc_dev, 100000); rt_adc_set_resolution(adc_dev, 12); /* Configure DMA */ rt_dma_set_callback(dma_dev, adc_dma_callback, &adc_dma_complete); rt_dma_set_source(dma_dev, RT_NULL); rt_dma_set_destination(dma_dev, adc_buffer); rt_dma_set_data_size(dma_dev, SAMPLE_NUM * sizeof(rt_uint16_t)); rt_dma_set_direction(dma_dev, RT_DMA_PERIPH_TO_MEMORY); rt_dma_set_peripheral_request(dma_dev, rt_device_get_irq(adc_dev)); /* Start DMA transfer */ rt_completion_init(&adc_dma_complete); rt_dma_start(dma_dev); rt_adc_start(adc_dev); /* Wait for DMA transfer to complete */ rt_completion_wait(&adc_dma_complete, RT_WAITING_FOREVER); /* Copy data from DMA buffer to user buffer */ // ... /* Cleanup */ rt_device_close(dma_dev); rt_device_close(adc_dev); } ``` 在此示例代码中,我们使用RT-Thread的`rt_adc_enable()`函数启用ADC,并使用`rt_adc_set_sample_rate()`和`rt_adc_set_resolution()`函数设置参数。然后,我们使用`rt_dma_set_callback()`函数设置DMA传输完成时的回调函数,并使用`rt_dma_set_source()`、`rt_dma_set_destination()`、`rt_dma_set_data_size()`和`rt_dma_set_direction()`函数设置DMA参数。最后,我们使用`rt_completion_init()`函数初始化一个RT-Thread完成对象,并使用`rt_dma_start()`和`rt_adc_start()`函数启动DMA和ADC传输。在DMA传输完成后,我们使用`rt_completion_wait()`函数等待DMA传输完成,并使用`rt_device_close()`函数关闭ADC和DMA设备。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HUAN~FLY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值