stm32f103c8t6学习笔记(学习B站up江科大自化协)-USART串口-软件部分

前言:

        本文属于软件部分,具体的串口硬件部分可见http://t.csdnimg.cn/afh48,对于串口的工作原理以及各个寄存器工作流程的记录十分详细。

一、接线图

二、stm32发送-电脑串口助手接收

1.USART初始化流程图

·1.开启时钟

        把需要使用的USART和GPIO的时钟打开

·2.GPIO初始化

        把TX配置成复用输出,RX配置成输入

·3.配置USART

        直接使用一个结构体即可将所有参数配置完成

·4.开关控制

        如果需要仅发送的功能,就直接开启USART,初始化到此结束

        如果还需要接收的功能,可能还需要配置中断,那么就在开启USART之前加上IT_Config和NVIC的代码即可。

2.代码-发送字节数据

        在Serial.c部分输入以下代码,并将两个函数放到头文件声明

#include "stm32f10x.h"void Serial_Init()
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); // USART1是APB2的外设RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);	//引脚是PA9和PA10(根据表),需开启时钟GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;	//TX引脚是USART外设控制的输出引脚,要用复用推挽输出GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	GPIO_Init(GPIOA,&GPIO_InitStructure);	USART_InitTypeDef USART_InitStruture;USART_InitStruture.USART_BaudRate = 9600;USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用流控,选择noneUSART_InitStruture.USART_Mode = USART_Mode_Tx;//如果既需要发送又接收就 TX | RXUSART_InitStruture.USART_Parity = USART_Parity_No;//无需校验位USART_InitStruture.USART_StopBits = USART_StopBits_1; //一位停止位USART_InitStruture.USART_WordLength = USART_WordLength_8b; //无需校验位USART_Init(USART1,&USART_InitStruture);USART_Cmd(USART1,ENABLE);
}void Serial_sendByte(uint8_t Byte)
{USART_SendData(USART1,Byte);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}

        然后到main.c包含头文件#include "Serial.h" 。main部分的代码为

#include "stm32f10x.h"                
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"int main()
{OLED_Init();Serial_Init();Serial_sendByte(0x11);while(1){}
}

        接下来需要打开串口助手,注意串口助手内的参数要和代码一致。切记按下打开串口以及选择正确的串口号。

         在接收区将会受到11,烧录程序的时候自动复位会发送一次,后续手动按下复位键也会发送11.

3.数据模式

        

        ·HEX模式,有的地方也称为十六进制模式或二进制模式,这些称呼都是一个意思,他表示的都是以原始数据的形式显示,收到什么数据就把这个数据本身显示出来,在这种模式下,只能显示一个个的十六进制数,比如11 7A 8B 33,不能显示文本比如helloworld 和各种符号!,。等

        ·如果要显示文本就要对一个个的数据进行编码了。这就叫文本模式或字符模式,是以原始数据编码后的形式显示,在这种模式下每一个字节数据通过查找字符集编码成一个字符。图中做下角的表就是ASCII码字符集

        ·右方模式图描述的是字符和数据在发送和接收的转换关系

4.代码-发送数组

        在Serial.c部分加入一下代码,记得在Serial.h里面进行声明

void Serial_SendArray(uint8_t *Array,uint16_t Length)	//数组的传递需要指针
{uint16_t i;for(i = 0;i < Length;i++) //对数组进行遍历{Serial_sendByte(Array[i]);	//一次取出数组的每一项,通过sendbyte进行发送}
}

        同时在main函数加入以下函数,

main()
{	uint8_t MyArray[] = {0x42,0x43,0x44,0x45,0x46};Serial_SendArray(MyArray,5);while(1){}
}

        编译烧录得到以下结果

5.代码-发送字符串

        同上,在Serial.c部分加入一下代码,记得在Serial.h里面进行声明

void Serial_SendString(char *String)	//字符串自带一个结束标志位,所以不需要再传递长度参数
{uint8_t i;for(i = 0; String[i] != 0;i++)	//这里的0对应空字符,是字符串结束标志位 如果不等于0就还没结束{										//也可以写成字符的形式 '\0' Serial_sendByte(String[i]);}								
}

        在main的函数如下:


main()
{	Serial_SendString("helloword!!\r\n");	//在写完这个字符串之后,编译器会自动补上结束标志位,//所以字符串的存储空间会比字符大一//如果要执行换行操作,要使用	\r\n	两个转义字符	都是不可见的控制字符while(1){}
}

        在串口助手记得选上文本模式。第一行的数字是我选择了hex模式时出现的不正常显示的现象

6.代码-发送数字

        在Serial.c部分加入一下代码,记得在Serial.h里面进行声明

void Serial_SendNumber(uint32_t Number, uint8_t Length)
{	//需要将Number的个位十位百位以十进制拆分开,依次变成字符数字对应的数据发送出去uint8_t i;for(i = 0;i < Length;i ++)	//参数会以十进制由高位向低位依次发送{					
//	 由于最终是以字符的形式显示,所以要根据ASCII表进行偏移 + 0x30 或 '0'Serial_sendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');}
}

        举个例子,假设取的数字是12345,那么取万位就是12345 / 10000 % 10 =1

