文章目录
前言
此篇将结合有限状态机与循环队列,构建一个稳定可靠的自定义串口协议,如果代码有点难懂,可以直接移植使用,看使用demo就好啦,我都封装好了。
Gitee链接在这里:
基于串口的有限状态机
简单提一下实现的主要功能:
一·:命令模式,根据输入字符与缓冲区的内容进行比对,返回真假值。
二·:调参模式,根据输入字符与缓冲区的内容进行比对,返回传入数字的值。这个用来调试参数还是比较好用的,比如调试PID就可以直接输入PID_P=0.111之类的命令,就返回输入参数的1000倍,用来在线调参。
一、有限状态机
有限状态机,这里仅仅使用简单的4个状态,状态之间的转换可以根据下图。根据接收到的不同的字符类型进行状态之间的转换。
在此状态机中的,源状态是缓存区完成一次接受的空状态;触发事件便是接收到一个字符(所以一般放在中断里面进行判断);监护条件便是接受到的字符的类型,动作便是切换条件与存入缓冲。
下图中:箭头表示状态之间的切换,箭头旁边的字母便是切换状态的条件。图中IDEL表示准备好接收数据;HEAD接收到头状态;DATA接收数据状态(可以一直接收数据);TAIL接收到尾数据,END表示,接收数据有误,清楚错误数据。
在设计的自定义串口协议中:仅仅只有帧头和帧尾,帧头和帧尾都可以自己通过初始化函数进行设计。
有限状态机代码
根据上图可以设计如下代码,使用枚举设计4种状态。
/*协议有限状态机枚举*/
typedef enum
{
STATUS_IDEL=(uint8_t) 0,
STATUS_HEAD,
STATUS_DATA,
STATUS_END,
}COM_STATUS;
接收数据缓冲
为了更高效的处理数据,使用缓冲区,将接收到的正确的帧存入缓冲区,缓冲区可以存入n个有效帧。
缓冲要求
设计的缓冲区应该是先进先出的形式,也就是优先处理旧的指令。使用队列作为缓冲区是符合要求的,队列就像是我们排队一样,先排的人先处理,后加入的人后处理。
循环队列
在有限的单片机资源中,我们希望队列有人加入和出的时候,尽可能的少操作,也就是我们排队中不希望前面走了一个人,后面的人要跟上一个位置。这样会移动队列,浪费大家精力(CPU资源)。因此选择使用循环队列,将队列构成一个循环,利用一个游标,游标表示当前排到的个人。前面的人走了,游标便移动到下一个,后面的人不需要移动。这样移动游标就能知道下一个人是谁,不需要移动整个队列,减少了CPU的资源消耗。
一、循环队列
循环队列比较麻烦,想要了解具体的实现原理,请自己百度或者找教材来学习啦!这里就贴上基于面向对象的方式实现的循环队列,想要学习使用C语言实现面向对象,可以看我往期文章–>C语言实现面向对象
如果对void *指针还不是怎么了解的同学,可以看我往前文章–>C语言值Void *指针
代码实现
代码都挺好理解的(前提理解好队列和C语言实现面向对象的方式),就不写过多注释啦~
循环队列头文件:
#ifndef _QUE_OOP_H
#define _QUE_OOP_H
#define FALSE 0
#define TRUE 1
typedef unsigned char cbool;
typedef unsigned char uint8_t;
#define QUE_MAX_LEN 100
typedef char QUEUE_TYPE;
struct Que_vtable;
/*创建循环队列类*/
/*循环队列实际上只有K-1个空间能用*/
/*队列为空:头=尾 */
/*队列满:头=尾+1%最大长度,留出一个空位作为满的条件,不然头=尾的情况和空的情况重复*/
/*循环队列结构体*/
typedef struct Cir_queue
{
struct Que_vtable *c_vptr;
QUEUE_TYPE Queue_Buffer[QUE_MAX_LEN];//缓冲数组
int head,tail,max_len,lenth;
}Cir_queue;
/*队列虚表*/
struct Que_vtable
{
// void (*create_queue)(void * Me);
void (*delete_queue)(void * const Me);
cbool (*empty)(void const * const Me);
cbool (*full)(void const * const Me);
cbool (*pop)(void * const Me,QUEUE_TYPE *Get);
cbool (*push)(void * const Me,QUEUE_TYPE value);
cbool (*head_push)(void * const Me,QUEUE_TYPE value);
cbool (*tail_pop)(void * const Me,QUEUE_TYPE *Get);
cbool (*back)(void const * const Me,QUEUE_TYPE *Get);
cbool (*front)(void const * const Me,QUEUE_TYPE *Get);
int (*lenth)(void const * const Me);
}Que_vtable;
/*多态类*/
// void Qcreate_queue(void * const Me);
void Qdreate_queue(void * const Me);
cbool Qempty(void const * const Me);
cbool Qfull(void const * const Me);
cbool Qtail_pop(void * const Me,QUEUE_TYPE *Get);
cbool Qhead_push(void * const Me,QUEUE_TYPE value);
cbool Qpop(void * const Me,QUEUE_TYPE *Get);
cbool Qpush(void * const Me,QUEUE_TYPE value);
cbool Qback(void const * const Me,QUEUE_TYPE *Get);
cbool Qfront(void const * const Me,QUEUE_TYPE *Get);
int Q_lenth(void const * const Me);
/*循环队列*/
void Cir__Qcreate_queue(Cir_queue * const Me);
void Cir__Qdreate_queue(Cir_queue * const Me);
cbool Cir__Qempty(Cir_queue const * const Me);
cbool Cir__Qfull(Cir_queue const * const Me);
cbool Cir_Qtail_pop(Cir_queue * const Me,QUEUE_TYPE *Get);
cbool Cir_Qhead_push(Cir_queue * const Me,QUEUE_TYPE value);
cbool Cir__Qpush(Cir_queue * const Me,QUEUE_TYPE value);
cbool Cir__Qpop(Cir_queue * const Me,QUEUE_TYPE *Get);
cbool Cir__Qback(Cir_queue const * const Me,QUEUE_TYPE *Get);
cbool Cir__Qfront(Cir_queue const * const Me,QUEUE_TYPE *Get);
int Cir__Q_lenth(Cir_queue const * const Me);
源文件:
void * my_memset(void *source,int dest,int n)
{
char *c_s=(char *)source;
while(n--) *c_s++=dest;
return source;
}
/*多态实现*/
inline void Qdreate_queue(void * const Me)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
_Me->c_vptr->delete_queue(Me);
}
inline cbool Qempty(void const * const Me)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->empty(Me);
}
inline cbool Qfull(void const * const Me)
{
Cir_queue const * const _Me=(Cir_queue const * const)Me;
return _Me->c_vptr->full(Me);
}
inline cbool Qpop(void * const Me,QUEUE_TYPE *Get)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->pop(Me,Get);
}
inline cbool Qpush(void * const Me,QUEUE_TYPE value)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->push(Me,value);
}
inline cbool Qtail_pop(void * const Me,QUEUE_TYPE *Get)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->pop(Me,Get);
}
inline cbool Qhead_push(void * const Me,QUEUE_TYPE value)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->push(Me,value);
}
inline cbool Qback(void const * const Me,QUEUE_TYPE *Get)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->back(Me,Get);
}
inline cbool Qfront(void const * const Me,QUEUE_TYPE *Get)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->front(Me,Get);
}
inline int Q_lenth(void const * const Me)
{
Cir_queue const * const _Me=(Cir_queue * const)Me;
return _Me->c_vptr->lenth(Me);
}
void Cir__Qcreate_queue(Cir_queue * const Me)
{
/*清零*/
my_memset(Me,0,sizeof(Cir_queue));
/*函数表绑定*/
static struct Que_vtable table;
// table.create_queue=(void (*)(void *))(void(*)(Cir_queue *))Cir__Qcreate_queue;
table.delete_queue=(void (*)(void *))(void(*)(Cir_queue *))Cir__Qdreate_queue;
table.empty=(cbool (*)(void const *const ))(cbool(*)(Cir_queue const* const))Cir__Qempty;
table.full=(cbool (*)(void const * const ))(cbool(*)(Cir_queue const *const))Cir__Qfull;
table.pop=(cbool (*)(void *const ,QUEUE_TYPE *Get))(cbool(*)(Cir_queue *const,QUEUE_TYPE *Get))Cir__Qpop;
table.push=(cbool (*)(void *const,QUEUE_TYPE value))(cbool(*)(Cir_queue *const,QUEUE_TYPE value))Cir__Qpush;
table.tail_pop=(cbool (*)(void *const,QUEUE_TYPE *Get))(cbool(*)(Cir_queue *const,QUEUE_TYPE *Get))Cir_Qtail_pop;
table.head_push=(cbool (*)(void *const,QUEUE_TYPE value))(cbool(*)(Cir_queue *const,QUEUE_TYPE value))Cir_Qhead_push;
table.back=(cbool (*)(void const* const,QUEUE_TYPE *Get))(cbool(*)(Cir_queue const*const ,QUEUE_TYPE *Get))Cir__Qback;
table.front=(cbool (*)(void const* const,QUEUE_TYPE *Get))(cbool(*)(Cir_queue const*const ,QUEUE_TYPE *Get))Cir__Qfront;
table.lenth=(int (*)(void const* const))(int (*)(Cir_queue const*const))Cir__Q_lenth;
Me->c_vptr=&table;
Me->max_len=QUE_MAX_LEN;
}
void Cir__Qdreate_queue(Cir_queue * const Me)
{
return ;
}
cbool Cir__Qempty(Cir_queue const * const Me)
{
if(Me->head==Me->tail) return TRUE;
else return FALSE;
}
cbool Cir__Qfull(Cir_queue const * const Me)
{
if(Me->head==(Me->tail+1)%QUE_MAX_LEN) return TRUE;
else return FALSE;
}
cbool Cir__Qpop(Cir_queue * const Me,QUEUE_TYPE *Get)
{
if(Cir__Qempty(Me)) return FALSE;
else
{
*Get=Me->Queue_Buffer[Me->head];
Me->head=(Me->head+1)%QUE_MAX_LEN;
Me->lenth--;
return TRUE;
}
}
cbool Cir__Qpush(Cir_queue * const Me,QUEUE_TYPE value)
{
if(Cir__Qfull(Me)) return FALSE;
else
{
Me->Queue_Buffer[Me->tail]=value;
Me->tail=(Me->tail+1)%QUE_MAX_LEN;
Me->lenth++;
return TRUE;
}
}
cbool Cir_Qtail_pop(Cir_queue * const Me,QUEUE_TYPE *Get)
{
if(Cir__Qempty(Me)) return FALSE;
else
{
Me->tail=((Me->tail-1+QUE_MAX_LEN)%QUE_MAX_LEN);
*Get=Me->Queue_Buffer[(Me->tail)];
Me->lenth--;
return TRUE;
}
}
cbool Cir_Qhead_push(Cir_queue * const Me,QUEUE_TYPE value)
{
if(Cir__Qfull(Me)) return FALSE;
else
{
Me->head=((Me->head-1+QUE_MAX_LEN)%QUE_MAX_LEN);
Me->Queue_Buffer[Me->head]=value;
Me->lenth++;
return TRUE;
}
}
cbool Cir__Qback(Cir_queue const * const Me,QUEUE_TYPE *Get)
{
if(Cir__Qempty(Me)) return FALSE;
else
{
*Get=Me->Queue_Buffer[(Me->tail-1)%QUE_MAX_LEN];
return TRUE;
}
}
cbool Cir__Qfront(Cir_queue const * const Me,QUEUE_TYPE *Get)
{
if(Cir__Qempty(Me)) return FALSE;
else
{
*Get=Me->Queue_Buffer[Me->head%QUE_MAX_LEN];
return TRUE;
}
}
int Cir__Q_lenth(Cir_queue const * const Me)
{
return Me->lenth;
}
二、有限状态机与解码
有限状态机核心实现代码
有限状态机,主要思想是下面的是实现代码,也就是上面提到的那图的的代码实现,配合图片是用更佳。
/*有限状态机读入缓存函数*/
void Com_rxUsart_data(Cir_queue * const QMe,Usart_Trm *const Me,uint8_t bydata)
{
switch (Me->usart_status)
{
case STATUS_IDEL:
if(bydata==Me->head)
{
Me->usart_status=STATUS_HEAD;
Me->last_pos++;
}
else Me->usart_status=STATUS_END;
break;
case STATUS_HEAD:
if(bydata!=Me->teal)
{
Me->usart_status=STATUS_DATA;
Me->last_pos++;
}
else Me->usart_status=STATUS_END;
break;
case STATUS_DATA:
if(bydata!=Me->teal&&!QMe->c_vptr->full(QMe))
{
Me->usart_status=STATUS_DATA;
Me->last_pos++;
}
else if(bydata==Me->teal)
{
Me->last_pos=0;/*成功接收到头和尾,没有错误数据*/
Me->Uartx_frame++; /*队列中有效帧+1*/
Me->usart_status=STATUS_IDEL;
}
else Me->usart_status=STATUS_END;
break;
case STATUS_END: break;
default:Me->usart_status=STATUS_END;break;
}
if(Me->usart_status==STATUS_END)
{
QUEUE_TYPE temp;
/*把出错数据取出,从尾取出*/
while(Me->last_pos)
{
QMe->c_vptr->tail_pop(QMe,&temp);
Me->last_pos--;
}
Me->usart_status=STATUS_IDEL;
}
else
{
QMe->c_vptr->push(QMe,bydata);/*数据入队列*/
}
}
有限状态机与解码
解码主要是个人便于开发的两个简单的小功能,核心实现就是比对缓冲区的内容,进行有效的判断和处理。
处理过程如下:
- 串口接收有效数据进入队列缓冲区
- 主函数调用Frame_deal对传入的指令进行比对,如果缓冲区内没有相应的指令,则取出最找传入的一帧数据。有相应指令则进行处理。
头文件
头文件包含循环队列的头文件
#ifndef __STATUS__U
#define __STATUS__U
/*协议有限状态机枚举*/
typedef enum
{
STATUS_IDEL=(uint8_t) 0,
STATUS_HEAD,
STATUS_DATA,
STATUS_END,
}COM_STATUS;
#define COMMAND_LIST_MAX 10 /*命令列表长度*/
#define COMMAND_LEN_MAX 20 /*命令最大长度*/
struct Usart_vtable;
/*协议有限状态机结构体*/
typedef struct Usart_Trm
{
/*有效帧格式: (data),不支持中文输入*/
uint8_t Uartx_frame; /*有效数据帧数*/
uint8_t head; /*头帧标志*/
uint8_t teal; /*尾帧标志*/
COM_STATUS usart_status; /*状态机*/
int last_pos; /*错误数据计数*/
struct Usart_vtable *c_vptr;/*函数表,提供接口接口使用*/
}Usart_Trm;
struct Usart_vtable
{
// void (*create_ComUsart)(Usart_Trm *Me,uint8_t set_h,uint8_t set_t);
void (*rx_buff)(Cir_queue * const QMe,Usart_Trm *const Me,uint8_t bydata);
int (*fram_num)(Usart_Trm *Me);
long (*fram_deal)(Cir_queue *QMe,Usart_Trm *Me,const char *sdata);
}Usart_vtable;
void Create_ComUsart(Usart_Trm *Me,uint8_t set_h,uint8_t set_t);
void Com_rxUsart_data(Cir_queue * const QMe,Usart_Trm *const Me,uint8_t bydata);
int Get_fram_num(Usart_Trm *Me);
long Deal_com(Cir_queue *QMe,Usart_Trm *Me,const char *sdata);
void Com_tailpush(Cir_queue *QMe,Usart_Trm *Me,char const *get_str);
#endif
源文件
/*有限状态机初始化*/
void Create_ComUsart(Usart_Trm *Me,uint8_t set_h,uint8_t set_t)
{
static struct Usart_vtable vtable;
my_memset(Me,0,sizeof(Usart_Trm));
vtable.rx_buff=&Com_rxUsart_data;
vtable.fram_deal=&Deal_com;
vtable.fram_num=&Get_fram_num;
Me->c_vptr=&vtable;
Me->head=set_h;
Me->teal=set_t;
Me->usart_status=STATUS_IDEL;
}
/*有限状态机读入缓存函数*/
void Com_rxUsart_data(Cir_queue * const QMe,Usart_Trm *const Me,uint8_t bydata)
{
switch (Me->usart_status)
{
case STATUS_IDEL:
if(bydata==Me->head)
{
Me->usart_status=STATUS_HEAD;
Me->last_pos++;
}
else Me->usart_status=STATUS_END;
break;
case STATUS_HEAD:
if(bydata!=Me->teal)
{
Me->usart_status=STATUS_DATA;
Me->last_pos++;
}
else Me->usart_status=STATUS_END;
break;
case STATUS_DATA:
if(bydata!=Me->teal&&!QMe->c_vptr->full(QMe))
{
Me->usart_status=STATUS_DATA;
Me->last_pos++;
}
else if(bydata==Me->teal)
{
Me->last_pos=0;/*成功接收到头和尾,没有错误数据*/
Me->Uartx_frame++; /*队列中有效帧+1*/
Me->usart_status=STATUS_IDEL;
}
else Me->usart_status=STATUS_END;
break;
case STATUS_END: break;
default:Me->usart_status=STATUS_END;break;
}
if(Me->usart_status==STATUS_END)
{
QUEUE_TYPE temp;
/*把出错数据取出,从尾取出*/
while(Me->last_pos)
{
QMe->c_vptr->tail_pop(QMe,&temp);
Me->last_pos--;
}
Me->usart_status=STATUS_IDEL;
}
else
{
QMe->c_vptr->push(QMe,bydata);/*数据入队列*/
}
}
/*实现两种命令方式,自定义根据有没有'='号自行判断命令类型*/
/*判断命令类型*/
static int Com_type(const char * const str)
{
char *buf_ptr=(char *)str;
int count = 0;
/*内容为空*/
if(*(buf_ptr+count)=='\0') return 0;
while (*(buf_ptr+count)!='='&&*(buf_ptr+count)!='\0')//'='是判断的位置,也是命令停止标志
{
count++;
}
if(*(buf_ptr+count)=='=')
{
return count;
}
else
{
return 0;
}
}
/*读取取出1帧的内容,保存帧头和帧尾,返回读取到的内容*/
static char * Get_fram_data(Cir_queue *QMe,Usart_Trm *Me)
{
static char buff[50],get_char;
int i=0;
my_memset(buff,0,sizeof(buff));
while(Me->Uartx_frame!=0&&QMe->c_vptr->pop(QMe,&get_char))
{
if(get_char==')')
{
Me->Uartx_frame--;
break;
}
buff[i++]=get_char;
}
buff[i++]=')';
return buff;
}
/*将命令放在尾部*/
void Com_tailpush(Cir_queue *QMe,Usart_Trm *Me,char const *get_str)
{
Me->usart_status==STATUS_END;
/*解决传入命令不完成的问题,比如原来数据:(111,还未接收完成(标记为错误,重新接收),插入就会变成(111(112)*/
if(Me->last_pos!=0)
{
QUEUE_TYPE temp;
for(int i=Me->last_pos;i>0;i--) QMe->c_vptr->tail_pop(QMe,&temp);
Me->usart_status=STATUS_IDEL;
Me->last_pos=0;
}
/*把当前数据的帧头和帧尾放进列表尾部*/
while(*get_str!='\0')
{
Me->c_vptr->rx_buff(QMe,Me,*get_str);/*存入数据进入缓冲区*/
get_str++;
}
Me->c_vptr->rx_buff(QMe,Me,*get_str);/*将帧尾存入*/
}
/*返回缓存区的帧数*/
int Get_fram_num(Usart_Trm *Me)
{
return Me->Uartx_frame;
}
/*返回0则为命令假,返回其他则为真,数据默认扩大1000倍,返回1则为命令模式*/
long Deal_com(Cir_queue *QMe,Usart_Trm *Me,const char *sdata)
{
char com_buf[COMMAND_LEN_MAX]={
0};//临时保存内容数据
int eque_pos=0,back_data;
float temp=0;
char *get_string;
int i=0;
/*遍历队列中所有命令,找到符合命令的*/
while (i<=Me->c_vptr->fram_num(Me))
{
i++;
get_string=Get_fram_data(QMe,Me); /*获取帧内容*/
#ifdef CIRDEBUG
printf("Get string is %s \n",get_string);
#endif
/*遍历一遍找到了,则返回*/
if(strncmp((const char *)get_string+1,sdata,strlen(sdata))==0) break;
Com_tailpush(QMe,Me,get_string);
}
/*遍历完都没有找到,丢弃当前帧,当前为最先插入的帧*/
if(i==Me->c_vptr->fram_num(Me)+1) return 0;
eque_pos=Com_type(++get_string);
/*返回输入命令中的数字*/
if(eque_pos!=0&&*get_string!='\0')
{
/*把尾帧去掉*/
for(int i=0;i<strlen(get_string)-(eque_pos+1);i++)
{
com_buf[i]=*(get_string+eque_pos+i+1);/*把等于号后面的全部数字保存*/
}
temp=atof(com_buf);/*字符串转换成为浮点型数字*/
back_data=temp*1000;/*扩大100倍*/
return back_data;
}
/*符合命令,不反回参数*/
else if(eque_pos==0&&*get_string!='\0') return 1;
/*其他情况*/
else return 0;
}
三、使用小例子
小例子代码如下
int main()
{
long x;
Usart_Trm usart_trm;
Cir_queue que;
int cout=0;
/*假设实际接收到的数据*/
char AS[20]="(PID_P=1.11)";
char BS[20]="(PID_I=2.22)";
char CS[20]="(PID_D=3.33)";
char COM[20]="(SET)";
Create_ComUsart(&usart_trm,'(',')');
Cir__Qcreate_queue(&que);
/*模拟串口存入数据*/
for(int j=0;j<5;j++)
{
for(int i=0;i<strlen(AS);i++)
{
/*逐个数据存入*/
if(j==0) usart_trm.c_vptr->rx_buff(&que,&usart_trm,AS[i]);/*PID_P*/
if(j==1) usart_trm.c_vptr->rx_buff(&que,&usart_trm,BS[i]);/*PID_I*/
if(j==2) usart_trm.c_vptr->rx_buff(&que,&usart_trm,CS[i]);/*PID_D*/
if(j==3) usart_trm.c_vptr->rx_buff(&que,&usart_trm,AS[i]);/*PID_P*/
if(j==4) usart_trm.c_vptr->rx_buff(&que,&usart_trm,COM[i]);/*SET*/
}
}
if(Get_fram_num(&usart_trm))
{
/*调参模式*/
/*传入指令*/
x=usart_trm.c_vptr->fram_deal(&que,&usart_trm,"PID_D=");
printf("x=%d\n",x);
x=usart_trm.c_vptr->fram_deal(&que,&usart_trm,"PID_D=");
printf("x=%d\n",x);
x=0;
x=usart_trm.c_vptr->fram_deal(&que,&usart_trm,"PID_I=");
printf("x=%d\n",x);
x=0;
x=usart_trm.c_vptr->fram_deal(&que,&usart_trm,"PID_P=");
printf("x=%d\n",x);
x=0;
/*命令模式*/
x=usart_trm.c_vptr->fram_deal(&que,&usart_trm,"SET");
printf("x=%d\n",x);
}
system("pause");
}
运行结果:
x=3330
x=0
x=2220
x=1110
x=1
总结
使用起来还是比较方便的,也能够存入相关数据,可以通过函数指针进行访问。
Gitee链接在这里:
基于串口的有限状态机
之前我也写过一篇类似的文章,里面有介绍大概怎么在STM32移植,这篇是基于STM32的自定义串口协议。这个是半年前写的,代码有点惨不忍睹,看看怎么移植就好了。
参考文章: