STM32串口配置性能对比

Preface

上一篇博客说了下STM32串口丢失首字符相关的问题,这篇博客呢,想好好的讨论一下基于串口的printf函数。

Attention:三种方式已经上传到GitHub,基于正点原子探索者开发板!都是以工程文件夹的形式,下载编译运行即可

Github地址

这个函数相信大家都不陌生,你开始在学习C语言的时候是不是这样的呢

#include "stdio.h"
int main(void)
{
    
    
    printf("hello world\n");
    return 0;
}

我们今天说的这个printf不在于它源码的实现,而在于使用它对嵌入式系统性能的影响。

1.printf

我们在嵌入式开发的过程中,经常用到串口这个外设。我们总是使用printf来打印调试信息,但是你知道吗,这个函数可没有你想象的那么好。

printf函数源码太大,我们写个简易版本的(对系统的影响没有区别)。

static void myprintf(char *str)//这是串口发送函数
{
    
    
    while (*str)
    {
    
    
        USART1->DR = (u8)*str;
        while ((USART1->SR & 0X40) == 0)
            ; //循环发送,直到发送完毕
        str++;
    }
}

上面函数就是实现了字符串的打印输出。

看起来没有问题,但是你有没有发现:

当cpu向串口数据寄存器写完数据,必须等待写入的数据全部发送出去以后才能够再次写入

那么这就意味在这段时间内cpu做不了其它的事,如果待发送的数据量大的话,那效率就太低了。

void task1_task(void *pvParameters)
{
    
    
  char recv_item = 0;
  while (1)
  {
    
    
    if (xQueueReceive(public_queue, &recv_item, 1000 / portTICK_PERIOD_MS) == pdTRUE)
    {
    
    
      putchar(recv_item);
    }
    else
    {
    
    
      printf("hello world!!!\r\n");
    }
  }
}

这个任务的功能是:每过1s中通过串口发送hello world。

再看systemview:

Context

task1占用cpu0.14%的资源,看起来不是很多。

Timeline

每调用一次函数,差不多要用到1.5ms!!!

竟然需要花费这么多的时间,这下你就知道为什么不要在中断函数里面调用printf了!!!

时间实在是太长了,freertos的心跳时间就是1个ms,差不多你调用一个printf,其它任务也就别执行了(假设调用printf的任务优先级最高)。

这显然是对嵌入式系统有很大的影响,特别是随着系统任务的增加。

Cpu Load

当你使用printf的时候,就相当于从那个时刻开始的之后一段时间内,cpu的资源都要给printf

总结

在嵌入式开发过程中,如果涉及到大量且频繁的数据这种方法肯定是不行的。

2.printf+interrupt

针对于单纯的使用printf函数所带来的总总问题,可以采用串口加中断的方式,以下是基于STM32的实现方法

2.1串口的初始化

void uart_init(u32 bound)
{
    
    
	//GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);  //使能GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); //使能USART1时钟

	//串口1对应引脚复用映射
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1);  //GPIOA9复用为USART1
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); //GPIOA10复用为USART1

	//USART1端口配置
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; //GPIOA9与GPIOA10
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;			//复用功能
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//速度50MHz
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;			//推挽复用输出
	GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;			//上拉
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//初始化PA9,PA10

	//USART1 初始化设置
	USART_InitStructure.USART_BaudRate = bound;										//波特率设置
	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_Cmd(USART1, ENABLE); //使能串口1

	//USART_ClearFlag(USART1, USART_FLAG_TC);

#if EN_USART1_RX
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //开启相关中断
    <font color='red'>注意,这个时候不要开启发送中断</font>
	//Usart1 NVIC 配置
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;		   //串口1中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 14; //抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;		   //子优先级3
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;			   //IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);							   //根据指定的参数初始化VIC寄存器、

#endif
}

在这里要强调一下:千万不要串口初始化的时候开启发送中断。

因为STM32串口上电复位的TXE标志位为1。我上一篇博客有说!!!

2.2串口中断服务函数