取千位就是 12345 / 1000 %10 = 2 取百位就是 12345 / 100 % 10 = 3,以此类推。也就是取某一位就是将数字除以10的位数次方,这是为了去掉这一位数右边的数,再对10取余,这是去掉这一位左边的数。 所以需要写一个次方函数,得到X的Y次方


uint32_t Serial_Pow(uint32_t X,uint32_t Y)	//计算数字的某一位对应的数位(百位或千位等)
{uint32_t result = 1;while(Y --){result *= X;}return result;
}

         在main函数写下如下内容


main()
{	Serial_SendNumber(12345,5);while(1){}
}

        得到运行结果如下

三、printf函数的移植 

 1、准备工作

使用printf之前需要先打开工程选项,把use microLIB选项打开。microlib是keil为嵌入式平台优化的一个精简库,本文使用到的printf将会用到这个microlib。

2、对printf进行重定向

        将printf打印的东西输出到串口,由于printf默认输出到屏幕,但是单片机没有屏幕,所以要进行重定向。

        a.在串口的c文件里加上#include<stdio.h>

        b.在后面重写fputc函数

int fputc(int ch, FILE *f)	//参数按此配置即可,
{	//将fputc重定向到串口Serial_sendByte(ch);	return ch;	
}

        c.fputc和printf之间的联系

        fputc是printf的底层,printf函数在打印的时候,就是不断调用fputc一个一个打印的。我们把fputc重定向到串口,那么printf自然就输出到串口。

        d.main函数中调用

        经过上面的步骤,printf已经移植完成。在主函数输入下面内容

#include "stm32f10x.h"                  
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"int main()
{OLED_Init();Serial_Init();printf("date:%d\r\n",20240312);while(1){}
}

        程序烧录后,单片机将会直接在串口输出        date:20240312        这行内容,并自动进行换行处理(\r\n)。

3、多串口使用printf

        使用sprintf,sprintf可以把格式化字符输出到一个字符串里面。

	char string[100];sprintf(string,"date:%d\r\n",2024031266);Serial_SendString(string);	//把字符串string通过串口发送出去//因为sprintf可以指定打印位置,不涉及重定向的东西,所以每个串口都可以使用sprintf进行格式化打印

4、封装sprintf

        由于printf这类函数比较特殊,支持可变参数。

首先在串口的头文件里添加#include<stdarg.h>,然后在最末尾处对printf函数进行封装。

void Serial_printf(char *format,...) //第一个参数用来接收格式化字符串 三个点用来接收可变参数列表
{char string[100];va_list arg;	//定义一个参数列表变量 va_list是类型名 arg是变量名va_start(arg,format);	//	从format位置开始接收参数表,放在arg里面vsprintf(string,format,arg);//这里的sprintf要改成vsprintf 前者只能接收直接写的参数 对于封装格式要用vsprintfva_end(arg);//释放参数表Serial_SendString(string);//把string发送出去
}

在main部分进行调用

Serial_printf("date:%d\r\n",20240312);

 输出结果如下

5、printf显示汉字的方法

        在keil里面我们选择的汉字编码格式是utf8,所以发送到串口的时候汉字会以utf8的方式编码。在串口助手也得选择utf8才能解码正确,为了防止写入中文的时候编译器报错,需要先在小魔术棒里面的c/c++处输入下述参数。(这个步骤是针对于使用utf8的用户)

        

--no-multibyte-chars

        由于我编译器使用的是GB2312编码,所以串口助手处要使用GBK解码。如图,正确解码得到“你好,世界”,中间的乱码部分是因为我选择了UTF8进行解码。

四、串口接收数据

        1.代码-接收字节数据

#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>void Serial_Init()
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); // USART1是APB2的外设RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);	//引脚是PA9和PA10(根据表),需开启时钟GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;	//TX引脚是USART外设控制的输出引脚,要用复用推挽输出GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	GPIO_Init(GPIOA,&GPIO_InitStructure);	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;	//RX PA10初始化GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	GPIO_Init(GPIOA,&GPIO_InitStructure);USART_InitTypeDef USART_InitStruture;USART_InitStruture.USART_BaudRate = 9600;USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用流控,选择noneUSART_InitStruture.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//既发送又接收USART_InitStruture.USART_Parity = USART_Parity_No;//无需校验位USART_InitStruture.USART_StopBits = USART_StopBits_1; //一位停止位USART_InitStruture.USART_WordLength = USART_WordLength_8b; //无需校验位USART_Init(USART1,&USART_InitStruture);//中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd =ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority =1;NVIC_InitStructure.NVIC_IRQChannelSubPriority =1;NVIC_Init(&NVIC_InitStructure);USART_Cmd(USART1,ENABLE);
}void Serial_sendByte(uint8_t Byte)
{USART_SendData(USART1,Byte);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}void Serial_SendArray(uint8_t *Array,uint16_t Length)	//数组的传递需要指针
{uint16_t i;for(i = 0;i < Length;i++) //对数组进行遍历{Serial_sendByte(Array[i]);	//一次取出数组的每一项,通过sendbyte进行发送}
}void Serial_SendString(char *String)	//字符串自带一个结束标志位,所以不需要再传递长度参数
{uint8_t i;for(i = 0; String[i] != 0;i++)	//这里的0对应空字符,是字符串结束标志位 如果不等于0就还没结束{										//也可以写成字符的形式 '\0' Serial_sendByte(String[i]);}								
}uint32_t Serial_Pow(uint32_t X,uint32_t Y)	//计算数字的某一位对应的数位(百位或千位等)
{uint32_t result = 1;while(Y --){result *= X;}return result;
}void Serial_SendNumber(uint32_t Number, uint8_t Length)
{	//需要将Number的个位十位百位以十进制拆分开,依次变成字符数字对应的数据发送出去uint8_t i;for(i = 0;i < Length;i ++)	//参数会以十进制由高位向低位依次发送{					//	 由于最终是以字符的形式显示,所以要根据ASCII表进行偏移 + 0x30 或 '0'Serial_sendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');}
}int fputc(int ch, FILE *f)	//参数按此配置即可,
{	//将fputc重定向到串口Serial_sendByte(ch);	return ch;	
}void Serial_printf(char *format,...) //第一个参数用来接收格式化字符串 三个点用来接收可变参数列表
{char string[100];va_list arg;	//定义一个参数列表变量 va_list是类型名 arg是变量名va_start(arg,format);	//	从format位置开始接收参数表,放在arg里面vsprintf(string,format,arg);//这里的sprintf要改成vsprintf 前者只能接收直接写的参数 对于封装格式要用vsprintfva_end(arg);//释放参数表Serial_SendString(string);//把string发送出去
}

        这部分代码和串口发送数据部分没什么本质区别,仅仅添加了开启引脚PA10,以及或上了RX部分。

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

