STM32 DMA配置与应用实践

01 DMA简介

DMA(Direct Memory Access),直接内存存取,是一种AMBA 先进高性能总线(AHB)模块,是独立于CPU的一种数据高速传输的方式。

DMA的功能

DMA 可以让数据的传输工作在后台进行,能够在没有CPU干预的情况下快速实现数据的转移。但并非不需要占用系统总线,只是可以在不显著影响系统性能的情况下进行大量数据的传输。

DMA 主要用于实现不同外设模块的集中数据缓冲和存储

DMA的工作原理

DMA从本质上看,是从“地址”到“地址”的方式来实现数据传输的。当设定好"源地址"、"目标地址"和"需要传输的数据量"后,DMA控制器就会启动传输,直至剩余传输数据量到0为止(非循环模式下)。DMA主要有四种数据传输类型:

  1. 外设到存储器
  2. 存储器到外设
  3. 存储器到存储器
  4. 外设到外设

最后一个“外设到外设”的传输类型,是指从外设的数据存储器到外设的数据存储器,外设的数据存储器实质上也是一种数据存储单元。后面章节会具体讲述其中的原理

02 STM32中的DMA

以下内容以F4xx系列为例。

DMA总线架构

由下面的总线架构图中可以看到,一个DMA控制器通过一个专用的AHB主端口连接到AHB总线矩阵。而总线矩阵则采用主/从结构。

整个架构中只有CPU和DMA充当主机,其他所有连接的部件都只作为从机访问。

在总线矩阵内,只要两个独立的AHB主机针对不同的AHB总线矩阵从机端口提交两个并发AHB传输,就不存在总线矩阵仲裁。例如,当CPU从闪存读取指令,而DMA从另一个存储器读取数据时,它们不会以任何方式相互限制。换句话说,就是仲裁只在两个主机需要访问相同的从机存储器或外设时发生。

从以上的描述能得出两个结论:

  1. DMA和CPU如果同时访问同一外设或存储器时,会发生总线仲裁
  2. DMA传输数据时会占用总线带宽

总线带宽:定义为总线可以在固定时间内传输的数据量,通常以每秒传输多少数据量来表示。它由时钟速度、总线宽度和总线管理开销决定

注意

1.图中有一个64KB的CCM data RAM只能由D-Bus总线访问,而DMA是无权访问的,后续应用时需要留意。

2.DMA1 控制器 AHB 外设端口与 DMA2 控制器的情况不同,不连接到总线矩阵,因此,仅 DMA2 数据流能够执行存储器到存储器的传输

DMA的结构

DMA的结构如下,并有几个大特点。

  1. 双 AHB 主总线架构,一个用于存储器访问,另一个用于外设访问
  2. 每个 DMA 控制器有 8 个数据流,每个数据流有多达 8 个通道。每次数据流可选择的通道数多达8个,可由软件配置,允许几个外设启动 DMA请求
  3. 每个数据流有单独的四级 32 位先进先出存储器缓冲区 (FIFO),可用于 FIFO 模式或直接模式
  4. DMA 数据流请求之间的优先级可用软件编程
  5. 5 个事件标志(DMA 半传输、DMA 传输完成、DMA 传输错误、DMA FIFO 错误、直接模式错误),进行逻辑或运算,从而产生每个数据流的单个中断请求

要传输的数据项的数目可以由 DMA 控制器或外设管理:

1.DMA 流控制器:要传输的数据项的数目是 1 到 65535,可用软件编程

2.外设流控制器:要传输的数据项的数目未知并由源或目标外设控制,这些外设通过硬件发出传输结束的信号

DMA支持的内存访问区域

源传输和目标传输支持在整个 4 GB 区域(地址在 0x0000 0000 和 0xFFFF FFFF 之间)内寻址外设和存储器。

DMA事务

DMA 事务由给定数目的数据传输序列组成。要传输的数据项的数目及其宽度(8 位、16 位 或 32 位)可用软件编程。

每个 DMA 传输包含三项操作:

  1. 通过 DMA_SxPAR 或 DMA_SxM0AR 寄存器寻址,从外设数据寄存器或存储器单元中加载数据
  2. 通过 DMA_SxPAR 或 DMA_SxM0AR 寄存器寻址,将加载的数据存储到外设数据寄存器或存储器单元
  3. DMA_SxNDTR 计数器在数据存储结束后递减,该计数器中包含仍需执行的事务数

DMA进行数据传输的必要条件

  1. 剩余传输数据量大于0
  2. DMA通道传输使能
  3. 通道上DMA数据传输有事件请求

存储器对存储器的置位,就相当于相应通道的事件有效。对应通道的事件有效和存储器对存储器的置位,就是传输的触发位。每次传输的事件置位一次,完成一次传输。如果是由外设引发的DMA传输,则传输完成后,相应传输事件会置为无效,而存储器对存储器的传输,则一次传输完成后,相应事件一直有效,直至完成设定的传输量

外设通道选择

每个数据流都与一个 DMA 请求相关联,这个请求可以从 8 个可能的通道请求中选出。

而来自外设的 8 个请求独立连接到每个通道,具体连接方式每个系列都不一样,我们需要选择将某个外设作为DMA数据流的源地址或目标地址。下表是F4xx系列的通道请求映射。

每个外设请求都占用一个数据流通道,如数据流0的通道0占用了数据流通道,那么其他通道(1-7)则处于不可用状态。

相同外设请求可以占用不同数据流通道,比如SDIO可以同时占用DMA2的数据流3和数据流6(一个作TX,一个作RX)

DMA1请求映射

DMA2请求映射

注意:只有DMA2可以使用“M2M”模式

仲裁器

一个DMA 控制器对应8 个数据流,数据流包含要传输数据的源地址、目标地址、数据等信息。如果同一个DMA同时有多个外设请求时,则需要分配优先级。

