STM32_串口

串口调试_重定向

在使用串口调试时,可以使用printf、scanf来进行调试。

使用之前需要做两步工作:1、Keil配置使用微库。2、重写fputc、fgetc;

1、Keil配置使用微库

点击Keil的魔术棒,如下图

点击Target,勾选Use McroLIB即可配置使用微库

2、重写fputc、fgetc

首先需要包含两个头文件

#include "stdio.h"
#include "string.h"

重写两个函数,函数原型如下:

int fputc(int c, FILE * stream){
	
	uint8_t ch[1]={c};
	
	HAL_UART_Transmit(&huart1,ch,1,0xffff);
	
	return c;
}

int fgetc(FILE * stream){
	
	int c;
	
	HAL_UART_Receive(&huart1,(uint8_t*)&c,1,0xffff);
	
	return c;
}

3、编写调试程序

编写一个简单的调试程序来验证printf、scanf能否使用

while (1)
{
	scanf("%s",buf);
	printf("r:%s\r\n",buf);
	HAL_GPIO_TogglePin(LED1_Port,LED1_Pin);
}

验证结果为:当处于scanf时,程序会堵塞,直到scanf接收到数据,这与c语言中的scanf有着相同的效果。

注意点是,printf、scanf都是使用阻塞法进行发送、接收,只适合用于调试,而不是具体功能的实现。

环形缓冲区

串口的接收只有一个DR寄存这个位置。如果不断接收到新的数据,DR寄存器中的数据还未来得及进行处理,这时DR寄存器中的数据就会覆盖,造成数据的丢失。这里引用环形缓冲区的算法,来解决数据丢失的问题。

模型

缓冲区就是一个大一点的数组,可以用来存放多个数据。环形,就是指当存到数组的最后一个位置之后,将从头继续存入数据。

缓冲区具体模型如下:

根据上述描述,环形缓冲区需要做3件事:

申请一个内存,来实现一个缓冲区的作用,即:定义一个数组

对缓冲区进行写数据操作,即:将DR寄存器的数值写入数组

对缓冲区进行读数据操作,即:将数组中数据读出来并进行处理

为了方便管理,将定义一个结构体来包含上述内容,结构体声明如下:

#define RING_BUF_SIZE 100

typedef struct{
	
	uint8_t buf[RING_BUF_SIZE];	                /* 缓冲区 */
	int R_point;								/* 读指针 */
	int W_point;								/* 写指针 */
	
}Ring_Buf_Type;

防止数组溢出小算法

在编程之前,先引入一个防止数组溢出的小算法。因为编写环形缓冲区时,指针需要不断的偏移,这就使得指针可能会偏移到数组的空间外面。

使用取余%的算法即可很好的解决这个问题。具体算法如下:

W_point = (W_point+1)%BUF_SIZE;

我们让指针偏移之后,对整个数组的大小进行取余,那么结果的范围就是0~BUF_SIZE-1,这正好是数组的索引范围。这样就解决了数组溢出的问题。

1、初始化环形缓冲区

初始化环形缓冲区就是将读写指针都指向0,缓冲区数据清空。

具体代码实现如下:

/* 环形缓冲区初始化 */
void Ring_Buf_Init(Ring_Buf_Type* buf){
	
	memset(buf,'\0',RING_BUF_SIZE);	//数组清零
	buf->W_point = 0;								//写指针清零
	buf->R_point = 0;								//读指针清零
	
}

2、写缓冲区

写缓冲区需要进行三步:

  • 判断缓冲区是否已经写满
  • 将数据写入缓冲区
  • 写指针+1

判断写满就是判断 ” 当前的写指针+1位置 “ 是否等于 “ 当前的读指针位置 ”。如果等于,就代表缓冲区已经满了,该数据就禁止写入,选择丢弃。

写入缓冲区就是将数据写入当前写指针指向的位置。

具体的代码如下:

/* 环形缓冲区写数据 */
/* 返回值: 1:成功写入 0:写入失败 */
uint8_t Ring_Buf_Write_Data(Ring_Buf_Type* buf,uint8_t data){
	
	/* 1.判断缓冲区是否已经满了 */
	if((buf->W_point+1)%RING_BUF_SIZE == buf->R_point){
		return 0;
	}
	
	/* 2.写缓冲区 */
	buf->buf[buf->W_point] = data;
	
	/* 3.写指针+1 */
	buf->W_point = (buf->W_point+1)%RING_BUF_SIZE;
	
	return 1;
}

3、读缓冲区

读缓冲区与写缓冲区类似,也需要3步

  • 判断缓冲区是否为空
  • 读取缓冲区到一个变量里面
  • 读指针+1

判断缓冲区是否为空就是判断读指针和写指针是否重合了。

读取缓冲区就是将读指针的位置的数据读出来。

具体的代码如下:

/* 环形缓冲区读操作 */
uint8_t Ring_Buf_Read_Data(Ring_Buf_Type* buf,uint8_t* data_tmp){
	
	/* 1.判断缓冲区是否为空 */
	if(buf->R_point == buf->W_point){
		return 0;
	}
	
	/* 2.读缓冲区 */
	*data_tmp = buf->buf[buf->R_point];
	
	/* 3.读指针+1 */
	buf->R_point = (buf->R_point+1)%RING_BUF_SIZE;
	
	return 1;
}

4、动态过程分析

动态写入过程

初始阶段读写指针都指向0位置。之后写入一个数据,写指针偏移一位。再写入一个数据,写指针偏移一位。再写入一个数据,写指针+1 = 读指针,代码已经写满,此次不进行写入数据。

