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;//不使用流控,选择none
	USART_InitStruture.USART_Mode = USART_Mode_Tx;//如果既需要发送又接收就 TX | 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_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 前者只能接收直接写的参数 对于封装格式要用vsprintf
	va_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;//不使用流控,选择none
	USART_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 前者只能接收直接写的参数 对于封装格式要用vsprintf
	va_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;	//如果收到一个数据包,就置一个RxFlag


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;//不使用流控,选择none
	USART_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 前者只能接收直接写的参数 对于封装格式要用vsprintf
	va_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灯

  • 66
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值