你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

【经验分享】STM8单片机串口同时识别自定义协议和Modbus协议

[复制链接]
STMCU小助手 发布时间:2022-3-18 22:04
  在单片机开发中,串口是最常用的和外界交换数据的渠道,要使用串口,那必不可少的就是通信协议,通信协议就是单片机和外界通信的语言,要想正常和其他设备正常交流,首先语言必须相通。

  在实际开发过程中由于各种原因,导致很多时候单片机和外界其他设备协议不兼容,在使用的时候就比较麻烦。比如单片机要和两个设备通信,但是这两个设备的通信协议的不一样,在使用时单片机就必须使用两个串口分别和两个设备通信。如果这两个设备同时使用时还不感觉到资源浪费,如果每次只接一个设备,那么另一个串口也不能作为其他功能使用,还得留着备用。这样的话单片机的资源就被白白浪费掉了。于是想着能不能在一个串口上支持两个协议,让单片机自动去识别接收到的数据使用的是哪个协议。

  一般使用通信协议接收数据时,都需要通过判断数据头和数据尾来确定什么时候开始接收数据,什么时候停止接收数据。但是如果要兼容多个协议的话,就不能使用这个方式去接收数据。必须先将一组数据接收完毕,然后根据数据的特点去分析数据使用的是哪个协议。

  首先要实现的就是如何判断一组数据是否接收完毕。

  实现的大概思路就是,单片机使用中断去接收数据,同时记录接收到的数据长度,在主函数中循环的去读取串口接收到的字符长度,如果超过一定时间之后,串口中接收到的数据长度没有发生变化,就说明一组数据接收完毕了。

  比如在主函数中读取到了当前串口数据长度不为0,说明此时串口正在接收数据,此时记录下当前串口数据长度,延时一段时间再去读取一次串口接收数据的长度,如果此时数据长度和上一次数据长度一样,说明串口接收数据结束了,就可以去处理接收到的数据了,如果此时数据长度和上一次的数据长度不一样,说明串口正在接收数据,接收数据还未结束,不能去处理数据。

  下面就通过一个工程案例来演示一个串口兼容两种协议的使用方法。

  设备默认使用的是自定义协议,协议格式如下:
[ 头1 ] (0xA5) [ 头2 ] (0x5A) [ 地址 ] [ 命令 ] [ 数据高位 ] [ 数据低位 ] [ 尾1 ] (0x55) [尾2] (0xAA)

  后来设备需要和市场上其他的工业设备对接,而大多数工业设备使用的都是Modbus协议,如果直接将设备协议修改为Modbus协议的话,那么好多旧设备和新设备就会不兼容,为了兼容旧设备同时又要对接其他工业设备,那么设备要在自定义协议的基础上兼容Modbus协议。Modbus协议格式如下:

