先上代码
 private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
 {//串口的波特率为9600
            List<byte> received=new List<byte>() ;
            int loopCount=0;//检测共分几次读取串口
            while (serialPort1.BytesToRead > 0)
            {
                loopCount++;
                int len = serialPort1.BytesToRead;
                byte[] temp = new byte[len];
                serialPort1.Read(temp, 0, len);
                received.AddRange(temp);
                Thread.Sleep(10);//延迟10ms,等待串口的数据缓冲好
            }
//其余处理代码
 }
问题是:经过调试发现,同样的波特率,但是却发现接收的不稳定,
        第一组数据从下位机发送了21个字节过来,有时是1次就读取了(通过查看loopCount的值发现),       有时是2次读取串口。当1次性读取串口时,第一次serialPort1.BytesToRead=21;分为两次读取时,第一次serialPort1.BytesToRead=16,第二次循环时,serialPort1.BytesToRead=5,很奇怪这是为什么呢还有种情况是:第一次读取了16个字节,在Thread.Sleep(10)后,serialPort1.BytesToRead=0,于是数据就接收不完整了,此时就会跳出while (serialPort1.BytesToRead > 0)循环,我很奇怪呀,这10ms的时间里,用示波器可以看到下位机是有数据传送过来的,为什么这些数据没有进入serialPort1的缓冲区呢?
一样的代码,一样的波特率,为什么有时候进入触发事件时,缓冲区读到的字节数是16,有时候会超过16?为什么有时候Thread.Sleep(10);剩下的数据会存入缓冲区,有时候却不会?不知道描述清楚了没,求达人解答呀

