引言
在工業(yè)控制中,Modbus 是十分普遍的一個通信協(xié)議,這里借此次比賽的機會,使用樹莓派4開發(fā)板來制作一個Modbus網關
開箱、點亮
活動過程中出了點小插曲,樹莓派的內存不知道怎么的壞了,然后換了一下,立馬給它打印了個外殼穿上防止再次受傷
然后為了方便操作,用HDMI采集器和鍵鼠KVM工具把樹莓派連接到電腦,插上系統(tǒng)卡開機
硬件設計
Modbus協(xié)議一般不是常用的TTL電平標準,一般會使用RS232和RS485電平標準,所以做了個USB轉RS232/RS485的一個模塊,原理圖如下
PCB的3D仿真圖
到手焊好的樣子
下面的撥動開關用來切換RS485還是RS232
軟件開發(fā)
對于Modbus這個已經有了幾十年壽命的通信協(xié)議,在各個系統(tǒng)中都有各式各樣的開源軟件庫了,由于樹莓派也是基于Linux系統(tǒng)的,所以這里使用libmodbus來完成Modbus網關的開發(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)中已經安裝好了 libmodbus 的庫,硬件也完成了連接,現在寫一個例程去調用 libmodbus 用 modbus RTU 協(xié)議讀取溫濕度傳感器的數據,代碼如下,因為是 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;
}
// 讀取保持寄存器的數量和起始地址
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的溫濕度傳感器數據了,其中0和1地址的保持寄存器就是溫度和濕度數據
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 界面了
搭建網關 HMI
繪制網關 HMI方才已經完成了 libmodbus、SDL2、LVGL 的適配,接下來就是 HMI 界面的開發(fā)了,因為我連接的是兩個溫濕度傳感器,所以這里做了一個對于兩個從站的數據采集界面,如果按照商用網關的標準是需要實現增刪改查的功能,提供給用戶自己對從站的數量、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 函數中的的 lv_demo_widgets 替換為
setup_ui(&guider_ui); custom_init(&guider_ui);
編譯運行
對接數據接口
使用 Linux 的 RT 庫來創(chuàng)建一個定時器如下
timer_t timer_id;
struct sigevent sev;
struct itimerspec ts;
// 設置定時器的事件通知方式為信號通知,指定回調函數
sev.sigev_notify = SIGEV_THREAD; // 使用線程回調
sev.sigev_notify_function = timer_handler; // 定時器到期時調用的回調函數
sev.sigev_notify_attributes = NULL; // 默認屬性
sev.sigev_value.sival_ptr = &timer_id; // 傳遞給回調函數的參數
// 創(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);
}
在定時器的回調函數中去更新界面的顯示
// 定時器回調函數
void timer_handler(union sigval arg) {
// 這里寫定時器每次觸發(fā)時執(zhí)行的代碼
printf("定時器觸發(fā),執(zhí)行任務...\n");
{
// 讀取保持寄存器的數量和起始地址
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);
{
// 讀取保持寄存器的數量和起始地址
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);
}
在這個回調函數中就實現了溫濕度的采集和對于 HMI 界面的數據更新任務,編譯運行結果如下
程序源碼