★★★ 关于 usart.c 中 void USART1_IRQHandler(void) 函数的bug分析及问题的解决

2019-10-16 00:19发布

STM32开发指南-库函数版本V1.2.pdfP142和《原子教你玩STM32(库函数版)》P107中,都有这样一句话:“当收到回车(回车的表示由2个字节组成:0X0D0X0A)的第一个字节0X0D时,计数器将不再增加,等待0X0A的到来,而如果0X0A没有来到,则认为这次接收失败,重新开始下一次接收”。   usart.c中,void USART1_IRQHandler(void) 的实现如下:   void USART1_IRQHandler(void)  //串口1中断服务程序 {     u8 Res;     if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)     {         Res =USART_ReceiveData(USART1);  //读取接收到的数据         if((USART_RX_STA&0x8000)==0)  //接收未完成         {            if(USART_RX_STA&0x4000)  //接收到了0x0d            {                if(Res!=0x0a)  USART_RX_STA=0;  //接收错误,重新开始                else  USART_RX_STA|=0x8000; //接收完成了             }             else   //还没收到0X0D             {                 if(Res==0x0d)  USART_RX_STA|=0x4000;                 else                 {                     USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;                     USART_RX_STA++;                     if(USART_RX_STA>(USART_REC_LEN-1))  USART_RX_STA=0;  //接收数据错误,重新开始接收                    }               }         }        }    请注意上述内容的红 {MOD}加粗部分即为bug。因为当收到0X0D后,如果下一个不是0X0A,也不应该“USART_RX_STA=0;//接收错误重新开始一旦你所发的数据中含有0x0D,那么,你所发的所有的数据都会遭殃。 按照上述内容易知,如果我们发给STM32的数据中含有0X0D(这里的0X0D不是回车键的0X0D 0X0A中的0X0D),那么从0X0D后面从第二个数开始,将会依次被放入USART_RX_BUF[0]USART_RX_BUF[1]USART_RX_BUF[2]、…… 比如:通过串口给STM32发送数据 01 02 0D 04 05 06  (16进制发送,且选择发送新行),那么在接收到0D后,再接收到04时,因为0x04=0x0A,所以执行“USART_RX_STA=0;//接收错误,重新开始”,也就是说,0x05会被放入USART_RX_BUF[0]中,0x06会被放入USART_RX_BUF[1]中。 现在我们来测试usart.c中,void USART1_IRQHandler(void) 到底是不是这样。我用原子提供的Template工程模板,写了这样的main()函数:   int main(void) {     u8 i;     delay_init();             NVIC_Configuration();    uart_init(9600);           for(i=0;i<6;i++)  USART_RX_BUF=0;  //首先清零USART_RX_BUF[0~5],我们就用前6个元素来测试       while(1)     {         if(USART_RX_STA&0x8000)  //USART1接收完成         {
                 /* 输出刚刚收到的数据USART_RX_BUF[0~5]并换行 */              for(i=0;i<6;i++) printf("%d ",USART_RX_BUF); printf(" ");                USART_RX_STA=0;  //清状态标志              for(i=0;i<6;i++)  USART_RX_BUF=0;  //USART_RX_BUF[0~5]          }      } }   结果如下图所示。当所发数据中不含0X0D时,一切正常:发 01 02 03 04 05 06 ,收到的也是01 02 03 04 05 06.
        
      

    而当所发数据中含有0X0D(我把上面的03改成0D),就糟糕了:发 01 02 0D 04 05 06 ,我们收到的却是05 06 0 0 0. 

                 原因,我上面已经分析了。 可见,“当收到第一个字节0X0D时,计数器将不再增加,等待0X0A的到来,而如果0X0A没有来到,则认为这次接收失败,重新开始下一次接收”是不正确的。   围绕带下划线的这句,我对void USART1_IRQHandler(void) 作了如下修改(黑体加粗部分)   void USART1_IRQHandler(void)  //串口1中断服务程序 {     u8 Res;     if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)  //接收中断(接收到的数据必须是0x0d 0x0a结尾)     {         Res =USART_ReceiveData(USART1);  //读取接收到的数据         if((USART_RX_STA&0x8000)==0)  //接收未完成         {             if(USART_RX_STA&0x4000)  //接收到了0x0d             {               //if(Res!=0x0a)  USART_RX_STA=0;  //接收错误,重新开始
                 /* 
                     如果当前接收到的数据不是0x0a,说明刚刚接收到的数据0x0D不是回车的0X0D 0X0A中的0X0D
                                          
只是用户所发的普通数据0x0D而已 
                  */ if(Res!=0x0a) {                      USART_RX_BUF[USART_RX_STA&0X3FFF]=0X0D; //补上刚刚接收到的0X0DUSART_RX_BUF[]        USART_RX_STA++;        USART_RX_BUF[USART_RX_STA&0X3FFF]=Res;   //将当前接收到的数据放入USART_RX_BUF[]
           USART_RX_STA++;
           USART_RX_STA &= 0xBFFF;   //清除刚刚0x0d的接收标志 (0xBFFF = 1011 1111 1111 1111)
       }                 else  USART_RX_STA|=0x8000; //接收完成了              }             else //还没收到0X0D             {                 ……………………                               }         }        }    main()不变,我们再来试试修改后的void USART1_IRQHandler(void) 正确与否,如下图所示:   当所发数据中不含0X0D时,一切正常:发 01 02 03 04 05 06 ,收到的也是01 02 03 04 05 06.

     


同样我把上面的03改成0D:发 01 02 0D 04 05 06 ,我们收到的是01 02 13 04 05 06. (注意:0x0D10进制是13)

         所以,修改后的void USART1_IRQHandler(void) 是可行有效的。   其实,原USART1_IRQHandler出错的概率也不大,256分之一吧,但是问题的严重性在于,一旦你所发的数据中含有0x0D,那么,你所发的所有的数据都会遭殃,我们不可能保证我们发给STM32的数据(即使是字符或string,其ASCII码中也很有可能包含0x0D)绝对不含0x0D。况且,对任何程序而言,小概率从来都不是我们容忍bug的理由。所以此问题有待重视。 另外,从这个角度出发,以回车键(0X0D+0X0A)来判断串口是否接受结束也有其局限性,因为我们的数据中绝不能含有0X0D 0X0A这样的序列(尽管概率只有1/(256*256),但是严重性仍不容忽视),否则,又得遭殃了。
    只要问题发现了,解决的办法总归是有的,只是提醒大家,在做实际项目时,数据传输协议的字头字尾的定义一定要用心。
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
15条回答
aleda303
2019-10-16 01:56
一般 0X0D 0X0A  都是用来 作 ASCII 帧 截止符号的, 而在ASCII字符串中 一般不会出现 无显示的标识符 [范围0x20~0x7f] 所以用 <0x20 或>=0x80 的字符作为传送控制字符。

对于 任意字符发送[串口助手的十六进制发送方式], 肯定不能用 0xd ,0x0a 作为停止符号
会有更合理的规定, 比如连续的0xff, 0xfe  或以空闲时间长度作为停止标志【比如Modbus】

一周热门 更多>