我通过学习江科大的视频以及CSDN一位大佬的博客,在下面记录下我对DMA的理解。
对于存储器,上一篇我写了ROM和RAM,尤其是SRAM、flash和外设寄存器,这里江科大提到了一个知识点,有助于理解。
首先,比如定义一个16进制数:
uint8_t a=0x66;
那么编译后,会在SRAM中开拓一片地址空间给a,比如地址为0x20000000对应a,因为a是变量,所以是SRAM。
如果,加上关键字:const,将变量变为常量,
const uint8_t a=0x66;
那么:现在a被存储在了flash里面,地址可能为0x080000FF。因为flash存储的是只读,常量无法被改变,因此是只读属性。因此,对于一些字库或者查找表,可以加const,让其被存储在flash中,释放SRAM空间。
另外,如果要查询外设寄存器地址,首先在数据手册里查找存储器映像,找到比如USART2的起始地址,然后找到USART2章节的具体寄存器映像,如果查找到这个寄存器偏移量,则起始地址+偏移量=具体寄存器地址。如果要从代码里面寻找:
例如:我要查找USART3的DATAR数据寄存器地址,
那么要找到USART2的起始地址和偏移量。如下图:
USART3的基地址是APB1外设基地址+0x4800的偏移量
也可以看到flash、SRAM及总线外设的基地址,说明APB1外设基地址为0x40000000,那么就知道了USART3的基地址,那USART3的偏移是多少呢?这里用了一个巧妙的办法,即结构体变量指代偏移量。
这时USART的结构体成员变量,它与实际寄存器在地址中的顺序一致,结构体的每个成员正好映射实际每个寄存器,实际就是指定了结构体成员的地址与对应外设寄存器地址一致。如此便解决了偏移的问题。&USART3->DATAR便是指定USART3的结构体指针,指向DATAR成员就是加上偏移地址。
这里要另外说明一个知识点:
在嵌入式系统中,常用的数据类型包括uint8_t、uint16_t、uint32_t等,因为它们的位数较小,能够节省内存空间。在进行数据传输时,可以根据实际需要选择合适的数据类型进行传输。
uint8_t:表示8位无符号整数,取值范围为0~255;
uint16_t:表示16位无符号整数,取值范围为0~65535;
uint32_t:表示32位无符号整数,取值范围为0~4294967295。
同理,int8_t是一个有符号8位整数类型,它的取值范围是-128~127。
u8和uint8_t的作用是相同的,都是用于表示无符号8位整数。但是,u8通常是一些特定场景(如嵌入式编译器下自定义的类型,而uint8_t则是C语言中内置的类型。
也就是说,比如我们拥有的数据最大不超过255时,可以设置数组为uint8_t data[100],这样便可以极大的节省内存空间。同时,比如串口接收发送数据都是以一个字节为单位的,设置8位整数,也有利于串口的功能。
这里我采用双开发板,一块是STM32F103RCT6开发板,另一块是沁恒CH32V307开发板,要实现的功能是:
STM32作为使用DMA+串口的发送方,创建两个函数,分别生成温度和湿度数值,将其保存到一个数组中,利用串口+DMA的方式把该数组的数据发送给接收端。
CH32V307作为使用DMA+串口的接收方,把接收的数组里面的数据区分开,并用串口调试助手打印出来。
uint8_t data[2];
uint8_t get_temperature(){
data[0]=rand() % 126 ;
return data[0];// 限定温度在-40到85摄氏度之间
}
uint8_t get_humi(){
data[1]=rand() % 101; // 限定湿度在0到100%之间
return data[1];
}
这里采用串口2。查看数据手册,发现USART2的TX功能对应DMA1的通道7,配置时要注意。
void Init_USART2(){
GPIO_InitTypeDef GPIO_InitStructure;//声明一个结构体对象
USART_InitTypeDef USART_InitStructure;
//NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2,ENABLE);//USART2挂载APB1总线
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//GPIOA挂载APB2总线
//对于哪个应用挂载哪个APB总线,可以根据代码自动补全功能快捷判断
//TX端口-PA2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;//这个对象的成员变量GPIO_Pin取值为pin2
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;//模式为复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//50MHZ速度
GPIO_Init(GPIOA,&GPIO_InitStructure);
//RX端口-PA3
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;//这个对象的成员变量GPIO_Pin取值为pin3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//模式为浮空输入模式
GPIO_Init(GPIOA,&GPIO_InitStructure);
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//
USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//收发模式并存
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_StopBits = USART_StopBits_1;//1位停止位
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//八位数据位
USART_Init(USART2,&USART_InitStructure);
//USART_ITConfig(USART2,USART_IT_RXNE,ENABLE);//开启串口2的中断接收
USART_Cmd(USART2,ENABLE);
}
void USART2_DMA_Tx_Configuration(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2 , ENABLE); //DMA2时钟使能
DMA_DeInit(DMA1_Channel7);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DR; //DMA外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART2_DMA_TX_Buffer; //发送缓存指针
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向,从内存到外设
DMA_InitStructure.DMA_BufferSize = USART2_DMA_TX_BUFFER_MAX_LENGTH; //传输长度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度:BYTE
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //内存数据宽度:BYTE
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //循环模式:否//(注:DMA_Mode_Normal为正常模式,DMA_Mode_Circular为循环模式)
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //优先级:高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //内存:内存(都)
DMA_Init(DMA1_Channel7 , &DMA_InitStructure); //初始化DMA1_Channel4
//DMA_ClearFlag(DMA1_FLAG_GL4);
DMA_ClearFlag(DMA1_FLAG_GL7);
DMA_Cmd(DMA1_Channel7 , DISABLE); //禁用DMA通道传输
USART_DMACmd(USART2, USART_DMAReq_Tx, ENABLE); //开启串口DMA发送
}
void DMA_send(){
//开启计数器,在传输过程中,DMA控制器会持续地递减该计数器的值,直到计数器为0,表示数据传输完成。
int len = sizeof(data);
memcpy(USART2_DMA_TX_Buffer, (uint8_t*)data, len);
DMA_SetCurrDataCounter(DMA1_Channel7,USART2_DMA_TX_BUFFER_MAX_LENGTH);
DMA_Cmd(DMA1_Channel7, ENABLE);//开启DMA传输
while(DMA_GetFlagStatus(DMA1_FLAG_TC7) != SET);
DMA_Cmd(DMA1_Channel7, DISABLE);//关闭DMA传输
DMA_ClearFlag(DMA1_FLAG_TC7);
}
int main(void)
{
delay_init(); //延时函数初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
uart_init(9600);
Init_USART2();
USART2_DMA_Tx_Configuration();
printf("666");
while(1)
{ get_humi();
get_temperature();
DMA_send();
delay_ms(1000);
}
}
首先先采用普通的串口接收方法。
void USART2_IRQHandler(void)
{
u8 Res;
int i;
if(USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
static uint8_t idx=0;//当前接收到的字节数
static uint8_t* ptr=(uint8_t*)res_data;//将数据转化为字节数组
Res=USART_ReceiveData(USART2);//读取接收到的字节
if(idx<data_length*sizeof(int)){//如果数据还未接受完
ptr[idx++]=Res;//将接收到的数据存储到数组中
}
if (idx==data_length*sizeof(int)) {//如果数据接收完毕
idx=0;
for ( i = 0; i < data_length;i++) {
res_data[i]=*((int*)(ptr+i*sizeof(int)));
}
}
USART_ClearITPendingBit(USART2, USART_IT_RXNE); // 清除接收中断标志位
}
}
这个方法比较普通,而且占用CPU资源,比如我发送100字节的数据,那CPU要频繁进入100次中断,明显不如DMA,把所有数据打包发送完,才进一次中断。
同样使用CH32V307的串口二进行DMA接收配置。
//DMA1的通道6对应USART2的RX
void DMA_RX_init(){
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1 , ENABLE); //DMA2时钟使能
DMA_DeInit(DMA1_Channel6);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART2->DATAR; //DMA外设地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)USART2_RxBuf; //发送缓存指针
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,从外设到内存
DMA_InitStructure.DMA_BufferSize = USART_MAX_LEN; //传输长度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设递增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //内存递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度:BYTE
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //内存数据宽度:BYTE
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //循环模式:否//(注:DMA_Mode_Normal为正常模式,DMA_Mode_Circular为循环模式)
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //优先级:高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //内存:内存(都)
DMA_Init(DMA1_Channel6 , &DMA_InitStructure); //初始化DMA1_Channel4
//DMA_ClearFlag(DMA1_FLAG_GL4);
//DMA_ClearFlag(DMA1_FLAG_GL6);
DMA_Cmd(DMA1_Channel6 , DISABLE); //禁用DMA通道传输
USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE); //开启串口DMA接收
USART_Cmd(USART2, ENABLE); //使能串口
}
void USART2_Server(){
uint16_t i,len;
// len = USART_MAX_LEN - DMA_GetCurrDataCounter(DMA1_Channel6); // 获取接收到的数据长度 单位为字节
//DMA_SetCurrDataCounter(DMA1_Channel6,USART_MAX_LEN); // 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目
DMA_Cmd(DMA1_Channel6 , ENABLE);
DMA_SetCurrDataCounter(DMA1_Channel6,USART_MAX_LEN); // 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目
//USART_ReceiveData(USART2); // 清除空闲中断标志位(接收函数有清标志位的作用)
//printf("data=%d\r\n",USART_ReceiveData(USART2));
// printf("data1=%d\r\n",USART2_RxBuf[0]);
//DMA_Cmd(DMA1_Channel6, DISABLE); // 关闭DMA1_Channel6不再接收数据
while(DMA_GetFlagStatus(DMA1_FLAG_TC6)==RESET);
DMA_Cmd(DMA1_Channel6, DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC6); // 清DMA1_Channel6接收完成标志位
//DMA_SetCurrDataCounter(DMA1_Channel6,USART_MAX_LEN); // 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目
//for (i = 0; i < len; ++i) { // 把接收到的数据转移到发送数组
// res_data[i] = USART2_RxBuf[i];
// printf("res%d=%d\r\n",i,res_data[i]);
}
extern uint8_t USART2_RxBuf[USART_MAX_LEN]; //接收缓存
extern uint8_t res_data[2];
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
SystemCoreClockUpdate();
Delay_Init();
USART_Printf_Init(115200);
printf("SystemClk:%d\r\n", SystemCoreClock);
//printf( "ChipID:%08x\r\n", DBGMCU_GetCHIPID() );
printf("RTC Test\r\n");
// USART3_INIT(9600);
uart2_init(9600);
DMA_RX_init();
printf(" Test\r\n");
while(1)
{
USART2_Server();
printf("res_data[0]=%d\r\n",USART2_RxBuf[0]);
printf("res_data[1]=%d\r\n",USART2_RxBuf[1]);
Delay_Ms(1000);
}
}
当发送端产生随机数据时,数据从内存被搬运到串口2的数据寄存器,并发送给接收端。接收端的数据寄存器通过DMA搬运到指定的内存中。如图所示,实验结果正确。
遇到这个错误的背景是:我的主机(发送数据端)测得相关数据后就利用DMA发送给从机(接收数据端)。发送数据的速度很快,没什么,但是我的从机的主程序里每次执行DMA_res()函数的反应慢,需要等待其他程序执行完才能执行接收,
这就造成了一个问题:DMA接收数据发生上溢错误
具体解释:我们知道,接收端采用串口+DMA接收数据时,常规方法是:
即先禁用DMA,再开启串口DMA接收,再使能开启串口,这时主机发送过来的数据便先保存在了串口数据寄存器中,但还没有利用DMA转移到内存中存储的地址去。
主程序里循环执行这些语句:使能DMA,使得这些滞留的数据被DMA转移到内存中,然后再禁用DMA,完成一次传输。注意,这时串口始终是使能的,也就是说串口数据寄存器正源源不断的接收数据,而由于我主程序调用DMA转移数据的速度很快,因此也不怕数据滞留,使得数据很快地从串口数据寄存器->DMA运输->内存。
然鹅,如果我的主程序由于一些其他操作,导致调用DMA转移数据的速度变慢,使得串口数据寄存器中已经接收到一个数据,但没有被及时转移走(由于DMA还没有被及时开启),后面的数据又紧跟着来了,从而导致后面的数据无法存入,产生了上溢错误
解决方法:让DMA的使能、禁用与串口的使能、禁用保持一致
意思是说,串口初始化后,调用DMA接收函数时,修改为如下:
void USART3_Server(){
USART_Cmd(USART3, ENABLE); //使能串口
DMA_Cmd(DMA1_Channel3, ENABLE);
DMA_SetCurrDataCounter(DMA1_Channel3,USART_MAX_LEN); // 重新赋值计数值,必须大于等于最大可能接收到的数据帧数目
while(DMA_GetFlagStatus(DMA1_FLAG_TC3)==RESET);
USART_Cmd(USART3, DISABLE); //禁用串口
DMA_Cmd(DMA1_Channel3, DISABLE);
DMA_ClearFlag(DMA1_FLAG_TC3); // 清DMA1_Channel6接收完成标志位
}
主程序里,每次调用这个函数时,才开启串口,接收数据,再开启DMA转移数据,转移结束后关闭串口,使得新的数据不会滞留在串口数据寄存器中,在关闭DMA,退出函数,直到下一次调用该函数完成后续数据的转移。
DMA真的很有用。但是DMA+中断我还没有使用,后面可能会试试。