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

  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现PC与开发板UDP收发数据需要以下步骤: 1. 配置STM32f103的网络模块ENC28J60,使其能够连接到局域网或者互联网。 2. 使用STM32f103的网络库LwIP来实现UDP协议的收发功能,包括创建UDP连接、发送UDP数据包和接收UDP数据包等。 3. 在PC端使用相应的UDP工具来发送和接收UDP数据包,比如UDP Test Tool、Wireshark等。 下面分别介绍这些步骤的具体实现方法: 1. 配置STM32f103的网络模块ENC28J60 ENC28J60是一款低成本、高性能的以太网控制器芯片,可以通过SPI接口与STM32f103进行通讯。在进行ENC28J60配置之前,需要先确定开发板的IP地址、子网掩码、网关等网络参数。 首先需要在STM32f103中配置SPI接口,然后通过SPI接口与ENC28J60进行通讯,进行寄存器的读写操作,设置ENC28J60的工作模式、MAC地址、IP地址、子网掩码、网关等参数。具体的ENC28J60配置方法可以参考ENC28J60的数据手册和STM32f103的LwIP库的使用说明。 2. 使用STM32f103的网络库LwIP来实现UDP协议的收发功能 LwIP是一个轻量级的TCP/IP协议栈,适用于嵌入式系统。在STM32f103中,可以使用LwIP库来实现UDP协议的收发功能。 首先需要在STM32f103中配置LwIP库,然后创建UDP连接、发送UDP数据包和接收UDP数据包等操作。具体的LwIP库使用方法可以参考LwIP的使用手册和STM32f103的LwIP库的使用说明。 3. 在PC端使用相应的UDP工具来发送和接收UDP数据包 在PC端,可以使用相应的UDP工具来发送和接收UDP数据包。比如UDP Test Tool是一款简单易用的UDP测试工具,可以用来测试UDP服务器和客户端的收发功能。Wireshark是一款强大的网络协议分析工具,可以抓取和分析UDP数据包。通过这些工具,可以向STM32f103发送UDP数据包,并查看STM32f103返回的UDP数据包。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值