引言
在工业控制中,Modbus 是十分普遍的一个通信协议,这里借此次比赛的机会,使用树莓派4开发板来制作一个Modbus网关
开箱、点亮
活动过程中出了点小插曲,树莓派的内存不知道怎么的坏了,然后换了一下,立马给它打印了个外壳穿上防止再次受伤
然后为了方便操作,用HDMI采集器和键鼠KVM工具把树莓派连接到电脑,插上系统卡开机
硬件设计
Modbus协议一般不是常用的TTL电平标准,一般会使用RS232和RS485电平标准,所以做了个USB转RS232/RS485的一个模块,原理图如下
PCB的3D仿真图
到手焊好的样子
下面的拨动开关用来切换RS485还是RS232
软件开发
对于Modbus这个已经有了几十年寿命的通信协议,在各个系统中都有各式各样的开源软件库了,由于树莓派也是基于Linux系统的,所以这里使用libmodbus来完成Modbus网关的开发
编译安装 libmodbus
使用终端工具和 SSH 连接到树莓派,我这里使用的是 Tabby,安装需要的工具包
sudo apt-get install automake autoconf libtool cmake git
进到 libmodbus 的文件夹,然后执行自动初始化
./autogen.sh
./configure 完成自动化配置
然后执行 make 和 sudo make install 命令
至此 libmodbus 就完成了编译安装
测试 libmodbus
硬件连接测试例程
系统中已经安装好了 libmodbus 的库,硬件也完成了连接,现在写一个例程去调用 libmodbus 用 modbus RTU 协议读取温湿度传感器的数据,代码如下,因为是 USB 转 RS485,所以在系统中是 /dev/ttyUSB0
include <modbus/modbus.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> int main() { // 创建并初始化 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; }
执行命令如下,就可以读取到站号为1的温湿度传感器数据了,其中0和1地址的保持寄存器就是温度和湿度数据
gcc test.c -o test -lmodbus
./test
适配 SDL2 和 LVGL
SDL2 是一个跨平台的开发库,用于简化游戏和多媒体应用程序的开发,LVGL是近些年十分流行的 GUI 开发框架,在本项目中使用了 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
编译完成的样子
进入到树莓派的桌面环境,然后进入到 build 目录中执行 ./lv_examples,可以看到
LVGL 也适配完成了,然后就可以基于 LVGL 库绘制属于自己的 HMI 界面了
搭建网关 HMI
绘制网关 HMI方才已经完成了 libmodbus、SDL2、LVGL 的适配,接下来就是 HMI 界面的开发了,因为我连接的是两个温湿度传感器,所以这里做了一个对于两个从站的数据采集界面,如果按照商用网关的标准是需要实现增删改查的功能,提供给用户自己对从站的数量、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 库来创建一个定时器如下
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; // 传递给回调函数的参数 // 创建定时器 if (timer_create(CLOCK_REALTIME, &sev, &timer_id) == -1) { perror("timer_create"); exit(EXIT_FAILURE); } // 设置定时器:首次触发延迟 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) { // 这里写定时器每次触发时执行的代码 printf("定时器触发,执行任务...\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 界面的数据更新任务,编译运行结果如下
程序源码
以下为视频演示: