RS485通信
概述
本章演示如何使用 YFAir8000 的 4 路 RS485 接口,通过 Modbus RTU 主站协议 轮询 YF3610-TH 温湿度传感器数据(1 路温度 + 1 路湿度)。
Modbus RTU 主站库来自开源项目 luatos-modbus-rtu,支持帧构建/解析、CRC16 校验、UART 收发及回调机制。
技术栈
- 运行环境:LuatOS(合宙 Air8000W 模组)
- 串口:UART1 / UART3 / UART11 / UART12(对应 RS485-1 ~ RS485-4)
- 协议:Modbus RTU 主站
- 从站地址:
0xFD,功能码0x03(读保持寄存器)
注意
- UART3 使用芯片引脚 25(RXD) / 26(TXD),需通过
pins_Air8000W.json配置引脚复用(见下方引脚配置) - UART11 / UART12 由 WiFi 芯片提供,需先烧录 WiFi 固件
程序流程
main.lua
│
├─ 1. require("ModbusRTU_Master") → 加载主站库
│
├─ 2. ModbusRTU_Master:new() ×4 → 创建 4 路主站实例
├─ 3. init({uartid=n, baudrate=9600}) → 初始化串口
│
├─ 4. set_receive_callback() → 注册接收回调(解析温湿度)
│
├─ 5. sys.taskInit() 发送任务 → 循环轮询
│ UART12 → UART11 → UART3 → UART1
│ 间隔 300ms,每轮间隔 1s
│
└─ 6. 接收回调 → 解析响应帧 → 大端序取温湿度
项目结构
02 - 4路485测试/
├── main.lua # 主程序入口
├── ModbusRTU_Master.lua # Modbus RTU 主站协议库
└── json/
└── pins_Air8000W.json # 芯片引脚定义(UART3 引脚复用依赖此文件)
复用引脚配置(pins_Air8000W.json)
YFAir8000 网关使用芯片引脚 25(RXD) / 26(TXD) 作为 UART3,这两个引脚默认并非串口功能,需通过 Luatools 的引脚复用功能配置(详见 合宙官方教程)。
操作步骤:
- 打开 Luatools → 项目设置 → 引脚复用
- 选择芯片型号,勾选
UART3功能 - 配置将自动生成到
json/pins_Air8000W.json,文件名和路径必须保持一致,Luatools 才能识别 文件名一定要是和路径最好和我的一样json下pins_Air8000W.json文件

