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

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

[复制链接]
STMCU小助手 发布时间:2022-3-18 22:04
  在单片机开发中,串口是最常用的和外界交换数据的渠道,要使用串口,那必不可少的就是通信协议,通信协议就是单片机和外界通信的语言,要想正常和其他设备正常交流,首先语言必须相通。% z5 D4 o3 _8 k$ x! O* h
& n; ^( V  }  ~
  在实际开发过程中由于各种原因,导致很多时候单片机和外界其他设备协议不兼容,在使用的时候就比较麻烦。比如单片机要和两个设备通信,但是这两个设备的通信协议的不一样,在使用时单片机就必须使用两个串口分别和两个设备通信。如果这两个设备同时使用时还不感觉到资源浪费,如果每次只接一个设备,那么另一个串口也不能作为其他功能使用,还得留着备用。这样的话单片机的资源就被白白浪费掉了。于是想着能不能在一个串口上支持两个协议,让单片机自动去识别接收到的数据使用的是哪个协议。% e  D9 M. R  e3 _; n  ?4 J4 I6 m

2 ?* O; N; G$ x! p# X" r  一般使用通信协议接收数据时,都需要通过判断数据头和数据尾来确定什么时候开始接收数据,什么时候停止接收数据。但是如果要兼容多个协议的话,就不能使用这个方式去接收数据。必须先将一组数据接收完毕,然后根据数据的特点去分析数据使用的是哪个协议。- o( }& V& Z, i& Y. s/ }/ g- d

& ~3 b; k2 G0 z* \% Y) `: _  首先要实现的就是如何判断一组数据是否接收完毕。
5 P5 T1 i2 y3 g# e
9 P1 j5 n$ A) Z  实现的大概思路就是,单片机使用中断去接收数据,同时记录接收到的数据长度,在主函数中循环的去读取串口接收到的字符长度,如果超过一定时间之后,串口中接收到的数据长度没有发生变化,就说明一组数据接收完毕了。
) F9 Z& O5 m' ]2 r. f! }: [6 G6 f/ n3 _- K+ V3 k5 I& Y* I# e
  比如在主函数中读取到了当前串口数据长度不为0,说明此时串口正在接收数据,此时记录下当前串口数据长度,延时一段时间再去读取一次串口接收数据的长度,如果此时数据长度和上一次数据长度一样,说明串口接收数据结束了,就可以去处理接收到的数据了,如果此时数据长度和上一次的数据长度不一样,说明串口正在接收数据,接收数据还未结束,不能去处理数据。
8 T% y. l9 D2 Y; |* P0 Z8 E. v. m1 n, z7 k$ n- p6 w" @- Y9 {
  下面就通过一个工程案例来演示一个串口兼容两种协议的使用方法。) r9 j+ e0 m+ p) |, }
, P$ ~* R1 z. l, d/ u) I5 a6 n
  设备默认使用的是自定义协议,协议格式如下:. N: Q* ?* o& Y" @. E
[ 头1 ] (0xA5) [ 头2 ] (0x5A) [ 地址 ] [ 命令 ] [ 数据高位 ] [ 数据低位 ] [ 尾1 ] (0x55) [尾2] (0xAA)
+ q0 z: k4 `+ Y. O# m9 u! @8 o( v% m7 n/ O* V. x% O0 R
  后来设备需要和市场上其他的工业设备对接,而大多数工业设备使用的都是Modbus协议,如果直接将设备协议修改为Modbus协议的话,那么好多旧设备和新设备就会不兼容,为了兼容旧设备同时又要对接其他工业设备,那么设备要在自定义协议的基础上兼容Modbus协议。Modbus协议格式如下:9 G& Z' x2 G9 |: w7 V0 p

