Skip to main content

使用 CycBox 通过 Modbus RTU 调试 Senseair S8 NDIR CO2 传感器

在本案例研究中,我们通过 3.3V UART TTL 连接,使用 Modbus RTU 协议与 Senseair S8 商用 NDIR CO2 传感器成功建立通信。借助 CycBox 的串口传输层和 Modbus RTU 编解码器,我们详细解析了设备初始化序列,验证了连续测量轮询循环,并解码了 CO2 ppm 读数及关键系统诊断标志。

CycBox Senseair S8 Debugging Dashboard

被测设备

  • 型号 / 厂商 / 类别:Senseair S8 Commercial(Article No. 004-0-0010, 004-0-0075)/ Senseair / NDIR CO2 传感器
  • 在系统中的作用:测量环境 CO2 浓度,用于 HVAC 控制、室内空气质量监测和安全系统。
  • 电气接口:UART 3.3V CMOS 逻辑(如用于长总线需外接 RS-485 收发器)。
  • 供电需求:供电电压(VCC/G+)4.5V – 5.25V。传感器平均电流 30 mA,但在测量灯循环期间需处理 300 mA 峰值电流。
  • 关键数据手册规格
    • 测量范围:400 – 2000 ppm(标准精度),最高可报告至 10000 ppm(扩展)。
    • 精度:±30 ppm ±读数的 3%。
    • 测量间隔:2 秒。
    • 自动基线校正(ABC):默认 8 天。
  • 参考文档PSP103.pdf(硬件规格),TDE2067.pdf(Modbus 协议参考)。

线路协议与 CycBox 协议栈

Senseair S8 严格作为 Modbus RTU 从机通过串口连接工作。

  • 传输层serial_port_transport,配置为 /dev/ttyACM0,9600 波特,8 数据位,无校验,1 停止位。注意:数据手册规定传感器以 1 个停止位接收,但以 2 个停止位发送。实际上,将 CycBox(或主机 MCU)配置为 1 个停止位可完美工作,因为传感器发送的额外停止位会融入帧间空闲时间。
  • 编解码器modbus_rtu_codec,接收超时设为 20 ms。

CycBox Senseair S8

协议详解:Senseair S8 上的 Modbus RTU

该传感器实现了 Modbus RTU 的一个子集,并有严格的负载限制。它支持读保持寄存器(0x03)、读输入寄存器(0x04)和写单个保持寄存器(0x06)。关键在于,传感器能处理的最大数据包长度为 39 字节(含地址和 CRC)。任何超过此长度的帧都将被静默丢弃。

虽然单个传感器有特定的节点地址(1–247),但地址 2540xFE)充当"任意传感器"广播地址。这对于点对点调试或在设备出厂地址未知时初始化传感器非常有用。

下表列出了 Senseair S8 重要 Modbus 寄存器及其功能、类型和缩放因子。

地址(十六进制)Modbus 类型字段名称描述刻度 / 单位
0x0000输入寄存器 (04)MeterStatus系统故障和错误位域位域(0x0000 = 正常)
0x0002输入寄存器 (04)Output Status离散告警和 PWM 状态位域
0x0003输入寄存器 (04)Space CO2当前 CO2 浓度测量值1 ppm
0x001B输入寄存器 (04)Map Version固件内存映射版本原始整数
0x001C输入寄存器 (04)Firmware Version主.次版本(高/低字节)高字节=主版本,低字节=次版本
0x001D输入寄存器 (04)Sensor ID High序列号高 16 位原始整数
0x001E输入寄存器 (04)Sensor ID Low序列号低 16 位原始整数
0x0000保持寄存器 (03)Acknowledgement校准执行标志位域
0x0001保持寄存器 (03)Special Command触发背景/零点校准0x7C06 = 背景校准
0x001F保持寄存器 (03)ABC Period自动基线校正间隔小时(0 = 禁用)

CycBox 配置流程

传输层与编解码器配置

