STM32(8)-使用DMA实现双开发板之间的串口数据收发

我通过学习江科大的视频以及CSDN一位大佬的博客,在下面记录下我对DMA的理解。

文章目录

  • 一、存储器、寄存器
  • 二、具体代码
  • 1.STM32(发送方)DMA配置
  • (1)作为发送方,自然数据要从内存发往串口数据寄存器去,我定义一个数组,并编写两个函数,分别返回温度和湿度数值,作为数组里的元素。
  • (2)配置DMA,方向是从内存到外设寄存器
  • (3)配置DMA初始化
  • (4)DMA开启传输函数
  • (5)主程序
  • 2.CH32V307(接收方)采用普通的串口中断接收
  • 3.CH32V307(接收方)DMA配置
  • (1)作为接收方,自然数据要从串口数据寄存器发往内存去,因此DMA配置要更改。
  • (2)DMA启动程序
  • (3)主程序
  • 三、结果:
  • 四、一个开发中遇到的问题,比较有意思: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+串口的接收方,把接收的数组里面的数据区分开,并用串口调试助手打印出来

    1.STM32(发送方)DMA配置

    (1)作为发送方,自然数据要从内存发往串口数据寄存器去,我定义一个数组,并编写两个函数,分别返回温度和湿度数值,作为数组里的元素。

    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)配置DMA,方向是从内存到外设寄存器

    这里采用串口2。查看数据手册,发现USART2的TX功能对应DMA1的通道7,配置时要注意。


    首先正常配置串口2,波特率为9600。
    代码如下:

    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);
    	
    }
    
    

    (3)配置DMA初始化

    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发送
    }
    

    (4)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);
    
    }
    

    (5)主程序

     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);
    	}
     }
    

    2.CH32V307(接收方)采用普通的串口中断接收

    首先先采用普通的串口接收方法。

    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,把所有数据打包发送完,才进一次中断。

    3.CH32V307(接收方)DMA配置

    同样使用CH32V307的串口二进行DMA接收配置。

    (1)作为接收方,自然数据要从串口数据寄存器发往内存去,因此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);      //使能串口
    }
    

    (2)DMA启动程序

    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]);
            }
    

    (3)主程序

    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发送给从机(接收数据端)。发送数据的速度很快,没什么,但是我的从机的主程序里每次执行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+中断我还没有使用,后面可能会试试。

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32(8)-使用DMA实现双开发板之间的串口数据收发

    发表评论