一.串口收发数据包
数据包:数据包的作用就是将同一批的数据进行打包分割方便接收方识别接收,分割方式可以自己设计,可以选择将数据的高位进行改变作为标志位,但是因为会改变数据本身所以不常用,通常自行定义的是添加包头包尾的形式
Hex数据包适合作用于需要设备发送原始数据时使用,缺点是不够灵活;在数据包内容中如果不会出现与包头包尾重复的部分则可以选择可变包长
文本数据包相对更灵活,因为发送的是字符串所以可以直接对发送的字符串进行判断操作,通常会以换行作为包尾,更方便
二.串口收发实验
1.串口收发HEX数据包
原理图
创建状态状态机进行数据不同位置的判断达成效果。
配置代码:
//三个变量都要放到头文件中进行extern声明
uint8_t Serial_TxPacket[4];
uint8_t Serial_RxPacket[4];
uint8_t Serial_RXFlag;
void Serial_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_N;
GPIO_N.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出,使用复用功能
GPIO_N.GPIO_Pin=GPIO_Pin_9;
GPIO_N.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_N);
GPIO_N.GPIO_Mode=GPIO_Mode_IPU;//上拉输入
GPIO_N.GPIO_Pin=GPIO_Pin_10;
GPIO_N.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_N);
USART_InitTypeDef USART_A;
USART_A.USART_BaudRate=9600;//波特率
USART_A.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//不使用硬件流控制
USART_A.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;//发送+接收模式
USART_A.USART_Parity=USART_Parity_No;//无校验位
USART_A.USART_StopBits=USART_StopBits_1;//一位停止位
USART_A.USART_WordLength=USART_WordLength_8b;//数据帧长度8位
USART_Init(USART1,&USART_A);
//开启RXNE标志位到NVIC的输出
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组
NVIC_InitTypeDef NVIC_M;
NVIC_M.NVIC_IRQChannel=USART1_IRQn;//选择USART1通道
NVIC_M.NVIC_IRQChannelCmd=ENABLE;
NVIC_M.NVIC_IRQChannelPreemptionPriority=1;
NVIC_M.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_M);
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte){//传递单独的数据
USART_SendData(USART1,Byte);//调用输出
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);//等待传输到输出寄存器,因为写操作会自动清0所以不需要手动清0
}
void Serial_SendArray(uint8_t *Array,uint16_t Length){//传递数组,需要用指针指向数组首地址
uint16_t i;
for(i=0;i<Length;i++){
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String){//传递字符串,因为结束时自带标志位所以不用传递长度
uint8_t i;
for(i=0;String[i]!='\0';i++){
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X,uint32_t Y){//封装的次方函数,结果是X^Y
uint32_t result=1;
while(Y--){
result*=X;
}
return result;
}
void Serial_SendNum(uint32_t Num,uint8_t Length){//发送一串数字
uint8_t i;
for(i=0;i<Length;i++){
//从高位到低位依次发送
//假设数字串为12345
//12345/10000%10即为最高位,12345/1000%10即为第二位,向后依次排列
//最后+'0'是因为传输过去的是原始数据想要正确显示则要加上偏移
Serial_SendByte(Num/Serial_Pow(10,Length-i-1)%10+'0');
}
}
//将printf函数引入到keil中
//前置需要在魔法棒->target中开启MicroLIB,并引入头文件
int fputc(int ch,FILE *f){//将fputc重定向到串口,fputc是printf的底层
Serial_SendByte(ch);
return ch;
}
//封装格式化打印,直接使用
void Serial_Printf(char *format, ...){
char String[100];
va_list arg;
va_start(arg,format);
vsprintf(String,format,arg);
va_end(arg);
Serial_SendString(String);
}
void Serial_SendPacket(void){
Serial_SendByte(0xFF);
Serial_SendArray(Serial_TxPacket,4);
Serial_SendByte(0xFE);
}
uint8_t Serial_GetRXFlag(void){//获取标志位
if(Serial_RXFlag==1){
Serial_RXFlag=0;
return 1;
}
return 0;
}
void USART1_IRQHandler(void){//使用状态机执行接受逻辑,任务是接收数据包并存在RX...中
static uint8_t RxState=0;//定义静态变量作为状态标志位
static uint8_t pRxPacket=0;//定义静态变量作为数组位的标志
if(USART_GetITStatus(USART1,USART_IT_RXNE)){
uint16_t RxData=USART_ReceiveData(USART1);
if(RxState==0)//等待包头
{
if(RxData==0xFF)
{
RxState=1;
pRxPacket=0;
}
}
else if(RxState==1)//根据设置接收指定长度的数据
{
Serial_RxPacket[pRxPacket]=RxData;
pRxPacket++;
if(pRxPacket>=4)
{
RxState=2;
}
}
else if(RxState==2)//等待包尾,将标志位置0继续等待包头
{
if(RxData==0xFE)
{
RxState=0;
Serial_RXFlag=1;//表示接收到了数据,用于主函数读出
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
主函数部分:
uint8_t RXData;
uint8_t KeyNum;
int main(void)
{
OLED_Init();
Serial_Init();
KEY_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_TAP();
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_RXFlag==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);
}
}
}
2.串口收发文本数据包
原理及状态机的判定:
因为此实验用的是可变长度的数据包所以在s=1的状态判定中要先判断是否为包尾
配置部分代码:
char Serial_RxPacket[100];
uint8_t Serial_RXFlag;
void Serial_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_N;
GPIO_N.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出,使用复用功能
GPIO_N.GPIO_Pin=GPIO_Pin_9;
GPIO_N.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_N);
GPIO_N.GPIO_Mode=GPIO_Mode_IPU;//上拉输入
GPIO_N.GPIO_Pin=GPIO_Pin_10;
GPIO_N.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_N);
USART_InitTypeDef USART_A;
USART_A.USART_BaudRate=9600;//波特率
USART_A.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//不使用硬件流控制
USART_A.USART_Mode=USART_Mode_Tx|USART_Mode_Rx;//发送+接收模式
USART_A.USART_Parity=USART_Parity_No;//无校验位
USART_A.USART_StopBits=USART_StopBits_1;//一位停止位
USART_A.USART_WordLength=USART_WordLength_8b;//数据帧长度8位
USART_Init(USART1,&USART_A);
//开启RXNE标志位到NVIC的输出
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组
NVIC_InitTypeDef NVIC_M;
NVIC_M.NVIC_IRQChannel=USART1_IRQn;//选择USART1通道
NVIC_M.NVIC_IRQChannelCmd=ENABLE;
NVIC_M.NVIC_IRQChannelPreemptionPriority=1;
NVIC_M.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_M);
USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t Byte){//传递单独的数据
USART_SendData(USART1,Byte);//调用输出
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);//等待传输到输出寄存器,因为写操作会自动清0所以不需要手动清0
}
void Serial_SendArray(uint8_t *Array,uint16_t Length){//传递数组,需要用指针指向数组首地址
uint16_t i;
for(i=0;i<Length;i++){
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String){//传递字符串,因为结束时自带标志位所以不用传递长度
uint8_t i;
for(i=0;String[i]!='\0';i++){
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X,uint32_t Y){//封装的次方函数,结果是X^Y
uint32_t result=1;
while(Y--){
result*=X;
}
return result;
}
void Serial_SendNum(uint32_t Num,uint8_t Length){//发送一串数字
uint8_t i;
for(i=0;i<Length;i++){
//从高位到低位依次发送
//假设数字串为12345
//12345/10000%10即为最高位,12345/1000%10即为第二位,向后依次排列
//最后+'0'是因为传输过去的是原始数据想要正确显示则要加上偏移
Serial_SendByte(Num/Serial_Pow(10,Length-i-1)%10+'0');
}
}
//将printf函数引入到keil中
//前置需要在魔法棒->target中开启MicroLIB,并引入头文件
int fputc(int ch,FILE *f){//将fputc重定向到串口,fputc是printf的底层
Serial_SendByte(ch);
return ch;
}
//封装格式化打印,直接使用
void Serial_Printf(char *format, ...){
char String[100];
va_list arg;
va_start(arg,format);
vsprintf(String,format,arg);
va_end(arg);
Serial_SendString(String);
}
void USART1_IRQHandler(void){//使用状态机执行接受逻辑,任务是接收数据包并存在RX...中
static uint8_t RxState=0;//定义静态变量作为状态标志位
static uint8_t pRxPacket=0;//定义静态变量作为数组位的标志
if(USART_GetITStatus(USART1,USART_IT_RXNE)){
uint16_t RxData=USART_ReceiveData(USART1);
if(RxState==0)//等待包头
{
if(RxData=='@' && Serial_RXFlag==0)//为防止发送频率过快导致数据覆盖,所以在前一个发送完成前不发送下一个
{
RxState=1;
pRxPacket=0;
}
}
else if(RxState==1)//因为载荷数量不稳定,所以每次接受前要判断是否为包尾
{
if(RxData=='\r')
{
RxState=2;
}
else{
Serial_RxPacket[pRxPacket]=RxData;
pRxPacket++;
}
}
else if(RxState==2)//等待包尾,将标志位置0继续等待包头
{
if(RxData=='\n')
{
RxState=0;
Serial_RxPacket[pRxPacket]='\0';//给字符串加结束位,不然后面打印不知道停在哪
Serial_RXFlag=1;//表示接收到了数据,用于主函数读出
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}
主函数部分:
#include <string.h>
int main(void)
{
OLED_Init();
Serial_Init();
LED_Init();
OLED_ShowString(1,1,"TxPacket");
OLED_ShowString(3,1,"RxPacket");
while(1)
{
if(Serial_RXFlag==1){
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,Serial_RxPacket);
//使用strcmp函数进行判定,但要引入头函数
if(strcmp(Serial_RxPacket,"LED_ON")==0){
LED1_ON();
Serial_SendString("LED_ON_OK\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_OK\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;
}
}
}
注: 在接收数据包时如果发送频率太高可能会发生数据包错位覆盖的问题,HEX数据包因为常用于传输设备的原始数据所以是连续的数据所以影响不大,但是文本数据包因为是独立数据所以影响较大,因此对代码判定方式进行改变以防止错位发生,避免错位的思路为:
在状态机中判定包头的同时判定是否读取完毕,如果读取标志位Serial_RXFlag为0则接受新数据,而在主函数中直接以Serial_RXFlag作为开始打印和判断的标志,同时在主函数行动执行完毕后将Serial_RXFlag置0,以示数据读取完毕,但是仍有弊端,这种方法要求数据传输效率不可太快。
3.本章作业
通过使用指令缓存区的方式来避免错位覆盖。