购物车0种商品
IC邮购网-IC电子元件采购商城
基于51串口通讯编程软件架构剖析
(2011/10/31 9:57:00)
前言:
串口通讯对于所有的嵌入式工程师十分常见,对于一个与外界交互的系统必须依赖一些手段,比如串口、USB、红外、GPRS之类的数据通讯传输方式。而串口作为一种廉价的短距离可靠的通讯方式得到了广泛应用。
废话少说了,就此打住,进入正题。
本文主要从软件结构上讲解如何在资源比较缺乏的系统上实现通讯协议的串口通讯编程,以及如何优化程序效率,从而使系统更快、更稳定运行。

正文:
我们以51单片机为例。51中一般针对串口通讯编程,通常采取中断接受查询发送的方式。中断函数在接受数据到达时被重复调用,其实是个重复入栈的过程,所以不宜将函数写的太长,函数太长一般会导致栈太深占用系统资源,二是处理时间过长,可能导致通讯出错。为了防止在处理数据过程中不受干扰,通常在处理接受数据前关闭中断,处理完后再开。
通常的的编程方式如下:
staticvoidUartInterruptService(void)interrupt4
{
ES=0;
RI=0;
uart_process(SBUF);
ES=1;
}
下面重点介绍数据处理函数uart_process(SBUF);
其实很多时候,对于通讯传输的数据处理才是关键,尤其对于设计通讯协议而言。笔者在刚刚做的一个系统上就碰到这样的问题,当系统庞大了,资源十分有限的情况下,数据处理一旦占用资源太多,效率太低将导致系统崩溃而无法运行。
到了这里,很多工程师可能会考虑开个大的缓冲区FIFO将接收到的数据保存在缓冲区,然后对其进行解析、判断进行下一步程序编写,当然这在系统资源比较丰富的情况下是没有问题的,ARM上采取的就是这样的方式。但如何系统庞大呢,留给的资源缺乏则不行。这样做的一个很大缺点必须是将数据帧接收完了才能够判断,降低了效率和运行速度。
其实还有另外的方式,可以采取在每接收一个字节就对其解析,解析完判断转到下一个状态,并将其中的有用数据存储在相应的数据结构中去,可以采取状态机实现。

将状态机设计为两个控制状态,一是串口状态——uart_state,一是命令类型状态——cmd_state.
(1)状态机开始状态:串口状态为CMD_NO
(2)接受到STX_CMD,状态变为CMD_START.
(3)接下来将自动进入接受命令帧的状态,再开启命令状态的状态机,对发送来的有用数据进行解析,保存,校验等。处理完毕后将uart_state设为CMD_END状态进行下一步的接受完毕判断,将cmd_state设置为初始的NO_CMD状态。
(4)最后进行ETX_CMD判断,判断数据接收是否完毕。


voiduart_process(U8u8)
{
if(uart_state==CMD_NO)
{
if(u8==STX_CMD)
{
uart_state=CMD_START;
}

}
elseif(uart_state==CMD_START)
{
switch(cmd_state)
{
caseNO_CMD:
cmd_state=u8;
break;

caseCOST_CMD:
//解析存储有用数据到相应数据结构中
//进行CRC校验
……
uart_state=CMD_END;
cmd_state=NO_CMD;
CRC=0;
break;
……
}
……
}
elseif(uart_state==CMD_END)
{
uart_state=CMD_NO;
if(u8==ETX_CMD)
{
//接受完毕
//可以考虑抛出一个消息main函数循环中进行响应处理。
}

}
}


接下来我们要讨论解析后我们数据存储的问题,其实在资源比较足够的情况下或者能够挤出data区的情况下可以考虑用结构体,我们构造好相应结构体,将接收到的数据存储进去,要应用的时候就十分方便。但这也有个矛盾,一般c51定义的结构体都被存储在data区,一般通讯的字节量大空间必然不够,存在一个矛盾,可以采用联合体union进行存储效果会好一点。当然也可以在保存数据时采用定义在xdata区(片外)的buffer来存储。这样在一定程序上优化了程序的执行效率,在程序处理立即抛出消息处理,提高了通讯数据的处理速度。对于通常资源比较丰富的系统,比如ARM上一般采取的做法是这样的,将数据存在缓冲区,接收完一帧数据后再转换成相应的数据结构,再进行分析、校验。