提个醒,自己的stm32接收不到数据,但是程序没有任何问题,接线也正常的情况下,应该是stm32内部的ttl和串口转ttl模块冲突了,可参考http://t.csdnimg.cn/q08TD这篇文章。由于我也出现了这种情况,所以暂时没有实验现象。

2.代码-中断接收字节数据并回传

在serial.c里面添加两个定义

uint8_t Serial_RXData;
uint8_t Serial_RXFlag;

并在末尾部分添加如下代码

//实现读后自动清除的功能
//意思是返回这个标志位,1就返回1,0就返回0,但给这个1复位一下,使得查一次就能复位1次
uint8_t Serial_GetRxFlag(void)
{if(Serial_RXFlag == 1){Serial_RXFlag = 0;return 1;}return 0;
}uint8_t Serial_GetRxData(void)
{return Serial_RXData;
}void USART1_IRQHandler(void)
{if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){Serial_RXData = USART_ReceiveData(USART1);Serial_RXFlag = 1;USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}

接下来在main部分函数添加

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"uint8_t RXData;int main()
{OLED_Init();Serial_Init();OLED_ShowString(1,1,"RxData:");while(1){if(Serial_GetRxFlag() == 1){RXData = Serial_GetRxData();Serial_sendByte(RXData);OLED_ShowHexNum(1,8,RXData,2);}}
}

烧录后,在发送区输入数据后,将会显示到单片机连接的OLED显示屏上边,同时会返回一份数据到接收区

 五、数据包的定义  

        ·串口数据包:通常使用的是额外添加包头包尾的这种方式

        ·在HEX数据包里面,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一层编码和译码,最终表现出来的就是文本格式,但是实际上每个文本字节的背后都还是一个HEX数据。

        ·优缺点:

        HEX数据包优点:传输最直接,解析数据非常简单,比较适合一些模块发送原始的数据

                          缺点:灵活性不足,容易和包头包尾重复,

        文本数据包优点:数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合,比如蓝牙模块常用的AT指令,CNC和3D打印机常用的G代码,都是文本数据包的格式。

                          缺点:解析效率低,比如发送一个100,HEX数据包就是一个字节100,但是文本数据包就得是三个字节的字符‘1’‘0’‘0’,收到之后还要把字符转化成数据,才能得到100,

串口收发hex数据包

        ·固定包长:含包头包尾,每个数据包的长度都固定不变,数据包前面是包头,后面是包尾
        ·可变包长:含包头包尾,每一个数据包的长度可以是不一样的,前面是包头,后面是包尾。他的数据包格式可以根据用户需求自己规定。

        ·包头包尾和载荷重复解决办法:

        ·如果数据含有FF和FE,和包头包尾重复了怎么办?会引起误判,对于这个问题也有相应的几种解决方法:

        1.限制在和数据的范围,如果可以的话,可以在发送的时候对数据进行限幅。比如X、Y、Z三个数据的范围是0-100,那么可以在载荷中只发送0-100的数据,以此防止和包头包尾重复。

        2.如果无法避免载荷数据和包头包尾重复,则尽量使用固定长度的数据包,由于载荷数据是固定的,只要通过包头包尾对齐了数据,我们就可以严格知道,那个数据是包头包尾,哪个数据是载荷数据。

        在接收载荷数据的时候,我们并不会判断他是不是包头包尾,但是在判断包头包尾的时候会判断他是不是确实是包头包尾,用于数据对齐,在经过几个数据包对齐之后,剩下的数据包就不会出现问题了。

        3.增加包头包尾的数量,并且尽量让他呈现载荷数据出现不了的状态。比如我们使用FF、FE作为包头,FD、FC作为包尾,这样也可以避免包头包尾和载荷数据重复的情况发生。

        ·并不是所有的包头包尾都需要,可以只要一个包头,把包尾删掉,这样数据包的格式就是一个包头FF加四个数据。当监测到FF开始接收,当收够四个字节后置一个标志位,一个数据包接收完成。不过这样会加重载荷和包头重复的问题。最坏的情况下载荷全是FF,包头也是FF。如果加上了包尾FE,无论数据怎么变化都是可以分辨出包头包尾的。

        ·固定包长和可变包长的选择:

        对于HEX来说,如果载荷会出现包头和包尾重复的情况,最好是选择固定包长,以避免接收错误。如果重复还选择可变包长,数据容易乱套。如果包头包尾不会和载荷重复,可以选择可变包长

        ·关于各种数据转换为字节流的问题。

        数据包都一个字节一个字节组成的,如果想发送16、32位的整型数据,float、double甚至是结构体都没问题,因为内部是由一个字节一个字节组成的,仅需要用一个uint8_t的指针指向他,并把他们当做一个字节数组发送即可。

