STM32
直播中

李颜

7年用户 1391经验值
私信 关注
[问答]

STM32标准库DMA串口编程的一般步骤包括哪些

一个完整的DMA传输过程必须经过哪几个阶段?
STM32标准库DMA串口编程的一般步骤包括哪些?

回帖(1)

李蒙

2021-12-7 09:31:09
DMA

简介

DMA(Direct Memory Access,直接存储器访问) ,DMA 传输是将数据从一个地址空间复制到另外一个地址空间。CPU 只负责初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。
原理

一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束4个步骤。


  • DMA请求
    CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。
  • DMA响应
    DMA控制器对DMA请求判别优先级及屏蔽,向总线裁决逻辑提出总线请求。当CPU执行完当前总线周期即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经响应,通过DMA控制器通知I/O接口开始DMA传输。
  • DMA传输
    DMA控制器获得总线控制权后,CPU即刻挂起或只执行内部操作,由DMA控制器输出读写命令,直接控制RAM与I/O接口进行DMA传输。
    在DMA控制器的控制下,在存储器和外部设备之间直接进行数据传送,在传送过程中不需要中央处理器的参与。开始时需提供要传送的数据的起始位置和数据长度。
  • DMA结束
    当完成规定的成批数据传送后,DMA控制器即释放总线控制权,并向I/O接口发出结束信号。当I/O接口收到结束信号后,一方面停 止I/O设备的工作,另一方面向CPU提出中断请求,使CPU从不介入的状态解脱,并执行一段检查本次DMA传输操作正确性的代码。最后,带着本次操作结果及状态继续执行原来的程序。

STM32标准库编程

DMA初始化结构体

结构体定义

typedef struct
{
    //指定DMAy信道的外设基址
    uint32_t DMA_PeripheralBaseAddr;
    //指定DMAy信道的内存基地址。
    uint32_t DMA_MemoryBaseAddr;
    //指定这个外设是作为数据传输的目的地还是数据传输的来源
    uint32_t DMA_DIR;
    //指定信道缓存的大小
    uint32_t DMA_BufferSize;
    //指定外设地址寄存器是否递增
    uint32_t DMA_PeripheralInc;
    //指定内存地址寄存器是否递增
    uint32_t DMA_MemoryInc;
    //指定外设数据宽度。
    uint32_t DMA_PeripheralDataSize;
    //指定内存数据宽度。
    uint32_t DMA_MemoryDataSize;
    //指定DMAy信道x的操作模式。
    uint32_t DMA_Mode;
    //指定DMAy信道x的软件优先级
    uint32_t DMA_Priority;
    //指定DMAy通道x是否将在内存到内存传输中使用
    uint32_t DMA_M2M;
} DMA_InitTypeDef;


结构体参数取值
DMA_DIR
指定这个外设是作为数据传输的目的地还是数据传输的来源
//外设作为数据传输的目的地
#define DMA_DIR_PeripheralDST              ((uint32_t)0x00000010)
//外设作为数据传输的来源
#define DMA_DIR_PeripheralSRC              ((uint32_t)0x00000000)


DMA_PeripheralInc
指定外设地址寄存器是否递增
//递增
#define DMA_PeripheralInc_Enable           ((uint32_t)0x00000040)
//不递增
#define DMA_PeripheralInc_Disable          ((uint32_t)0x00000000)


DMA_MemoryInc
指定内存地址寄存器是否递增
//递增
#define DMA_MemoryInc_Enable               ((uint32_t)0x00000080)
//不递增
#define DMA_MemoryInc_Disable              ((uint32_t)0x00000000)


DMA_PeripheralDataSize
指定外设数据宽度。
//一个字节(8位)
#define DMA_PeripheralDataSize_Byte        ((uint32_t)0x00000000)
//半个字(16位)
#define DMA_PeripheralDataSize_HalfWord    ((uint32_t)0x00000100)
//一个字(32位)
#define DMA_PeripheralDataSize_Word        ((uint32_t)0x00000200)


DMA_MemoryDataSize
指定内存数据宽度。
//一个字节(8位)
#define DMA_MemoryDataSize_Byte            ((uint32_t)0x00000000)
//半个字(16位)
#define DMA_MemoryDataSize_HalfWord        ((uint32_t)0x00000400)
//一个字(32位)
#define DMA_MemoryDataSize_Word            ((uint32_t)0x00000800)


DMA_Mode
指定DMAy信道x的操作模式。
//循环模式
#define DMA_Mode_Circular                  ((uint32_t)0x00000020)
//普通模式
#define DMA_Mode_Normal                    ((uint32_t)0x00000000)


DMA_Priority
指定DMAy信道x的软件优先级
//很高
#define DMA_Priority_VeryHigh              ((uint32_t)0x00003000)
//高
#define DMA_Priority_High                  ((uint32_t)0x00002000)
//中等
#define DMA_Priority_Medium                ((uint32_t)0x00001000)
//低
#define DMA_Priority_Low                   ((uint32_t)0x00000000)


DMA_M2M
指定DMAy通道x是否将在内存到内存传输中使用
//是
#define DMA_M2M_Enable                     ((uint32_t)0x00004000)
//不是
#define DMA_M2M_Disable                    ((uint32_t)0x00000000)


DMA串口编程的一般步骤



  • 配置NVIC分组
  • 初始化串口

    • 使能相应的串口以及对应的GPIO时钟
    • 配置NVIC结构体并应用
    • 配置GPIO结构体并应用
    • 配置Usart结构体并应用
    • 使能串口中断(看你需要什么中断,eg:使用USART_IT_IDLE)
    • 开启DMA接收
    • 使能串口


  • 初始化DMA

    • 使能相应的DMA时钟
    • 配置DMA结构体并应用
    • 使能DMA


  • 编写串口中断函数

示例代码

根据以上步骤书写代码,因为分文件书写这样看着会有点麻烦,所以我就将示例写在一个文件中,望各位大佬见谅
说明:
USART1与蓝牙进行连接,用于输入数据
USART2与PC进行连接,用于显示输入的数据

#include
#include
#include
//缓冲区的大小
#define Buff_Size 1024   
//此次接收结束的标志                  
volatile char rec_end_flag;
//接收二级缓存中的数量               
volatile unsigned short count;
//一级缓存,即DMA直接搬运数据的目的地            
volatile char usart1_recv_buff[Buff_Size];
/*二级缓存,即某次传输结束后将一级缓存的数据拷贝到此,
方便继续开启DMA使用一级缓存接收数据而数据不会被覆盖*/
volatile char recv_buff[Buff_Size];        


//设置NVIC的优先级分组
void NVIC_config(void)
{
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
}
//初始化串口以及GPIO
void usart1_init(void)
{
    //用于GPIO初始化的结构体
    GPIO_InitTypeDef GPIO_InitStructure;
    //用于串口初始化的结构体
    USART_InitTypeDef USART_InitStructure;


    NVIC_InitTypeDef NVIC_InitStruct;
    //1. 初始化结构体
    NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;
    //应用NVIC结构体
    NVIC_Init(&NVIC_InitStruct);


    //2. 使能相应的串口以及对应的GPIO时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);


    //3. 配置GPIO结构体并应用
    //初始化Usart1的Txd脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化Usart1的Rxd脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    //应用GPIO结构体
    GPIO_Init(GPIOA, &GPIO_InitStructure);


    //4. 配置Usart结构体并应用
    //初始化Usart1结构体
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART1, &USART_InitStructure);


    //5. 使能串口中断(看你需要什么中断,eg:使用USART_IT_IDLE)
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    //USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);


    //6. 开启DMA接收
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
    //7. 使能串口
    USART_Cmd(USART1, ENABLE);
}
//使用Debug的显示
void usart2_init(void)
{
    //用于GPIO初始化的结构体
    GPIO_InitTypeDef GPIO_InitStructure;
    //用于串口初始化的结构体
    USART_InitTypeDef USART_InitStructure;


    //开启该串口以及其对应GPIO的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);


    //初始化Usart2的Txd脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化Usart2的Rxd脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);


    //初始化Usart2结构体
    USART_InitStructure.USART_BaudRate = 9600;
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_Init(USART2, &USART_InitStructure);


    //使能Usart1
    USART_Cmd(USART2, ENABLE);
}