总体来说,这种采取状态机实时解析串口通讯数据的方式在一定程序提高了程序运行效率,使软件架构清晰明了,程序可扩展性大,有利于后续开发。以上是笔者的一点愚见,欢迎指教。


网友评论:学习啦.

网友评论:在我发出STX_CMD命令时,uart-state状态机将进入CMD_START状态,等待下一个命令的输入,而此时命令状态机cmd_state是NO_CMD状态,好了,现在外面发过来一个COST_CMD的命令,由于此时cmd_state正处在NO_CMD的状态,将执行caseNO_CMD的语句,也就是将cmd_state的状态进入COST_CMD,然后退出switch语句,这样就相当于没有去执行caseCOST_CMD状态的指令,这个命令就无效了,不知道我说清楚没有

网友评论:这种只能适合一些情况,不能适合所有~~内存小就换个大点的嘛,现在的单片机差型号那么多,肯定能找到合适的.

网友评论:你没有想清楚,我可以将cmd_state默认设置为NO_CMD,来个COST_CMD也就是cmd_state=u8,此时cmd_state=COST_CMD,接下来对命令帧有用数据进行解析.仔细分析下就很清楚的.

30楼说的很对,不是适合所有的情况,但大多数情况下还是合适的,如果通信协议加上上层的GUI设计的,通讯数据传输速度将影响整个设备的响应速度.因为不仅仅要传输数据还要解析,刷屏幕等,速度稍微快点响应上将好多了.



网友评论:谁需要直接可以工作的C51程序,可以来交流一下,QQ28591262

网友评论:我在使用modbusRTU模式时候遇到超时的问题,也用的是状态机站好功能码啥的都接受好了到了接受数据那一步了然后假如突然对方不发了怎么办啊,不就一直停在那个状态了啊,另一个消息来了就没法收了呵呵,虽然一般不会出现但是假如出现了喃然后就要用到modbus里面的间隔判断了然后那个间隔时间太短就1.5个字符,这要怎么搞啊,用定时器是不是可以?有没其他的方法喃!因为设备内部要跟DSP通信,还要跟外部的设备通信所以就要用到两个串口,如果两个都用定时器的话,那怎么搞一会定时器还给他们两用完了.

网友评论:可以仿下TCP/IP的超时处理机制.
协议每执行到一个状态,进行一次检查.

网友评论:开8个BYTE的缓冲足够了~中断函数只负责接收,在主程序循环中处理.

staticvoidUartInterruptService(void)interrupt4
{
ES=0;
RI=0;

cSerialReciveBuffer[cSerialWriteBufferPos]=cData;

if(cSerialWriteBufferPos>=UART0_RX_BUFFER_SIZE)
{
cSerialWriteBufferPos=0;
}
cSerialReciveNum++;

ES=1;
}

//主函数循环处理
while(cSerialReciveNum)
{
}


网友评论:你这种方式极其不好,程序效率低下,不能够老在while(1)循环处理一些东西,降低了程序消息,采取消息机制会好多了,关于你所说的缓冲处理我不做评价.

网友评论:这个也是收到数据马上处理,如果收到多个就处理多个,设置缓冲就是为了提高效率啊~~数据不在中断中处理是好事~

网友评论:while(1)一直被循环调用,一般处理是放在外面再通过消息机制发消息到while(1)里判断处理,如果连串口数据处理也放在while(1)里程序将写的很长,我是比较反对这种将main弄的很长的做法:)

网友评论:可以编写一个串口处理的函数,把串口数据处理放在一个函数里面,这样你就会觉得舒服多了~呵呵~~
我一直认为提高效率的方法是中断函数尽量精简~~

网友评论:大家写串口的时候都是怎么搞的,开缓冲的多还是不开的多,还是刚开始不开,以后成为高手后就开了.

网友评论:是不开缓冲的少,我只是探讨下一个特殊情况下的处理以及如何优化程序.我不是什么高手.