存在问题就是,根据上述的算法,虽然缓存区的大小为3,但实际上能写的数据只有两个

即:实际空间 = 缓存区大小 - 1

个人认为,这个不必解决。多申请一些空间即可。

具体的动态过程如下:

动态读取过程

接着上面的情况,当前的缓存区已满。之后读取一个数据,读指针偏移一位。再读取一个数据,读指针偏移一位。再读取一个数据,读指针 = 写指针 ,代表已经全部读完,本次不进行读取。

读取的过程没有任何问题,不进行读取的位置就是写指针不进行写入的位置,这样就可以实现读取的数据一定是有效数据。

具体的动态过程如下:

串口不定长接收(单字符处理)

单字符处理的方法就是:接收到一个字符,处理一个字符,从而达到不定长数据处理的效果。

串口的接收是一个字符一个字符接收的,这些数据会存放在一个8位的DR寄存器中。即:DR寄存器只能存一个字符,下一个字符来到后,会覆盖这个字符。因此在接收到数据之后,需要将DR寄存器的值立刻读取出来,存放到一个自己可控的区域去。这就使用到了上述的环形缓冲区。

STM32串口接收相关寄存器

在编程之前,首先需要了解一下STM32串口相关接收方面寄存器的特点

主要的寄存器位是RXNE位,特点如下:

  • RXNE位处在USART_SR寄存器的第5位
  • 当DR寄存器接收到一个字符后,RXNE会被写1。
  • 如果开启了中断,那么当接收一个字符后后,会产生一个中断
  • 当读取DR寄存器后,RXNE会被自动清除。

根据上述特点,我们就可以在中断中进行单字节的处理,并把数据依次存入到环形缓冲区中。

单字节处理(中断)

单字节处理的步骤如下:

  • 在中断中,需要先判断是谁产生的中断
  • 对DR寄存器进行读取
  • 写入接收缓冲区

添加头文件

在stm32f1xx_it.c中包含环形缓冲区的头文件ring_buf.h

修改中断函数

修改USART_IRQHandler中断处理函数,具体修改如下:

if((USART1->SR & (1<<5)) != 0)这一句的含义是获取RXNE位是1还是0,因为RXNE在SR寄存器的第5位。

data = USART1->DR;这一句是读取串口接收到的字符数据,因为DR存放该数据

这里面没有RXNE清零操作,因为在读取DR时会自动清除RXNE

uint8_t data;
/* 1.判断是谁到达的中断 */
if((USART1->SR & (1<<5)) != 0){//1.&要加括号 2.不能写 = 1
	
	data = USART1->DR;
	Ring_Buf_Write_Data(&ring_buf,data);
	
	if(data == '!'){/* 结束标志 */
		Ring_buf_rx_flag = 1;
	}
}

修改之后的USART_IRQHandler的样子

编写main函数

main函数中通过判断Ring_buf_rx_flag来判断是否已经接收完成。

接收完成之后,将数据进行打印。

while (1)
{
	if(Ring_buf_rx_flag == 1){
		while(Ring_Buf_Read_Data(&ring_buf,&c)){
			printf("%c",c);
		}
		printf("\r\n");
		Ring_buf_rx_flag = 0;
	}
}

调试结果

因为在改写中断处理时,编写了 ! 作为结束标志,所以发送时必须加 ! 才能算是接收完成。

这可以根据需求修改结束标志的字符。

具体调试结果如下:

单字节处理相关思考

环形缓冲区

在这次示例中,我所感受到的环形缓冲区的作用就是不需要用memset去清零它。但是环形缓冲区依旧有丢失数据的风险。

当 ring_buf 的空间小于一次性发出的数据量时,比如空间只有100,但一次发出了1000个,这样的话并不会覆盖前100个数据,但是会丢弃后面900个数据。不管怎样,100的数组大小最终只能接收100大小的数据。

我总想有一个方法,比如有1000个数据,我读完50个之后,把这50个进行处理一下,之后扔掉。然后再读50个,再扔掉。这样100大小的数组就可以处理1000的数据量了。但现在还不能实现该情况。

数据处理

在单字节处理下,可以知道接收的每个字节是什么。

上述存在一次性接收不了太多的数据,那可以多定义几个缓冲区。又因为可以知道每个字节是什么,那么就可以设定一个结束符,将数据一段段的存入相应的缓冲数组。

我在解析GPS数据遇到的问题是:这个GPS一次性发送的数据很多,大概500个字符。但是指令是一条一条的,一个指令也就100个字符不到。我当时使用的方法定义了一个超级大的数组,来存放这些数据,之后才对数据进行了解析。这样效率就大大下降。

在单字节处理下,我可以判断是否当前为$符号,如果是,就让他存入一个数组,之后再检测到$符号,就存入下一个数组。这样就在接收时自动存入了相应的数组。方便了后面的数据解析。

GPS一次性传输的数据如下:

$GPRMC,051522.00,A,3155.40269,N,11852.46401,E,0.542,,170424,,,A*75
$GPVTG,,T,,M,0.542,N,1.005,K,A*24
$GPGGA,051522.00,3155.40269,N,11852.46401,E,1,04,8.80,41.3,M,2.7,M,,*59
$GPGSA,A,3,06,30,14,07,,,,,,,,,12.79,8.80,9.28*3B
$GPGSV,3,1,09,06,30,240,41,07,05,186,30,14,84,240,35,19,,,17*4D
$GPGSV,3,2,09,21,13,049,22,29,,,21,30,21,214,40,31,,,09*7D
$GPGSV,3,3,09,40,14,255,35*45
$GPGLL,3155.40269,N,11852.46401,E,051522.00,A,A*6B

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值