三类传感器,三种协议方言

PipeMonitor 接入了三类 Modbus 传感器,它们的寄存器布局、数据编码、通道数量各不相同:

传感器从站地址功能码数据编码
超声波流量计0x02FC03 保持寄存器REAL4 (IEEE754)
PXW 温压一体0x03FC03 保持寄存器整数 + 小数位寄存器
PT100 多通道0x04/0x05FC04 输入寄存器有符号 16 位 *10℃

如果在业务代码里直接写 Modbus_ServiceReadRegisters(addr=2, reg=1, ...) 这样的调用,业务层就会被寄存器地址绑死,换一种流量计就得改业务代码。

所以设计了设备驱动层,把每种传感器的”方言”翻译成统一的”普通话”。

流量计驱动

超声波流量计一次 Flowmeter_Update 调用返回三个值:瞬时流量、累积流量、流速。

uint8_t Flowmeter_Update(FlowmeterHandle *handle,
                         float *flow_lpm,
                         float *total_l,
                         float *velocity_ms)
{
    float instant_flow_m3h;
    float total_m3;

    if (Flowmeter_ReadInstantFlow(&instant_flow_m3h) != 0)
        return 0U;

    if (Flowmeter_ReadPositiveTotal(&total_m3) != 0)
        return 0U;

    if (Flowmeter_ReadVelocity(&velocity_value_ms) != 0)
        velocity_value_ms = 0.0f;  /* 流速失败不影响主数据 */

    /* 单位换算:m³/h → L/min */
    handle->flow_lpm = instant_flow_m3h * (1000.0f / 60.0f);
    handle->total_l = total_m3 * 1000.0f;

    return 1U;  /* 成功 */
}

内部实现涉及 REAL4 解析(后面会专门讲字节序问题),但业务层完全不需要知道这些。

PXW 温压一体驱动

PXW 传感器的寄存器布局比较特殊:温度和压力共用一个小数位寄存器(Reg 25),值本身是整数,需要除以 10^decimal_places 得到真实值。

static float PXW_ApplyUnsignedDecimal(uint16_t raw_value, uint16_t decimal_places)
{
    float scale = 1.0f;
    while (decimal_places > 0U)
    {
        scale *= 10.0f;
        decimal_places--;
    }
    return ((float)raw_value) / scale;
}

对外接口只有两个函数:

int Temperature_Read(float *temperature_value);  /* 返回 ℃ */
int Pressure_Read(float *pressure_value);        /* 返回 MPa */

业务层调用 Temperature_Read(&temperature) 就拿到温度,不需要知道寄存器 25 存的是小数位、寄存器 40 存的是温度原始值。

PT100 多通道驱动

PT100 模块比较灵活:一个从站可以有 2 通道或 4 通道。用配置结构体来描述:

typedef struct
{
    uint8_t  slave_addr;
    uint8_t  channel_count;
    uint16_t input_reg_base;
    uint32_t comm_timeout_ms;
} pt100_config_t;

驱动支持单通道读取和批量读取:

/* 单通道 */
int pt100_read_channel_celsius(dev, channel, &temp_c);

/* 全部通道一次性读取 */
int pt100_read_all_deci_c(dev, temperature_deci_c);

批量读取利用了 Modbus 的连续读取能力——一次 FC04 请求读 N 个寄存器,比 N 次单通道读取快得多。

无效值的统一表示

三种传感器的故障码不同:

  • 流量计:读取失败返回 return 0U(调用方判断返回值)
  • PXW:寄存器读取失败返回负数错误码
  • PT100:原始值 0xFFFF 表示传感器故障,解析后用 INT16_MIN 表示无效

在业务层统一用 app_measurement_t 结构体的 _valid 标志位来表示:

typedef struct
{
    float flow_lpm;
    rt_uint8_t flow_valid;     /* 0 = 无效,1 = 有效 */

    float temperature;
    rt_uint8_t temp_valid;

    int16_t pt100_4_values[2];
    /* 无效通道值为 INT16_MIN */
} app_measurement_t;

显示层和上行层只需要检查 _valid 或比较 != INT16_MIN,不需要关心底层传感器的具体故障码。

通道映射的集中管理

现场有两块 PT100 模块:从站 0x04 有 2 通道,从站 0x05 有 4 通道。通道数在 app_measurement.h 里集中定义:

#define APP_PT100_SLAVE4_CHANNELS    2U
#define APP_PT100_SLAVE5_CHANNELS    4U

上行编码器、显示线程、存储线程都引用这个宏,避免硬编码数字散落在各处。如果现场换了 4+8 通道的配置,只改这一处即可。

采集顺序与总线节奏

采集线程按固定顺序轮询:流量计 → 温压 → PT100_4 → PT100_5,每次切换设备之间留 30ms 间隔:

Flowmeter_Update(&g_flow_meter, &flow_lpm, &total_l, &velocity_ms);
rt_thread_mdelay(30);

Temperature_Read(&temperature);
rt_thread_mdelay(30);

Pressure_Read(&pressure);
rt_thread_mdelay(30);

pt100_read_all_deci_c(&g_pt100_slave4, pt100_4_values);
rt_thread_mdelay(30);

pt100_read_all_deci_c(&g_pt100_slave5, pt100_5_values);

这个间隔是联调时验证过的——给 RS485 总线和从站留出切换/响应缓冲时间。太快会导致某些从站来不及响应。

小结

设备驱动层的价值在于:

  • 隔离变化:换传感器只改驱动,业务代码不动
  • 简化业务Flowmeter_Update 一次调用拿三个值,比三次寄存器读取清晰
  • 统一错误处理:不同传感器的故障码统一成 _valid 标志位

后续阅读