网友评论:因为能够多仔细考虑一些东西,在平台相互移植时候就会避免一些问题:)

网友评论:你的状态机是很高明的

网友评论:中间数据解析的时候就用了结构体做缓冲存储数据.我见到有个网友在我的基础上改了下状态机用于他的项目的一个文档,他针对他的应用自己设置了下状态的切换我觉得挺好的.我这个只是提供一个架构,具体应用大家仔细修改下就可以了.另外我绝对对于资源比较缺乏的ARM芯片比如LPC的就可以采用我的方法.



网友评论:说白了就是把数据处理放在中断里运行,这样不利于程序移植和维护,如果不是迫不得已不要这样干。

网友评论:受教了

其实只要我们在处理手里的案子,多注意下,像楼主说说的状态机之类的处理手
法已经应用很普遍了,在台湾过来的程式中特普遍

关键融会贯通,自己也能熟练的用这种处理方式,这个是难点

网友评论:其实我对TCP/IP理解在串口通讯之前,因为出现了一些问题才去思考一些解决的方法。过阵子贴个TCP/IP协议中实现上层的HTTP协议中的状态机,里面对于状态的校验和重发就做的很好。还是一句老话:“由俭入奢易,由奢入俭难”,很多一些从ARM上跑的好的程序到其他平台出问题都可能出这样那样的问题,这样一个好的架构是很有必要的。当然不是说我这个东西有什么高明的地方,只是平时写code的时候多想想一些问题可能会有意想不到的收获!

网友评论:这样写的话得保证发来的第二、第三、、、、等等数不别的器件当成地址或别的啊

网友评论:我在这个坛子另外个帖子里面提到避免这点,比如我将STX和ETX设为HEX,中间的数据帧为ASCII就不避免了……

网友评论:跟着大家学习一下^_^

网友评论:帮顶

网友评论:顶...以后有这样的文章多发一下..呵呵

网友评论:状态机的方式我也常用,应该是比较好的方法,但是我反对把解析等操作放在中断种进行。36楼中断中只接受数据放到FIFO中也许是不错的方法,楼下说while(1)效率低,可能他没有考虑到,确实不能这样傻等,效率太低,可以改进一下,采用轮询,每隔1mS或其他时间查询一下,同时可以计算超时。不过我建议将解析帧头放到接收中断中,用一个缓冲区来放数据,中断中帧头同步之后设定标志并依次接收存储每一个数据,中途有奇偶校验错误或者缓冲区益处则等待超时,主程序轮询并设定超时,超时后关中断,开始解析数据。把命令结构体直接指向缓冲区即可取得所需的数据。
现在的这种常用的通信协议都是帧头同步,帧尾超时来处理,所以我常用上面的方法。不过需要注意避免主程序和中断竞争数据。

网友评论:采用多机器通讯做同步最好了,无论小系统,还是大系统都统统可以~
采用缓冲方式效率高啊,放在主函数中检测,经过实际应用,效果非常好,不是傻等啊.

网友评论:不懂将所有的活都放到串口中断中去完成,会不会影响通讯质量和占用系统资源阿请教

网友评论:
SIGNAL(SIG_USART_RECV)
{
unsignedcharUARTemp;
UARTemp=UDR0;
UARTTimer=0x2;
if(!(UARTStatu&_BV(HeadIn)))
{
if(UARTemp!=HeadChar)
return;
else
{
UARTStatu|=_BV(HeadIn);
return;
}
}
if(!(UARTStatu&_BV(LengtIn)))
{
DataLong=UARTemp;
if(DataLong>MAXLength)
{
UARTStatu=0;
return;
}
UARTbuffer[0]=UARTemp;
ByteOfRead=0;
UARTStatu|=_BV(LengtIn);
return;
}
if(ByteOfRead<DataLong)
{
UARTbuffer[ByteOfRead+1]=UARTemp;
if(ByteOfRead<MAXLength)
ByteOfRead++;
if(ByteOfRead==DataLong)
{
if(UARTemp==Crc8fun(UARTbuffer,DataLong))
UARTStatu|=_BV(PackageOk);
else
UARTStatu=0;
}
}
}