[
{
"app": {
"app_transport": "serial_port_transport",
"app_codec": "modbus_rtu_codec",
"app_transformer": "disable_transformer",
"app_encoding": "UTF-8"
},
"serial_port_transport": {
"serial_port_transport_parity": "none",
"serial_port_transport_port": "/dev/ttyACM0",
"serial_port_transport_stop_bits": "1",
"serial_port_transport_flow_control": "none",
"serial_port_transport_baud_rate": 9600,
"serial_port_transport_data_bits": 8
},
"modbus_rtu_codec": {
"with_receive_timeout": 20
}
}
]

Lua 脚本

以下 Lua 脚本编排了 Modbus 通信流程。启动时,它查询静态设备标识和 ABC 校准周期。随后启动一个每 2 秒触发一次的定时器,以获取当前 CO2 测量值和系统健康状态。

local SLAVE_ADDR = 254
local POLL_MS = 2000
local timer_ms = 0

function on_start()
log("info", "Starting Senseair S8 Modbus polling script.")

-- 启动时一次性读取设备信息和配置
-- 从 0x001B 开始读取 4 个寄存器:映射版本、固件版本、传感器 ID 高位、传感器 ID 低位
modbus_rtu_read_input_registers(SLAVE_ADDR, 0x001B, 4, 100, 0)
-- 读取 0x001F 处的 1 个保持寄存器:ABC 周期
modbus_rtu_read_holding_registers(SLAVE_ADDR, 0x001F, 1, 200, 0)
end

function on_timer(now_ms)
timer_ms = timer_ms + 100

-- 每 2 秒定期读取活动数据(CO2、状态、告警)
if timer_ms >= POLL_MS then
-- 从 0x0000 开始读取 4 个寄存器:MeterStatus、AlarmStatus、OutputStatus、SpaceCO2
modbus_rtu_read_input_registers(SLAVE_ADDR, 0x0000, 4, 0, 0)
timer_ms = 0
end
end

function on_receive()
if message.connection_id ~= 0 then return false end
local modified = false

-- 解析 Space CO2(协议地址 0x0003)
local co2 = message:get_value(string.format("modbus_rtu_%d:input_0003", SLAVE_ADDR))
if co2 then
message:add_int_value("CO2_ppm", co2)
modified = true
end

-- 解析 MeterStatus(协议地址 0x0000)
local status = message:get_value(string.format("modbus_rtu_%d:input_0000", SLAVE_ADDR))
if status then
message:add_int_value("MeterStatus", status)
if status ~= 0 then
local fatal = bit.band(status, 0x01)
local out_of_range = bit.band(status, 0x20)
if fatal > 0 then log("error", "Sensor reported FATAL ERROR (bit 0)") end
if out_of_range > 0 then log("warn", "Sensor reading OUT OF RANGE (bit 5)") end
end
modified = true
end

-- 解析 OutputStatus / 告警(协议地址 0x0002)
local out_status = message:get_value(string.format("modbus_rtu_%d:input_0002", SLAVE_ADDR))
if out_status then
-- 告警状态在原始值中为反逻辑,但输出位 0 反映离散告警
local alarm = (bit.band(out_status, 0x01) > 0)
message:add_bool_value("Alarm_Active", alarm)
modified = true
end

-- 解析固件版本(协议地址 0x001C)
local fw = message:get_value(string.format("modbus_rtu_%d:input_001C", SLAVE_ADDR))
if fw then
local main_ver = bit.rshift(fw, 8)
local sub_ver = bit.band(fw, 0xFF)
message:add_string_value("Firmware_Version", string.format("%d.%d", main_ver, sub_ver))
modified = true
end

-- 解析传感器 ID(合并协议地址 0x001D 和 0x001E)
local id_hi = message:get_value(string.format("modbus_rtu_%d:input_001D", SLAVE_ADDR))
local id_lo = message:get_value(string.format("modbus_rtu_%d:input_001E", SLAVE_ADDR))
if id_hi and id_lo then
local sensor_id = id_hi * 65536 + id_lo
message:add_int_value("Sensor_ID", sensor_id)
modified = true
end

-- 解析 ABC 周期(协议地址 0x001F)
local abc = message:get_value(string.format("modbus_rtu_%d:holding_001F", SLAVE_ADDR))
if abc then
message:add_int_value("ABC_Period_Hours", abc)
modified = true
end

