- function pointer 是 C 高級技巧
- 學習 C++ 讓我變成更好的 C 程式員。
libmodbus
libmodbus 是處理 Modbus protocol 的一個開源 C 程式庫,Modbus protocol 之前小弟已經寫過很多文章解釋過了,這邊就快速帶過。一言以蔽之,無論是走 serial line (Modbus RTU/ASCII) 或是 TCP (Modbus TCP),通訊方式都是採取一問一答的形式,差別只在於編碼方式,所以相較於 http 這種大部頭協議,Modbus 大概是 10 分鐘就可以講完的東西:
如上圖所示,Master 發命令給 Slave,然後等待 Slave 回應,就這麼一站一站問下去 (Command 與 Response 都帶有站號),沒什麼特別的吧?(相較於 http 還有一堆長連接與 proxy 之類的鬼東西)。本篇文章將聚焦在 Slave 的行為上。
編譯 libmodbus
還記得小弟之前寫過的文章嗎?不要傻傻的直接從 main() 開始追起,讓程式展現自我吧!
- ./configure
- make CFLAGS='-pg -g -O0 -Wall' LDLIBS+='-pg' LDFLAGS+='-static -ldl'
這裡你必須先對 unit-test-server.c 動點手腳以免 process 被殺掉後一無所獲,這在另一篇文章中也分享過了,這邊不再重複。
執行 unit-test-server
進入到 test/ 目錄,執行 ./unit-test-server rtu,如果你沒有 USB to Serial 的裝置,可以改下 ./unit-test-server tcp,接著用 modscan or modpoll 試著對 unit-test-server 通訊一下,然後用 kill -SIGUSR1 PID 殺掉 unit-test-server process。
觀察 call graph
透過 gprof + graphviz,我們得到下面這張圖:
Slave 接收 Command 的地方在 modbus_receive():
modbus_receive() 原始碼如下:
1 2 3 4 5 6 7 8 9 10 | /* Receive the request from a modbus master */ int modbus_receive(modbus_t *ctx, uint8_t *req) { if (ctx == NULL) { errno = EINVAL; return -1; } return ctx->backend->receive(ctx, req); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const modbus_backend_t _modbus_rtu_backend = { _MODBUS_BACKEND_TYPE_RTU, _MODBUS_RTU_HEADER_LENGTH, _MODBUS_RTU_CHECKSUM_LENGTH, MODBUS_RTU_MAX_ADU_LENGTH, _modbus_set_slave, _modbus_rtu_build_request_basis, _modbus_rtu_build_response_basis, _modbus_rtu_prepare_response_tid, _modbus_rtu_send_msg_pre, _modbus_rtu_send, _modbus_rtu_receive, //<------ _modbus_rtu_recv, _modbus_rtu_check_integrity, _modbus_rtu_pre_check_confirmation, _modbus_rtu_connect, _modbus_rtu_close, _modbus_rtu_flush, _modbus_rtu_select, _modbus_rtu_free }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const modbus_backend_t _modbus_tcp_backend = { _MODBUS_BACKEND_TYPE_TCP, _MODBUS_TCP_HEADER_LENGTH, _MODBUS_TCP_CHECKSUM_LENGTH, MODBUS_TCP_MAX_ADU_LENGTH, _modbus_set_slave, _modbus_tcp_build_request_basis, _modbus_tcp_build_response_basis, _modbus_tcp_prepare_response_tid, _modbus_tcp_send_msg_pre, _modbus_tcp_send, _modbus_tcp_receive, //<------ _modbus_tcp_recv, _modbus_tcp_check_integrity, _modbus_tcp_pre_check_confirmation, _modbus_tcp_connect, _modbus_tcp_close, _modbus_tcp_flush, _modbus_tcp_select, _modbus_tcp_free }; |
物件導向 C 語言
先來看看 modbus_backend_t 本尊長相:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | typedef struct _modbus_backend { unsigned int backend_type; unsigned int header_length; unsigned int checksum_length; unsigned int max_adu_length; int (*set_slave) (modbus_t *ctx, int slave); int (*build_request_basis) (modbus_t *ctx, int function, int addr, int nb, uint8_t *req); int (*build_response_basis) (sft_t *sft, uint8_t *rsp); int (*prepare_response_tid) ( const uint8_t *req, int *req_length); int (*send_msg_pre) (uint8_t *req, int req_length); ssize_t (*send) (modbus_t *ctx, const uint8_t *req, int req_length); int (*receive) (modbus_t *ctx, uint8_t *req); ssize_t (*recv) (modbus_t *ctx, uint8_t *rsp, int rsp_length); int (*check_integrity) (modbus_t *ctx, uint8_t *msg, const int msg_length); int (*pre_check_confirmation) (modbus_t *ctx, const uint8_t *req, const uint8_t *rsp, int rsp_length); int (*connect) (modbus_t *ctx); void (*close) (modbus_t *ctx); int (*flush) (modbus_t *ctx); int (*select) (modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length); void (* free ) (modbus_t *ctx); } modbus_backend_t; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | class modbus_backend_t { public : virtual ~modbus_backend_t(); virtual int backend_type()=0; virtual int header_length()=0; virtual int checksum_length()=0; virtual int max_adu_length()=0; virtual int set_slave(modbus_t *ctx, int slave)=0; virtual int build_request_basis(modbus_t *ctx, int function, int addr, int nb, uint8_t *req)=0; //.......... virtual int flush(modbus_t *ctx)=0; virtual int select(modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length)=0; virtual void free (modbus_t *ctx)=0; }; class _modbus_tcp_backend : public modbus_backend_t { public : _modbus_tcp_backend(); ~_modbus_tcp_backend(); virtual int backend_type(){ return _MODBUS_BACKEND_TYPE_TCP; } virtual int header_length(){ return _MODBUS_TCP_HEADER_LENGTH; } virtual int checksum_length(){ return _MODBUS_TCP_CHECKSUM_LENGTH; } //... } |
Template method pattern in C
還記得前面提到 _modbus_rtu_receive() 與 _modbus_tcp_receive() 都呼叫了 _modbus_receive_msg() 了嗎?這個 function 並未被嵌入到 backend 裡,這隻共用的 function 則會依據選擇的 backend(RTU/TCP) 呼叫對應的 backend function:
如果打開 _modbus_receive_msg() 瞧瞧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) { int rc; fd_set rset; struct timeval tv; struct timeval *p_tv; int length_to_read; int msg_length = 0; _step_t step; if (ctx->debug) { if (msg_type == MSG_INDICATION) { printf ( "Waiting for a indication...\n" ); } else { printf ( "Waiting for a confirmation...\n" ); } } /* Add a file descriptor to the set */ FD_ZERO(&rset); FD_SET(ctx->s, &rset); /* We need to analyse the message step by step. At the first step, we want * to reach the function code because all packets contain this * information. */ step = _STEP_FUNCTION; length_to_read = ctx->backend->header_length + 1; if (msg_type == MSG_INDICATION) { /* Wait for a message, we don't know when the message will be * received */ p_tv = NULL; } else { tv.tv_sec = ctx->response_timeout.tv_sec; tv.tv_usec = ctx->response_timeout.tv_usec; p_tv = &tv; } while (length_to_read != 0) { rc = ctx->backend->select(ctx, &rset, p_tv, length_to_read); if (rc == -1) { _error_print(ctx, "select" ); if (ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) { int saved_errno = errno ; if ( errno == ETIMEDOUT) { _sleep_response_timeout(ctx); modbus_flush(ctx); } else if ( errno == EBADF) { modbus_close(ctx); modbus_connect(ctx); } errno = saved_errno; } return -1; } rc = ctx->backend->recv(ctx, msg + msg_length, length_to_read); if (rc == 0) { errno = ECONNRESET; rc = -1; } if (rc == -1) { _error_print(ctx, "read" ); if ((ctx->error_recovery & MODBUS_ERROR_RECOVERY_LINK) && ( errno == ECONNRESET || errno == ECONNREFUSED || errno == EBADF)) { int saved_errno = errno ; modbus_close(ctx); modbus_connect(ctx); /* Could be removed by previous calls */ errno = saved_errno; } return -1; } /* Display the hex code of each character received */ if (ctx->debug) { int i; for (i=0; i < rc; i++) printf ( "<%.2X>" , msg[msg_length + i]); } /* Sums bytes received */ msg_length += rc; /* Computes remaining bytes */ length_to_read -= rc; if (length_to_read == 0) { switch (step) { case _STEP_FUNCTION: /* Function code position */ length_to_read = compute_meta_length_after_function( msg[ctx->backend->header_length], msg_type); if (length_to_read != 0) { step = _STEP_META; break ; } /* else switches straight to the next step */ case _STEP_META: length_to_read = compute_data_length_after_meta( ctx, msg, msg_type); if ((msg_length + length_to_read) > ( int )ctx->backend->max_adu_length) { errno = EMBBADDATA; _error_print(ctx, "too many data" ); return -1; } step = _STEP_DATA; break ; default : break ; } } if (length_to_read > 0 && (ctx->byte_timeout.tv_sec > 0 || ctx->byte_timeout.tv_usec > 0)) { /* If there is no character in the buffer, the allowed timeout interval between two consecutive bytes is defined by byte_timeout */ tv.tv_sec = ctx->byte_timeout.tv_sec; tv.tv_usec = ctx->byte_timeout.tv_usec; p_tv = &tv; } /* else timeout isn't set again, the full response must be read before expiration of response timeout (for CONFIRMATION only) */ } if (ctx->debug) printf ( "\n" ); return ctx->backend->check_integrity(ctx, msg, msg_length); } |
差別只在於 Modbus TCP Header 多了一個 header 拿掉 checksum 而已,所以 libmodbus 作者的高明之處就是 Modbus Slave 他用同一個 SOP 處理 Modbus RTU/TCP,而沒有寫出兩份 code。例如 RTU 有 checksum,但 TCP 不用,那作者怎麼處理的呢?
1 2 3 4 | static int _modbus_tcp_check_integrity(modbus_t *ctx, uint8_t *msg, const int msg_length) { return msg_length; } |
1 2 3 4 5 6 7 8 9 10 11 | /* The check_crc16 function shall return 0 is the message is ignored and the message length if the CRC is valid. Otherwise it shall return -1 and set errno to EMBADCRC. */ static int _modbus_rtu_check_integrity(modbus_t *ctx, uint8_t *msg, const int msg_length) { uint16_t crc_calculated; uint16_t crc_received; int slave = msg[0]; //........................ } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | class modbus_backend_t { protected : virtual int select(modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length)=0; virtual ssize_t recv(modbus_t *ctx, uint8_t *rsp, int rsp_length)=0; virtual int check_integrity(modbus_t *ctx, uint8_t *msg, const int msg_length)=0; public : virtual ~modbus_backend_t(); //... //call select, recv, check_integrity of _modbus_rtu(tcp)_backend int _modbus_receive_msg(modbus_t *ctx, uint8_t *msg, msg_type_t msg_type) //... }; class _modbus_rtu_backend : public modbus_backend_t { protected : virtual int select(modbus_t *ctx, fd_set *rset, struct timeval *tv, int msg_length)=0; virtual ssize_t recv(modbus_t *ctx, uint8_t *rsp, int rsp_length)=0; virtual int check_integrity(modbus_t *ctx, uint8_t *msg, const int msg_length)=0; public : _modbus_tcp_backend(); ~_modbus_tcp_backend(); } |
尾聲
相信看到這裡,很多人跟小弟一樣恍然大悟原來真的可以用 C 實作物件導向,如果您所在的平台只有 C 可以用或者 C++ 不成熟或者您覺得 C++ 有太多陷阱,此種手法對您發展大型 C 程式一定相當有助益,歡迎留言討論 :)
這種寫法第一次看到是在STM32F401C 的CubeMx 看到的,想說真的是可以全部靠C 語言寫就夠了XDDD
回覆刪除只知道這種做法很棒啊,可以抽象化,不知道在C++ 裡面也是這樣實作的....
感謝回應,小弟只挖出了這一點例子,盼更多人分享相關知識
刪除最近剛寫MCU modbus 找了很多資料 想請問一下 該如何實作 傳送一個陣列的值給device端 好比說array[]={0x08,0x03,0x00,0x01..} 讓device端接收並回傳的範例code 感謝!!!!
回覆刪除一切盡在libmodbus中,其實關鍵就在libmodbus的_modbus_receive_msg裡,sample code範圍太大沒辦法回答喔,也不知您用何種MCU
刪除OK 我在try看看 :)感謝你
刪除套句電影的台詞「刀法我給了,得多少,在你們」
刪除正確來說,c可以寫物件,但無物件導向的特性。
回覆刪除而Design pattern是用來解決c/c++原本能做,而java這種純物件導向語言做起來很麻煩的技巧整理。