核心代码详解
1. 串口初始化
LuatOS 中串口直接用 ID 号操作:
local modbus_uart1 = ModbusRTU_Master:new()
local modbus_uart3 = ModbusRTU_Master:new()
local modbus_uart11 = ModbusRTU_Master:new()
local modbus_uart12 = ModbusRTU_Master:new()
-- 默认配置:波特率9600, 8数据位, 1停止位, 无校验
modbus_uart1:init({uartid = 1, baudrate = 9600})
modbus_uart3:init({uartid = 3, baudrate = 9600})
modbus_uart11:init({uartid = 11, baudrate = 9600})
modbus_uart12:init({uartid = 12, baudrate = 9600})
2. 发送 Modbus 请求帧
-- 功能码 0x03:读保持寄存器
-- 从站地址 0xFD,起始地址 0x0000,读取 2 个寄存器
modbus_uart12:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
3. 接收回调与温湿度解析
local function create_recv_handler(uart_id)
return function(frame)
if frame.fun == 0x03 and #frame.payload >= 4 then
-- 大端序解析(高位在前)
local t = (frame.payload:byte(1) * 256 + frame.payload:byte(2)) / 10.0
local h = (frame.payload:byte(3) * 256 + frame.payload:byte(4)) / 10.0
log.info("UART" .. uart_id,
string.format("温度:%.1f℃ 湿度:%.1f%%RH", t, h))
end
end
end
modbus_uart1:set_receive_callback(create_recv_handler(1))
modbus_uart3:set_receive_callback(create_recv_handler(3))
modbus_uart11:set_receive_callback(create_recv_handler(11))
modbus_uart12:set_receive_callback(create_recv_handler(12))
4. 4 路轮询时序
sys.taskInit(function()
while true do
modbus_uart12:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(300)
modbus_uart11:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(300)
modbus_uart3:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(300)
modbus_uart1:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(1000) -- 每轮间隔 1 秒
end
end)
5. CRC16 校验与帧构建
-- CRC16 Modbus 计算
function ModbusRTU_Master:crc16(data)
return crypto.crc16_modbus(data)
end
-- 构建完整 Modbus 帧:地址 + 功能码 + 数据 + CRC
function ModbusRTU_Master:build_frame(addr, fun, data)
local frame = string.char(addr, fun) .. data
local crc = self:crc16(frame)
local pack_crc = pack.pack("H", crc) -- 大端打包
return frame .. pack_crc
end
全部代码
ModbusRTU_Master.lua
ModbusRTU_Master.lua
local ModbusRTU_Master = {}
local DEFAULT_CONFIG = {
uartid = 1,
baudrate = 9600,
databits = 8,
stopbits = 1,
parity = uart.None,
endianness = uart.LSB,
buffer_size = 1024,
}
function ModbusRTU_Master:new()
local obj = {}
setmetatable(obj, self)
self.__index = self
return obj
end
function ModbusRTU_Master:init(config)
config = config or {}
for key, default_value in pairs(DEFAULT_CONFIG) do
if config[key] == nil then config[key] = default_value end
end
self.config = config
uart.setup(
config.uartid, config.baudrate, config.databits,
config.stopbits, config.parity, config.endianness,
config.buffer_size, config.gpio_485,
config.rx_level, config.tx_delay
)
log.info("ModbusRTU_Master init:", json.encode(config))
end
function ModbusRTU_Master:crc16(data)
return crypto.crc16_modbus(data)
end
function ModbusRTU_Master:parse_frame(data)
if not data or #data < 5 then
return nil, "帧长度不足"
end
local addr = data:byte(1)
local fun = data:byte(2)
local byte_len = data:byte(3)
if #data < (3 + byte_len + 2) then
return nil, "数据域不完整"
end
local payload = data:sub(4, 4 + byte_len - 1)
local crc_data = data:sub(-2, -1)
local idx, crc_received = pack.unpack(crc_data, "H")
local crc_calculated = self:crc16(data:sub(1, -3))
if crc_received == crc_calculated then
return {
addr = addr, fun = fun, byte = byte_len,
payload = payload, crc = crc_received
}
else
return nil, "CRC 校验失败"
end
end
function ModbusRTU_Master:build_frame(addr, fun, data)
local frame = string.char(addr, fun) .. data
local crc = self:crc16(frame)
local pack_crc = pack.pack("H", crc)
return frame .. pack_crc
end
function ModbusRTU_Master:send_command(addr, fun, data, interval)
local uartid = self.config.uartid
local cmd = self:build_frame(addr, fun, data)
if interval then
sys.timerLoopStart(function()
uart.write(uartid, cmd)
end, interval * 1000)
else
uart.write(uartid, cmd)
end
end
function ModbusRTU_Master:set_receive_callback(callback)
local uartid = self.config.uartid
uart.on(uartid, "receive", function(id, len)
local data = uart.read(id, len or 0)
if #data > 0 then
local frame, err = self:parse_frame(data)
if frame then
callback(frame)
else
log.warn("Modbus解析失败", err)
end
end
end)
end
function ModbusRTU_Master:set_sent_callback(callback)
uart.on(self.config.uartid, "sent", function(id)
if callback then callback(id) end
end)
end
return ModbusRTU_Master
main.lua
main.lua
-- 从上往下分别是串口12,串口11,串口3,串口1
-- 使用uart11、12之前需要先烧录wifi固件
PROJECT = "modbus_rtu_gateway"
VERSION = "1.0.0"
local sys = require("sys")
local ModbusRTU_Master = require("ModbusRTU_Master")
if wdt then
wdt.init(9000)
sys.timerLoopStart(wdt.feed, 3000)
end
log.setLevel(log.LOG_INFO)
-- 创建4个Modbus主站实例
local modbus_uart1 = ModbusRTU_Master:new()
local modbus_uart3 = ModbusRTU_Master:new()
local modbus_uart11 = ModbusRTU_Master:new()
local modbus_uart12 = ModbusRTU_Master:new()
modbus_uart1:init({uartid = 1, baudrate = 9600})
modbus_uart3:init({uartid = 3, baudrate = 9600})
modbus_uart11:init({uartid = 11, baudrate = 9600})
modbus_uart12:init({uartid = 12, baudrate = 9600})
local sensor_data = {
uart1 = {t = 0, h = 0},
uart3 = {t = 0, h = 0},
uart11 = {t = 0, h = 0},
uart12 = {t = 0, h = 0}
}
local function create_recv_handler(uart_id)
return function(frame)
if frame.fun == 0x03 and #frame.payload >= 4 then
local t = (frame.payload:byte(1) * 256 + frame.payload:byte(2)) / 10.0
local h = (frame.payload:byte(3) * 256 + frame.payload:byte(4)) / 10.0
sensor_data["uart" .. uart_id].t = t
sensor_data["uart" .. uart_id].h = h
log.info("UART" .. uart_id,
string.format("温度:%.1f℃ 湿度:%.1f%%RH", t, h))
end
end
end
modbus_uart1:set_receive_callback(create_recv_handler(1))
modbus_uart3:set_receive_callback(create_recv_handler(3))
modbus_uart11:set_receive_callback(create_recv_handler(11))
modbus_uart12:set_receive_callback(create_recv_handler(12))
sys.taskInit(function()
while true do
modbus_uart12:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(300)
modbus_uart11:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(300)
modbus_uart3:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(300)
modbus_uart1:send_command(0xFD, 0x03, string.char(0x00, 0x00, 0x00, 0x02))
sys.wait(1000)
end
end)
sys.run()