概述
串口通信是目前单片机和DSP等嵌入式系统之间,以及嵌入式系统与 PC 机或无线模块之间的一种非常重要且普遍使用的通信方式。在嵌入式系统的硬件结构中,通常只有一个8位或16位的CPU,不仅要完成主流程的工作,同时还要处理随时发生的各种中断,因而嵌入式系统中的串口通信程序设计与PC机有很大的不同。若嵌入式系统中的中断服务子程序在系统运行过程中占用了较多的时间,就有可能在中断服务子程序正运行时,又产生一个同类型或其它类型的中断,从而造成主程序得不到执行或后续中断数据丢失等问题。所以嵌入式系统中的串口通信虽然看似简单,但其中仍有许多问题值得研究,例如串口通信过程中的帧同步问题。
串口通信的数据帧结构
现在的单片机系统中,往往需要由多个独立的控制模块来共同完成功能。不同模块之间可以通过串口,RS232,SPI等不同的通信接口来进行通信,在通信过程中我们可以加入一些通信协议,以提高系统的可靠性和稳定性;而要完成特定的通信协议,就得有一定的同步机制。下面介绍一下简化的通信数据帧结构,以便分析说明嵌入式系统通信过程中的帧同步方法。
假定串口发送的数据帧结构为:
其中:“帧头”用于同步,一般是一个或多个字节,本文中假定数据帧同步头有2字节(0xFE、0xEF);“帧长度”表示数据包中除去“帧头”和“帧长度”的字节数,即“帧类型”、“帧数据”和“校验”的总长度;“帧类型”为通信协议里规定的命令类型;“帧数据”为应发送的主要信息;“校验”可以采用简单的单字节“异或”的方法,复杂点的可以采用“CRC校验”。
基于有限状态机的帧同步方法
下面我们就来说说“基于有限状态机的帧同步方法”,该方法是将数据帧的接收过程分为若干个状态:“接收帧头HEAD1状态”、“接收帧头HEAD2状态”、“接收帧长度状态”、“接收帧类型状态”、“接收数据状态”及“接收校验和状态”。系统的初始状态为“接收帧头HEAD1状态”,各接收状态间的状态转移图如下图所示。随着串口接收中断新数据的接收,系统的接收状态依次为 HEAD1→HEAD2→LEN→TYPE→DATA→CHECK。该方法也快速、有效地实现了同步;但是需要注意的是,在每一次接收完1帧完整的数据之后,必须把系统的接收状态重新设置为HEAD1,否则将会影响下一帧的数据接收。
此后,程序按照协议开始依次接收数据帧长度、命令类型、数据和校验位。接收完后,重新设置系统接收状态为HEAD1,同时对该数据帧进行校验。校验正确后,利用消息机制通知主程序根据命令类型对数据帧进行处理或执行相应的命令操作。
下面给出该方法在CC2530裸机中的实例程序:
#include "iocc2530.h"#define TRUE 1#define FALSE 0// 状态机状态#define HEAD1 0x00#define HEAD2 0x01#define LEN 0x02#define TYPE 0x03#define DATA 0x04#define CHECK 0x05// 命令#define COMMAND1 0x01#define COMMAND2 0x02// 状态机用到的全局变量unsigned char g_datrev[48]; // 串口数据缓存unsigned char g_cmd; // 接收的命令unsigned char g_recok; // 串口事件标志位unsigned char g_recstate = HEAD1; // 接收状态unsigned char g_len = 0; // 已接收的数据长度unsigned char g_check_sum = 0; // 校验和unsigned char g_lentotal = 0; // 包长void main(void){// 初始化程序// 任务轮询while(1){// 有新的事件发生if(g_recok==TRUE){// 清除串口事件标志位g_recok = FALSE;// 处理命令switch(g_cmd){case COMMAND1:{// 处理命令1}break;case COMMAND2:{// 处理命令2}break;default:break;}}}}// 串口0中断处理函数#pragma vector = URX0_VECTOR__interrupt void UART0_ISR(void){// 接收串口数据unsigned char recdata = 0;// 清中断标志URX0IF = 0;// 接收串口0数据recdata = U0DBUF;// 进入接收状态机switch(g_recstate){case HEAD1:{// 如果接收到HEAD1的值0xFE,则进入状态HEAD2if(recdata==0xFE){g_recstate = HEAD2;}}break;case HEAD2:{// 如果接收到HEAD2的值0xEF,则进入状态LEN接收帧数据长度if(recdata==0xEF){g_recstate = LEN;}// 如果接收到的还是HEAD1的数据0xFE,则不跳转,等待下一个数据else if(recdata==0xFE){g_recstate = HEAD2;}// 如果没有接收到HEAD1或HEAD2的数据则返回HEAD1状态重新再来else{g_recstate = HEAD1;}}break;case LEN:{// 将状态机指向下一个状态“TYPE”g_recstate = TYPE;// 接收帧数据长度g_lentotal = recdata;// 异或校验,先校验“HEAD1”和“HEAD2”g_check_sum = 0xFE^0xEF;}break;case TYPE:{// 将状态机指向下一个状态“DATA”g_recstate = DATA;// 接收帧命令数据g_cmd = recdata;// 校验命令数据g_check_sum = g_check_sum ^ recdata;// 将下一状态要用的“g_len”先清零g_len = 0;}break;case DATA:{// 将接收到的帧数据存储在数据缓存数组中g_datrev[g_len] = recdata;// 校验帧数据g_check_sum = g_check_sum ^ recdata;// 记录接收了多少字节数据g_len++;// 接收总帧长度的数据后转向“CHECK”状态校验整个帧是否正确if(g_len>=g_lentotal){g_recstate = CHECK;}}break;case CHECK:{// 检测接收到的帧数据是否正确,正确则通知主函数有新的事件;错误则抛弃此帧,重置状态机if(g_check_sum==recdata){g_recok = TRUE;}g_recstate = HEAD1;}break;default:{g_recstate = HEAD1;}break;}}
由于采用了状态机和消息机制的结构,上述设计思路快速有效地实现了串口通信的同步,而且程序结构清晰,便于维护,也易于向其他的串口通信协议移植。另外,串口中断服务子程序中需要处理的工作很少,大大减轻了串口接收中断服务程序的压力,缓解了嵌入式系统有限资源与需求之问的矛盾,提高了嵌入式系统的稳定性。

