本文以Air780EHV 系列模组为例,讲解工业通信中 485 总线的硬件设计细节及 Modbus 协议在 LuatOS 的开发应用,介绍了 485 总线与 UART 的关联、电平匹配的处理方式和不同防护等级的 TVS 器件选型,还提及为降低开发难度推出的 exmodbus 扩展库,并给出了该库实现 Modbus RTU 主站通信的代码示例及相关文档查阅渠道。

Air780EHV 系列模组的相关参数说明如下:

  • 该模组可适配多种外设,包含 SPI 串口屏、墨水屏、OLED 单色屏、30 万像素摄像头,同时具备 CAN、RJ45 以太网、485、USB、UART、SPI、I2C、PWM、GPIO 等接口。
  • 网络通信方面,模组支持 TCP/UDP、TCP-SSL/TCP-TLS、FTP、MQTT、HTTP、WebSocket、NTP、Modbus 协议。
  • 模组集成 4G 通信与音频模块,可实现语音通话、录音播放以及 TTS 功能。

工业通信中非常经典的485总线,硬件设计中需要注意的细节,Modbus协议在LuatOS开发中的应用,详见下文。请添加图片描述

一、485总线接口与UART的关系

485总线接口本质上是UART总线接口的一种应用,需要搭配485收发器芯片实现。

最新参考设计文件详见资料中心。

下图为典型的参考设计:

请添加图片描述

二、电平匹配问题

在UART与485收发器芯片的搭配中,最常见需要注意的一个问题是电平匹配。

  • 由于上一章节参考设计中Air780EHV和SP3485都是3.3V的IO电平,所以不再需要分立元器件电平转换电路或电平转换芯片。

  • 当双方电平不一致时,则需要分立元器件电平转换电路或电平转换芯片。

常见的分立元器件电平转换电路如下:

三、485总线接口的TVS防护

工业现场环境复杂,485总线经常面临静电、浪涌等威胁,因此接口保护必不可少。

485接口用TVS,常用的型号有SM712系列,如果防护等级要求较高,也可以选择如下推荐的型号。

  • ESD等级防护:适用于一般静电防护场景。型号:应能微ASM712

  • TVS等级防护:具备2KV 1.2/50uS浪涌能力。型号:应能微SMBJ7.0CAW

  • TSS等级防护:具备4KV 10/700uS浪涌能力。型号:应能微P0080SA

四、Modbus通信协议

与485总线接口相关的通信协议是Modbus。LuatOS的modbus核心库,但使用难度较高。而exmodbus扩展库——在核心库的基础上封装了更简洁易用的API,降低开发难度,易于开发者集成Modbus通信。

exmodbus最新API文档详见资料中心

核心示例持续更新中!

PROJECT = "RTU_MASTER"
VERSION = "001.000.000"

-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)

local exmodbus = require("exmodbus")

-- 使用 Air8000 开发板测试打开这两个
gpio.setup(16, 1)         -- RS485 芯片供电引脚
local rs485_dir_gpio = 17 -- RS485 方向引脚

-- 使用 Air780EPM 开发板测试打开这三个;
-- gpio.setup(1, 1)          -- Air780EPM RS485 芯片供电引脚
-- gpio.setup(23, 1)         -- Air780EPM vref 脚拉高
-- local rs485_dir_gpio = 24 -- Air780EPM RS485 方向引脚(V1.2 是 25,V1.3 是 24)

-- 创建 RTU 主站配置参数;
-- 说明:创建 RTU 主站时只需要配置如下参数即可;
local create_config = {
    -- 串口配置参数;
    mode = exmodbus.RTU_MASTER,      -- 通信模式
    uart_id = 1,                     -- UART 端口号
    baud_rate = 115200,              -- 波特率
    data_bits = 8,                   -- 数据位
    stop_bits = 1,                   -- 停止位
    parity_bits = uart.None,         -- 校验位
    byte_order = uart.LSB,           -- 字节顺序
    rs485_dir_gpio = rs485_dir_gpio, -- RS485 方向引脚
    rs485_dir_rx_level = 0,          -- RS485 接收方向电平
}

-- 初始化从站 1 数据结构
-- 用于记录从站 1 保持寄存器 0-1 的值;
local slave1_data = {}