[ 地址 ] [ 功能码 ] [ 起始地址高 ] [ 起始地址低] [ 总寄存器数高 ] [ 总寄存器数低 ] [ CRC低 ] [ CRC高 ]

  下面分析这两种协议的特点。

  首先看自定义协议,自定义协议的数据长度是固定的,同时数据的开始和结尾都是由两个字节标识。那么识别自定义协议就和容易了,直接判断数据头和数据为就行了。当串口接收一组数据结束后,判断接收到的数据 是不是以 0xA5和0X5A 开头,同时以 0x55 和0xAA结尾。如果是那么就使用自定义协议去解析数据。

  接下来分析Modbus协议,由于设备都是单独使用的,不需要级联,所以设备的地址是固定的0x01,同时由于设备的功能比较简单,所以在使用Modubus协议的时候,只用到了读保持寄存器(0x03)和写保持寄存器(0x06)这两个功能,所以Modbus的数据开头只有两种情况 0x01 0x03 和 0x01 0x06,如果数据开头是这两种情况,那么就使用Modbus协议去解析数据。

  通过对协议的分析,思路已经很清晰了,接下来使用代码来实现。

  1. struct uart_info
  2. {
  3.     u8 cnt;
  4.     u8 rec_buf[10];
  5. };

  6. struct uart_info uart1;

  7. //在Library Options中将Printf formatter改成Large
  8. //重新定向putchar函数,使支持printf函数
  9. int putchar( int ch )
  10. {
  11.     while( !( UART1_SR & 0X80 ) ); //循环发送,直到发送完毕
  12.     UART1_DR = ( u8 ) ch;
  13.     return ch;
  14. }
  15. static void uart_io_init( void )
  16. {
  17.     PD_DDR |= ( 1 << 5 ); //输出模式 TXD
  18.     PD_CR1 |= ( 1 << 5 ); //推挽输出
  19.     PD_DDR &= ~( 1 << 6 ); //输入模式 RXD
  20.     PD_CR1 &= ~( 1 << 6 ); //浮空输入
  21. }
  22. //波特率最大可以设置为38400
  23. void uart_init( unsigned int baudrate )
  24. {
  25.     unsigned int baud;

  26.     uart_io_init();

  27.     baud = 16000000 / baudrate;
  28.     UART1_CR1 = 0;
  29.     UART1_CR2 = 0;
  30.     UART1_CR3 = 0;
  31.     UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) );
  32.     UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) );
  33.     UART1_CR2_bit.REN = 1;        //接收使能
  34.     UART1_CR2_bit.TEN = 1;        //发送使能
  35.     UART1_CR2_bit.RIEN = 1;       //接收中断使能
  36. }


  37. //接收中断函数 中断号18
  38. #pragma vector = 20                             // IAR中的中断号,要在STVD中的中断号上加2
  39. __interrupt void UART1_Handle( void )
  40. {
  41.     unsigned char res = 0;
  42.     UART1_SR &= ~( 1 << 5 );                    //RXNE 清零

  43.     res = UART1_DR;
  44.     if( uart1.cnt < 9 )
  45.         uart1.rec_buf[uart1.cnt++] = res;
  46. }
复制代码

  定义一个结构体来存储接收到的数据长度和数据,由于自定义协议和Modbus协议最长的数据只有8个字节,所以这里的数组长度设置10就可以了。下面初始化串口使用的IO口,设置数据位和波特率。最后在中断中接收数据并存储到数组中。

  接下来在编写一个函数用来分析接收到的数据。

  1. //检测串口数据
  2. void read_uart( void )
  3. {
  4.     static u8 recevie_buf[10];
  5.     static u8 recevie_cnt = 0;
  6.     u8 i = 0;

  7.     delay_ms( 10 ); //隔一段时间检测一次串口数据长度,如果数据长度没有发生变化说明串口接收数据完成

  8.     if( ( uart1.cnt != recevie_cnt ) && ( uart1.cnt > 6 ) )
  9.     {
  10.         recevie_cnt = uart1.cnt;
  11.         for( i = 0; i < recevie_cnt; i++ )            //拷贝数据
  12.         {
  13.             recevie_buf<i> = uart1.rec_buf<i>;
  14.         }
  15.         //根据接收到的数据区分协议
  16.         //自定义协议
  17.         if( ( recevie_buf[0] == 0xA5 ) && ( recevie_buf[1] == 0x5A ) )
  18.         {
  19.             self_define_protocol( recevie_buf, recevie_cnt );
  20.         }
  21.         //modbus协议
  22.         if( ( recevie_buf[0] == 0x01 ) && ( ( recevie_buf[1] == 0x03 ) || ( recevie_buf[1] == 0x06 ) ) )
  23.         {
  24.             modbus_protocol( recevie_buf, recevie_cnt );
  25.         }

  26.         //清空数组
  27.         for( i = 0; i < 10; i++ )
  28.         {
  29.             uart1.rec_buf<i> = 0;
  30.             recevie_buf<i> = 0;
  31.         }
  32.         uart1.cnt = recevie_cnt  = 0;
  33.     }
  34. }</i></i></i></i>