+ y3 T3 p7 `  {* e1 x; t[ 地址 ] [ 功能码 ] [ 起始地址高 ] [ 起始地址低] [ 总寄存器数高 ] [ 总寄存器数低 ] [ CRC低 ] [ CRC高 ]
/ y# u6 s1 ?+ D% L: N' ^6 n. h4 w0 a" ~) q! S2 B) k' u. B8 X
  下面分析这两种协议的特点。5 h+ _9 ^7 o1 z/ E; p

. W& b+ a  I0 p3 u& ~# U  首先看自定义协议,自定义协议的数据长度是固定的,同时数据的开始和结尾都是由两个字节标识。那么识别自定义协议就和容易了,直接判断数据头和数据为就行了。当串口接收一组数据结束后,判断接收到的数据 是不是以 0xA5和0X5A 开头,同时以 0x55 和0xAA结尾。如果是那么就使用自定义协议去解析数据。0 W8 l7 o; ?4 k8 w- i

: O; U6 E, B/ b2 H  接下来分析Modbus协议,由于设备都是单独使用的,不需要级联,所以设备的地址是固定的0x01,同时由于设备的功能比较简单,所以在使用Modubus协议的时候,只用到了读保持寄存器(0x03)和写保持寄存器(0x06)这两个功能,所以Modbus的数据开头只有两种情况 0x01 0x03 和 0x01 0x06,如果数据开头是这两种情况,那么就使用Modbus协议去解析数据。
  f' l# s6 b, b, I7 D2 p/ \4 X3 y' N1 ~9 E
  通过对协议的分析,思路已经很清晰了,接下来使用代码来实现。$ f  J. ~7 u. X/ w, X

4 ^- H+ L. V: \
  1. struct uart_info4 B8 r, H9 U( I+ x2 u
  2. {  l; t7 k  o; u1 d! S3 X2 ?+ p  K3 Y
  3.     u8 cnt;
    2 j) ?- n  d9 z# F  O! e
  4.     u8 rec_buf[10];7 F* p. O3 x' N. {7 N2 }
  5. };. l4 [  _. F5 r* H& |5 ^9 Y
  6. 4 Y* Z" t" A. ]' i1 G2 ?
  7. struct uart_info uart1;0 z8 T8 W4 m# X  n( T# e

  8. : b( b4 N: Z. `  ^" K
  9. //在Library Options中将Printf formatter改成Large8 @6 H! O# w3 X4 g4 H
  10. //重新定向putchar函数,使支持printf函数
    , _- B8 q' O( Q1 n) E
  11. int putchar( int ch )1 j, J. d8 ]6 L+ b3 x) m/ U0 C
  12. {
    - v  Y$ ^( G  k( s" J+ o  W; d
  13.     while( !( UART1_SR & 0X80 ) ); //循环发送,直到发送完毕3 K$ {/ x/ y% U5 s( m
  14.     UART1_DR = ( u8 ) ch;
    ( U9 r" T' l, w
  15.     return ch;3 c; f* i8 O0 F& M+ Q" w7 G% M) N
  16. }2 P# e  c. u6 B( J
  17. static void uart_io_init( void )
    . [% E1 D- c' O& b  ?, Z
  18. {
    , c% Q+ L3 Q( d7 d9 @/ c7 Q
  19.     PD_DDR |= ( 1 << 5 ); //输出模式 TXD
    8 E. \- {7 ~! \
  20.     PD_CR1 |= ( 1 << 5 ); //推挽输出5 u$ g9 P% |% x& O
  21.     PD_DDR &= ~( 1 << 6 ); //输入模式 RXD1 f. O9 e; |! m' _4 G
  22.     PD_CR1 &= ~( 1 << 6 ); //浮空输入
    ; e( c, @# T7 e- R- x
  23. }- N5 o; R/ k3 r
  24. //波特率最大可以设置为38400# H+ ]2 u. M3 f2 ?0 Z, }! f7 [
  25. void uart_init( unsigned int baudrate )4 B& U0 Y9 r: t! R2 x0 H
  26. {: q% I$ p! ^9 y# U
  27.     unsigned int baud;9 n6 k; E8 q4 `: |

  28. 6 |$ `5 c* Q' V& Z
  29.     uart_io_init();% _3 k3 K8 T3 p( K. `2 W  l7 F' h: J

  30. % A8 d9 i8 |/ _1 q0 e/ F0 o/ S
  31.     baud = 16000000 / baudrate;6 E# }/ \, I& J: j, I5 u7 U
  32.     UART1_CR1 = 0;5 s- [9 K% }3 Y
  33.     UART1_CR2 = 0;
    % B' B: ?1 P% a3 h
  34.     UART1_CR3 = 0;
    2 c  [* {3 E6 I
  35.     UART1_BRR2 = ( unsigned char )( ( baud & 0xf000 ) >> 8 ) | ( ( unsigned char )( baud & 0x000f ) );
    3 B1 Y" y7 ~8 U
  36.     UART1_BRR1 = ( ( unsigned char )( ( baud & 0x0ff0 ) >> 4 ) );
      T1 y6 y* W8 o- q3 i5 Q
  37.     UART1_CR2_bit.REN = 1;        //接收使能
    : J: W- K3 m4 I1 n7 p3 P! a
  38.     UART1_CR2_bit.TEN = 1;        //发送使能# ~  i8 `0 H8 c2 V9 |. u% a8 i# b
  39.     UART1_CR2_bit.RIEN = 1;       //接收中断使能" i, R% i: q0 L: x/ z, k
  40. }9 V- {6 |; k! B- W( u

  41. - \4 A+ J  H+ R/ A2 N1 {3 O! h

  42. 9 `; Q, @$ G* x% S9 S
  43. //接收中断函数 中断号18& R* x2 M( i  x, Q" E* o0 p
  44. #pragma vector = 20                             // IAR中的中断号,要在STVD中的中断号上加2; _& _1 U0 q5 j5 W% t$ X0 _
  45. __interrupt void UART1_Handle( void )/ V% i& p2 x- F; m
  46. {
    2 Z! I: }0 o$ m7 ]  e
  47.     unsigned char res = 0;
    , z- [) P: T6 `
  48.     UART1_SR &= ~( 1 << 5 );                    //RXNE 清零1 D6 o' G0 b8 d+ ?7 @: y- L/ r
  49. 4 |% ?( [  n% t( `
  50.     res = UART1_DR;
    ! e( F' n. A, O! e7 r# }5 S3 L7 M) z( B
  51.     if( uart1.cnt < 9 )  j: ?0 |0 E/ t9 S' U
  52.         uart1.rec_buf[uart1.cnt++] = res;
    1 E4 ?) w, Q7 M( ^: b- }
  53. }
复制代码
! v- T4 u% c& |) t/ u
  定义一个结构体来存储接收到的数据长度和数据,由于自定义协议和Modbus协议最长的数据只有8个字节,所以这里的数组长度设置10就可以了。下面初始化串口使用的IO口,设置数据位和波特率。最后在中断中接收数据并存储到数组中。
$ J" Y6 R& ?) }4 p0 U* q8 b$ B0 c3 {: ^3 b* Z
  接下来在编写一个函数用来分析接收到的数据。% U! c: _$ M/ o2 X% j

: h) B3 O; E. u# H! O1 U
  1. //检测串口数据
    5 B! ^+ K9 C4 g  O; V! `( R
  2. void read_uart( void )
    1 D8 Q# b. h. ?2 z  y( t& w
  3. {- s* w+ L- V7 m  w4 n  p0 O
  4.     static u8 recevie_buf[10];2 ^# o: F  x& f! q% r: g/ w8 z2 j
  5.     static u8 recevie_cnt = 0;
    / Y$ c4 B8 X  _" ^) F  I) p# a
  6.     u8 i = 0;5 Y; I7 w* Y) h' G, l* r" {* t
  7. 7 I. j# q- o' ~# l
  8.     delay_ms( 10 ); //隔一段时间检测一次串口数据长度,如果数据长度没有发生变化说明串口接收数据完成1 R8 {0 U3 f2 [/ f! C3 m2 d
  9. - {) o1 T, j: i; x0 {# c1 Y/ q3 E* W
  10.     if( ( uart1.cnt != recevie_cnt ) && ( uart1.cnt > 6 ) )
    6 D5 T' E9 P5 }, A( y
  11.     {
    9 i+ O' J, l  j8 ]6 M
  12.         recevie_cnt = uart1.cnt;
    % `9 ^( ?- Y( B6 @! q# z
  13.         for( i = 0; i < recevie_cnt; i++ )            //拷贝数据- g# y: T# O3 h( J3 S6 }2 e
  14.         {3 O! A1 ^$ m+ i5 L4 G0 K
  15.             recevie_buf<i> = uart1.rec_buf<i>;7 s: Q4 A4 g: i( z; S
  16.         }
    4 J" c5 m) _" Z- |
  17.         //根据接收到的数据区分协议% _8 b: c" ^% o4 J$ Q" N5 |
  18.         //自定义协议
    & m$ N! A& o/ W- ]3 m
  19.         if( ( recevie_buf[0] == 0xA5 ) && ( recevie_buf[1] == 0x5A ) )7 W3 P6 X; A7 ]3 F* ~
  20.         {+ s; x: }' L6 [
  21.             self_define_protocol( recevie_buf, recevie_cnt );% S0 ~. s( ^2 X9 T
  22.         }( L9 C4 i3 T4 ~
  23.         //modbus协议: o9 u* O( ?6 U
  24.         if( ( recevie_buf[0] == 0x01 ) && ( ( recevie_buf[1] == 0x03 ) || ( recevie_buf[1] == 0x06 ) ) )/ n7 ^- x$ y0 y8 c6 Q
  25.         {
    , {& U: b4 _& m1 ?
  26.             modbus_protocol( recevie_buf, recevie_cnt );
    - Z2 @& G$ j0 Z
  27.         }
    1 k& h  W8 v2 Z) T! \5 C
  28. + [* }  S1 o6 `) |* l& Y
  29.         //清空数组
    ' O* {2 p* V2 y" S& l4 r1 u, h
  30.         for( i = 0; i < 10; i++ )) c6 [/ t. L( E# X  L+ E
  31.         {) F6 S! }; ]! X/ L, K8 i
  32.             uart1.rec_buf<i> = 0;( b, Q' |( }# H9 A7 z, B
  33.             recevie_buf<i> = 0;
    5 h  {4 k8 m- f( \
  34.         }
    * w# M( w- @, [0 I/ s, z
  35.         uart1.cnt = recevie_cnt  = 0;# V: k4 O* Q" \' C
  36.     }
    ! ^/ B; }- l- [* \6 _
  37. }</i></i></i></i>
复制代码

/ P# i- ~- h$ t" V: H: _$ N
8 |: b1 J$ Q1 ]' R$ r" W: L  ]: \, x' u  由于此设备中两种协议的数据长度都比较小,所以这里并没有分两次去判断数据长度,然后根据数据长度来判断数据接收是否完毕。只是延时10ms之后去判断数据长度,当接收的数据长度大于6时,说明数据基本已经接收完成了。由于数据的最大长度是8,所以接收的数据长度大于6时,基本数据已经接收完了,在中断中接收8个数据速度还是非常快的。如果数据量比较大,数据比较长时,最好还是通过数据长度去判断比较可靠。这里为了编写方便,就简单的时候延时去判断了。
/ o$ h5 |, M1 c( L9 q" b
7 d9 M' R2 h" G1 S# f2 k7 h/ t  当串口接收的数据长度大于6时,说明一组数据已经接收完毕了,此时需要将串口接收的数据拷贝一份出来在使用。那为什么要将数据拷贝出来,而不是直接使用串口接收缓存区的数据呢?这是为了防止在处理数据的过程中国,串口又接收到了新的数据,这样新数据就会将旧的数据覆盖掉,有可能导致数据异常。为了数据的安全性将数据拷贝一份使用,即使在处理数据过程中串口缓存区的数据发生了变化,也不会破坏掉上一次接收的数据。. z4 [, A! M$ v3 l

# r. W4 F  p7 t* I  i  数据拷贝结束后,就根据数据的特点来判断当前接收到的数据使用的是哪种协议。如果接收到的数据前两个是 0xA5和0x5A那么就直接调用自定义协议处理函数,如果前面的数据是0x01和0x03或者是0x01和0x06那么就直接调用Modbus协议去处理函数。- @, u9 w5 o) \3 Z) f

, h$ w8 l* Z3 t' D7 ~" M; [6 z  接下来就可以单独编写两个函数分别处理这这种协议。
! |' Z  B& M8 q3 V, I! d6 N" g
4 J2 w- o& s) y8 _5 r- R
  1. //处理自定义协议
    9 ~2 U' ]! [3 }" |7 ?' F
  2. // [头1](0xA5) [头2](0x5A) [地址]  [命令]  [数据高位] [数据低位]  [尾1](0x55) [尾2](0xAA)4 l! `  ?) |( n. G
  3. //A5 5A 00 01 01 90 55 AA# w2 t6 X) N6 O& j% P8 r
  4. void self_define_protocol( u8 arr[], u8 size )% O3 J, d. i4 j! W- l0 x
  5. {
    8 q- F/ [/ b  G* n8 L/ b

  6. $ A* y7 ?, H% h' R( D5 T
  7.     if( ( arr[0] == 0xA5 ) && ( arr[1] == 0x5A ) && ( arr[6] == 0x55 ) && ( arr[7] == 0xAA ) )  U/ W5 g- ]3 `9 L7 i3 ~; A
  8.     {4 M4 \( I& L6 ]& a: _5 `4 [+ A8 t
  9.                 //根据命令值执行不同的动作( f% h' V/ x" V7 |: Z6 _- g
  10.     }
    $ M1 E2 S7 [6 Y4 Y
  11. }/ r7 S* n, `/ b+ Q# ]
  12. . P9 E/ h% J" |5 f- j
  13. //处理modbus协议$ o- M7 [1 k/ G4 p& [
  14. //[地址][功能码][起始地址高][起始地址低][总寄存器数高][总寄存器数低][CRC低][CRC高]2 t. ~9 ~, ~2 }7 H3 O* r& X4 e, N
  15. //01 03 00 00 00 01 84 0A. J4 s) {! j3 g8 r% a9 z7 a$ ~, c
  16. //01 06 00 00 03 E8 89 74* `! T. V. Y# K* a' _9 G% _9 J. R
  17. void modbus_protocol( u8 arr[], u8 size )
    ) H3 p; R0 B  b- @  W" J
  18. {
    ) T) q/ @- p% Z  z
  19.     //调用 modbus相关处理代码
    4 y4 R% D1 T0 H9 Q& g5 y; n
  20. }
复制代码
: C/ z/ t! \; A7 Y# b. R' J% E6 G
  将数据协议识别出来之后,就可以按照通常处理协议的方式去处理数据了。最后在主函数中循环的调用数据查询函数,检查串口是否有接收到数据。
/ k. g  x3 c8 a5 _2 e" I, W3 R9 B  X" e: j% O
  1. void main( void )
    2 X- |' ?) b& I9 R( D
  2. {
    5 q! Q: ?4 D: d

  3. , \( V! f5 \3 C$ H
  4.     __asm( "sim" );                             //禁止中断1 z- d" d' M/ ?  q7 S
  5.     SysClkInit();2 p; D# ^2 D3 |7 `
  6.     delay_init( 16 );5 y+ C2 [" f7 I1 G* M0 `8 M
  7.     LED_GPIO_Init();
    / A5 A+ s* v$ s, A/ s# H
  8.     uart_init( 9600 );
    ; ]0 w0 l4 }) ?
  9.     __asm( "rim" );                             //开启中断& k+ i# p* b, l# A9 X& d
  10.     while( 1 )
    ( e- `/ j. l" G' D
  11.     {# D( A+ Z1 }3 i$ o7 T& y
  12.         read_uart();
    6 q4 i$ ?3 C; ?1 |
  13.     }: r, N: v' D( w8 _& z
  14. }
复制代码

; c( e2 I6 k- J9 D' S  接下来分别用这两种协议测试代码。8 Y6 l: u" a% W7 s9 n
%2`%K($}]H$PC4%(DS@DLG0.png 1 \/ L+ G2 B& t
# ^4 I+ G) R* g' v
  使用串口助手发送自定义协议时,代码就会调用自定义协议处理函数。3 f) R  r* W- w/ M: }8 |9 W! |% ~7 }

8 I8 i* j+ \( r- n# `8 q. `
5 w9 q1 p1 z0 r- {$ x. M8 {9 Y) |5 N3 i
  发送Modbus协议时,代码就会调用Modbus协议处理函数。6 u1 m6 F& ]# V9 p0 u. B

. V% ~. B- P- V( q8 B  这样根据协议的特点可以通过代码自动去识别协议的类型,用一个串口就可以实现不同协议的解析。按照同样的方法还可以解析更多的协议。当前在不同的协议使用的波特率要相同,否则波特率不同解析出来的数据就会出现错误,导致协议解析失败。' p$ }: ~4 b: W+ D' M
' \# o5 B5 s$ |, }) ]: k6 ^7 F
. S( u1 o! m+ O7 ?4 c

$ S8 P/ p! b+ L! f/ t- W
CWIQ[I{_N_O[@_TGBBAB.png
赞 收藏 评论0 发布时间:2022-3-18 22:04

举报

0个回答

所属标签

相似技术帖

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版