解决方案 »

  1.   

    求大神啊,为什么在相同的条件下,接收的时候读取的字节数是不一样的,从触发事件到读取串口,似乎不是每次的时间都是一样的,是.net消息机制的问题还是什么呢?我还试过这样
    private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
     {//串口的波特率为9600
         int length=serialPort1.BytesToRead;
         //other codes       
    }
    即使是这样,进行多次调试,每次从下位机传送过来的一帧数据都是21个字节,并且保证波特率一样,通过示波器可以证明下位机传输的数据不存在问题,但是读取的length值是不一样的,有时是16,有时是21,为什么啊,想不通想不通。
      

  2.   

    说的似乎也蛮合理的,是说我调试的C#程序不是每时每刻都在运行,于是导致读取串口的时候读到的字节不同,因为我用的波特率是9600,一个字节约为1ms,进程间如果差了几个毫秒,就会导致读取串口的字节数不一样。就是不知道系统是怎么给进程分配的,不同进程间调度是多大,比如允许一个进程占用多长时间,等到下次运行这个进程的时候要间隔多久。不知道还有没其他解释
      

  3.   

    建议你的sleep(10)不要放在串口的读取循环里面吧,另外你取数据的地方改成下面这样试试:
                List<byte> received=new List<byte>() ;
              int loopCount=0;//检测共分几次读取串口             
                while (serialPort1.BytesToRead > 0 || received.Length < 16)
               {
               }
      

  4.   

    每次返回的长度不一样很正常,你开个buffer 全局变量(比如 List<byte>),把每次收到的包丢到buffer里面去。然后在buffer里面根据你的协议截包。感觉跟TCP接受数据流模式有点像。
      

  5.   

    我确定一样,用示波器测过从下位机发送的数据,每个字节发送间隔约为500us,也就是0.5ms。
    一开始我也怀疑是下位机发送数据不连续,但是从示波器读取的总线电平来看,下位机发送的数据是很连续的。
      

  6.   


    首先,你做了一个判断,当有数据的时候你就会去接收 while (serialPort1.BytesToRead > 0)
    我们知道串口是有延时性的,当第一次21个字节的时候,你碰巧是一次性读完了,那么这个时候一次成功,但是有时候缓存里只有16个字节,你已经开始读了,此时你读的只有16个字节,于是进入到你的循环,,延时了10MS,导致后5个字字节也读进来了,建议,延时放在循环前,让串口有足够的时间去接收,。。这样就可以保证一次读完了个人见解
      

  7.   


    延时了10MS,导致后5个字字节也读进来了-------------正如你所言,我也是这个意思,延时就是为了让数据进入缓冲区,然后会继续判断 while (serialPort1.BytesToRead > 0),但是我发现的问题是,串口一直有数据过来,但是延时了10ms后,这些数据并没有进入缓冲区,导致serialPort1.BytesToRead=0,于是循环就进行不下去了
      

  8.   

    1楼有个说法就是说操作系统对各进程的分配,延时太短的话,可能这个时间段里,进程不在运行中,所以并没有对串口的数据进行处理,导致serialPort1.BytesToRead=0,目前我偏向于这个理解。
    看来我要延迟的时间长一点呀,确认数据都已经进入缓冲区。
    在写代码的时候,想的都是程序在一直不断的运行,却忽略了系统的机制。
      

  9.   

    下班前再顶下有没有其他解释呢?难道幕后黑手就是操作系统对各进程的分配吗另外想补充问个:.net里是由什么负责将串口的数据输入缓冲区的?
      

  10.   

    我确实做的不够好,因为之前从来没有人教我,都是靠的自学。目前来说我也做了好几个工控系统了。都是多串口多设备,多线程数据采集,设备工作调度的。尚未碰到不能解决之串口通讯问题。
    你这个代码,首先一点,你既然用while来检测数据并接收,那就别用DataReceived这个事件。
    直接把while放线程了不就得了。不然的话,你在while循环,而那边却又因为新的数据接收导致触发了DataReceived事件,可能又在进入另一个while。
      

  11.   


    this.DataReceived+=new SerialDataReceivedEventHandler(RSSerialPort_DataReceived);  
      

  12.   


    你说的这个确实是我疏忽了,可能在while的时候,又会再次触发DataReceived事件,不过这个我也不懂啦,不知道DataReceived事件没处理完的话会不会再次触发进入。
    之所以用DataReceived事件是因为采取的一问一答的方式,确认接收到的数据没问题后,再发送下一个命令,不用触发事件,不晓得该怎么处理接收到的数据,也许这个是我太固执了,喜好用DataReceivd事件。不过你要是有好的想法,我非常愿意倾听,一个人的想法难免陷入死胡同。简略描述下我要做的事情:按下一个button后,发送第一个命令,AA 01 00 01 55,然后单片机返回数据是AA 02 00 (16个字节的数据)(校验和)55,PC端确认校验和正确之后发送下一个命令 AA 01 01 02 55,单片机继续返回数据为AA 02 01 (1个字节的数据)(校验和)55,PC再次确认校验和,正确就发送下一个指令,错误就重发指令。如此类推,一共大约10个命令,基本要求就是检验单片机发送过来的数据是否符合格式,检验和是否正确,正确了就发送下一个指令,错误就重发指令。从头至尾只按过一次button,后面的都要自发完成,不要再手动。可能我思维定式了,想来想去就觉得DataReceived事件好处理点,不知你有什么好的思路不
      

  13.   

    不要指望下面一次发多少给你,你就能一次read就能全部收到。如果下面不是连续向你发数据的话,你还可以控制下时间,每次rece的时候sleep一段时间,说不定能一次收到。如果下面是间隔的时间不停的向上发送东西(或者说你上面在不停的查数据),你还是老实的弄个buffer,然后自己在buffer里面按协议截取一条完整命令出来。这就跟UDP跟tcp流模式接收数据一样,你用UDP连续的向服务器发送两条查询命令,你rece肯定是分两次分别收到两个完整的包,但是你用TCP的话就不一样了,你可能一次就收到了两次的响应包,也可能4~5次收全这两次的数据包(服务器是两次分别回复的)。.NET里面串口的接收模式就跟那个TCP流模式一样。  你还是老实存buffer,然后在buffer里面截取命令,其实也不复杂。
      

  14.   

    serialPort1_DataReceived 用这个必须要有协义头才行
    如 
     协议号
     长度
     内容
    这三个
    要是不明白百度一下,Socket粘包原理,跟这个一样的。这个事件要这么写 private ByteQueue queue=new ByteQueue()<-这里要自己写个环型Buffer的缓冲区
     private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
     {//串口的波特率为9600
              
             
                    int len = serialPort1.BytesToRead;
                    if(len>0)
                    {
                      byte[] temp = new byte[len];
                      serialPort1.Read(temp, 0, len);                  //下面主要是要处理好贴包
                      queue.EnQueue(tmp,0.len) <- 装入缓冲区
                      while(queue.Length>0)
                      {
                         这要里处理收到的包是否满足条件。
                         包号
                         包长
                         if(包长<queue.Length)
                         {
                            读包
                         }else
                         {
                           break;
                         }
                      }
                     }
                
     }
      

  15.   


    谢谢,额,不懂TCP
    我的想法是加长延时,不过这不是我最关心的,毕竟解决办法是有的。
    我比较奇怪的是,为什么在进行多次试验时,明明串口总线上的数据是一样的,但是读取缓冲区的时候却存在差别,有时读到的是16字节,有时是21字节,甚至有时是8字节,从发帖到现在,对于这个问题,我看到合理点的就是1楼的说法了,即系统对进程的调度,但真正的原因是什么我却不得而知。希望有知道的大神来解答下,或者是说说自己的看法。
    另外,不知道你说的buffer是指什么,由于我要从下位机接收到多组数据,接受一组解析一组,然后发送下一个指令。如果采用查询的方式,不知道该在何时处理接收到的数据了
      

  16.   


    谢谢你的回复;协议我是肯定定好的,不然不知道收到的数据是否符合要求,以及数据的含义。
    不懂为什么要while(queue.Length>0),这个循环是干嘛用的,不会死循环吗?
      

  17.   

    死循环,是不会的。
    上面那个 读包 就是把数据读出来后,queue.Count自然就item就少了。
    还有最好你把那个协议说明弄上来吧,我可以帮你看看,前一段时间写过COM口喷码机的协议做法就是我自己所说的那样也是用serialPort1_DataReceived这个事件来接收,
      

  18.   

    协议简化下大致如下:(所有命令和数据都是AA开头,0X55结尾,55前面的一个字节为AA之后的所有数据之和不包括AA和55,即和校验位.PC发送的指令,01代表命令类型,01后面的字节代表序号,从00到0A。单片机回复的数据中 AA后面的02代表是回复01这个命令的,02后面的字节为序号,也是从00到0A)
    PC发送 AA 01 00 01 55
    单片机回答:AA 02 00 16个字节的数据 和校验 55PC发送 AA 01 01 02 55
    单片机回答:AA 02 01 10个字节的数据 和校验 55PC发送AA 01 02 03 55
    单片机回答:AA 02 02 12个字节的数据 和校验 55
    ......一共10个命令,按下一个button后,完成所有这些通讯,PC接收到数据后,先判断和校验是否正确,正确的话就存储下数据,并发送下一个命令。至于数据长度,每个序号对应不同的长度,只要知道了AA 02后面的那个字节是什么就可以知道数据长度。
    你有什么好想法参考下不
    其实就是先找到AA,然后再找55,如果和校验是正确就认为数据有效,那么就发送下一个指令。如果接收到的数据不符合要求就无效,重复指令
      

  19.   

    loopCount不管是1,还是直到21,都是正常的。你不能根据这个就说人家.net“不稳定”,这都是正常的。不然还是抛开低层次系统自己去拿c“开发”操作系统驱动吧。
      

  20.   

    用DataReceived并没有错。你也看过我之前那个基础类是继承了SerialPort,然后扩展了一发一收式的命令交互功能。你别说这个基类有什么不合理啊之类的,但我之前的几个项目的串口通讯确确实实就是基于这个再扩展的。工控测试,都是运行一个多月的不间断串口通讯的,都没有出现问题过。即使偶尔的收发错误也可以通过重发解决。
      

  21.   

    和yeqi3000聊过,他还是很有经验的。回答你的两个问题:
    1、在使用断点调试时和不用断点调试时,你看到的结果是不一样的。
    如果在断点的情况下,21个字节,可能第一次只能接收到5个byte,第二次可以收到16个字节。。
    如果是非断点的情况下,多半这21个字节可以一次性收完,因为数据量很小。所以,千万不要让断点调试误导你。
    2、这里没有必要使用thread.sleep,我认为只有在打开串后,和马上向串口写入的情况下,需要短暂sleep下。见msdn
    3、既然是固定长度的报文,可以在事件中判断serialPort1.BytesToRead的值,如果小于21,则直接return.
    否则的话,你就声明一个byte[21]的数组,而且你仅从缓冲区中读取21个填充进来。
    未读取下的数据在下次事件触发时,再进行读取(再读取21个)。
    这里也存在这样的一个问题,如果下拉机发的非常的快,而你每次只读取21个的话,就有可能超出缓冲区的大小(导致很多数据丢失,缓冲区溢出),因为缓冲区中存在很多数据,而你每次只读走了21个。。
    这个问题就可以通过声明一个字段级的List<byte>来解决。
      

  22.   

    感谢你们的热心回答,但是我真的要泪奔了我真正想问的问题似乎一直被大家忽略了。
    进行多次调试的时候,我没有用断点,是通过查看loopCount知道共读取了几次串口的。有时是1次,有时是2次,所以才感觉不稳定,因此我真正想问的是,为什么会出现这种现象呢。
    另外,接收的数据不是固定长度的,只是第一组数据是21个字节,后面的是十几个字节的,也有几个字节的。
    用sleep是因为我希望在一次DataReceived事件里就把数据读取完毕,而不是进入多次DataReceived事件。
    最后,缓冲区的是不会溢出的,因为PC端发送一个指令时,下位机只回答一次。第一次的时候,下位机只发了21个字节。谢谢你的回答,但是那个,还是希望能看清我的问题一定是我没注意分段,没用红字标明,才会模糊问题的焦点
      

  23.   

    我是小白不懂呀,求大神指导。
    就是搞不懂为什么同样的情况,为什么有时候是这样,有时候是那样,能说下引起这种现象的原因吗。为什么有时候触发DataReceived事件的时候,串口已经有21个字节进入缓冲区(当然可能会更多),有时却只有16个字节,甚至有时是8个字节?说下你的见解嘛,我比较笨,懂的又不多,想不出有什么原因可能导致这么个结果。
      

  24.   

    串口编程时有任何的延时行为都有可能导至接收丢数据。因此我的策略是使用异步或者开一个线程去读数据(这就确保了不会有延时)。读到数据后再使异步调用处理函数(这样就可以无视处理时间了)。自从使用了这个方案后从来没有发生过丢数据的情况(受干扰除外)。附代码
    serialPort = new System.IO.Ports.SerialPort();
    serialPort.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(serialPort_DataReceived);
    ..........
    serialPort.Open();
            void serialPort_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
            {
                System.Threading.Thread.CurrentThread.Priority = System.Threading.ThreadPriority.AboveNormal;//调高优先级,保证线程不会被其它线程阻塞。
                if (e.EventType == System.IO.Ports.SerialData.Chars)
                {
                    while (serialPort.BytesToRead > 0)
                    {
                        BuffEventArgs arg = new BuffEventArgs();
                        arg.Connection = this;
                        arg.Data = new byte[512];
                        arg.Length = serialPort.Read(arg.Data, 0, arg.Data.Length);
                        CallOnReceiveDataInvoke(this, arg); //使用异步方式引发接收事件                  
                    }
                }
            }
      

  25.   

    谢谢,周一我去试试你的方法,现在不在办公室。
    你的程序里有的我都没见过,还要先去查下MSDN资料,伤不起。
      

  26.   

        首先,串口通讯也好,其他通讯也好,一般都会规定传输指令的头、长度、负载(具体的数据)、校验、尾.
        另外你在接受数据的事件处理中没必要加延时10ms,这样反而会出问题(我猜的哈)。每次收到的数据个数不一样应该就是系统底层调度的问题,每次你的这个线程获取到线程锁的时间长度是不一样的,所以就会出现这样的问题。
        我最近也在用串口通讯,自己对SerialPort又做了一个封装:支持协议加头、校验、尾、以及数据完整性的校验(做法就是每收到一些字节就放到自己建立的缓冲区里,去找指令头,找到之后就去读取指令头后面两个字节定义的长度个字节,如果找到了协议尾,那就把这条完整的指令作为EventArgs发给订阅者),这样使用起来很方便,上层业务不需要了解具体的实现,能保证数据的完整性和正确性。
      

  27.   


    帮你实现了简单的字节缓冲器。 /// <summary>
        /// 字节缓冲器
        /// </summary>
        public class ByteQueue
        {
            private List<byte> m_buffer = new List<byte>();
            public bool Find()
            {
                if (m_buffer.Count == 0)
                    return false;
                int HeadIndex = m_buffer.FindIndex(o => o == 0xAA);            if (HeadIndex == -1)
                {
                    m_buffer.Clear();
                    return false; //没找到AA
                }            else if (HeadIndex != 0) //不为开头移掉之前的字节
                {
                    if (HeadIndex > 1)
                        m_buffer.RemoveRange(0, HeadIndex);
                }            int length= GetLength();            if (m_buffer.Count <length)
                {
                    return false;
                }            int TailIndex = m_buffer.FindIndex(o => o == 0x55); //查找55的位置            if (TailIndex == -1)
                {
                    //这一步为防止连发一个AA开头的包后,没发55,而又发了一个AA
                    int head = m_buffer.FindLastIndex(o => o == 0xAA);
                    if (head > -1)
                    {
                        m_buffer.RemoveRange(0, head);
                    }
                    return false;
                }
                else if (TailIndex + 1 != length) //计算包尾是否与包长度相等
                {
                    m_buffer.RemoveRange(0, TailIndex);
                    return false;
                }            return true;
            }        /// <summary>
            /// 命令类型
            /// </summary>
            /// <returns></returns>
            public byte Cmd()
            {
                if (m_buffer.Count >= 2)
                {
                    return m_buffer[1];
                }
                return 0;
            }        /// <summary>
            /// 序号
            /// </summary>
            /// <returns></returns>
            public byte Number()
            {
                if (m_buffer.Count >= 3)
                {
                    return m_buffer[2];
                }
                return 0;
            }        /// <summary>
            /// 包长度
            /// </summary>
            /// <returns></returns>
            public int GetLength()
            {
                int len = 5;//AA 命令类型 序号 校验和 55
                if (m_buffer.Count >= 3)
                {
                    switch (m_buffer[2]) //第三字节为序号
                    { 
                        case 0x00: //序号
                            return len + 16;
                        case 0x01: //序号
                            return len + 10;
                        case 0x02: //序号
                            return len + 12;
                    }
                }
                return 0;
            }
            /// <summary>
            /// 提取数据
            /// </summary>
            public void Dequeue(byte[] buffer, int offset,int size)
            {
                m_buffer.CopyTo(0,buffer,offset,size);
                m_buffer.RemoveRange(0, size);
            }        /// <summary>
            /// 队列数据
            /// </summary>
            /// <param name="buffer"></param>
            public void Enqueue(byte[] buffer)
            {
                m_buffer.AddRange(buffer);
            }
        }这是调用例子 private ByteQueue queue = new ByteQueue();
     private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
            {
                int len = serialPort1.BytesToRead;
                if (len > 0)
                {
                    byte[] temp = new byte[len];
                    serialPort1.Read(temp, 0, len);
                    queue.Enqueue(temp);
                    while (queue.Find()) //while可处理同时接收到多个AA ... 55 ,AA...55的包
                    {
                        int length = queue.GetLength();
                        byte[] readBuffer = new byte[len];
                        queue.Dequeue(readBuffer, 0, length);
                        OnReceiveData(readBuffer); //<这里自己写一个委托吧就OK了
                    }            }        }
      

  28.   

    50楼也是理解了DataReceived与BytesToRead的人。
      

  29.   

    不过有一点,触发DataReceived的时候。
    执行int len = serialPort1.BytesToRead;
    len怎么着也是大于0的,这个判断有点无意义。
      

  30.   


    好想法,之前我都不晓得List是个啥东西,现在才发现很好用啊,可以任意的Add和Remove。
    你思路和想法很好,但是0x55也有可能在数据部分出现,不一定是结束字节,我要是想拿过来用的话还要再改改。我的想法是:
    1、找到AA,AA之前的字节移除
    2、根据AA之后的两个字节判断数据长度,与queue的长度比较,queue的长度不够就继续接收
    3、判断结束字节是不是55,比如开头为AA 00 00,那就判断AA之后的第20个字节是不是55,如果是就判断校验和是否正确,不是55就寻找下一个AA但是如果在数据的传输中,有字节被遗漏或者受干扰出错了,那样的话就会一直在等待正确的数据了,此时串口已经没有数据发送过来了,但程序还是一直在等串口发送正确的数据过来。不知道对于这种情况你是怎么处理的?
      

  31.   


    请问下这句的详解:CallOnReceiveDataInvoke(this, arg); //使用异步方式引发接收事件 
    怎么用异步方式引发?
      

  32.   

    换行符的ASCII码是0x0A,数据里也很有可能出现0x0A的。
    额,可能我就是那种喜欢打破砂锅问到底的吧,一定要弄明白谢谢你的建议
      

  33.   


    如果内容中有55 那就把Find()中的改一下就行了
                 int length= GetLength();            if (m_buffer.Count <length)
                {
                    return false;
                }            if(m_Buffer[length]!=0x55)
                {
                    m_buffer.RemoveRange(0, length);
                    return false;
                }
               这样就可以了。接下来自己要怎么判断还是怎么判断你所说的
    但是如果在数据的传输中,有字节被遗漏或者受干扰出错了,那样的话就会一直在等待正确的数据了,此时串口已经没有数据发送过来了,但程序还是一直在等串口发送正确的数据过来。不知道对于这种情况你是怎么处理的? 字节出错问题 如果数据没有接到收完整的包,他是要一直等待的,这是正常的。你要是防止等待过长,那就加个超时功能,在向单片机发送数据后,如果超过你指定的时间内,没收到回应。那先将缓冲区的数据清空,重新发一个指令给单片机就是了。
      

  34.   


    你光打字人家看不懂呀,直接给Code来的快些
      

  35.   


    因为你说的我都懂,对我遇到的问题木有帮助哎
    不过很感谢你的回答
    sorry啦,就只有100分,就只给了对我的问题有帮助的回复
      

  36.   

    今天经试验发现,一个DataReceived事件里的代码没跑完时,是不会再次进入该事件的,必须等跑完才行
      

  37.   

    既然你测试过,那就行。我的实现方式是不在DataReceived做while,怕的就是扰乱了serialPort的工作机制。
    我也说过了,我的基类是实战所用,百分百可靠通讯。至于你用不用,看不看,则随便了。我当初写出来也是让想用的人多一种思路。
      

  38.   

    自己给自己的帖子一个回答吧,是关于在sleep(10)的期间内,数据未进入缓冲区的问题。
    用很多有代表性的数据进行了多次测试,猜测如下:串口硬件在收到8个字节的数据时,会将这些数据存到SerialPort的输入缓存区,不足8个字节时,会等待足够8个字节,但在一定时间内还不足8个字节,还是会将这些数据存至SerialPort的输入缓存区,这个时间为10ms~15ms。
    总结起来就是:串口每接收到8个字节便会送到SerialPort的输入缓存区,不足8个字节,每10ms~15ms将接受到的数据存至SerialPort的输入缓存区。(这也就不难理解,为什么在用触发事件读取串口时,当数据多于8个字节时,会多次进入接收事件,每8个字节触发一次)以上是根据自己进行多次试验得出的结论,不敢保证百分百正确。关于串口硬件和软件衔接方面的资料太少了,没找到相关的,只能自己用数据试验。