网友评论:在大量数据交换通讯应用中,使用状态机确实会拉低系统实时性,使用超时解析对于大量数据突发访问会带来溢出风险,视不同应用采取不同处理方法,实时性要求很强的通讯我一般在协议上下功夫,定义一个结束符,一旦判断到结束符在中断中解析完并发送消息到队列中,保证不丢失消息,但需要评估解析需要消耗的CPU运行时间必须小于一个字节的接收时间。

网友评论:不错,读这样的文章如饮甘露。

网友评论:“为了防止在处理数据过程中不受干扰,通常在处理接受数据前关闭中断,处理完后再开。
static void UartInterruptService(void) interrupt 4
{
ES = 0;
RI = 0;
uart_process(SBUF);
ES=1;
}”
--------51的串口中断函数里就不用再关闭串口中断允许了吧,因为自己是不能抢占自己的。

“其实很多时候,对于通讯传输的数据处理才是关键,尤其对于设计通讯协议而言。笔者在刚刚做的一个系统上就碰到这样的问题,当系统庞大了,资源十分有限的情况下,数据处理一旦占用资源太多,效率太低将导致系统崩溃而无法运行。”

---------这很有可能是没有充足的堆栈空间造成的。一般至少得留下十几个字节的RAM做堆栈。

网友评论:不错,值得一看

网友评论:做个记号,以后用的时候方便!

网友评论:来一下!

网友评论:陈明计和周航慈有详细的介绍,陈明计的在《嵌入式实时操作系统SMALL RTOS51原理及应用》,周航慈在那本介绍ucos的书里面

网友评论:呵呵,我做串口都怎么做的!

网友评论:if(RI)
{
RI=0;
//P22=0;
Incept_data=SBUF;
if(Incept_Status==0x00) //中断处于接收起始帧阶段
{
if(Incept_data==Frame_Head)
{ Incept_Status=0x01;//更新接收状态标志为接收命令码
//P23=0;
}
else
Incept_Status=0x00;
}
if(Incept_Status==0x01)
{
if(Incept_data==Frame_SetSimulate_Orde) //如果是设置帧命令,则立设置标志
{ //P34=0;
command_marker=Frame_SetSimulate_Orde;
Incept_Status=0x02;//更新接收状态标志为接收帧长码
}
else if(Incept_data==Frame_SetDigital_Orde)
{ command_marker=Frame_SetDigital_Orde;
Incept_Status=0x02; // P11=0;
}
else if(Incept_data==Frame_Gather_Orde) //实时数据采集帧命令,则立设置标志
{
command_marker=Frame_Gather_Orde;
Incept_Status=0x02;

}

syscheckData=0; //准备进行数据和校验
return;
}
if(Incept_Status==0x02) //接收帧长
{// P35=0;
FrameLen=Incept_data;
syscheckData+=FrameLen; //更新累加和校验信息
Incept_Status=0x03; //更新接收状态标志为接收数据码
return;
}
if(Incept_Status==0x03)
{
Incept_DataStream[RecCount]=Incept_data;
syscheckData+=Incept_DataStream[RecCount]; //更新累加和校验信息
RecCount++;
if(command_marker==Frame_SetSimulate_Orde)
{
if(RecCount==FrameLen)
{ // P36=0;
RecCount=0;
Incept_Status=0x04; //更新接收状态为接收校验码信息
}

}
if(command_marker==Frame_SetDigital_Orde)
{
if(RecCount==FrameLen)
{
RecCount=0;
Incept_Status=0x04; //更新接收状态为接收校验码信息
}

}
if(command_marker==Frame_Gather_Orde)
{
if(RecCount==FrameLen)
{
RecCount=0;
Incept_Status=0x04 ; ////更新接收状态为接收校验码信息
}

}
return;

}
if(Incept_Status==0x04)
{
checkData=Incept_data;
if(syscheckData!=checkData) //如校验出错
Incept_Status=0x00;
else
{Incept_Status=0x0537=0;} //P15=0;
syscheckData=0;
return;

}
if(Incept_Status==0x05)
{
if(Incept_data!=Frame_End)//错误帧标志
b_valid_Frame=0;
else
{ b_valid_Frame=1;} //P16=0;

Incept_Status=0x00;
}

}
}

