本章利用 DMA 来实现串口数据传送,并在LCD 模块上显示当前的传送进度。
DMA 简介
DMA,全称为:Direct Memory Access,即直接存储器访问。DMA 传输方式无需 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为 RAM 与 I/O 设备开辟一条直接传送数据的通路,能使 CPU 的效率大为提高。
STM32F429 最多有 2 个 DMA 控制器,共 16 个数据流(每个控制器 8 个),每一个 DMA控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达 8 个通道(或称请求)。每个数据流通道都要一个仲裁器,用于处理 DMA 请求间的优先级。
STM32F429 的 DMA 有以下一些特性:
① 双 AHB 主总线架构,一个用于存储器访问,另一个用于外设访问
② 仅支持 32 位访问的 AHB 从编程接口
③ 每个 DMA 控制器有 8 个数据流,每个数据流有多达 8 个通道(或称请求)
④ 每个数据流有单独的四级 32 位先进先出存储器缓冲区(FIFO),可用于 FIFO 模式或直接模式。
⑤ 通过硬件可以将每个数据流配置为:
1,支持外设到存储器、存储器到外设和存储器到存储器传输的常规通道
2,支持在存储器方双缓冲的双缓冲区通道
⑥ 8 个数据流中的每一个都连接到专用硬件 DMA 通道(请求)
⑦ DMA 数据流请求之间的优先级可用软件编程(4 个级别:非常高、高、中、低),在软件优先级相同的情况下可以通过硬件决定优先级(例如,请求 0 的优先级高于请求 1)
⑧ 每个数据流也支持通过软件触发存储器到存储器的传输(仅限 DMA2 控制器)
⑨ 可供每个数据流选择的通道请求多达 8 个。此选择可由软件配置,允许几个外设启动 DMA 请求
⑩ 要传输的数据项的数目可以由 DMA 控制器或外设管理:
1,DMA 流控制器:要传输的数据项的数目是 1 到 65535,可用软件编程
2,外设流控制器:要传输的数据项的数目未知并由源或目标外设控制,这些外设通过硬件发出传输结束的信号。
⑪ 独立的源和目标传输宽度(字节、半字、字):源和目标的数据宽度不相等时,DMA 自动封装/解封必要的传输数据来优化带宽。这个特性仅在 FIFO 模式下可用。
⑫ 对源和目标的增量或非增量寻址
⑬ 支持 4 个、8 个和 16 个节拍的增量突发传输。突发增量的大小可由软件配置,通常等于外设FIFO 大小的一半。
⑭ 每个数据流都支持循环缓冲区管理
⑮ 5 个事件标志(DMA 半传输、DMA 传输完成、DMA 传输错误、DMA FIFO 错误、直接模式错误),进行逻辑或运算,从而产生每个数据流的单个中断请求。
DMA 框图
STM32F429有两个 DMA控制器,DMA1和DMA2。每个 DMA 控制器有 8 个数据流,每个数据流有 8 个通道(或称请求)。DMA2 支持通过软件触发存储器到存储器的传输。
图中,我们标记了 6 处位置,起作用分别是:
① DMA 控制器的从机编程接口,通过该接口可以对 DMA 的相关控制寄存器进行设置,从而配置 DMA,实现不同的功能。
② DMA 控制器的外设接口,用于访问相关外设,特别的,当外设接口设置的访问地址是内存地址的时候,DMA 就可以工作在内存到内存模式了。
③ DMA 控制器的 FIFO 区,每个数据流(总共 8 个数据流)都有一个独立的 FIFO,可以实现存储器接口到外设接口之间的数据长度非对齐传输。
④ DMA 控制器的存储器接口,用于访问外部存储器,特别的当存储器地址是外设地址的时候,可以实现类似外设到外设的传输效果。
⑤ DMA 控制器的仲裁器,用于仲裁数据流 0~7 的请求优先级,保证数据有序传输。
⑥ DMA 控制器数据流多通道选择。
通过 DMA_SxCR 寄存器控制,每个数据流有多达 8 个通道请求可以选择,如图所示:
外设的 8 个请求独立连接到每个通道,由 DMA_SxCR 控制数据流选择哪一个通道,每个数据流有 8 个通道可供选择,每次只能选择其中一个通道进行 DMA 传输。DMA1 各数据流通道映射具体见表 31.1.1.1,DMA2 各数据流通道映射具体见表 31.1.1.2。
DMA 寄存器
⚫ DMA 中断状态寄存器(DMA_LISR 和 DMA_HISR)
DMA 中断状态寄存器,该寄存器总共有 2 个:DMA_LISR 和 DMA_HISR,每个寄存器管理 4 数据流,DMA_LISR 寄存器用于管理数据流 0~3,而 DMA_HISR 用于管理数据流 4~7。这两个寄存器各位描述都完全一模一样,只是管理的数据流不一样。
DMA 中断状态寄存器 LISR 和 HISR 用来存放对应 4 个数据流对应的 5 个传输状态对应的中断标志位,分别是 传输完成中断标志、半传输中断标志、传输错误中断标志、直接模式错误中断标志、FIFO 错误中断标志。注意此寄存器为只读寄存器,所以在这些位被置位之后,只能通过其他的寄存器来清除。DMA_HISR 寄存器各位描述通 DMA_LISR寄存器各位描述完全一样,只是对应数据流 4~7 。
⚫ DMA 中断标志清除寄存器(DMA_LIFCR 和 DMA_HIFCR)
DMA 中断标志清除寄存器, 该寄存器同样有 2 个:DMA_LIFCR 和 DMA_HIFCR,同样是每个寄存器控制 4 个数据流,DMA_LIFCR 寄存器用于管理数据流 0~3,而 DMA_ HIFCR 用于管理数据流 4~7。
LIFCR 的各位就是用来清除 LISR 的对应位的,通过写1清除。在 LISR 被置位后,我们必须通过向该位寄存器对应的位写入 1 来清除。
⚫ DMA 数据流 x 配置寄存器(DMA_SxCR)(x=0~7,下同)
该寄存器控制着 DMA 的很多相关信息,包括数据宽度、外设及存储器的宽度、优先级、增量模式、传输方向、中断允许、使能等都是通过该寄存器来设置的。所以 DMA_SxCR 是 DMA 传输的核心控制寄存器。
⚫ DMA 数据流 x 数据项数寄存器(DMA_SxNDTR)
该寄存器控制 DMA 数据流 x 的每次传输所要传输的数据量。其设置范围为 0~65535。且该寄存器的值会随着传输的进行而减少,当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存器的值来知道当前 DMA 传输的进度。特别注意,这里是数据项数目,而不是指的字节数。比如设置数据位宽为 16 位,那么传输一次(一个项)就是 2 个字节。
DMA数据传输实验
例程:按下按键,串口以 DMA 方式发送数据,同时 LCD 上显示传输进度。
DMA 的 HAL 库驱动:
DMA 在 HAL 库中的驱动代码在 stm32f4xx_hal_dma.c 文件(及其头文件)中。
DMA 的初始化函数,其声明如下:
HAL_StatusTypeDef HAL_DMA_Init(DMA_HandleTypeDef *hdma);
⚫ 函数描述:
用于初始化 DMA1,DMA2。
⚫ 函数形参:
形参 1 是 DMA_HandleTypeDef 结构体类型指针变量,其定义如下:
typedef struct __DMA_HandleTypeDef
{
DMA_Stream_TypeDef *Instance; /* 寄存器基地址 */
DMA_InitTypeDef Init; /* DAM 通信参数 */
HAL_LockTypeDef Lock; /* DMA 锁对象 */
__IO HAL_DMA_StateTypeDef State; /* DMA 传输状态 */
void *Parent; /* 父对象状态,HAL 库处理的中间变量 */
void (*XferCpltCallback)( struct __DMA_HandleTypeDef *hdma);/*DMA 传输完成回调*/
/* DMA 一半传输完成回调 */
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
/* DMA 传输完整的 Memory1 回调 */
void (* XferM1CpltCallback)( struct __DMA_HandleTypeDef * hdma);
/* DMA 传输半完全内存回调 */
void (* XferM1HalfCpltCallback)( struct __DMA_HandleTypeDef * hdma);
/*DMA 传输错误回调*/
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma);
/* DMA 传输中止回调 */
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma);
__IO uint32_t ErrorCode; /* DMA 存取错误代码 */
uint32_t StreamBaseAddress;/* DMA 通道基地址 */
uint32_t StreamIndex; /* DMA 通道索引 */
}DMA_HandleTypeDef;
Instance:是用来设置寄存器基地址,例如要设置的对象是串口 1 的发送,那么就要参考表 31.1.1,串口 1 的 DMA 传输需要用到的是 DMA2 的数据流 7,即 DMA2_Stream7。
Parent:是 HAL 库处理中间变量,用来指向 DMA 通道外设句柄。
StreamBaseAddress 和 StreamIndex 是数据流基地址和索引号,这个是 HAL 库处理的时候会自动计算,用户无需设置。
其他成员变量是 HAL 库处理过程状态标识变量。
接下来我们重点介绍 Init,它是 DMA_InitTypeDef 结构体类型变量,该结构体定义如下:
typedef struct
{
uint32_t Channel; /* 传输通道,例如:DMA_CHANEL_4 */
uint32_t Direction; /* 传输方向,例如存储器到外设 DMA_MEMORY_TO_PERIPH */
uint32_t PeriphInc; /* 外设(非)增量模式,非增量模式 DMA_PINC_DISABLE */
uint32_t MemInc; /* 存储器(非)增量模式,增量模式 DMA_MINC_ENABLE */
uint32_t PeriphDataAlignment;/* 外设数据大小:8/16/32 位 */
uint32_t MemDataAlignment; /* 存储器数据大小:8/16/32 位 */
uint32_t Mode; /* 模式:外设流控模式/循环模式/普通模式 */
uint32_t Priority; /* DMA 优先级:低/中/高/非常高 */
uint32_t FIFOMode; /* FIFO 模式开启或者禁止 */
uint32_t FIFOThreshold; /* FIFO 阈值选择 */
uint32_t MemBurst; /* 存储器突发模式:单次/4 个节拍/8 个节拍/16 个节拍 */
uint32_t PeriphBurst; /* 外设突发模式:单次/4 个节拍/8 个节拍/16 个节拍 */
}DMA_InitTypeDef;
我们通过该结构体成配置 DMA_SxCR 寄存器和 DMA_SxFCR 寄存器的相应位。
DMA方式传输串口数据配置步骤
1)使能 DMA 时钟
DMA 的时钟使能是通过 AHB1ENR 寄存器来控制的,这里我们要先使能时钟,才可以配置 DMA 相关寄存器。
__HAL_RCC_DMA1_CLK_ENABLE(); /* DMA1 时钟使能 */
__HAL_RCC_DMA2_CLK_ENABLE(); /* DMA2 时钟使能 */
2)初始化 DMA
调用 HAL_DMA_Init 函数初始化 DMA 的相关参数,包括配置通道,外设地址,存储器地
址,传输数据量等。
HAL 库为了处理各类外设的 DMA 请求,在调用相关函数之前,需要调用一个宏定义标识符,来连接 DMA 和外设句柄。例如要使用串口 DMA 发送,所以方式为:
__HAL_LINKDMA(&g_uart1_handle, hdmatx, g_dma_handle);
其中 g_uart1_handle是串口初始化句柄,我们在 usart.c中定义过了。g_dma_handle是 DMA 初始化句柄。hdmatx 是外设句柄结构体的成员变量,实际就是 g_uart1_handle 的成员变量。
3)使能串口的 DMA 发送,启动传输
串口 1 的 DMA 发送实际是串口控制寄存器 CR3 的位 7 来控制的,在 HAL 库中操作该寄存器来使能串口 DMA 发送的函数为 HAL_UART_Transmit_DMA。调用该函数后会开启相应的 DMA 中断。
4)查询 DMA 传输状态
查询通道3传输完成标志位
__HAL_DMA_GET_FLAG(&g_dma_handle, DMA_FLAG_TCIF3_7);
获取当前传输剩余数据量:
__HAL_DMA_GET_COUNTER(&g_dma_handle);
设置对应的 DMA 数据流传输的数据量大小,函数为:
__HAL_DMA_SET_COUNTER (&g_dma_handle, 1000);
5)DMA 中断使用方法
DMA 中断对于每个流都有一个中断服务函数。
HAL 库提供了通用 DMA 中断处理函数 HAL_DMA_IRQHandler,在该函数内部,会对 DMA 传输状态进行分析,然后调用相应的中断处理回调函数:
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); /* 发送完成回调函数 */
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);/* 发送一半回调函数 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart); /* 接收完成回调函数 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);/* 接收一半回调函数 */
void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart); /* 传输出错回调函数 */