串口收发文本数据包

        ·由于数据译码成了字符形式,这样就会存在大量的字符可作为包头包尾,可以有效的避免载荷和包头包尾重复的问题。比如以@ 作为包头,以\r\n这两个换行字符作为包尾,在载荷数据中间可以出现除了包头包尾的任意字符。文本数据包基本不用担心包头包尾和载荷重复的问题,可变包长、各种符号、字母、数据都可以随意使用

        ·当接收到载荷数据之后,得到的就是一个字符串,在软件中对字符串进行操作和判断,就可以实现各种指令控制的功能,而且字符串数据包表达的意义很明显,可以把字符串数据包直接打印到串口助手上,各种指令和数据都可以一眼看清。

        ·文本数据包通常会以换行作为包尾,在打印的时候就可以一行一行显示,比较方便。

六、数据包的收发流程

HEX数据包接收(固定包长)

        在接收的时候,每收到一个字节,程序都会进一遍中断,在中断函数里面可以拿到这一个字节,在拿到数据之后就得退出中断了,每拿到一个数据都是一个独立的过程。对于数据包来说很明显有一个前后关联性,包头之后是数据,数据之后是包尾。对于包头数据包尾这三种不同的状态,我们需要有不同的处理逻辑,在程序中需设计一个记住不同状态的机制,在不同状态执行不同操作,同时还要进行状态的合理转移,这种程序设计的思想叫做状态机,接下来将使用状态机的方法来接收一个数据包。

执行流程:

        最开是S = 0,收到一个数据进中断,根据S = 0进第一个状态的程序,判断包头是不是FF,如果是代表收到包头,之后置S = 1,退出中断,结束。这样一来下次再进中断,S = 1就可以进行接收数据的程序了。

        在第一个状态如果收到的不是FF,证明数据包没有对齐,我们应该等待数据包包头的出现,这是状态仍然是0,下次进中断还是判断包头的逻辑,直到出现FF才能转入下一个状态,进入下一个状态时收到数据将存入数组中,另外再用一个变量记录接收数据的个数,如果没接收够4个数据就一直处于接收状态,如果收够了就置S = 2。下次进中断就进入下一个状态。

        最后的状态是等待包尾,判断数据是不是FE,如果是的话就可以置S = 0,回到最初的状态,开始下一个轮回。这个数据也可能不是FE,比如数据和包头重复,导致包头位置判断错误,那么这个包尾位置有可能不是FE,这时进入重复等待包尾的状态,直到接收到真正的包尾。这样的判断更能预防因数据和包头重复造成的错误。

使用状态机的基本步骤:

        1.先根据项目要求定义状态,画几个圈,然后考虑好各个状态在什么情况下会进行转移,如何转移,画好线和转移条件,最后根据图编程。比如做个菜单就可以用到状态机的思维,按什么键切换什么菜单,执行什么程序,还有一些芯片内部逻辑也会用到状态机,比如什么情况下进入待机状态,什么情况进入工作状态。

文本数据包接收:

接收流程:

        第一个状态,等待包头,判断是不是我们规定的@符号,如果收到@就进入接收状态,在这个状态下依次接受数据,同时这个状态还应该要兼具等待接收包尾的功能。因为这个是可变包长,接收数据的同时要时刻监视,是否收到了包尾,一旦收到了包尾就立刻结束。这个状态的逻辑就是判断接收的一个数据是不是\r,如果不是就正常接收,如果是则不接收,同时跳到下一个状态,等待包尾\n,因为数据包有两个包尾\r\n,所以需要第三个状态,如果只有一个包尾,那么在出现一个包尾之后就可以直接回到初始状态,即只需要两个状态即可。因为接收数据和等待包尾需要在一个状态里同时进行,由于串口的包头包尾不会出现在数据中,所以基本不会出现数据错位的现象

七、串口收发hex数据包

接线图:

 HEX数据包格式:

        定义如同PPT的一样,固定包长,含包头FF包尾FE,载荷数据固定四个字节。为了收发数据包,定义两个缓冲区的数组,代码部分删去Serial_GetRxData()这个函数,以及中断里面的部分内容,编写函数Serial_SendPacket(),

代码-发送数据包

        添加这俩部分在serial.c,并进行声明