void USART1_IRQHandler(void) //串口1中断服务程序
{
    
    
	SEGGER_SYSVIEW_RecordEnterISR();
	u8 Res;
	extern char *pc_MAIN_myprintf1;
	if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
	{
    
    
		Res = USART_ReceiveData(USART1); //(USART1->DR);	//读取接收到的数据

		if ((USART_RX_STA & 0x8000) == 0) //接收未完成
		{
    
    
			if (USART_RX_STA & 0x4000) //接收到了0x0d
			{
    
    
				if (Res != 0x0a)
					USART_RX_STA = 0; //接收错误,重新开始
				else
					USART_RX_STA |= 0x8000; //接收完成了
			}
			else //还没收到0X0D
			{
    
    
				if (Res == 0x0d)
					USART_RX_STA |= 0x4000;
				else
				{
    
    
					USART_RX_BUF[USART_RX_STA & 0X3FFF] = Res;
					USART_RX_STA++;
					if (USART_RX_STA > (USART_REC_LEN - 1))
						USART_RX_STA = 0; //接收数据错误,重新开始接收
				}
			}
		}
	}
	if (USART_GetITStatus(USART1, USART_IT_TXE) != RESET) //发送中断
	{
    
    
		if (*pc_MAIN_myprintf1 == pdFALSE)
		{
    
    
			USART_ITConfig(USART1, USART_IT_TXE, DISABLE); //数据发送完成,串口发送中断失能
		}
		else
		{
    
    
			USART1->DR = (u8)*pc_MAIN_myprintf1++; //数据还没有发送完成
		}
	}
	SEGGER_SYSVIEW_RecordExitISR();
}

我这个代码都是基于正点原子的。

在正点原子原有的基础之上,添加中断相关的代码块即可。

2.3中断printf函数的实现

static void myprintf1(char *str) //但是需要注意,字符串以/0结尾
{
    
    
  USART_ClearFlag(USART1, USART_FLAG_TC);       //防止出现stm32第一个字符丢失的现象
  pc_MAIN_myprintf1 = str;                      //更新printf所指向的字符串
  USART_ITConfig(USART1, USART_IT_TXE, ENABLE); //开启发送中断
}

上面这个函数主要是针对字符串以’\0’结尾的。

开启发送中断即可直接进入中断服务函数

2.4效果

Context

睁大你们的双眼看看:0%

几乎没有占用cpu的资源,为什么,你可能很纳闷。看下面。

Timeline

看出什么毛病来了没有。

也就是说,cpu向串口数据寄存器发送数据后并不会等待,而是去执行其它任务,等发送完成了,接着进入中断。

以这种方式:每发送一个字符,就仅仅只需要2个us,而以printf的形式,就需要85个us。

这是节省了多少时间。而且随着字符串的增加,或者是说传输大量数据的时候,那肯定效率大很多。

总结

这种串口加中断的方式确实是比单纯printf确实是节省时间,但是呢,如果遇到高频率,高吞吐量的数据的时候,这个也不行,因为那样会造成大量的高频率的中断,对任务的运行没有好处。到了那个时候,cpu load那就大起来了。你看到的是0%的cpu load。那是因为:

每过1s中发送一帧数据

那么第三种方式就来了。

3.printf+DMA+中断

这种方式是我目前见过最牛逼的,它的性能将近是第一种方式的140多倍,甚至可能更多。

3.1DMA初始化

