- 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() 原始碼如下:
/* 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); }疑?可是我們上圖看到的是 _modbus_rtu_receive(),搜尋 _modbus_rtu_receive(),我們找到一段跟 backend & _modbus_rtu_receive() 有關的程式碼:
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 };接著如果您 ./unit-test-server rtu 與 ./unit-test-server tcp 都試過,你會發現進入點都是 modbus_receive(),也都呼叫了 _modbus_receive_msg():
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 };看來 modbus_backend_t 扮演了關鍵角色,我們接著往下看。
物件導向 C 語言
先來看看 modbus_backend_t 本尊長相:
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;有沒有發現?這其實就是 C++ 中的 abstract base class,甚至可以直接翻譯成 C++ 而不會有任何違和感(精確一點的說法,下面的 class modbus_backend_t 稱為 interface class):
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; } //... }所以 libmodbus 透過 backend 這個界面,就不需要知道需要呼叫 _modbus_rtu_receive() 還是 _modbus_tcp_receive(),此種實作手法在其他開源專案中也很常見,例如 libevent。
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() 瞧瞧:
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_receive_msg() 正在進行一個解封包的動作,而 Modbus TCP/RTU 封包結構其實很相近:
差別只在於 Modbus TCP Header 多了一個 header 拿掉 checksum 而已,所以 libmodbus 作者的高明之處就是 Modbus Slave 他用同一個 SOP 處理 Modbus RTU/TCP,而沒有寫出兩份 code。例如 RTU 有 checksum,但 TCP 不用,那作者怎麼處理的呢?
static int _modbus_tcp_check_integrity(modbus_t *ctx, uint8_t *msg, const int msg_length) { return msg_length; }bypass 直接返回 packet length,而 _modbus_rtu_check_integrity 則是進行 CRC 檢查:
/* 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]; //........................ }在 C++ 裡這種手法稱為 Template method,虛擬碼如下:
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(); }由上述程式可以看到我們不公開 select, recv, check_integrity 三個 member function,而且是 pure virtual member function 留給 modbus_backend_t 後代去實作,而 _modbus_receive_msg() 則在 modbus_backend_t 就已經定義實作完畢(注意他不是 virtual function),_modbus_receive_msg() 內部會呼叫 select, recv, check_integrity,這些細節則留到子代實作。細節可以參考 Effective C++。
尾聲
相信看到這裡,很多人跟小弟一樣恍然大悟原來真的可以用 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這種純物件導向語言做起來很麻煩的技巧整理。