//初始化DMA
void DMA1_init(void)
{
    DMA_InitTypeDef DMA_InitStructure;


    //1.使能DMA1时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);


    //将DMA的通道5的寄存器重设为缺省值
    DMA_DeInit(DMA1_Channel5);


    //2. 配置DMA结构体并应用
    //设置DMA源地址
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) & (USART1->DR);
    //内存地址基地址
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)usart1_recv_buff;
    //数据传输方向,从外设读取发送到内存
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;


    DMA_InitStructure.DMA_BufferSize = 1024; //DMA通道的DMA缓存的大小
    //外设地址寄存器不递增
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    //内存地址寄存器递增
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    //外设数据宽度为8位
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    //内存数据宽度为8位
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    //工作模式为正常模式
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    //DMA通道 x拥有中等优先级
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
    //此传输不是内存到内存传输
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    //根据DMA_InitStruct中指定的参数初始化DMAy通道x。
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);


    //3. 使能DMA
    DMA_Cmd(DMA1_Channel5, ENABLE);
}
//串口1中断函数
void USART1_IRQHandler(void)
{
    //判断是否为空闲中断
    if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET)
    {
        //数据接收完毕标志置1
        rec_end_flag = 1;
        //关闭DMA,准备重新配置
        DMA_Cmd(DMA1_Channel5, DISABLE);
        //clear DMA1 Channel5 global interrupt.
        DMA_ClearITPendingBit(DMA1_IT_GL5);


        //计算接收数据长度
        count = 1024 - DMA_GetCurrDataCounter(DMA1_Channel5);
        memcpy((void *)recv_buff, (void *)usart1_recv_buff, count);


        //重新配置
        DMA_SetCurrDataCounter(DMA1_Channel5, 1024);
        DMA_Cmd(DMA1_Channel5, ENABLE);


        //清除IDLE标志位
        USART1->SR;
        USART1->DR;
    }
}
//串口发送一个字符(调试使用)
void usart_send_ch(USART_TypeDef *USARTx, char ch)
{
    while (RESET == USART_GetFlagStatus(USARTx, USART_FLAG_TC))
        ;
    USART_SendData(USARTx, ch);
}
//串口发送字符串(调试使用)
void usart_send_str(USART_TypeDef *USARTx, char *str, int length)
{
    int i;
    for (i = 0; i < length; i++)
    {
        usart_send_ch(USARTx, *(str + i));
    }
    //字符串末尾加换行rn
    char enter_str[] = {'r', 'n'};
    for (i = 0; i < 2; i++)
    {
        usart_send_ch(USARTx, enter_str);
    }
}
int main()
{
    //  1. 配置NVIC分组
    NVIC_config();
    //  2. 初始化串口以及GPIO
    usart1_init();
    //使用Debug的显示
    usart2_init();
    //  3. 初始化DMA
    DMA1_init();
    //  4. 编写串口中断函数
    //见USART1_IRQHandler


    while (1)
    {
        //如此次传输完成
        if (rec_end_flag == 1)
        {
            //将二级缓存的数据发送到串口2进行显示输出
            usart_send_str(USART2, (char *)recv_buff, count);
            //处理完数据后将标志置0等待下次传输结束
            rec_end_flag = 0;
        }
    }
}


结果展示

手机端发送





发送了36个字符
PC端接收





36个字符加rn刚好38个字符,成功
接着又发送了长一点的数据,也没有任何问题





总结

使用DMA的好处就是可以不用占用CPU的资源,另外CPU进入串口的空闲中断(USART_IT_IDLE)会比串口的接收中断(USART_IT_RXNE)的次数要少。而且使用空闲中断完美的解决了接收任意长度的数据,如若大佬们认为以上代码有任何Bug或错请在评论指出,我也会不断的完善。
举报

更多回帖

×
20
完善资料,
赚取积分