1、前言
5 A( L- E- U7 ^ D0 j7 S1 t modbus rtu在嵌入式方面非常的常见和使用,嵌入式linux中可以使用libmodbus这个库,但是对于嵌入式单片机,开源的有FreeModbus这个库,但是只是从机,对于modbus rtu主机的实现,网上却找不到开源的库,或者找到了但是不方便移植,使用者想要去使用还要去搞明白是怎么实现的,本博客基于以上原因,实现了一套modbus rtu主机协议栈。
; X; A* e G; l( x5 m) _3 B6 D+ \+ x$ L. N8 Q6 [1 F% l
本主机协议栈优点如下:& |% ]- J3 x# X) H) d D
接口明确清晰,使用者无需关心协议栈内部实现) ~# w+ i, c2 W6 T" h
面向对象编程思想,使用C语言的struct作为一个modbus rtu主机的控制接口,此方法的好处是可以灵活的实现多个主机,例如:实现一个多主机的modbus pdu。
7 Q5 @) W& i U% ~/ O# G支持RTOS
; X) @6 I+ X3 b5 X. p. P: M可搭配FreeModbus协议栈无缝使用! k5 ?) P2 }$ |9 w
移植简单、可很方便的移植到其他单片机如GD32、MM32等3 x) q# J) L e6 H4 N; O- w/ i
源码简单、只有一个头文件、一个源文件、一个移植接口示例文件
4 P7 C5 j& B0 T2、协议栈API介绍8 F( P" V$ h$ H6 B( X8 w
2.1 控制结构9 n) e8 ^2 U1 O3 z
- typedef struct
+ ~: R# G9 U& ?4 ^ - {
' l8 y* a7 R7 Z5 H( e9 ] - /// M6 w4 d; J3 S, {! T t1 |# C" N& _
- // 收发数据缓存; [, o) p2 j* ?0 l
- //
3 ~6 H+ s/ j% C0 Q7 ?2 s1 D, N7 X& q) } - uint8_t ucBuf[128];
5 p' u/ Z2 r$ ~) p% p/ ] - 6 h( { d* l* G$ p1 L
- //
1 A( u* u9 u0 M. H1 f% ~ - // 收发数据状态6 G/ ]# _0 `0 O- [
- //5 n7 d, T3 O, F% F2 f$ M
- uint16_t usStatus;/ ?/ N* V" ]' R9 I+ p7 f: k
- + u. _: e3 _+ X! s5 U9 E
- //
# l3 W6 P7 X8 A/ { M1 e - // 如果使用了RTOS需要进行互斥,那么需要实现以下两个函数的绑定* K1 q9 j: d/ _: ~' |
- //
2 o* {; P6 g7 N; w2 Q& I* O& u/ A - void (*lock)(void);- P9 K- X2 j8 _ y) o2 E4 G. Y
- void (*unlock)(void);
$ Q" C: w* t4 y7 U - . g, J2 e, z! L+ ^$ `
- //
. ` T k0 w8 L2 W& b/ ^4 l - // 微秒延时函数,用于等待超时
! \7 V5 D6 ~4 W9 A8 {* Y, J, Y - //8 g% J7 W. \1 ~$ J) y
- void (*delayms)(uint32_t nms);
+ l4 w5 e! J! U r7 w, H- x' q - / z9 Q6 T7 e m r5 {. n
- //
; U* s0 k* ?- ?( R - // 定时器启动和停止函数
% L* ]- X( x! X6 D4 w- O) R - //5 ]3 K1 ]' a" q4 E/ o
- void (*timerStop)(void);- t0 j# H1 s0 l* Z* |0 D' D6 H
- void (*timerStart)(void);
/ F! E- j6 }5 g# T5 ~, v -
. a0 N; M* F% t( b+ y. d- K - //9 Z" |" d3 Y2 h2 K1 ^/ T
- // 发送数据函数,可以是串口、TCP等/ E% q4 H4 A9 l( y0 K0 R4 K
- //7 ?2 ?/ n3 ?- |4 @* u
- uint32_t (*sendData)(const void* buf, uint32_t len);/ F6 e. i5 z+ B6 ~( A
- 3 Z4 }* k9 \6 Z& ]: i$ r4 q/ Z
- }MBRTUMaterTypeDef;
复制代码
* u+ R- D5 d- F! q* q2.2 主机读线圈状态(CMD1)
I/ U0 ~" z: k( U- /**4 e6 B: e4 q7 D8 H
- * 主机读取线圈状态" s% n; h; c8 A7 F1 j; m
- * @param ucSlaveAddress 从机地址
P2 z* ~( n5 u; V - * @param usAddress 要读取的线圈起始地址
# R+ \% b5 R2 q# n- @7 k( U - * @param usNum 要读取的线圈数量
% v1 a8 C; n' {* x& U# Z - * @param usTimeout 超时时间,单位毫秒" s9 ]1 X2 n7 e( n- g; I) y
- * @param pucCoilsBuffer 存储读取到的线圈状态,一个字节代表一个线圈状态,值范围:0/11 c z3 K% ]' ]! W
- * @return 0:成功 <0:执行失败& }( A* u" I/ Z' @# s- Z$ z9 W
- */
. w3 @) M7 R$ Z* n7 w8 R0 Y - int MBRTUMasterReadCoils(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint8_t* pucCoilsBuffer)
复制代码
6 k) n7 z) D: Q2.2 主机读离散量输入(CMD2)7 g# E. }) b1 W9 O* r
- /**4 A. d& |1 r3 t* H) e1 L
- * 主机读取离散量输入2 X3 ^& K8 Q. W' O
- * @param ucSlaveAddress 从机地址
6 f: a6 D, a7 p8 L9 ^6 i - * @param usAddress 要读取的离散量起始地址
" s3 j( m. _ V. `! w - * @param usNum 要读取的离散量数量
) j& X3 h+ h5 i4 v9 X - * @param usTimeout 超时时间,单位毫秒
# l5 ]% a/ @! x6 \1 T' h - * @param pucDiscBuffer 存储读取到的离散量输入状态,一个字节代表一个离散量的状态,值范围:0/1
, e; `# t% h+ z, w$ {4 N - * @return 0:成功 <0:执行失败
5 Y$ h0 U" Y0 @0 P3 V - *// d/ ]. i$ ]! L) s8 F. f( B% c: `5 v' v
- int MBRTUMasterReadDiscreteInputs(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint8_t* pucDiscBuffer)
复制代码
) [7 [+ {6 }7 N n$ J: ?' z; g1 r2.2 主机读保持寄存器(CMD3)
; |$ x* B5 i4 r! L* P- /**
4 A6 U# y3 ]$ F% g; ~8 X, B- V Q" A: c - * 主机读取保持寄存器2 N4 }1 b* R9 ]/ T
- * @param ucSlaveAddress 从机地址$ u+ ?! Q5 v. j8 j6 a: x
- * @param usAddress 要读取的保持寄存器起始地址
4 [9 X" L& Z- |; X - * @param usNum 要读取的保持寄存器数量0 c7 c; }9 v- e8 q, J
- * @param usTimeout 超时时间,单位毫秒+ l0 y6 h! q- E) ?6 e
- * @param pusRegBuffer 存储读取到的寄存器值
& l) `0 u! a9 g* [; j1 @9 s0 V - * @return 0:成功 <0:执行失败4 [9 n$ W! d' v3 o" `3 n
- */
6 X# j H3 o/ ]6 R V - int MBRTUMasterReadHoldingRegisters(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint16_t* pusRegBuffer)
复制代码 + c1 ]* B! ?) C
2.2 主机读输入寄存器(CMD4)+ B9 O% c+ L: n9 ?, U& N0 S- B; \
- /**0 ]4 ?: \6 L& F. O. S
- * 主机读取输入寄存器9 P5 f6 u4 p9 c( J/ {" t( n
- * @param ucSlaveAddress 从机地址
$ X# F, q) t; R2 S; [! A - * @param usAddress 要读取的输入寄存器起始地址
$ W. p! \9 X) m; [, I5 Y* R5 @ - * @param usNum 要读取的输入寄存器数量
% v/ D7 V" U$ N0 {5 Z8 M& _9 w4 D - * @param usTimeout 超时时间,单位毫秒
7 o2 i6 E) J f; ~* z - * @param pusRegBuffer 存储读取到的寄存器值6 C9 H& ? I S' d" q2 x0 ~
- * @return 0:成功 <0:执行失败% x+ j5 f: L: A1 _/ S/ G
- */
7 D6 S5 _1 a4 U$ l - int MBRTUMasterReadInputRegisters(MBRTUMaterTypeDef* psModbus, uint8_t ucSlaveAddress, uint16_t usAddress, uint16_t usNum, uint16_t usTimeout, uint16_t* pusRegBuffer)
复制代码 1 V5 q/ }3 Q. ? b
2.2 主机写单个线圈(CMD5)
0 k q I U2 W0 J. {; Z- D2 I6 \7 z0 o/ Q. W) Q2 v) i3 n8 Y4 F
( f/ a9 ~2 w5 P. b, \1 X
1 J `( f# f2 w" o/ U) {' W6 m2.2 主机写单个寄存器(CMD6)# [; Z7 Z* h. I! C% @0 b& X
1 o# S4 S$ ~& X5 v) G) G
( x ]8 N* j' L: O( x. b8 @: S. d1 H ~) Z
2.2 主机写多个线圈(CMD15)
$ A% O! u, U$ t8 v% m0 O; G, R. d" G
' f: r& X0 P; p/ V& V5 |0 I
) x$ g3 W2 T' O K. p
- a: E2 Y8 b! O2 c$ H1 H+ P2.2 主机写多个寄存器(CMD16)
7 p- T+ U- V, H
7 v2 j8 ?% v+ d. N: C
# U- G {; L/ a, b. u }
* h) v- P: Q- `& ]- J# ~3、移植前的基础工程生成+ C2 W6 H1 v; q/ X5 w
基础工程这里我使用STM32CubeMX生成,使用的是STM32F103C8单片机,配置步骤如下,首先将时钟配置到72M:& s4 z- D8 l( w# Q! K6 |6 R
4 P- E5 [0 [% a0 O! L
. Y7 d2 s/ V1 s" r, s- k# Z7 I3 ]- ?1 N
配置串口1用于调试打印,配置串口3用于modbus主机通信:
5 P: U/ T2 y9 d. y6 t
4 A- ?3 o% b. y
/ ~) v: t8 Y, U) A2 h! s+ `
; J- G7 ]. c7 `7 K
$ g; n5 u* p3 B& d0 E
4 `. S& ~% { Q, o/ m配置用于检测3.5个字符超时时间的定时器,我配置成了5ms超时。5 G M5 Y! h% O7 r
' K/ [4 v: ?$ E* ]. l% y* N
这里需要跟你实际使用的波特率进行超时时间的计算,以:波特率9600、8bit数据位、1bit停止位,奇校验、无流控为例,那么1s内就可以传输9600bits÷(8+1+1)=960bytes,那么3.5个字节的时间就是1000ms÷960×3.5≈3.65ms,所以,我设置5ms的超时时间是没有问题的。
0 w5 f& t# s# m3 J
. W. A: N c0 }
" R- Q) T" a* D
( L/ e% T$ J- @1 P
开启定时器和串口中断,注意:串口的中断要比定时器中断等级高:6 ], {% U- d6 D" U
@% @) T7 B2 r' W; t# N5 d
0 q7 P# B# w1 b
; E( ^" @5 `+ u# w" t
最后输出工程就可以了:5 b5 X' ~8 C: i% x" W
$ I: i4 e& _; i4 C: O1 D, g
1 ]5 W, N4 q L. n# B n4 H' L
/ Y) e. `0 t6 i0 Y# w5 v; K; i# V0 d! }
1 p0 e& y5 q* ~0 Y, J: Q
" X6 N- ?9 z# v
: z2 I6 F: K! \ A5 p8 t* g4、移植主机协议栈5 F; O9 j3 u% j; o
主机协议栈源码就只有三个文件:4 u& ]" C4 l, F# a: j
* P& Z- m) k1 d& i( Q0 |( p7 S
, z9 M% z/ B0 |2 N4 T* g' y! @3 r) p, Q& k$ A! J
其中,mbrtu_master.h和mbrtu_master.c是协议栈实现,无需动,mbrtu_master_example.c是移植参考示例。
) A6 V& M7 B& U- o( c8 N% _( `+ C8 C
下面讲解一下移植过程。) h* s6 B% ^8 `" R5 V) @' z
^4 h1 A% j. d% Y
首先定义一个modbus主机的全局控制结构并初始化:
0 }" G* E' ]# p# B( f1 w
& a+ N5 x, f3 O: k+ C7 ^- MBRTUMaterTypeDef MBRTUHandle =
& C4 L( ?" d. U) A, u- [% X. X - {
3 v# e7 H: B4 }* f. [ w( g3 H - .delayms = delayms,; T) A N. |: [
- .timerStart = timerStart,
) Z! r% q3 F* l& x5 Q/ S4 S1 F - .timerStop = timerStop,
; e2 V/ l' x1 H; k! A, n" g - .sendData = sendData,
9 M4 T/ P, Y5 c$ U - 1 p. X& M. F5 k" Y
- #ifdef USE_RTOS // 使用了RTOS那么需要实现互斥 ^ U1 {- J/ o8 q: p
- .lock = mutex_lock,
+ u4 }2 {* i; j' R9 B% \$ ^- ?! y - .unlock = mutex_unlock,
) X# j! E+ q! c2 g0 B0 w. e; q) B$ y - #endif8 q* ?: B; p7 b5 L$ A" c
- };
复制代码 $ @3 u/ b7 P0 Q1 L# }* e
注意:如果使用了实时系统,需要实现lock和unlock函数。/ M+ k8 \/ P0 ]% n5 o B
3 u8 @- W- y' [1 F2 E: f) X1 J0 G结构体中的函数实现如下:1 b+ ~& j. B+ F, }& l1 S
8 O$ C& T8 E6 q/ Y3 z+ }/ Q1 L- #ifdef USE_RTOS
% F- y3 Y( Z& i- P
! ^6 v" R6 G" u- static void mutex_lock(void)8 Z5 d$ y% K X! r- T- M
- {7 }. K" i# t+ e& j6 Z8 y) P5 s
; s- u3 h8 R# g- }
5 R) ^- I' j; {& ^8 G* A - " Z0 T4 X* [3 q$ k2 W9 w0 b+ ^: R
- static void mutex_unlock(void) o: U) j0 x" l' }: U9 B: [
- {
* f& h. G/ m5 x- j |" A* E+ ~
. V. a# v7 F) ^& M4 S7 o4 E% O" R% b- }
- q# B1 G6 A6 b' b$ o - 7 J3 o/ x, I* X( w( [" g+ |! K
- #endif
3 [# D3 o7 U9 G( y - / c) l) I* n y
- static void timerStop(void)
4 g9 K4 J5 k( m% W& I# r, O - {9 ]' A' O" r' x5 ~; [* j
- HAL_TIM_Base_Stop_IT(&htim3);
9 l: Q! B0 Y& c8 } - }: X% P$ X* m9 ~; r/ u
* v' [9 q- x+ Q) j/ w- static void timerStart(void)# Q/ q S0 T3 N y7 @+ I3 u
- {
0 a9 B I1 Z! S; b - __HAL_TIM_SET_COUNTER(&htim3, 0);
1 w& _+ Y, r+ }# ~& K, y3 K* | m - HAL_TIM_Base_Start_IT(&htim3);: x- m+ }8 D/ Z; d7 e; r" D
- }2 H5 [. C2 k) _3 w/ S" u
; g5 `" `5 I$ k4 D- static void delayms(uint32_t nms)
- v; m: r- P& W- j4 T1 p - {5 J8 `( }' Q) p& e" W
- #ifdef USE_RTOS2 C2 X8 E1 J2 [" r
- osDelay(nms);1 Y6 q" d; Y8 ?
- #else7 Q- h; }7 c) J( E1 C
- HAL_Delay(nms);
9 v5 p D. B/ \3 Z - #endif9 j& Z+ F/ E9 O3 w
- }
7 ^. k1 w9 f2 c
" i5 d' P! P4 Y2 O8 k# H- static uint32_t sendData(const void* buf, uint32_t len)
* v3 u# G, C$ I% |- M5 M: M/ x - {" m, T- a# O Q! C# b
- if(HAL_UART_Transmit(&huart3, (uint8_t *)buf, len, 100) != HAL_OK)1 M' _) J! A8 E+ Y" s( s
- {
/ t* Y- f" D7 }6 g5 B - len = 0;
4 }" g. `$ R5 _ t - }
" t; f* p2 r0 E D4 x5 Q - return len;( H8 U& f1 G! R( n1 \
- }
复制代码 " y/ G2 _6 {( n' i! M5 `
将MBRTUMasterTimerISRCallback函数放置于定时器中断函数中,对于HAL库那就是这样的:4 H) u7 O9 V* v/ ~6 P
( a6 q3 H3 P5 u) P$ o- void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
) E; ^5 N; a1 y4 a - {2 T# P& A' U K3 m
- if(htim->Instance == htim3.Instance)
% M4 Z. ?1 K; |% _# p/ M! N' L - {- i5 Q5 s/ d0 H3 L- `' @
- MBRTUMasterTimerISRCallback(&MBRTUHandle);
. g1 J, H4 }: r* K% _ - }- J, T/ r8 q7 p
- }
复制代码
2 J( a0 \- s2 w& ?2 x# A将MBRTUMasterRecvByteISRCallback函数放置于串口中断函数中,对于HAL库那就是这样的:
* A$ F9 O. |% T4 s' N
" T9 \7 v9 [5 I3 d$ N- void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)8 m4 I# N- C0 [: B; p1 a5 d
- {
- k6 m3 O5 ?: O$ O# q - if(huart->Instance == huart3.Instance)
3 O6 p1 r& I- @ h - {
; T3 W9 Q$ I& i4 A) _ - MBRTUMasterRecvByteISRCallback(&MBRTUHandle, g_Uart3RxByte);
$ O3 h) e& ?, y' V - HAL_UART_Receive_IT(&huart3, &g_Uart3RxByte, 1); // 注册接收% _6 ]' y8 v) l8 S
- }
: @6 j* g6 S/ A7 U7 S: y - }
复制代码 4 q$ S- y( d$ m( r
重定向printf到串口1:& }5 z* r! l5 B0 R( j, I+ M+ x
4 H" m2 C( K% j- int fputc(int ch, FILE* fp). [) _ Q/ L0 L6 v6 j H% D3 V
- {, _2 l# a% K p
- HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100);
$ ]4 @2 e" z! q* |* s0 \' {; V+ j - return ch;& S% A' `! m$ m
- }
复制代码
2 v- v. C5 U* o9 g. M# J至此,就移植完毕了,测试函数如下:9 A0 ^6 C% X% q- p
5 p1 `$ k. K* y" @( J# \5 h
- int ret;# z$ d4 l& k2 Y s! J7 g2 Q, f
- uint8_t ucBuf[10];
8 M+ j) P$ L$ B+ J) s - uint16_t usBuf[10];
' h4 g" Y, Z. [# }0 t
: g5 [& q) A2 P1 e- int main_example(void): I: ]4 P7 b4 _) ]8 C# T! O
- {
) t" W- _! m% j0 H. {& o& ` - // 定时器初始化,设置为3.5个字符的超时时间& |- _* p3 ?8 Z# T2 c8 o
- // Timer_Init();3 E2 a- g8 _: ]
- 2 K: I. y" C" P) X/ w1 h- m& O
- // 串口初始化,初始化波特率等- }/ R3 x+ `3 J) Y5 p9 h
- // UART_Init();
; ~! C! R4 _! Q - ! F' l" ~ } ^8 Y7 Y+ V
- // 写单个线圈2 c$ A1 K* f+ Q& q( S% P
- ret = MBRTUMasterWriteSingleCoil(&MBRTUHandle, 1, 0, 1, 500);
( r# u8 t. X1 [0 e/ h5 N O - printf(" write single coil %s. \r\n", ret < 0 ? "failed" : "ok");
7 F+ q# E& E. k, e - HAL_Delay(100);* q8 c- S# c4 }& ~
! K. X( C* m' Y9 d4 @6 M- // 写单个寄存器
6 K2 H4 _, V, B - ret = MBRTUMasterWriteSingleRegister(&MBRTUHandle, 1, 0, 0XAABB, 500);
# y$ [1 F! p4 D - printf(" write single reg %s. \r\n", ret < 0 ? "failed" : "ok");% @+ Q0 `+ a9 T8 x
- HAL_Delay(100);" `) g. l5 z+ C2 S. e
- - q1 m0 J0 M9 Z& Y1 r7 Q. K
- // 写多个线圈
2 q; Y z, y! y5 w5 x9 Y3 m - memset(ucBuf, 0X01, 10);8 z& F. e- {$ b. V+ x' Z& W' T1 L9 k
- ret = MBRTUMasterWriteMultipleCoils(&MBRTUHandle, 1, 0, 10, ucBuf, 500);
* A! a+ u7 B2 `9 o' Q4 _ - printf(" write coils %s. \r\n", ret < 0 ? "failed" : "ok");" S# D6 e! f( s" E4 O$ q5 ?
- HAL_Delay(100);4 v% i' a" |0 T4 m. [
- 2 D1 s$ |& i/ I3 {; q0 d
- // 写多个寄存器
2 U& N' `! C; X$ n9 s3 W$ M - memset(usBuf, 0XFF, 20);0 Y% J X4 S8 L
- ret = MBRTUMasterWriteMultipleRegisters(&MBRTUHandle, 1, 0, 10, usBuf, 500);2 ^2 Q& r/ {* O ^4 Q" f' f1 `
- printf(" write regs %s. \r\n", ret < 0 ? "failed" : "ok");
U& t$ @. j/ X. J2 j/ B" V0 O - HAL_Delay(100);7 g$ i% ~ t7 k( k
- 2 a1 }- v# ^- K9 t$ ]5 q( n
- // 读线圈) }6 V7 V4 d; P/ ]9 T1 R) s, x: P
- MBRTUMasterReadCoils(&MBRTUHandle, 1, 0, 10, 500, ucBuf);
1 z, D3 G3 Y! X; F+ P8 \3 J1 b' i - printf(" read coils %s. \r\n", ret < 0 ? "failed" : "ok");- j2 R8 R0 G; }; n3 w5 ~1 C; E; H" X
- HAL_Delay(100);
H& J6 t1 ^4 g& r5 b
" }. W$ }5 }. C9 |- // 读离散量输入6 J: n5 D& J: ` [; Z9 j3 w0 x6 F" \
- MBRTUMasterReadDiscreteInputs(&MBRTUHandle, 1, 0, 10, 500, ucBuf);
9 ^( q1 v6 M: w) r/ Y - printf(" read discs %s. \r\n", ret < 0 ? "failed" : "ok");
! E' h- x5 ~ w# z5 q; e$ I, ~ - HAL_Delay(100);
# w# U4 g" e" K T2 }9 E% e - ) Q. k( v2 F6 J& {' `
- // 读保持寄存器& F F: W9 w9 s+ D8 X
- MBRTUMasterReadHoldingRegisters(&MBRTUHandle, 1, 0, 10, 500, usBuf);
% G6 t- O3 F7 _' r! E! q: n - printf(" read hold regs %s. \r\n", ret < 0 ? "failed" : "ok");2 X9 Y! x* @+ f
- HAL_Delay(100);
+ R! q9 `. c4 W A9 j- k - , D6 }3 \3 i4 L7 G; i
- // 读输入寄存器% o0 Q6 }& a* c+ V/ r' ^9 r2 V* P
- MBRTUMasterReadInputRegisters(&MBRTUHandle, 1, 0, 10, 500, usBuf);8 G4 H* Q: _: v) C/ u
- printf(" read input regs %s. \r\n", ret < 0 ? "failed" : "ok");
/ T* b% Q$ E, b- U/ B - HAL_Delay(100);
8 g0 C& b8 V6 N
5 {- n1 p4 K$ d# B- return 0;
5 F* v" F a/ o0 R- G - }
复制代码
4 x0 ^5 \) ` b3 J* n) k5、移植测试验证$ v1 r: Q0 H5 |3 n" P. D o" ]
移植完毕了现在需要测试,测试你可以使用MobusSlave软件模拟测试,也可以选用选用FreeModbus作为从机
. R+ M R1 A( t
/ n s5 n) y& O4 W2 ?! W' U% ^————————————————
1 q8 D9 H- y' _' q3 X版权转载自:雍正不秃头
: }* f' U" _- j5 j5 Z! D& o: N
5 I9 e% C$ ]7 {# S9 g% @
, v8 y1 y7 d6 \/ t. V: W
. G/ ]( q. v# F |