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:成功写入 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步
判断缓冲区是否为空就是判断读指针和写指针是否重合了。
读取缓冲区就是将读指针的位置的数据读出来。
具体的代码如下:
/* 环形缓冲区读操作 */
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位,特点如下:
根据上述特点,我们就可以在中断中进行单字节的处理,并把数据依次存入到环形缓冲区中。
单字节处理(中断)
单字节处理的步骤如下:
添加头文件
在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
作者:荣世蓥