• 回复
  • 收藏
  • 点赞
  • 分享
  • 发新帖

【 DigiKey DIY原创大赛】基于树莓派的 Modbus网关

引言

在工业控制中,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

硬件连接

硬件使用了两个 Modbus 温湿度传感器,其中一个站号设置为 0x1,另一个设置成了 0x2,将两个Modbus 温湿度模块和制作的 USB 转 RS485/RS232 模块连接到一条 Modbus 总线的 A、B上面,树莓派通过 USB 连接到 USB 转 RS485/RS232 模块

测试例程

系统中已经安装好了 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 界面的数据更新任务,编译运行结果如下

程序源码

lv_modbus_master.zip

以下为视频演示:

 

全部回复(0)
正序查看
倒序查看
现在还没有回复呢,说说你的想法