return modified
end

调试过程

步骤 1 — 通过广播地址验证设备标识和固件

  • 目标:确认物理层正常,并读取传感器固件版本。由于具体从机 ID 未知,我们使用通用地址 0xFE(254)。
  • 我们做了什么:向 0x001B 发送读输入寄存器(0x04)请求,读取 4 个寄存器,以获取映射版本、固件版本和 32 位传感器 ID。
  • 设备返回
    TX: fe 04 00 1b 00 04 95 c1
    RX: fe 04 08 00 31 01 5c 07 54 46 74 94 e6
  • 诊断:成功。有效载荷(00 31 01 5C 07 54 46 74)解码顺利。固件版本寄存器(0x001C)含有 0x015C,解析为 1.92(主版本 0x01,次版本 0x5C)。传感器 ID(0x0754 拼接 0x4674)对应十进制 122963572

步骤 2 — 读取自动基线校正(ABC)周期

  • 目标:验证出厂基线校准设置。
  • 我们做了什么:向 0x001F 发送读保持寄存器(0x03)请求,读取 1 个寄存器。
  • 设备返回
    TX: fe 03 00 1f 00 01 a1 c3
    RX: fe 03 02 00 b4 ac 27
  • 诊断:返回的有效载荷 0x00B4 十进制等于 180。由于单位为小时,180 小时正好等于 7.5 天,证实 ABC 功能处于激活状态且配置正确。

步骤 3 — 执行 2 秒遥测轮询循环

  • 目标:持续读取当前 CO2 ppm 值及系统状态标志。
  • 我们做了什么:向 0x0000 发送读输入寄存器(0x04)请求,读取 4 个寄存器,在单次事务中同时获取 MeterStatusAlarmStatusOutputStatusSpace CO2
  • 设备返回
    TX: fe 04 00 00 00 04 e5 c6
    RX: fe 04 08 00 00 00 00 00 01 02 06 c7 b8
  • 诊断:执行完美。
    • 字节 0-1(00 00):MeterStatus0,表示无内部故障。
    • 字节 4-5(00 01):OutputStatus 位 0 为高,表示正常运行。
    • 字节 6-7(02 06):Space CO2 十进制等于 518。传感器测量到环境 CO2 浓度为 518 ppm。此序列每 2 秒可预测地重复一次。

观察到的行为与验证

  • 时序约束:2 秒轮询间隔严格遵守数据手册的测量周期。更快的轮询不会产生新的 CO2 数据,且会占用不必要的总线时间。传感器对所有有效查询的响应时间始终在 40 ms 以内。
  • 广播寻址:使用 0xFE 无需知道出厂从机地址即可通信。注意,如果同一 RS-485 总线上有多个 Senseair S8 传感器,查询 0xFE 会导致数据包冲突,因为所有传感器会同时应答。
  • 电压差异:虽然串行线路工作在 3.3V CMOS 逻辑电平,但电源必须是能够吸收短暂 300 mA 峰值电流的非稳压 5V 电源。

移植到 MCU 固件(设计参考,无代码)

要为 Senseair S8 构建生产级驱动程序,固件工程师应围绕以下准则进行设计。

  • 推荐 MCU 系列:STM32G0/F4、ESP32 或 NXP i.MX RT。MCU 需要一个 UART 外设和一个用于轮询循环的定时器。

下表详细说明了串行接口所需的 MCU 外设具体设置。

外设参数必需设置备注
波特率9600 bps出厂固定,不可更改。
数据位8标准 Modbus RTU。
校验位标准 Modbus RTU。
停止位1传感器发送 2 个停止位,MCU 使用 1 个即可。
逻辑电平3.3V TTL推荐使用 5V 容限引脚,但非强制要求。
DE / RE 控制GPIO 切换仅在驱动外部 RS-485 收发器时需要。