uint8_t Serial_TxPacket[4];	//这四个数据只储存发送或接收的载荷数据,包头包尾不存 
uint8_t Serial_RxPacket[4];
void Serial_SendPacket(void)
{//第一步发送包头Serial_sendByte(0XFF);//依次将四个数据发送出去Serial_SendArray(Serial_TxPacket,4);//第三布发送包尾Serial_sendByte(0XFE);
}

在main.c部分加入

int main()
{OLED_Init();Serial_Init();Serial_TxPacket[0] = 0x01;Serial_TxPacket[1] = 0x02;Serial_TxPacket[2] = 0x03;Serial_TxPacket[3] = 0x04;Serial_SendPacket();while(1){}
}

烧录后得到实验结果

 代码-接收数据包

        接收数据包的缓存区和标志位已经定义好,在中断函数里需要用状态机来执行接收逻辑,接收数据包,将数据存在Rxpacket数组里面,

        注意使用状态机在进行状态转移的时候,要用 if ,else if 或者 switch case,保障每次进入程序只执行其中一个状态的代码。如果使用三个并列的 if 在状态转移的时候可能会出现问题,比如在状态0,想转移到状态1,就置RxState = 1,结果会造成下面状态1的条件立马满足,会出现两个if都同时成立的情况。

        中断函数内添加的内容如下