优先级管理分为两个阶段:

  1. 软件
    每个数据流优先级都可以在 DMA_SxCR 寄存器中配置。分为四个级别:
  • 非常高优先级
  • 高优先级
  • 中优先级
  • 低优先级
    1. 硬件
      如果两个请求具有相同的软件优先级,则编号低的数据流优先于编号高的数据流。例如,数据流 2 的优先级高于数据流 4

    DMA的控制和传输模式

    根据控制和传输模式的不同,DMA存储多种组合的配置,F4xx的DMA工作配置组合如下所示。

    控制模式

    FIFO模式

    1.FIFO特点

    每个数据流都有一个独立的 4 字 FIFO,阈值级别可由软件配置为 1/4、1/2、3/4 或Full。

    2.FIFO的结构

    FIFO 的结构随源与目标数据宽度的不同而不同。

    3.FIFO的数据传输

    FIFO 用于在源数据传输到目标地址之前临时存放这些数据。可以设置阈值,如果数据存储量达到阈值级别时,FIFO 内容将传输到目标中。

    1.FIFO模式对于源地址和目标地址数据宽度不同的情况非常有用,比如源数据是字节数据,而目标地址要求输出字宽度的数据,即在实现数据传输时同时把原来4 个8 位字节的数据拼凑成一个32 位字数据。此时可使用FIFO功能先把数据缓存起来,分别根据需要输出数据

    2.FIFO模式也常使用于突发(burst) 传输

    3.当在直接模式(禁止 FIFO)下将 DMA 配置为以存储器到外设模式传输数据时,DMA 会将一 个数据从存储器预加载到内部 FIFO,从而确保一旦外设触发 DMA 请求时则立即传输数据。

    4.FIFO 阈值与突发配置

    使用 FIFO 阈值和存储器突发大小配置时需要注意,FIFO 阈值所指向的内容必须与整数个存储器突发传输完全匹配。否则,当使能数据流时会报 FIFO 错误,然后DMA将自动禁止数据流传输。

    以下是FIFO阈值配置表。

    循环模式

    循环模式可用于处理循环缓冲区或连续数据流,比如ADC的多通道连续扫描。当Enable循环模式时,要传输的数据项的数目,在数据流配置阶段自动用设置的初始值进行加载,并持续响应 DMA 的请求。

    注意在循环模式下,如果同时为存储器配置了突发模式,那么必须遵循下列规则:

    DMA_SxNDTR = ((Mburst 节拍 ) × (Msize)/(Psize)) 的倍数

    其中,

  • DMA_SxNDTR = AHB 外设端口上要传输的数据项的数目
  • (Mburst 节拍 ) = 4、8 或 16(取决于 DMA_SxCR 寄存器中的 MBURST 位)
  • ((Msize)/(Psize)) = 1、2、4、1/2 或 1/4(Msize 和 Psize 表示 DMA_SxCR 寄存器中的 MSIZE 和 PSIZE 位。它们与字节相关)
  • 例如:

    Mburst 节拍 = 8 (INCR8),MSIZE =“00”(字节)和 PSIZE =“01”(半字),则可以计数得到:DMA_SxNDTR 必须是 (8 × 1/2 = 4) 的倍数,否则 DMA 行为和数据完整性得不到保证。

    NDTR 还必须是外设突发大小与外设数据大小乘积的倍数,否则会导致错误的 DMA 行为

    直接模式

    直接模式在每个外设请求时,都会立即启动对存储器传输的单次传输。同时要求源地址和目标地址的数据宽度必须一致,所以只有PSIZE 控制,而MSIZE 值会被忽略。默认情况下,DMA 工作在直接模式,不使能FIFO 阈值级别。

    直接模式不能用于存储器到存储器传输。

    双缓冲模式

    在此模式下,每次DMA事务结束时,DMA 控制器都从当前目标存储器转换到另一个目标存储器。 这样,当软件在处理当前存储器区域的同时,DMA 传输还可以填充或使用第二个存储器区域。以加快传输速度。

    1.使能双缓冲区模式时,自动使能循环模式,所以也不适用于M2M模式

    2.双缓冲模式在I2S解码或传输PDM信号时经常使用,使用该模式去播放音频时可以减少不流畅现象

    传输模式

    外设到存储器

    1.FIFO模式

    每次产生外设请求,数据流都会启动数据源到 FIFO 的传输。当数据量达到 FIFO 所设定的阈值级别时,FIFO 的内容将移出并存储到目标中。

    2.直接模式

    每完成一次从外设到 FIFO 的数据传输后,相应的数据立即就会移出并存储到目标中。

    不使用 FIFO 的阈值级别控制功能

    两种模式下,如果 DMA_SxNDTR 寄存器计数到零,或外设请求传输终止(在使用外设流控制器的情况下)或 DMA_SxCR 寄存器中的 EN 位由软件清零,传输都会立即停止。

    存储器到外设

    1.FIFO模式

    数据流会立即启动传输,从存储器源完全填充到FIFO中。每次发生外设请求,FIFO 的内容都会移出并存储到目标中。

    当 FIFO 的级别小于或等于预定 义的阈值级别时,将使用存储器中的数据完全重载 FIFO

    2.直接模式

    该模式下,一旦使能了数据流,DMA便会预装载第一个数据,将其传输到内部 FIFO。这时当发生外设请求数据传输,DMA便会将预装载的值传输到配置的目标。紧接着,DMA会使用要传输的下一个数据再次重载内部空 FIFO。预装载的数据大小为 DMA_SxCR 寄存器中 PSIZE 位字段的值。

    两种模式下,如果 DMA_SxNDTR 寄存器计数到零,或外设请求传输终止(在使用外设流控制器的情况下)或 DMA_SxCR 寄存器中的 EN 位由软件清零,传输都会立即停止。

    存储器到存储器

    默认使用FIFO模式,当使能数据流时,数据流会立即开始填充 FIFO,直至达到阈值级别。达到阈值级别后,FIFO 的内容便会移出,并存储到目标中。

    如果 DMA_SxNDTR 寄存器计数到零,或外设请求传输终止(在使用外设流控制器的情况下)或 DMA_SxCR 寄存器中的 EN 位由软件清零,传输都会立即停止。

    1.该模式下,不允许使用循环模式和直接模式,所以也不能使用双缓冲区模式(自动启用循环模式)

    2.只有 DMA2 控制器能够执行存储器到存储器的传输

    “外设到外设”

    参考手册中没有提到这个模式,但DMA的本质是“地址到地址”。
    如果源地址是外设,目标地址是内存,那么传输模式就是外设到内存;
    如果源地址是外设,目标地址也是外设,那传输模式就是外设到外设。

    例如用DMA把ADC数据寄存器的值直接传送到SPI数据寄存器,即属于“外设”到“外设”的传输模式。

    传输类型

    单次

    当 AHB 外设端口被配置为单次传输时,根据 DMA_SxCR 寄存器 PSIZE[1:0] 位的值,每个 DMA 请求产生一次字节、半字或字的数据传输。

    单次传输时必须通过AHB 的总线仲裁多次控制才传输完成

    突发

    突发传输就是用非常短时间结合非常高数据信号率传输数据,相对正常传输速度,突发传输就是在传输阶段把速度瞬间提高,实现高速传输,在数据传输完成后恢复正常速度。

    DMA 控制器可以产生单次传输或 4 个、8 个和 16 个节拍的增量突发传输。当 AHB 外设端口被配置为突发传输时,根据 DMA_SxCR 寄存器 PBURST[1:0] 和PSIZE[1:0] 位的值,每个 DMA 请求相应地生成 4 个、8 个或 16 个节拍的字节、半字或字的传输。

    突发模式下,当DMA请求总线成功后会连续传送数据,而不给CPU使用总线的机会,直到数据传送完毕。比如设置了4个节拍的突发传输,而传输宽度位为8 bit,则一个DMA请求会连续传送4个字节,是单次传输的4倍,大提高了传输的速度。

    突发传输需要结合FIFO 使用,具体配置可看FIFO阈值与突发配置章节。

    1.在直接模式下,数据流只能生成单次传输,而 MBURST[1:0] 和 PBURST[1:0] 位由硬件强制配置

    2.突发传输过程会一直占用AHB 总线,保证每个数据项在传输过程不被分割

    中断类型

    对于每个 DMA 数据流,可在发生以下事件时产生中断:

  • 达到半传输
  • DMA 数据传输达到一半时,HTIF 标志位会被置位。

  • 传输完成
  • DMA 数据传输完成时,TCIF 标志位会被置位。

  • 传输错误
  • DMA 访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器地址寄存器时,TEIF 标志位被置位。

  • FIFO 错误(上溢、下溢或 FIFO 级别错误)
  • 发生FIFO 下溢或者上溢时FEIF 标志位被置位。

  • 直接模式错误
  • 在外设到存储器的直接模式下,因为存储器总线没得到授权,使得先前数据没有完成被传输到存储器空间上,此时DMEIF 标志位被置1。

    在将使能控制位置‘1’前,应将相应的事件标志清零,否则会立即产生中断

    03 DMA的配置和应用

    DCMI + LCD DMA传输应用

    这里应用了“外设”到“存储器”的方式,把DCMI->DR寄存器的数据直接传送到挂在FSMC总线的LCD数据地址上。开启循环模式和FIFO,并将外设设为单次传输,而存储器设为8个节拍的突发传输。在字节宽度的设置上,DCMI的数据寄存器宽度为32bit,而LCD端的RGB565,宽度为16bit。
    上述使用了如下图红框所示的配置。

    整个传输过程是这样的,当DCMI的数据寄存器收到32bit数据才会触发一次DMA请求,然后存入DMA的FIFO中,当存满到4字节的FIFO阈值时,突发传输1次8个节拍的半字数据到LCD中显示。

    #define DEBUG_DCMI_DR_BASE               (uint32_t)&DCMI->DR
    #define DEBUG_DCMI_DMA_CLK               RCC_AHB1Periph_DMA2    
    #define DEBUG_DCMI_DMA_CHANNEL           DMA_Channel_1
    #define DEBUG_DCMI_DMA_STREAM            DMA2_Stream1
    
    #define FSMC_LCD_DATA_SIZE              1
    #define FSMC_LCD_ADDRESS                ((uint32_t) 0x68000002)
    
    void OV2640_DMAConfig(void)
    {
        DMA_InitTypeDef  DMA_InitStructure;
    
        /* 配置DMA从DCMI中获取数据*/
        /* 使能DMA*/
        RCC_AHB1PeriphClockCmd(DEBUG_DCMI_DMA_CLK, ENABLE);  
        DMA_Cmd(DEBUG_DCMI_DMA_STREAM,DISABLE);
        while (DMA_GetCmdStatus(DEBUG_DCMI_DMA_STREAM) != DISABLE)
        {
    
        }   
    
        DMA_InitStructure.DMA_Channel = DEBUG_DCMI_DMA_CHANNEL;  
        DMA_InitStructure.DMA_PeripheralBaseAddr = DEBUG_DCMI_DR_BASE;
        DMA_InitStructure.DMA_Memory0BaseAddr = FSMC_LCD_ADDRESS;
        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
        DMA_InitStructure.DMA_BufferSize = FSMC_LCD_DATA_SIZE;
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable;
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
        DMA_InitStructure.DMA_Priority = DMA_Priority_High;
        DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable;
        DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
        DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_INC8;
        DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
    
        /* DMA初始化 */
        DMA_Init(DEBUG_DCMI_DMA_STREAM, &DMA_InitStructure);
    
        DMA_Cmd(DEBUG_DCMI_DMA_STREAM,ENABLE);
        while(DMA_GetCmdStatus(DEBUG_DCMI_DMA_STREAM) != ENABLE)
        {
    
        }
    }
    

    ADC DMA传输应用

    这里应用了“外设”到“存储器”的方式,因为ADC数据寄存器地址不会随通道不同而变化,所以使能了存储器增量模式,禁止外设增量。而控制模式使用“循环模式” + “单次传输”的方式。

    /** 定义ADC 相关信息*/
    #define DEBUG_ADC_BASE          ADC1_BASE
    #define DEBUG_ADC_DR_ADDR       ((uint32_t)DEBUG_ADC_BASE + 0x4C)
    
    /** 定义数据存储数组*/
    #define ADC_CONV_CH_SIZE       3
    uint16_t adcConvertedValue[ADC_CONV_CH_SIZE];
    
    /** 定义ADC DMA相关信息*/
    #define DEBUG_ADC_DMA_CLK       RCC_AHB1Periph_DMA2
    #define DEBUG_ADC_DMA_CHANNEL   DMA_Channel_0
    #define DEBUG_ADC_DMA_STREAM    DMA2_Stream0
    
    /** 初始化ADC相应的DMA*/
    void ADC_DMAConfig(void)
    {
        DMA_InitTypeDef DMA_InitStructure;
        
        RCC_AHB1PeriphClockCmd(DEBUG_ADC_DMA_CLK, ENABLE);
    
        /** ADC 数据寄存器地址*/
        DMA_InitStructure.DMA_PeripheralBaseAddr = DEBUG_ADC_DR_ADDR;
        DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)&adcConvertedValue;
        DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory;
        /** buffer size 和需扫描的ADC通道数一致*/
        DMA_InitStructure.DMA_BufferSize = ADC_CONV_CH_SIZE;
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
        /** 存储器地址递增*/
        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
        /** ADC DR数据大小为半字,即两个字节*/
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
        /** 存储器数据大小为半字*/
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    
        DMA_InitStructure.DMA_Priority = DMA_Priority_High;
        DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
        DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull;
        DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;
        DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;
        DMA_InitStructure.DMA_Channel = DEBUG_ADC_DMA_CHANNEL;
        DMA_Init(DEBUG_ADC_DMA_STREAM, &DMA_InitStructure);
        DMA_Cmd(DEBUG_ADC_DMA_STREAM, ENABLE);
    }
    

    串口DMA传输应用

    这里应用了“存储器”到“外设”的方式,因为USART数据寄存器地址是固定的,所以使能了存储器增量模式,禁止外设增量。控制模式使用“循环模式” + “单次传输”的方式。

    #define TX_BUFFER_SIZE    100
    
    #define DEBUG_USART_DR_BASE               (USART1_BASE + 0x04)
    #define DEBUG_USART_DMA_CLK               RCC_AHB1Periph_DMA2        
    #define DEBUG_USART_DMA_CHANNEL           DMA_Channel_4
    #define DEBUG_USART_DMA_STREAM            DMA2_Stream7
    
    uint8_t txBuffer[TX_BUFFER_SIZE];
    
    
    void USART_DMAConfig(void)
    {
        DMA_InitTypeDef DMA_InitStructure;
    
        /* 开启DMA时钟*/
        RCC_AHB1PeriphClockCmd(DEBUG_USART_DMA_CLK, ENABLE);
    
        /* 复位初始化DMA数据流 */
        DMA_DeInit(DEBUG_USART_DMA_STREAM);
    
        /* 确保DMA数据流复位完成 */
        while (DMA_GetCmdStatus(DEBUG_USART_DMA_STREAM) != DISABLE)  {
        }
    
        /* usart1 tx对应DMA2,通道4,数据流7 */        
        DMA_InitStructure.DMA_Channel = DEBUG_USART_DMA_CHANNEL;  
        /* 设置DMA源:串口数据寄存器地址*/
        DMA_InitStructure.DMA_PeripheralBaseAddr = DEBUG_USART_DR_BASE;         
        /* 存储器地址*/
        DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)txBuffer;
        /* 从内存到外设*/                
        DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;        
        /* 传输大小*/        
        DMA_InitStructure.DMA_BufferSize = TX_BUFFER_SIZE;
        
        DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; 
        /* 内存地址自增*/
        DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;        
        /* 外设数据单位*/        
        DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
        /* 内存数据单位 8bit*/
        DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;        
        /* 开启循环循环*/
        DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;         
    
        DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;      
        /* 禁用FIFO*/
        DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;        
        DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;    
        /* 存储器单次传输*/
        DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;    
        /* 外设单次传输*/
        DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single;    
               
        DMA_Init(DEBUG_USART_DMA_STREAM, &DMA_InitStructure);
    
        /*使能DMA*/
        DMA_Cmd(DEBUG_USART_DMA_STREAM, ENABLE);
    
        /* 等待DMA数据流有效*/
        while(DMA_GetCmdStatus(DEBUG_USART_DMA_STREAM) != ENABLE)
        {
        }   
    }
    

    参考文献

    1.[STM32 DMA使用详解]

    2.[AN4031]

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32 DMA配置与应用实践

    发表评论