-- 配置读取从站 1 保持寄存器 0-1 的值;
local read_config = {
    raw_request = string.char(
        0x01,       -- 从站地址
        0x03,       -- 功能码:读取保持寄存器
        0x00, 0x00, -- 寄存器起始地址
        0x00, 0x02, -- 寄存器数量
        0xC4, 0x0B  -- CRC16校验码
    ),
    timeout = 1000  -- 超时时间 1000 ms
}

-- 创建 RTU 主站实例
local rtu_master = exmodbus.create(create_config)

-- 判断主站是否创建成功并记录日志
if not rtu_master then
    log.info("exmodbus_test", "rtu_master 创建失败")
else
    log.info("exmodbus_test", "rtu_master 创建成功")
end

-- 读取从站 1 保持寄存器数据的函数
local function read_slave1_holding_registers()
    log.info("exmodbus_test", "开始读取从站 1 保持寄存器 0-1 的值")

    -- 执行读取操作
    local read_result = rtu_master:read(read_config)

    -- 根据返回状态处理结果
    if read_result.status == exmodbus.STATUS_SUCCESS then
        local resp = read_result.raw_response

        -- 特别说明:
        -- 接下来的判断是针对 modbus RTU 标准响应格式的应答原始帧来解析的
        -- 在实际项目中,应根据自己项目中的实际应答原始帧格式进行解析
        -- 如果实际格式与此处演示的格式不一致,需要修改接下来的解析代码

        -- 1. 检查总长度:必须为 9 字节(1 地址 + 1 功能码 + 1 字节数 + 4 数据 + 2 CRC)
        if #resp ~= 9 then
            log.info("exmodbus_test", "响应长度错误,期望 9 字节,实际:", #resp)
            return
        end

        -- 2. 检查从站地址
        if string.byte(resp, 1) ~= 0x01 then
            log.info("exmodbus_test", "从站地址不匹配,收到:", string.byte(resp, 1))
            return
        end

        -- 3. 检查功能码
        local func_code = string.byte(resp, 2)
        if func_code == 0x83 then
            local exc_code = string.byte(resp, 3)
            log.info("exmodbus_test", "从站返回异常响应,异常码:", exc_code)
            return
        elseif func_code ~= 0x03 then
            log.info("exmodbus_test", "功能码错误,收到:", func_code)
            return
        end

        -- 4. 检查字节数字段(应为 4)
        local byte_count = string.byte(resp, 3)
        if byte_count ~= 4 then
            log.info("exmodbus_test", "字节数字段错误,期望 4,实际:", byte_count)
            return
        end

        -- 5. 校验CRC
        -- 计算前 7 字节的 CRC
        local crc_calculated = crypto.crc16_modbus(resp:sub(1, 7))
        -- 提取接收到的 CRC
        local crc_received = string.unpack("<I2", resp, 8)
        -- 比较 CRC
        if crc_calculated ~= crc_received then
            log.info("exmodbus_test", "CRC 校验错误,计算值:", crc_calculated, ",接收值:", crc_received)
            return
        end

        -- 6. 解析寄存器数据(从第 4 字节开始,大端序)
        local data1 = string.unpack(">I2", resp, 4) -- 寄存器 0,偏移 4
        local data2 = string.unpack(">I2", resp, 6) -- 寄存器 1,偏移 6

        -- 7. 记录数据
        slave1_data[0] = data1
        slave1_data[1] = data2

        -- 8. 记录日志
        log.info("exmodbus_test", "成功读取到从站 1 保持寄存器 0-1 的值,寄存器 0 数值为", slave1_data[0], ",寄存器 1 数值为", slave1_data[1])
    elseif read_result.status == exmodbus.STATUS_TIMEOUT then
        log.info("exmodbus_test", "未收到从站 1 的响应(超时)")
    end
end

-- 定时任务函数:每 2 秒调用一次读取函数
local function task()
    while true do
        if rtu_master then
            -- 每 2 秒调用一次读取函数
            read_slave1_holding_registers()
        else
            log.info("exmodbus_test", "rtu_master 未创建,无法执行 read_slave1_holding_registers()")
        end
        sys.wait(2000)
    end
end

-- 初始化任务
sys.taskInit(task)

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

以上为 485 总线硬件设计核心要点及 LuatOS 环境下 Modbus 协议应用的相关分享,内容涵盖 485 总线与 UART 的关系、电平匹配、TVS 防护以及 exmodbus 扩展库的优化内容,相关实操内容可供工程技术人员参考,用于解决实际应用中的相关问题。

今天的内容就分享到这里了。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