void USART1_IRQHandler(void)
{	static uint8_t RxState = 0;		// RxState 当做静态变量 S//这个静态变量类似于全局变量,函数进入只会初始化一次0,在函数退出之后数据仍然有效//但 与全局变量不同的是 静态变量只能在本函数使用static uint8_t pRxPacket = 0;if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){uint8_t RxData = USART_ReceiveData(USART1);if(RxState == 0)	//进入等待包头的程序{if(RxData == 0XFF){RxState = 1;pRxPacket = 0;//提前清0}}else if(RxState == 1)	//进入接收数据的程序{Serial_RxPacket[pRxPacket] = RxData;//将rxdata存在接收数组里pRxPacket++;//移动到下一个位置//每进一次接收状态 数据就转存一次缓存数组 同时存的位置++if(pRxPacket >= 4){RxState = 2;}}else if(RxState == 2)	//进入等待包尾的程序{if(RxData == 0XFE){RxState = 0;Serial_RXFlag = 1;}}USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}

main函数中添加内容如下:

	while(1){if(Serial_GetRxFlag() == 1){OLED_ShowHexNum(1,1,Serial_RxPacket[0],2);OLED_ShowHexNum(1,4,Serial_RxPacket[1],2);OLED_ShowHexNum(1,7,Serial_RxPacket[2],2);OLED_ShowHexNum(1,10,Serial_RxPacket[3],2);}}

在电脑串口助手输入在oled小显示屏上面也显示出了 11 22 33 44。如果说输入的数据是FF FF FF 22 FE FE呢?因为程序在接收载荷数据的时候并不会判断包头包尾,这时即使载荷和包头包尾重复也干扰不到。

        这个程序隐藏着一个小问题,这个RxPackrt数组是同时被读出和写入的数组,在中断函数里会依次写入,在主函数又会依次读出,这会造成数据包可能混在一起。如果读出的过程太慢了,前面两个数据读出来等待一会才往后继续读取,那么后面的数据有可能会刷新为下一个数据包的数据,也就是读出的数据可能一部分属于上一个数据,一部分属于下一个数据包。解决办法是在接收部分加入一个判断,在每一个数据包接收处理完之后再接收下一个数据包。很多情况下可能不进行处理,比如传输各种传感器的每个独立数据,比如陀螺仪的x、y、z数据,温湿度数据,相邻的数据包之间的数据具有连续性,即使相邻数据包混在一起也没关系,这种情况下则不需要关心这个问题。

 最终程序现象

        main.c代码部分

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"uint8_t KeyNum;int main()
{OLED_Init();GPIO_Key_Init();Serial_Init();OLED_ShowString(1,1,"TxPacket");OLED_ShowString(3,1,"RxPacket");Serial_TxPacket[0] = 0x01;Serial_TxPacket[1] = 0x02;Serial_TxPacket[2] = 0x03;Serial_TxPacket[3] = 0x04;while(1){KeyNum = Key_GetNum();if(KeyNum == 1){Serial_TxPacket[0]  ++;Serial_TxPacket[1]  ++;Serial_TxPacket[2]  ++;Serial_TxPacket[3]  ++;Serial_SendPacket();OLED_ShowHexNum(2,1,Serial_TxPacket[0],2);OLED_ShowHexNum(2,4,Serial_TxPacket[1],2);OLED_ShowHexNum(2,7,Serial_TxPacket[2],2);OLED_ShowHexNum(2,10,Serial_TxPacket[3],2);}if(Serial_GetRxFlag() == 1){OLED_ShowHexNum(4,1,Serial_RxPacket[0],2);OLED_ShowHexNum(4,4,Serial_RxPacket[1],2);OLED_ShowHexNum(4,7,Serial_RxPacket[2],2);OLED_ShowHexNum(4,10,Serial_RxPacket[3],2);}}
}

        按下按键的时候oled屏幕显示01 02 03 04,再次按下显示02 03 04 05,每按一下每个数都增加1,但是在串口助手 显示的是FF 01 02 03 04 FE,然后是FF  02 03 04 05 FE依次递增。

        在发送区按包头+载荷+包尾,然后OLED显示屏将会显示载荷部分的内容

 

八、串口收发文本数据包

接线图:

 代码-点灯

       这部分代码加上了防止数据包错位的功能(连续发送数据包,程序处理不及时会导致数据包错位)

        main.c部分

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
#include "Key.h"
#include "LED.h"
#include <string.h>uint8_t KeyNum;int main()
{OLED_Init();GPIO_Key_Init();Serial_Init();LED_Init();OLED_ShowString(1,1,"TxPacket");OLED_ShowString(3,1,"RxPacket");while(1){
//		if(Serial_GetRxFlag() == 1)if(Serial_RXFlag == 1){OLED_ShowString(4,1,"				");//相当于进行擦除处理OLED_ShowString(4,1,Serial_RxPacket);if(strcmp(Serial_RxPacket,"LED_ON") == 0){LED1_ON();Serial_SendString("LED_ON\r\n");OLED_ShowString(2,1,"			");OLED_ShowString(2,1,"LED_ON_OK");}else if(strcmp(Serial_RxPacket,"LED_OFF") == 0){LED1_OFF();Serial_SendString("LED_OFF\r\n");OLED_ShowString(2,1,"			");OLED_ShowString(2,1,"LED_OFF_OK");}else {Serial_SendString("ERROR_COMMAND\r\n");OLED_ShowString(2,1,"				");OLED_ShowString(2,1,"ERROR_COMMAND");}Serial_RXFlag = 0;}}
}

        serial.c部分代码

#include "stm32f10x.h"
#include <stdio.h>
#include <stdarg.h>char Serial_RxPacket[100];uint8_t Serial_RXFlag;	//如果收到一个数据包,就置一个RxFlagvoid Serial_Init()
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); // USART1是APB2的外设RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);	//引脚是PA9和PA10(根据表),需开启时钟GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AF_PP;	//TX引脚是USART外设控制的输出引脚,要用复用推挽输出GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_9;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	GPIO_Init(GPIOA,&GPIO_InitStructure);	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;	//RX PA10初始化GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_10;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	GPIO_Init(GPIOA,&GPIO_InitStructure);USART_InitTypeDef USART_InitStruture;USART_InitStruture.USART_BaudRate = 9600;USART_InitStruture.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//不使用流控,选择noneUSART_InitStruture.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//既发送又接收USART_InitStruture.USART_Parity = USART_Parity_No;//无需校验位USART_InitStruture.USART_StopBits = USART_StopBits_1; //一位停止位USART_InitStruture.USART_WordLength = USART_WordLength_8b; //无需校验位USART_Init(USART1,&USART_InitStruture);//中断USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&NVIC_InitStructure);USART_Cmd(USART1,ENABLE);
}void Serial_sendByte(uint8_t Byte)
{USART_SendData(USART1,Byte);while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) == RESET);
}void Serial_SendArray(uint8_t *Array,uint16_t Length)	//数组的传递需要指针
{uint16_t i;for(i = 0;i < Length;i++) //对数组进行遍历{Serial_sendByte(Array[i]);	//一次取出数组的每一项,通过sendbyte进行发送}
}void Serial_SendString(char *String)	//字符串自带一个结束标志位,所以不需要再传递长度参数
{uint8_t i;for(i = 0; String[i] != 0;i++)	//这里的0对应空字符,是字符串结束标志位 如果不等于0就还没结束{										//也可以写成字符的形式 '\0' Serial_sendByte(String[i]);}								
}uint32_t Serial_Pow(uint32_t X,uint32_t Y)	//计算数字的某一位对应的数位(百位或千位等)
{uint32_t result = 1;while(Y --){result *= X;}return result;
}void Serial_SendNumber(uint32_t Number, uint8_t Length)
{	//需要将Number的个位十位百位以十进制拆分开,依次变成字符数字对应的数据发送出去uint8_t i;for(i = 0;i < Length;i ++)	//参数会以十进制由高位向低位依次发送{					//	 由于最终是以字符的形式显示,所以要根据ASCII表进行偏移 + 0x30 或 '0'Serial_sendByte(Number / Serial_Pow(10,Length - i - 1) %10 + '0');}
}int fputc(int ch, FILE *f)	//参数按此配置即可,
{	//将fputc重定向到串口Serial_sendByte(ch);	return ch;	
}void Serial_printf(char *format,...) //第一个参数用来接收格式化字符串 三个点用来接收可变参数列表
{char string[100];va_list arg;	//定义一个参数列表变量 va_list是类型名 arg是变量名va_start(arg,format);	//	从format位置开始接收参数表,放在arg里面vsprintf(string,format,arg);//这里的sprintf要改成vsprintf 前者只能接收直接写的参数 对于封装格式要用vsprintfva_end(arg);//释放参数表Serial_SendString(string);//把string发送出去
}//实现读后自动清除的功能
//意思是返回这个标志位,1就返回1,0就返回0,但给这个1复位一下,使得查一次就能复位1次
//uint8_t Serial_GetRxFlag(void)
//{
//	if(Serial_RXFlag == 1)
//	{
//		Serial_RXFlag = 0;
//		return 1;
//	}
//	return 0;
//}//void Serial_SendPacket(void)
//{
//	//第一步发送包头
//	Serial_sendByte(0XFF);
//	//依次将四个数据发送出去
//	Serial_SendArray(Serial_TxPacket,4);
//	//第三布发送包尾
//	Serial_sendByte(0XFE);
//}void USART1_IRQHandler(void)
{	static uint8_t RxState = 0;		// RxState 当做静态变量 S//这个静态变量类似于全局变量,函数进入只会初始化一次0,在函数退出之后数据仍然有效//但 与全局变量不同的是 静态变量只能在本函数使用static uint8_t pRxPacket = 0;if(USART_GetITStatus(USART1,USART_IT_RXNE) == SET){uint8_t RxData = USART_ReceiveData(USART1);if(RxState == 0)	//进入等待包头的程序{if(RxData == '@' && Serial_RXFlag == 0){RxState = 1;pRxPacket = 0;//提前清0}}else if(RxState == 1)	//进入接收数据的程序{if(RxData == '\r')	//判断是不是包尾{RxState = 2;}else				//不是包尾才接收数据{Serial_RxPacket[pRxPacket] = RxData;//将rxdata存在接收数组里pRxPacket++;//移动到下一个位置//每进一次接收状态 数据就转存一次缓存数组 同时存的位置++}}else if(RxState == 2)	//进入等待包尾的程序{if(RxData == '\n'){RxState = 0;Serial_RxPacket[pRxPacket] = '\0';Serial_RXFlag = 1;//接收到之后,还需要给这个字符数组的最后加一个字符串结束标志位\0 方便后续对字符串的处理//不然在showstring没有结束标志位不知道这个字符串具体长度}}USART_ClearITPendingBit(USART1,USART_IT_RXNE);}
}