网友评论:编程序每个人的思路都不同,感觉收发都在中断里做会更好,这个程序进来就关中断不是太好吧?

网友评论:挖坟?

网友评论:正遇到这个问题,不过我现在也是楼主这么做的,

网友评论:顶一个,学习一下!

网友评论:

我总感觉楼主的程序是垃圾程序的典范!
嗨!


看出来了,俺水平太差,看了半天我也没有理解楼主是什么意思!
哪位给我指点指点!
其实很多时候,对于通讯传输的数据处理才是关键,尤其对于设计通讯协议而言。笔者在刚刚做的一个系统上就碰到这样的问题,当系统庞大了,资源十分有限的情况下,数据处理一旦占用资源太多,效率太低将导致系统崩溃而无法运行。
到了这里,很多工程师可能会考虑开个大的缓冲区FIFO将接收到的数据保存在缓冲区,然后对其进行解析、判断进行下一步程序编写,当然这在系统资源比较丰富的情况下是没有问题的,ARM上采取的就是这样的方式。但如何系统庞大呢,留给的资源缺乏则不行。这样做的一个很大缺点必须是将数据帧接收完了才能够判断,降低了效率和运行速度。
其实还有另外的方式,可以采取在每接收一个字节就对其解析,解析完判断转到下一个状态,并将其中的有用数据存储在相应的数据结构中去,可以采取状态机实现。
这段意思是不是这样:楼主以为先判断后接收效率高、省资源。 先接收后判断就“降低了效率和运行速度”。

我就没有看出到底哪里省资源了?
效率怎么就高了?

谁能指点一下?多谢!

网友评论:直接把处理函数写在中断处理函数中不利于代码移植重用和维护,在大程序中这种架构不应提倡

网友评论:NO,NO
要用FIFO
要用CRC

网友评论:小顶一下

网友评论:顶顶

网友评论:不错,挺好的

网友评论:
static void UartInterruptService(void) interrupt 4
{
ES = 0;
RI = 0;
uart_process(SBUF);
ES=1;
}
LZ的开篇确实值得质疑,上面66楼网友讲的正根:“ES不可能自己抢占自己。”串口中断硬件内部已经设计好了这种关系:(Intel硬件设计师是干什么的?如果我是硬件设计师,我怎么会把这种关系留给软件设计员?)——首次ES中断发生并进入ISR之后,必然阻断后面的同级中断(如若有的话)并使其进入中断队列排序等待,只有当前ISR完成,RETI 退出串口中断程序之后,此前纪录的、位于队列中的ES=1才会再次触发串口中断。所以,LZ的开篇程序是不是一开始就没写好?后面.....

static void UartInterruptService(void) interrupt 4
{
//ES = 0;
RI = 0;
uart_process(SBUF);
// ES=1;
}

其实我最近一直不好处理的问题是:你在一帧数据接收过程中,如果半中间突然应该来的数据中止不来了,怎么办?你的while(RI !=1) ; RI=0;该如何处理才不至于死等?才可能尽快退出当前状态?——你如何尽快感知到通信失效?我知道是利用T0超时,可是怎么写才简单?接收100字节一帧,就要设置100次T0初值?太呆板了吧。(当然可以用宏指令)如若每接收到第50字节时就发生中止或错误接收现象,只好一帧重新来过,以前的都白干!
如何才能提高通信系统可靠性?

网友评论:mark下串口通信,最近正在搞这个。

浏览:(1513)| 评论( 1 )
博文评论
Umesh:2012/12/3 16:33:00
Your posting ralley straightened me out. Thanks!

  • 昵 称:
  • 内 容:10~250个字符
  • 验证码: 验证码看不清楚?请点击刷新验证码
  •                      
  • 博文分类

    热点博文

    最新博文

    最新评论

    IC电子元件查询
    IC邮购网电子元件品质保障