使用libmodbus實作Modbus協定

Modbus 是一個簡單且實用的工業用的網路協定,因為標準開放且容易實作,非常廣泛的應用於許多工業設備如PLC, AMI, … 等。Modbus協定目前由 Modbus Organization 維護,可以到該網站下載相關 specifications。Modbus 在 OSI 分層架構中屬於應用層,多數建構於串列傳輸 Serial (RS-232/RS-485) 或乙太網 (TCP/IP) 之上。(關於 Modbus 也可以參考小弟的拙於 slideshare) 即便 Modbus 協定相當容易實作,但基於"不要重新製造輪子"的立場,使用 libmodbus 函式庫來幫助我們快速開發,降低錯誤率與減少偵錯時間是個明智的選擇。

libmodbus

linmodbus 是一個開源函示庫(open source library)使用 LGPL 授權,這意味著我們撰寫的程式若只是單純使用此函示庫,並沒有加以修改的情況下,是可以不用開放原始碼的。也就是說開發商業軟體也可以使用此 library 而不用公開程式碼。唯獨原始碼中的 test 資料夾內的程式碼是屬於 GPLv3 授權,但若不擷取此程式碼作使用是不會有影像的。除此之外,libmodbus 還支援多重平台,不論是 Linux, MAC OS, 或是 Win32 都可以使用。 但以下說明仍以 Linux 平台為主。目前最新版本為 v3.0.5,以下也將會以此版本作為基礎。

Installation

舉例以 Ubuntu 或 Debian 來說,作業系統的套件資料庫已經包含了 libmodbus 的套件,只需要輸入以下指令即可完成安裝:

$ sudo apt-get install libmodbus5 libmodbus-dev

但須注意由此安裝的版本往往都不會是最新的,如果有新版本的需求,還是得透過網路下載最新版後手動安裝:

$ wget http://libmodbus.org/site_media/build/libmodbus-3.0.5.tar.gz
$ sudo chmod 755 libmodbus-3.0.5.tar.gz
$ tar zxvf libmodbus-3.0.5.tar.gz -C .
$ cd libmodbus-3.0.5/
$ ./configure
$ make
$ sudo make install

於此即完成安裝。

Usage

完整的使用方式請參考官方的 Manual Page,僅舉例以兩個例子做為參考:1. Modbus Slave/Server over serial 2. Modbus Client/Master over TCP/IP

Modbus Slave over Serial


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

#include <modbus.h>

#define MODBUS_SERIAL_DEV           "/dev/ttyUSB0"
#define MODBUS_SERIAL_BAUDRATE      9600    /* 9600, 38400, 115200, ... */
#define MODBUS_SERIAL_PARITY        'N'     /* 'N', 'E', or 'O' */
#define MODBUS_SERIAL_DATABITS      8       /* 5, 6, 7, or 8 */
#define MODBUS_SERIAL_STOPBITS      1       /* 1 or 2 */
#define MODBUS_DEVICE_ID            1
#define MODBUS_TIMEOUT_SEC          3
#define MODBUS_TIMEOUT_USEC         0
#define MODBUS_DEBUG                ON
#define MODBUS_RO_BITS              32
#define MODBUS_RW_BITS              32
#define MODBUS_RO_REGISTERS         64
#define MODBUS_RW_REGISTERS         64