serial.h部分代码

#ifndef __SERIAL_H
#define __SERIAL_H#include <stdio.h>extern char Serial_RxPacket[];
extern uint8_t Serial_RXFlag;void Serial_Init();
void Serial_sendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array,uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
uint32_t Serial_Pow(uint32_t X,uint32_t Y);
void Serial_printf(char *format,...);
uint8_t Serial_GetRxFlag(void);#endif

 实验现象

        如果在串口助手输入@LED_ON,打上回车之后发送,将会点亮LED灯。@LED_OFF将会熄灭LED灯

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/756303.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

golang面试题总结

零、go与其他语言 0、什么是面向对象 在了解 Go 语言是不是面向对象&#xff08;简称&#xff1a;OOP&#xff09; 之前&#xff0c;我们必须先知道 OOP 是啥&#xff0c;得先给他 “下定义” 根据 Wikipedia 的定义&#xff0c;我们梳理出 OOP 的几个基本认知&#xff1a; …

2024年阿里云服务器搭建幻兽帕鲁游戏_保姆级教程

玩转幻兽帕鲁服务器&#xff0c;阿里云推出新手0基础一键部署幻兽帕鲁服务器教程&#xff0c;傻瓜式一键部署&#xff0c;3分钟即可成功创建一台Palworld专属服务器&#xff0c;成本仅需26元&#xff0c;阿里云服务器网aliyunfuwuqi.com分享2024年新版基于阿里云搭建幻兽帕鲁服…

SpringBoot集成WebService

1&#xff09;添加依赖 <dependency><groupId>org.apache.cxf</groupId><artifactId>cxf-spring-boot-starter-jaxws</artifactId><version>3.3.4</version><exclusions><exclusion><groupId>javax.validation<…

【Linux】线程预备知识{远程拷贝/重入函数与volatile关键字/认识SIGCHILD信号/普通信号/实时信号}

文章目录 0.远程拷贝1.重入函数与volatile关键字2.认识SIGCHILD信号3.普通信号/实时信号 0.远程拷贝 打包资源&#xff1a;tar czf code.tgz *远程传输&#xff1a;scp code.tgz usr服务器ip:/home/usr/路径解压&#xff1a;tar xzf code.tgz 1.重入函数与volatile关键字 先看…

默写单词cpp(初学者版本)

笔摔坏了直接使用版:yum:仔细学习版:yum:1.直接使用版:yum:&#xff08;文件使用规范&#xff09;(1)文件(2)使用规范 2.仔细学习版。将会讲各个函数的功能和细节。今天太晚了&#xff0c;明天再写。 笔摔坏了 在一个阳光明媚的早晨&#xff0c;我愉快的奋笔疾书&#xff0c;抄…

推荐4个c++进度条开源库

在C中&#xff0c;有许多开源库可以帮助你创建进度条。以下是一些常用的C进度条库&#xff1a; 1. **indicators**: - GitHub链接: [https://github.com/p-ranav/indicators](https://github.com/p-ranav/indicators) - 特点: 轻量级&#xff0c;易于使用&#xff0c;支…

OpenCV学习笔记(十)——利用腐蚀和膨胀进行梯度计算以及礼帽和黑帽

梯度计算 在OpenCV中&#xff0c;梯度计算是图像处理中的一个基本操作&#xff0c;用于分析图像中像素值的变化速率的方向&#xff0c;其中梯度的方向是函数变化最快的方向&#xff0c;因此在图像中&#xff0c;沿着梯度方向可以找到灰度值变化最大的区域&#xff0c;这通常是…