复制代码


  由于此设备中两种协议的数据长度都比较小,所以这里并没有分两次去判断数据长度,然后根据数据长度来判断数据接收是否完毕。只是延时10ms之后去判断数据长度,当接收的数据长度大于6时,说明数据基本已经接收完成了。由于数据的最大长度是8,所以接收的数据长度大于6时,基本数据已经接收完了,在中断中接收8个数据速度还是非常快的。如果数据量比较大,数据比较长时,最好还是通过数据长度去判断比较可靠。这里为了编写方便,就简单的时候延时去判断了。

  当串口接收的数据长度大于6时,说明一组数据已经接收完毕了,此时需要将串口接收的数据拷贝一份出来在使用。那为什么要将数据拷贝出来,而不是直接使用串口接收缓存区的数据呢?这是为了防止在处理数据的过程中国,串口又接收到了新的数据,这样新数据就会将旧的数据覆盖掉,有可能导致数据异常。为了数据的安全性将数据拷贝一份使用,即使在处理数据过程中串口缓存区的数据发生了变化,也不会破坏掉上一次接收的数据。

  数据拷贝结束后,就根据数据的特点来判断当前接收到的数据使用的是哪种协议。如果接收到的数据前两个是 0xA5和0x5A那么就直接调用自定义协议处理函数,如果前面的数据是0x01和0x03或者是0x01和0x06那么就直接调用Modbus协议去处理函数。

  接下来就可以单独编写两个函数分别处理这这种协议。

  1. //处理自定义协议
  2. // [头1](0xA5) [头2](0x5A) [地址]  [命令]  [数据高位] [数据低位]  [尾1](0x55) [尾2](0xAA)
  3. //A5 5A 00 01 01 90 55 AA
  4. void self_define_protocol( u8 arr[], u8 size )
  5. {

  6.     if( ( arr[0] == 0xA5 ) && ( arr[1] == 0x5A ) && ( arr[6] == 0x55 ) && ( arr[7] == 0xAA ) )
  7.     {
  8.                 //根据命令值执行不同的动作
  9.     }
  10. }

  11. //处理modbus协议
  12. //[地址][功能码][起始地址高][起始地址低][总寄存器数高][总寄存器数低][CRC低][CRC高]
  13. //01 03 00 00 00 01 84 0A
  14. //01 06 00 00 03 E8 89 74
  15. void modbus_protocol( u8 arr[], u8 size )
  16. {
  17.     //调用 modbus相关处理代码
  18. }
复制代码

  将数据协议识别出来之后,就可以按照通常处理协议的方式去处理数据了。最后在主函数中循环的调用数据查询函数,检查串口是否有接收到数据。

  1. void main( void )
  2. {

  3.     __asm( "sim" );                             //禁止中断
  4.     SysClkInit();
  5.     delay_init( 16 );
  6.     LED_GPIO_Init();
  7.     uart_init( 9600 );
  8.     __asm( "rim" );                             //开启中断
  9.     while( 1 )
  10.     {
  11.         read_uart();
  12.     }
  13. }
复制代码

  接下来分别用这两种协议测试代码。
%2`%K($}]H$PC4%(DS@DLG0.png

  使用串口助手发送自定义协议时,代码就会调用自定义协议处理函数。



  发送Modbus协议时,代码就会调用Modbus协议处理函数。

  这样根据协议的特点可以通过代码自动去识别协议的类型,用一个串口就可以实现不同协议的解析。按照同样的方法还可以解析更多的协议。当前在不同的协议使用的波特率要相同,否则波特率不同解析出来的数据就会出现错误,导致协议解析失败。



CWIQ[I{_N_O[@_TGBBAB.png
收藏 评论0 发布时间:2022-3-18 22:04

举报

0个回答
关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
st-img 微信公众号
st-img 手机版