int main(int argc, char *argv[])
{
    modbus_t            *ctx;
    modbus_mapping_t    *data_mapping;
    struct timeval      timeout;
    int                 ret, go = 1;
    uint8_t             query[MODBUS_TCP_MAX_ADU_LENGTH];

    ctx = modbus_new_rtu(MODBUS_SERIAL_DEV,
            MODBUS_SERIAL_BAUDRATE,
            MODBUS_SERIAL_PARITY,
            MODBUS_SERIAL_DATABITS,
            MODBUS_SERIAL_STOPBITS);

    if (ctx == NULL) {
        fprintf(stderr, "Unable to create the libmodbus context\n");
        exit(-1);
    }

    /* set slave device ID */
    modbus_set_slave(ctx, MODBUS_DEVICE_ID);

    /* Debug mode */
    modbus_set_debug(ctx, MODBUS_DEBUG);

    data_mapping = modbus_mapping_new(MODBUS_RO_BITS, MODBUS_RW_BITS,
                                    MODBUS_RO_REGISTERS, MODBUS_RW_REGISTERS);

    if (data_mapping == NULL) {
        fprintf(stderr, "Failed to allocate the mapping: %s\n",
                modbus_strerror(errno));
        modbus_free(ctx);
        return -1;
    }

    /* FIXME: initiate all data here */

    /* open serial interface */
    modbus_connect(ctx);

    while(go) {

        ret = modbus_receive(ctx, query);
        if (ret >= 0) {
            /* IF SOME ERROR OCCOR */
                /* modbus_reply_exception(ctx, query, MODBUS_EXCEPTION_XXXX); */
            /* ELSE */
                modbus_reply(ctx, query, ret, data_mapping);
        } else {
            /* Connection closed by the client or server */
            break;
        }
    }

    printf("Quit the loop: %s\n", modbus_strerror(errno));

    modbus_mapping_free(data_mapping);

    modbus_free(ctx);

    return 0;
}

Line 31 – 建立一個 modbus RTU over serial,注意帶入的參數要符合實際情況。
Line 43 – 設定 Slave (也就是本機)的 Device ID
Line 46 – 設定 debug mode
Line 48 – 建立 modbus 資料的 mapping。再把對應的 input bits, coils, input regs, holding regs 資料擺放進去。
Line 61 – 開啟 modbus 介面。
Line 65 – 接收封包。
Line 70 – 回覆封包。
Line 79 – 釋放 mapping 記憶體。
Line 81 – 釋放連線內容記憶體。

Modbus Client over TCP/IP(IPv4)


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>

#include <modbus.h>

#define MODBUS_SERVER_IP            "127.0.0.1"
#define MODBUS_SERVER_PORT          502
#define MODBUS_DEVICE_ID            100
#define MODBUS_TIMEOUT_SEC          3
#define MODBUS_TIMEOUT_USEC         0
#define MODBUS_DEBUG                ON

#define MODBUS_DISCRETE_ADDR        0
#define MODBUS_DISCRETE_LEN         32
#define MODBUS_COIL_ADDR            0
#define MODBUS_COIL_LEN             32
#define MODUBS_INPUT_ADDR           0
#define MODUBS_INPUT_LEN            64
#define MODUBS_HOLDING_ADDR         0
#define MODUBS_HOLDING_LEN          64

