三类传感器,三种协议方言
PipeMonitor 接入了三类 Modbus 传感器,它们的寄存器布局、数据编码、通道数量各不相同:
| 传感器 | 从站地址 | 功能码 | 数据编码 |
|---|---|---|---|
| 超声波流量计 | 0x02 | FC03 保持寄存器 | REAL4 (IEEE754) |
| PXW 温压一体 | 0x03 | FC03 保持寄存器 | 整数 + 小数位寄存器 |
| PT100 多通道 | 0x04/0x05 | FC04 输入寄存器 | 有符号 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标志位