我的自建博客之旅04之Halo

我的自建博客之旅04之Halo Halo是我无意间发现的一款博客框架,如果你讨厌Hexo,Vuepress等静态框架本地编辑,构建部署等方式,如果你想要一款一次搭建,前台是博客,后台是文章维护,并且支持各种定制化折腾的博客框架,可能Halo会比较适合你。 因为我个人还是比较偏技术,…

【数据结构取经之路】栈

目录 引言 栈的性质 顺序栈 栈的基本操作 初始化 销毁 插入 删除 判空 取栈顶元素 栈的大小 完整代码&#xff1a; 引言 栈(stack)&#xff0c;可以用数组实现&#xff0c;也可以用链表实现。用数组实现的栈叫顺序栈&#xff0c;用链表实现的栈叫链式栈&#…

wayland(xdg_wm_base) + egl + opengles 使用 Assimp 加载材质文件Mtl 中的纹理图片最简实例(十六)

文章目录 前言一、3d 立方体 model 属性相关文件1. cube.obj2. cube.Mtl3. 纹理图片 cordeBouee4.jpg二、代码实例1. 依赖库和头文件1.1 assimp1.2 stb_image.h2. egl_wayland_obj_cube.cpp3. Matrix.h 和 Matrix.cpp4. xdg-shell-client-protocol.h 和 xdg-shell-protocol.c5.…

SCI一区 | Matlab实现GWO-TCN-BiGRU-Attention灰狼算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测

SCI一区 | Matlab实现GWO-TCN-BiGRU-Attention灰狼算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测 目录 SCI一区 | Matlab实现GWO-TCN-BiGRU-Attention灰狼算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测预测效果基本介绍模型描述程序…

Hero Talk|无缝扩展:Kubernetes 上的 Amazon Aurora 分片和流量管理

亚马逊云科技 Data Hero 潘娟正在打开开源之门。作为“2020 中国开源先锋人物”以及“2021 OSCAR 尖峰开源人物”奖项获得者&#xff0c;她致力于赋能数据领域的开发者&#xff0c;助力他们把握先机。在亚马逊云科技 re:Invent 2023 大会上&#xff0c;潘娟就 Kubernetes 上的 …

【Godot4.2】 基于SurfaceTool的3D网格生成与体素网格探索

概述 说明&#xff1a;本文基础内容写于2023年6月&#xff0c;由三五篇文章汇总而成&#xff0c;因为当时写的比较潦草&#xff0c;过去时间也比较久了&#xff0c;我自己都得重新阅读和理解一番&#xff0c;才能知道自己说了什么&#xff0c;才有可能重新优化整理。 因为我对…

打造精美响应式CSS日历:从基础到高级样式

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

ARM开发板实现24位BMP图片缩放

ARM开发板实现24位BMP图片缩放 一、linux平台bmp图片缩放 最近想在ARM开发板实现BMP图片的缩放&#xff0c;查看了一些资料&#xff0c;大家部分理论知识可参考&#xff1a; akynazh博主 &#xff0c;这位博主程序以window平台为主进行显示&#xff0c;发现在linux平台下编译…

堆排序(数据结构)

本期讲解堆排序的实现 —————————————————————— 1. 堆排序 堆排序即利用堆的思想来进行排序&#xff0c;总共分为两个步骤&#xff1a; 1. 建堆 • 升序&#xff1a;建大堆 • 降序&#xff1a;建小堆 2. 利用堆删除思想来进行排序. 建堆和堆删…

12|检索增强生成:通过RAG助力鲜花运营

什么是 RAG&#xff1f;其全称为 Retrieval-Augmented Generation&#xff0c;即检索增强生成&#xff0c;它结合了检 索和生成的能力&#xff0c;为文本序列生成任务引入外部知识。RAG 将传统的语言生成模型与大规模 的外部知识库相结合&#xff0c;使模型在生成响应或文本时可…

LeetCode 每日一题 Day 102-108

2864. 最大二进制奇数 给你一个 二进制 字符串 s &#xff0c;其中至少包含一个 ‘1’ 。 你必须按某种方式 重新排列 字符串中的位&#xff0c;使得到的二进制数字是可以由该组合生成的 最大二进制奇数 。 以字符串形式&#xff0c;表示并返回可以由给定组合生成的最大二进…

3.18号arm

4 跳转指令 实现汇编程序跳转的两种方式 直接修改PC的值 mov pc , #0x04 通过跳转指令跳转 b 标签 程序跳转到指定的标签下执行&#xff0c;此时LR寄存器不保存返回地址 bl 标签 程序跳转到指定的标签下执行&#xff0c;此时LR寄存器保存返回地址 5 内存读写指令&#xff0…

Vue+SpringBoot打造用户画像活动推荐系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 数据中心模块2.2 兴趣标签模块2.3 活动档案模块2.4 活动报名模块2.5 活动留言模块 三、系统设计3.1 用例设计3.2 业务流程设计3.3 数据流程设计3.4 E-R图设计 四、系统展示五、核心代码5.1 查询兴趣标签5.2 查询活动推荐…