设备初始化序列

  1. 上电延时:在施加 5V 电源后,至少等待 2 秒再发送第一帧,以允许传感器启动并完成内部自检。
  2. 标识同步:向地址 0x001B 发送读输入寄存器(0x04)请求,以获取固件版本并建立总线通信。如果已分配 ID 未知,使用节点 ID 0xFE
  3. 验证 ABC 配置:向 0x001F 发送读保持寄存器(0x03)请求。如果应用环境从不达到 400 ppm(如温室),应通过向此寄存器写入 0x0000 来禁用 ABC。
  4. 进入轮询:进入操作状态机。

消息发送与帧构造

当组装用于读取 CO2 的 Modbus 帧时,固件必须为目标地址 0xFE读输入寄存器命令生成以下精确的字节序列:

  • 偏移 0(从机 ID)0xFE
  • 偏移 1(功能码)0x04
  • 偏移 2-3(起始地址)0x000x00
  • 偏移 4-5(寄存器数量)0x000x04
  • 偏移 6-7(CRC)0xE50xC6(使用 CRC16-IBM 计算,多项式 0xA001,以 LSB 优先发送)。

帧间静默:在发送新帧之前,确保至少 3.5 个字符时间(9600 波特下约 4 ms)的延迟,以重置传感器的 Modbus 状态机。

错误处理与状态机

下表概述了一个简单的应用状态机,用于处理 2 秒 CO2 轮询生命周期及异常情况。

状态事件 / 触发条件下一状态 / 动作
INIT上电等待 2000 ms,进入 IDENTIFY
IDENTIFY固件读取成功进入 POLL_READY
POLL_READY2000 ms 定时器到期发送读输入寄存器(0-3),进入 WAIT_RX
WAIT_RX收到有效 Modbus 帧解析 MeterStatus。若正常,解码 CO2 ppm。进入 POLL_READY
WAIT_RX接收超时(> 180 ms)失败计数器递增。进入 POLL_READY
WAIT_RXMeterStatus 位 0(致命错误)为 1触发告警系统。需要物理断电重启。
WAIT_RXMeterStatus 位 5(超出范围)为 1向遥测系统标记"超出范围"。测量值无效。

常见问题

Senseair S8 默认使用哪个 Modbus 地址?

出厂时具体地址可能有所不同,但传感器始终响应广播地址 2540xFE)。只要它是 UART/RS-485 总线上的唯一设备,您可以立即使用此地址与其通信。

为什么传感器静默忽略我的 Modbus 读取请求?

Senseair S8 有严格的 39 字节最大缓冲区限制。如果您在单次事务中尝试读取过多寄存器(例如一次读取 20 个保持寄存器),所需的响应帧将超过 39 字节,传感器会直接丢弃该请求,而不会发出异常。请将每次读取的寄存器数量限制在 8 个或更少。

如何检查传感器是否处于错误状态?

读取输入寄存器 0x0000MeterStatus)。值为 0x0000 表示传感器正常。如果值非零,请检查具体位:位 0 表示需要重启的致命错误,位 5 表示 CO2 读数超出范围。

为什么我的 CO2 读数在几周内缓慢升高?

如果传感器从未接触到新鲜室外空气(400 ppm),自动基线校正(ABC)算法将错误地重新校准基线,导致读数偏高。如果您的环境持续有人占用或人为富集 CO2,必须通过向保持寄存器 0x001F 写入 0 来禁用 ABC。

注意事项与建议

  • 停止位不对称:数据手册规定接收使用 1 个停止位,发送使用 2 个停止位。如果您的 MCU UART 无法处理不对称停止位,将外设设置为 8-N-1 完全没问题。传感器发送的额外停止位仅作为总线空闲时间。
  • 电源电流峰值:NDIR 灯每 2 秒吸收 300 mA 电流。如果从无法提供此峰值电流的 MCU 5V 引脚为传感器供电,MCU 可能发生欠压复位。请确保传感器 VCC 引脚附近有足够的大容量滤波电容。
  • 写入限制:传感器仅支持 Modbus 功能码 0x06(写单个寄存器),不支持 0x10(写多个寄存器)。固件必须每次写入一个寄存器来配置参数。
  • 现场校准:如果执行手动背景校准(向保持寄存器 0x0001 写入),固件必须持续轮询确认寄存器(0x0000),以在恢复正常轮询前验证校准流程确实已完成。