前言
本系列,将stm32常用片上外设进行整理,包括大致原理和代码,主要是熟悉如何根据手册去编写代码。便于以后需要做实验时,能够快速编写基本的驱动,然后编写应用代码。
本系列基于“标准库”整理,开发板用的是正点原子精英版V1.5,单片机是STM32F103ZET6。开发工具KEIL。主要参考资料有《STM32F103xCDE中文参考手册》《STM32F103xCDE中文数据手册》、《CM3权威指南》、《STM32F103xCDE闪存编程手册》、《精英版原理图V1.5》。
》《STM32F1xx Cortex-M3编程手册-英文版》。
一、DMA介绍
DMA(Direct Memory Access),即直接存储器访问。支持三种模式,分别是:内存<->内存、内存<->外设、外设<->内存。DMA就是实现搬运数据的功能,从而将CPU空出来,使CPU可以去干其它事情,例如用串口进行大量数据接受,串口最多8位或者9位,来一个字节的数据就得使用中断去读取,当来1000个字节时就得进入1千次中断,这时候CPU都去处理串口接受数据了,那么其它任务就被搁置了,所以DMA就来帮CPU搬运串口数据,CPU可以正常干其他事情。从而提高系统响应程度。
STM32F1系列有DMA1支持7个独立通道,大容量还要DMA2支持5个独立通道,所以一共12个独立通道。如果全部用来接受外设数据的话,可以接12个。
二、DMA框图
DMA框图由于框图比较简单,所以和系统框图十分相似。此图在《STM32F103xCDE中文参考手册》10.2的最后,如下图所示:
系统架构如下图所示:
从图中可以看出DMA多个通道同时发起请求时,是需要仲裁器来选择到底先响应那一通道请求的。
三、DMA源端宽度和目标宽度不一致时的对齐方式
在项目中遇到过这种情况,通过串口发送一串数据,通过DMA来搬运。此时源端是内存,目标是串口DR寄存器。如果将源端宽度设置位16位,串口的DR寄存器是8位的。此时接收端只会收到低字节,而丢掉高字节。如发送uint16-t array= {0x01,0x02,x03,0x04,0x05,0x06},使用DMA搬运,通过串口发送给串口调试助手,如果源大小16位,目标8位,此时串口调试助手只会收到0x01,0x03,0x05。此映射表在《STM32F103xCDE中文参考手册》的10.3.4,如下图所示:
四、DMA请求映射图
经常在配置代码时,不知道去哪找,串口1的tx到底是映射到DMA的那一路通道。其实此映射表在《STM32F103xCDE中文参考手册》的10.3.7 DMA请求映射,有图也有表,如下图所示。
五、串口使用DMA搬运数据配置
在标准库中配置DMA搬运串口数据,发送使用DMA,接受使用空闲中断和DMA,便于读者查看代码,全部在main函数里。以下代码实现,开机向串口助手发送hello,接受到串口助手发送的数据然后回发给串口助手。
#include "stm32f10x.h"
void usart1_dma_init(void);
void com_usart_write_byte(uint8_t * psrc,uint32_t unit_num);
void com_usart_start_recv(void);
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
usart1_dma_init();
com_usart_write_byte("hello",7);
com_usart_start_recv();//开启dma接受数据
while(1)
{
}
}
#define SEND_BUF_SIZE 256
#define RECV_BUF_SIZE 256
uint8_t g_sendbuff[SEND_BUF_SIZE]; //发送数据缓冲区
uint8_t g_recvbuff[RECV_BUF_SIZE]; //发送数据缓冲区
void usart1_dma_init(void)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟
//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9
//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//使能DMA1时钟
//根据手册映射,USART1 TX 映射到DMA1的通道4
DMA_DeInit(DMA1_Channel4); //将DMA的通道1寄存器重设为缺省值
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; //DMA外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = 0; //DMA内存基地址,初始化时不配置,留给发送函数
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //数据传输方向,从内存读取发送到外设
DMA_InitStructure.DMA_BufferSize = 0; //DMA通道的DMA缓存的大小,初始化时不配置,留给发送函数
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA1_Channel4, &DMA_InitStructure); //DMA1_Channel4
//根据手册映射,USART1 RX 映射到DMA1的通道5
DMA_DeInit(DMA1_Channel5); //将DMA的通道1寄存器重设为缺省值
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; //DMA外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = 0; //DMA内存基地址,初始化时不配置,留给接受函数
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设到内存
DMA_InitStructure.DMA_BufferSize = 0; //DMA通道的DMA缓存的大小,初始化时不配置,留给接受函数
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存地址寄存器递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //数据宽度为8位
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //工作在正常缓存模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA1_Channel5, &DMA_InitStructure); //DMA1_Channel4
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器
//USART 初始化设置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟
USART_InitStructure.USART_BaudRate = 115200;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
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_Rx | USART_Mode_Tx; //收发模式
USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);//开启串口空闲中断
USART_DMACmd(USART1,USART_DMAReq_Tx,ENABLE); //使能串口1的DMA发送
USART_DMACmd(USART1,USART_DMAReq_Rx,ENABLE); //使能串口1的DMA接受
USART_Cmd(USART1, ENABLE); //使能串口1
}
//使用USART1 TX发送数据时调用
void com_usart_write_byte(uint8_t * psrc,uint32_t unit_num)
{
uint16_t i;
if( unit_num > SEND_BUF_SIZE )
{
return;
}
for(i = 0; i< unit_num; i++)
{
g_sendbuff[i] = psrc[i];
}
DMA_Cmd(DMA1_Channel4, DISABLE ); //关闭USART1 TX DMA1 所指示的通道
DMA_SetCurrDataCounter(DMA1_Channel4,unit_num);//DMA通道的DMA缓存的大小
DMA1_Channel4->CMAR = (uint32_t) (&g_sendbuff[0]);//因为没有相关库函数,所以使用寄存器配置
DMA_Cmd(DMA1_Channel4, ENABLE); //使能USART1 TX DMA1 所指示的通道
}
//开启USART1 RX接受数据,有数据来时,DMA会将数据搬运到RecvBuff中,配置串口空闲中断,空闲中断产生时,RecvBuff中就会有数据
void com_usart_start_recv(void)
{
DMA_Cmd(DMA1_Channel5, DISABLE ); //关闭USART1 RX DMA1 所指示的通道
DMA_SetCurrDataCounter(DMA1_Channel5,RECV_BUF_SIZE);//DMA通道的DMA缓存的大小为RECV_BUF_SIZE
DMA1_Channel5->CMAR = (uint32_t)(&g_recvbuff[0]);//因为没有相关库函数,所以使用寄存器配置
DMA_Cmd(DMA1_Channel5, ENABLE); //使能USART1 RX DMA1 所指示的通道
}
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1,USART_IT_IDLE) == SET)
{
//根据手册串口寄存器SR的bit4介绍,先读SR再读DR,清除IDLE位
USART1->SR;
USART1->DR;
printf("recv:%s",g_recvbuff);
com_usart_start_recv();//开始从头接收数据
}
}
六、总结
熟读一遍手册中DMA介绍和DMA寄存器,加上有标准库,配置起来还是比较容易,DMA很好用,需要注意的是源大小和目标大小要一致(见二),第二就是外设和DMA通道的映射图(见三)。