引言
在工業(yè)控制中,Modbus 是十分普遍的一個通信協(xié)議,這里借此次比賽的機會,使用樹莓派4開發(fā)板來制作一個Modbus網(wǎng)關
開箱、點亮
活動過程中出了點小插曲,樹莓派的內存不知道怎么的壞了,然后換了一下,立馬給它打印了個外殼穿上防止再次受傷
然后為了方便操作,用HDMI采集器和鍵鼠KVM工具把樹莓派連接到電腦,插上系統(tǒng)卡開機
硬件設計
Modbus協(xié)議一般不是常用的TTL電平標準,一般會使用RS232和RS485電平標準,所以做了個USB轉RS232/RS485的一個模塊,原理圖如下
PCB的3D仿真圖
到手焊好的樣子
下面的撥動開關用來切換RS485還是RS232
軟件開發(fā)
對于Modbus這個已經(jīng)有了幾十年壽命的通信協(xié)議,在各個系統(tǒng)中都有各式各樣的開源軟件庫了,由于樹莓派也是基于Linux系統(tǒng)的,所以這里使用libmodbus來完成Modbus網(wǎng)關的開發(fā)
編譯安裝 libmodbus
使用終端工具和 SSH 連接到樹莓派,我這里使用的是 Tabby,安裝需要的工具包
sudo apt-get install automake autoconf libtool cmake git
進到 libmodbus 的文件夾,然后執(zhí)行自動初始化
./autogen.sh
./configure 完成自動化配置
然后執(zhí)行 make 和 sudo make install 命令
至此 libmodbus 就完成了編譯安裝
測試 libmodbus
硬件連接測試例程
系統(tǒng)中已經(jīng)安裝好了 libmodbus 的庫,硬件也完成了連接,現(xiàn)在寫一個例程去調用 libmodbus 用 modbus RTU 協(xié)議讀取溫濕度傳感器的數(shù)據(jù),代碼如下,因為是 USB 轉 RS485,所以在系統(tǒng)中是 /dev/ttyUSB0
include <modbus/modbus.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> int main() { // 創(chuàng)建并初始化 Modbus 連接 modbus_t *ctx = modbus_new_rtu("/dev/ttyUSB0", 9600, 'N', 8, 1); if (ctx == NULL) { fprintf(stderr, "Unable to create the libmodbus context\n"); return -1; } // 設置從站地址(例如,從站地址為 0x01) modbus_set_slave(ctx, 0x01); // 嘗試打開串口 if (modbus_connect(ctx) == -1) { fprintf(stderr, "Connection failed: %s\n", modbus_strerror(errno)); modbus_free(ctx); return -1; } // 讀取保持寄存器的數(shù)量和起始地址 uint16_t reg[6]; // 用于存儲讀取到的6個保持寄存器的值 int addr = 0; // 從寄存器地址 0 開始讀取 int num_regs = 6; // 讀取 6 個寄存器 int rc = modbus_read_registers(ctx, addr, num_regs, reg); if (rc == -1) { fprintf(stderr, "Failed to read registers: %s\n", modbus_strerror(errno)); modbus_close(ctx); modbus_free(ctx); return -1; } // 輸出讀取到的寄存器值 for (int i = 0; i < num_regs; i++) { printf("Register %d: %d\n", addr + i, reg[i]); } // 關閉并釋放 Modbus 連接 modbus_close(ctx); modbus_free(ctx); return 0; }
執(zhí)行命令如下,就可以讀取到站號為1的溫濕度傳感器數(shù)據(jù)了,其中0和1地址的保持寄存器就是溫度和濕度數(shù)據(jù)
gcc test.c -o test -lmodbus
./test
適配 SDL2 和 LVGL
SDL2 是一個跨平臺的開發(fā)庫,用于簡化游戲和多媒體應用程序的開發(fā),LVGL是近些年十分流行的 GUI 開發(fā)框架,在本項目中使用了 SDL2 來作為 HMI 的驅動層,同時使用 LVGL 來作為應用層的框架來使用
首先安裝好 SDL2 的庫
sudo apt install libsdl2-dev
然后 clone 下來 lvgl-sdl 開源代碼
git clone https://github.com/Ryzee119/lvgl-sdl.git
進入倉庫修改 lv_conf.h 文件,使能 LV_USE_DEMO_WIDGETS 這個宏,然后修改 main.c 中的代碼,調用 lv_demo_widgets();
在 example 目錄中使用 cmake 來編譯工程
cmake -Bbuild
cmake --build build
編譯完成的樣子
進入到樹莓派的桌面環(huán)境,然后進入到 build 目錄中執(zhí)行 ./lv_examples,可以看到
LVGL 也適配完成了,然后就可以基于 LVGL 庫繪制屬于自己的 HMI 界面了
搭建網(wǎng)關 HMI
繪制網(wǎng)關 HMI方才已經(jīng)完成了 libmodbus、SDL2、LVGL 的適配,接下來就是 HMI 界面的開發(fā)了,因為我連接的是兩個溫濕度傳感器,所以這里做了一個對于兩個從站的數(shù)據(jù)采集界面,如果按照商用網(wǎng)關的標準是需要實現(xiàn)增刪改查的功能,提供給用戶自己對從站的數(shù)量、HMI界面的自定義,本項目從簡之后的界面如下
使用的是 GUI Guider 進行的可視化設計,生成代碼后復制到樹莓派中
在 CMakeLists.txt 中添加 GUI Guider 生成的代碼和包含路徑
file(GLOB LVGL_DRIVER_FILES
"${PROJECT_DIR}/main.c"
"${PROJECT_DIR}/example.c"
"${PROJECT_DIR}/assets/*.c"
"${PROJECT_DIR}/gui/generated/*.c"
"${PROJECT_DIR}/gui/generated/images/*.c"
"${PROJECT_DIR}/gui/generated/guider_fonts/*.c"
"${PROJECT_DIR}/gui/custom/*.c"
)
include_directories(${PROJECT_DIR}/gui/custom)
include_directories(${PROJECT_DIR}/gui/generated)
include_directories(${PROJECT_DIR}/gui/generated/images)
include_directories(${PROJECT_DIR}/gui/generated/guider_fonts)
include_directories(${PROJECT_DIR}/gui/generated/guider_customer_fonts)
然后將 main 函數(shù)中的的 lv_demo_widgets 替換為
setup_ui(&guider_ui); custom_init(&guider_ui);
編譯運行
對接數(shù)據(jù)接口
使用 Linux 的 RT 庫來創(chuàng)建一個定時器如下
timer_t timer_id; struct sigevent sev; struct itimerspec ts; // 設置定時器的事件通知方式為信號通知,指定回調函數(shù) sev.sigev_notify = SIGEV_THREAD; // 使用線程回調 sev.sigev_notify_function = timer_handler; // 定時器到期時調用的回調函數(shù) sev.sigev_notify_attributes = NULL; // 默認屬性 sev.sigev_value.sival_ptr = &timer_id; // 傳遞給回調函數(shù)的參數(shù) // 創(chuàng)建定時器 if (timer_create(CLOCK_REALTIME, &sev, &timer_id) == -1) { perror("timer_create"); exit(EXIT_FAILURE); } // 設置定時器:首次觸發(fā)延遲 1 秒,周期為 1 秒 ts.it_value.tv_sec = 1; // 初始延遲 1 秒 ts.it_value.tv_nsec = 0; ts.it_interval.tv_sec = 1; // 周期 1 秒 ts.it_interval.tv_nsec = 0; // 設置定時器 if (timer_settime(timer_id, 0, &ts, NULL) == -1) { perror("timer_settime"); exit(EXIT_FAILURE); }
在定時器的回調函數(shù)中去更新界面的顯示
// 定時器回調函數(shù) void timer_handler(union sigval arg) { // 這里寫定時器每次觸發(fā)時執(zhí)行的代碼 printf("定時器觸發(fā),執(zhí)行任務...\n"); { // 讀取保持寄存器的數(shù)量和起始地址 uint16_t reg[2]= {0, 0}; // 用于存儲讀取到的6個保持寄存器的值 int addr = 0; // 從寄存器地址 0 開始讀取 int num_regs = 2; // 讀取 2 個寄存器 // 設置從站地址(例如,從站地址為 0x01) modbus_set_slave(ctx, 0x1); modbus_read_registers(ctx, addr, num_regs, reg); t1 = reg[0] / 10.0f; h1 = reg[1] / 10.0f; } usleep(5000); { // 讀取保持寄存器的數(shù)量和起始地址 uint16_t reg[2] = {0, 0}; // 用于存儲讀取到的6個保持寄存器的值 int addr = 0; // 從寄存器地址 0 開始讀取 int num_regs = 2; // 讀取 2 個寄存器 // 設置從站地址(例如,從站地址為 0x02) modbus_set_slave(ctx, 0x2); modbus_read_registers(ctx, addr, num_regs, reg); t2 = reg[0] / 10.0f; h2 = reg[1] / 10.0f; } printf("T1: %02.1f H1: %02.1f\n", t1, h1); printf("T2: %02.1f H2: %02.1f\n", t2, h2); char buff[512]; lv_ui *ui = &guider_ui; sprintf(buff, "從站 1: 溫度: %02.1f℃ 濕度: %02.1f%%", t1, h1); lv_label_set_text(ui->screen_label_1, buff); sprintf(buff, "從站 2: 溫度: %02.1f℃ 濕度: %02.1f%%", t2, h2); lv_label_set_text(ui->screen_label_2, buff); static int times = 0; if(times++ > 100) exit(0); }
在這個回調函數(shù)中就實現(xiàn)了溫濕度的采集和對于 HMI 界面的數(shù)據(jù)更新任務,編譯運行結果如下
程序源碼
以下為視頻演示: