事件的危机——调试手记之一[1]

[入库:2005年8月18日] [更新:2007年3月24日]

本文简介:选择自 leeky 的 blog

  相信许多读者对福尔摩斯的探案过程很感兴趣,《血字的研究》和《四签名》都提到了演绎法,福尔摩斯认为应从事实的结果找出原因,经过精密的分析和推断来破案;
  程序员的许多工作也类似于一个侦探,尤其当我们调试程序时,如何在数目众多的代码中找到逻辑错误,是一门必修的功课,程序中有许多我们已经验证为正确的事实,也有很多假定为正确的前提,如果程序有bug,那么无论这些假定正确的东西看起来是多么可靠,都值得我们怀疑,然后通过验证这些判断逐步缩小范围,剿灭bugs。
    本文通过一个对串口接收处理子程序的调试过程,向读者展示调试思路,同时对在下位机协议定义存在严重缺陷时如何使自己的程序具有良好的防错能力给出了解决方法。本例虽短小,但较典型,发生的错误让人意想不到。

  在本人写的一个小的工控系统中,通过串口从下位机读入许多点的GPS定位信息,下位机软件设计者定义了以下的回应格式(上位机命令略):
1、 读数据应答信令:
  表示在上位机开始读时,下位机先给出的一个应答(包含点数,时间范围等)
格式:20h,1dh,...chk1,chk2,0fh                  (25bytes)
            
2、清除数据应答信令:
  表示下位机在接收到上位机发出的清除数据命令后的应答;
格式:同读数据应答信令
  
3、数据上传信令:
  上位机要求下位机发送所有采集点后,下位机发送的GPS数据(时间、位置等);
   格式:20h,1eh,...chk1[1],chk2[1],0fh      (25bytes)
    
4、数据上传结束信令:
  当3完成时报告上位机。
  格式:同读数据应答信令

  这里我想指出协议存在的缺陷,以使读者明白它即将对我所论述的处理程序产生的不良影响,这里说明:以上1、2、4条所说的回应信令其实是没有的,其实质是:当下位机空闲时,就以一秒为间隔不断发送这样的数据,包含记录仪中的点数、采样点所在的起始时间与结束时间、巡查员内码等信息。

const
  leadlength=2;    // 命令头的长度
  我定义了以下的结构来保存串口的状态,其中data为串口接收到的一行数据(已经过初步处理<处理过程略>,形式如1、或3、的格式):
tcommdata=record
    commopened     :boolean;
    datafileopened :boolean;
    gotcommheader  :boolean;
    havedata:boolean;
                
    leaddata:array[1..leadlength] of byte;
    leadturn:integer;     //  读头字节存在leaddata中的位置,轮流读入一个字节,

    databytes:integer;    //  已经读入到data中的字节数。
    data :array[1..25] of byte;
    userid: integer;      //  用户内码  谁在巡查
    roadid: integer;      //  在哪条路巡查
  end;

上位机当前所执行的功能的集合:
  tcommfuncno=(funcwait,     // 上位机的命令。
           funcreadroad,
           funcreaddata,
           funcclear,
           funcfinish);
var
  currfunc   : tcommfuncno;

现在,在串口接收子程序中有如下判断:
 
{解算一行数据的代码,略;}
 case currfunc of
   funcwait:
     begin
       {略}
     end;
   funcreadroad:     //  读道路数据,   这两个功能是在上位机中加以区分的;
   funcreaddata:     //  读巡查数据
     begin
        savedatatofile(...);         //  保存已经采得的数据到文件。
        if (commdata.data[2]=$1e)    // 一条gps数据,见协议3
        then begin
          {略}
        end
        else if (commdata.data[2]=$1d)  
        then begin
               if (currfunc=funcreadroad)
               then processa       //  写入道路数据库
               else processb;      //  写入巡查数据库

               currfunc:=funcwait;    // 数据发送完毕,进入等待状态。
        end;
     end;
 end;

以上进入读数据功能后,当(commdata.data[2]=$1d)成立时,按照协议中“4、数据上传结束信令”的规定,应当是读数据结束应答信号,则存盘,然后处理并写入数据库。

调试时发现:
问题a.从未看到存盘文件有改变,数据库表也没有新记录
   a. 跟踪发现,数据是采集到了的,这是不可否认的事实;这就表明:采集到数据后上位机的当前功能不为funcreadroad 或 funcreaddata;而另一个事实是上位机发送读数命令时,已经把当前功能设为了这两个值之一。那么可推断出:当前功能又被设置成为其它值;
   b. 跟踪还发现,savedatatofile(...);确实被调用过。
   综合以上两点,作出判断:采集到数据之前,已经存过盘,但由于当时没数据,因此数据文件没变化;采集到数据后,当前功能已经改变。
   问题是程序为什么“急于”存盘?查找所有改变当前功能的代码,极可能是采集到数据之前已经执行过currfunc:=funcwait;也就是在采集到数据之前,if (commdata.data[2]=$1d) 就曾经成立。因此可给出解释:上位发送传数命令后并设currfunc为funcreadroad 或 funcreaddata,下位机并未反应过来,并没如我们所希望的立即发送数据,而是如常一样在空闲时发送1、中命令,但下位机此时把它判断成4、数据上传结束信令,其后采集到数据时因不处在读数据状态,故对此不予理睬,从而永远看不到存盘操作;这里就暴露出了协议的问题。

  问题的症结在于协议没有区分不同的状态,把不同的状态混为一谈,下位机程序倒是简单了,虽然可行,但在描述上不应如文初所示。

本文关键:调试 串口 事件
  相关方案
Google
 

本站最佳浏览方式为 分辨率 1024x768 IE 6.0(或更高版本的 IE浏览器)

go top