int main(int argc, char *argv[])
{
    modbus_t        *ctx;
    struct timeval  timeout;
    int             ret, ii;
    uint8_t         bits[MODBUS_MAX_READ_BITS] = {0};
    uint16_t        regs[MODBUS_MAX_READ_REGISTERS] = {0};

    ctx = modbus_new_tcp(MODBUS_SERVER_IP, MODBUS_SERVER_PORT);

    /* set device ID */
    modbus_set_slave(ctx, MODBUS_DEVICE_ID);

    /* Debug mode */
    modbus_set_debug(ctx, MODBUS_DEBUG);

    /* set timeout */
    timeout.tv_sec = MODBUS_TIMEOUT_SEC;
    timeout.tv_usec = MODBUS_TIMEOUT_USEC;
    modbus_get_byte_timeout(ctx, &timeout);

    timeout.tv_sec = MODBUS_TIMEOUT_SEC;
    timeout.tv_usec = MODBUS_TIMEOUT_USEC;
    modbus_set_response_timeout(ctx, &timeout);

    if (modbus_connect(ctx) == -1) {
        fprintf(stderr, "Connexion failed: %s\n",
                modbus_strerror(errno));
        modbus_free(ctx);
        exit(-1);
    }

    /* read discrete input (0x02 function code) */
    ret = modbus_read_input_bits(ctx,
            MODBUS_DISCRETE_ADDR, MODBUS_DISCRETE_LEN, bits);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    } else {
        printf("BITS DISCRETE:\n");
        for (ii=0; ii < ret; ii++) {
            printf("[%d]=%d\n", ii, bits[ii]);
        }
    }

    /* read coils (0x01 function code) */
    ret = modbus_read_bits(ctx, MODBUS_COIL_ADDR, MODBUS_COIL_LEN, bits);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    } else {
        printf("BITS COILS:\n");
        for (ii=0; ii < ret; ii++) {
            printf("[%d]=%d\n", ii, bits[ii]);
        }
    }

    /* read input registers (0x04 function code) */
    ret = modbus_read_input_registers(ctx,
            MODUBS_INPUT_ADDR, MODUBS_INPUT_LEN, regs);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    } else {
        printf("INPUT REGISTERS:\n");
        for (ii=0; ii < ret; ii++) {
            printf("[%d]=%d\n", ii, regs[ii]);
        }
    }

    /* read holding registers (0x03 function code) */
    ret = modbus_read_registers(ctx,
            MODUBS_HOLDING_ADDR, MODUBS_HOLDING_LEN, regs);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    } else {
        printf("HOLDING REGISTERS:\n");
        for (ii=0; ii < ret; ii++) {
            printf("[%d]=%d\n", ii, regs[ii]);
        }
    }

    /* write single coil (0x05 function code) */
    ret = modbus_write_bit(ctx, 1, TRUE);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    }

    /* write single register (0x06 function code) */
    ret = modbus_write_register(ctx, 1, 0x1234);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    }

    /* write multi coil (0x0F function code) */
    bits[1] = TRUE;
    bits[2] = FALSE;
    ret = modbus_write_bits(ctx, 1, 2, &bits[1]);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    }

    /* write multi registers (0x10 function code) */
    regs[1] = 0x1234;
    regs[2] = 0x5678;
    ret = modbus_write_registers(ctx, 1, 2, &regs[1]);
    if (ret < 0) {
        fprintf(stderr, "%s\n", modbus_strerror(errno));
    }

    /* Close the connection */
    modbus_close(ctx);
    modbus_free(ctx);

    exit(0);
}

Line 35 – 建立一個 modbus over TCP 組態,Server IP 和 port 須符合實際情況。
Line 38 – 設定欲連入的 device ID
Line 41 – 設定 debug mode
Line 50 – 設定 modbus 回應逾時,超過此時間則 time out。
Line 52 – 連線至 Modbus TCP Server
Line 60 – 讀取 input bits (modbus function code 0x02),需依照實際情況設定範圍。
Line 72 – 讀取 coils bits (modbus function code 0x01),需依照實際情況設定範圍。
Line 83 – 讀取 input registers (modbus function code 0x04),需依照實際情況設定範圍。
Line 95 – 讀取 holding registers (modbus function code 0x03),需依照實際情況設定範圍。
Line 107 – 寫入單一 coil (modbus function code 0x05),需依照實際情況設定位址與值。
Line 113 – 寫入單一 holding register (modbus function code 0x06),需依照實際情況設定位址與值。
Line 121 – 寫入多組 coils (modbus function code 0x0F),需依照實際情況設定位址與值。
Line 129 – 寫入多組 holding registers (modbus function code 0x10),需依照實際情況設定位址與值。
Line 135 – 關閉 modbus 連線。
Line 136 – 釋放連線內容記憶體。

小結

Modbus 是一個簡單且實用的協定,可以藉由理解 Modbus 開始理解各種不同的協定。可以深入 libmodbus 的程式碼了解其背後的運作方式,了解如何開啟一個 serial port 或是 TCP port。

4 則迴響於《使用libmodbus實作Modbus協定

  1. 您好,我有些問題,reply的資料應該是在data_mapping裡吧?那麼我要如何將data丟進去data_mapping呢?

    • 你要自己修改 modbus_mapping 去編程。 modbus_mapping 其實就是一個存放數據的內存區。

發佈回覆給「纪国瑞」的留言 取消回覆

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


− 二 = 4