static void BSP_DMA(DMA_Stream_TypeDef *DMA_Streamx, u32 chx, u32 par, u32 mar, u16 ndtr)
{
    
    

  DMA_InitTypeDef DMA_InitStructure;
  NVIC_InitTypeDef NVIC_InitStructure;
  if ((u32)DMA_Streamx > (u32)DMA2) //得到当前stream是属于DMA2还是DMA1
  {
    
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); //DMA2时钟使能
  }
  else
  {
    
    
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA1, ENABLE); //DMA1时钟使能
  }
  DMA_DeInit(DMA_Streamx);

  while (DMA_GetCmdStatus(DMA_Streamx) != DISABLE)
  {
    
    
  } //等待DMA可配置

  /* 配置 DMA Stream */
  DMA_InitStructure.DMA_Channel = chx;                                    //通道选择
  DMA_InitStructure.DMA_PeripheralBaseAddr = par;                         //DMA外设地址
  DMA_InitStructure.DMA_Memory0BaseAddr = mar;                            //DMA 存储器0地址
  DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral;                 //存储器到外设模式
  DMA_InitStructure.DMA_BufferSize = ndtr;                                //数据传输量
  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_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable;
  DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_Full;
  DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single;         //存储器突发单次传输
  DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; //外设突发单次传输
  DMA_Init(DMA_Streamx, &DMA_InitStructure);                          //初始化DMA Stream
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                     // 使能
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 14;          // 抢占优先级
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;                  // 子优先级
  NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream7_IRQn;
  NVIC_Init(&NVIC_InitStructure); // 嵌套向量中断控制器初始化

  DMA_ITConfig(DMA2_Stream7, DMA_IT_TC, ENABLE);
}

3.2 中断服务函数的编写

void DMA2_Stream7_IRQHandler(void) // 串口1 DMA发送中断处理函数
{
    
    
  SEGGER_SYSVIEW_RecordEnterISR();
  if (DMA_GetITStatus(DMA2_Stream7, DMA_IT_TCIF7) != RESET)
  {
    
    
    DMA_ClearFlag(DMA2_Stream7, DMA_FLAG_TCIF7);
    DMA_Cmd(DMA2_Stream7, DISABLE);          // 关闭DMA
    DMA_SetCurrDataCounter(DMA2_Stream7, 0); //传输数据量为0
    xSemaphoreGiveFromISR(private_MAIN_semaphore, NULL);
  }
  SEGGER_SYSVIEW_RecordExitISR();
}

3.DMA+中断的实现

static void myprintf1(char *str) //但是需要注意,字符串以/0结尾
{
    
    
  USART_ClearFlag(USART1, USART_FLAG_TC); //防止出现stm32第一个字符丢失的现象
  pc_MAIN_myprintf1 = str;                //更新printf所指向的字符串
  DMA_Cmd(DMA2_Stream7, DISABLE); //关闭DMA传输
  while (DMA_GetCmdStatus(DMA2_Stream7) != DISABLE)
  {
    
    
  }                                                                   //确保DMA可以被设置
  BSP_SDMA(DMA2_Stream7, DMA_Channel_4, (u32)&USART1->DR, (u32)pc_MAIN_myprintf1, strlen(pc_MAIN_myprintf1)); //配置简化版的DMA
  //DMA2_Stream7->M0AR =(U32)str;
  DMA_Cmd(DMA2_Stream7, ENABLE);                                       //开启DMA传输
}

3.4效果

Context

cpu load 为%0.49

什么,负载率这么高。

那是因为上面两种情况是每1s发送,而这个是全速运行。对,全速运行。

这是我测试后的数据:

10s的时间里:

printf+DMA+中断 发送了85618个字符

printf 发送了8272个字符

发送数据量时它的10倍

cpu load:

printf+DMA+中断 %0.49

printf %6.7

cpu负载时间时它的1/14。

相当于printf+DMA+中断是printf性能的140倍。并且随着数据量的增大会显得更加明显。

Timeline

你看看这时间在差不多2ms的时间里发送了一帧数据,然后单纯的prinf发一个字符串要占用1.5个ms。

我只能说:牛逼。

总结

在嵌入式系统开发的过程中,我们经常使用的printf是不是和你想象的有点不一样。

如果需要对系统性能上提升,需要合理的使用DMA。它可是一个宝藏,不然STM32也不会给每个DMA stream分配一个单独的中断号。

可见DMA在嵌入式开发过程中的重要性!!!

猜你喜欢

转载自blog.csdn.net/qq_46359697/article/details/120403243