STM32(8)-DMA+串口实现双开发板数据收发

我通过学习江科大的视频以及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+串口的接收方,把接收的数组里面的数据区分开,并用串口调试助手打印出来

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+中断我还没有使用,后面可能会试试。

猜你喜欢

转载自blog.csdn.net/qq_53092944/article/details/130673554