前言:
本文属于软件部分,具体的串口硬件部分可见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灯