PT100 模块的编码特殊在哪

PipeMonitor 用 PT100 模块测量管道多点温度,通过 Modbus RTU 接入 STM32F103VC。和 PXW 温压一体传感器不同,PT100 模块返回的不是普通的有符号整数,而是一种”符号位 + 绝对值”的自定义编码:

  • 一个 16 位寄存器就是一个通道的温度
  • 单位是 0.1℃,所以 231 表示 23.1℃
  • 最高位(0x8000)是符号位,1 表示负温度
  • 低 15 位是绝对值,不是补码

这意味着不能用 C 语言的 (int16_t)raw 直接强转——负温度会错得离谱。比如 0x8005 在补码下是 -32763,但在 PT100 编码下应该是 -5(即 -0.5℃)。

驱动层必须显式解析这种编码,并处理两种”无效”情况:模块自报的传感器故障、本轮总线读取失败。

原始值编码:符号位 + 绝对值

PT100 模块的 16 位原始值布局:

bit15      bit14 ... bit0
符号位     绝对值(15 位)

解析逻辑在 pt100_device.c 里:

/* 把 PT100 模块返回的 16 位原始值解析成 0.1℃ 单位的有符号整数。
   原始值高位是符号位(1 表示负),低 15 位是绝对值。 */
static int pt100_parse_raw_to_deci_c(uint16_t raw_value, int16_t *temperature_deci_c)
{
    int16_t magnitude;

    if (temperature_deci_c == NULL)
    {
        return -1;
    }

    if (raw_value == PT100_SENSOR_FAULT_RAW)
    {
        return -2;
    }

    magnitude = (int16_t)(raw_value & 0x7FFFU);

    if ((raw_value & 0x8000U) != 0U)
    {
        *temperature_deci_c = (int16_t)(-magnitude);
    }
    else
    {
        *temperature_deci_c = magnitude;
    }

    return 0;
}

关键三步:

  1. raw_value & 0x7FFFU 取低 15 位作为绝对值
  2. raw_value & 0x8000U 判断符号位
  3. 负温度时取相反数,正温度直接赋值

举例:

原始值符号位绝对值解析结果(0.1℃)物理温度
0x00E70231+231+23.1℃
0x80E71231-231-23.1℃
0x00000000.0℃
0x8000100(负零)0.0℃

注意 0x8000 这种”负零”是合法值,解析结果就是 0,不能误判为故障。

故障哨兵:PT100_SENSOR_FAULT_RAW

PT100 模块在通道断线、传感器损坏时会返回一个固定的特殊值 0xFFFF 表示故障:

/* PT100 模块固定的特殊原始值/无效值常量。
   raw == 0xFFFF 表示传感器故障;浮点用 9999.9 表示无效温度。 */
#define PT100_SENSOR_FAULT_RAW       0xFFFFU
#define PT100_INVALID_TEMPERATURE_C  9999.9f

为什么选 0xFFFF?因为正常编码下,0xFFFF 的符号位是 1、绝对值是 0x7FFF = 32767,对应 -3276.7℃,这个温度物理上不可能出现,所以模块厂商拿它当哨兵不会和真实测量值冲突。

解析函数在取绝对值之前先检查这个哨兵:

if (raw_value == PT100_SENSOR_FAULT_RAW)
{
    return -2;
}

返回 -2 表示”传感器自身故障”,和”参数错误”(-1)区分开。调用方可以根据返回码决定是否触发 APP_ALARM_CODE_SENSOR_FAULT 告警。

浮点层面用 PT100_INVALID_TEMPERATURE_C = 9999.9f 表示无效温度,供显示层判断。这个值同样在物理上不可能,不会和真实测量冲突。

通道号到寄存器地址的映射

PT100 模块的通道数据存放在连续的输入寄存器里,每通道占 1 个寄存器。驱动用 input_reg_base 配置起始地址,通道号从 1 开始:

static int pt100_channel_to_register(const pt100_device_t *dev, uint8_t channel, uint16_t *reg_addr)
{
    if ((dev == NULL) || (reg_addr == NULL))
    {
        return -1;
    }

    if ((channel == 0U) || (channel > dev->config.channel_count))
    {
        return -2;
    }

    *reg_addr = dev->config.input_reg_base + (uint16_t)(channel - 1U);
    return 0;
}

注意 (channel - 1U) 这个偏移:通道 1 对应 input_reg_base,通道 2 对应 input_reg_base + 1,以此类推。通道号是 1-based,寄存器偏移是 0-based,这是 Modbus 设备的常见约定。

通道号边界检查用 channel > dev->config.channel_count,这样不同通道数的模块(2 通道、4 通道)共用同一套校验逻辑,不需要硬编码。

Modbus 读取与单通道解析

底层的 Modbus 读取封装在 pt100_read_input_registers 里,使用 FC04(读输入寄存器)功能码:

static int pt100_read_input_registers(const pt100_device_t *dev,
                                      uint16_t start_addr,
                                      uint16_t quantity,
                                      uint16_t *dest)
{
    if ((dev == NULL) || (dest == NULL) || (quantity == 0U))
    {
        return -1;
    }

    if (Modbus_ServiceReadRegisters(dev->config.slave_addr,
                                    MODBUS_MASTER_FUNC_READ_INPUT,
                                    start_addr,
                                    quantity,
                                    dest,
                                    dev->config.comm_timeout_ms) != RT_EOK)
    {
        return -2;
    }

    return 0;
}

单通道读取把”映射地址 → 读寄存器 → 解析编码”三步串起来:

int pt100_read_channel_deci_c(const pt100_device_t *dev, uint8_t channel, int16_t *temperature_deci_c)
{
    uint16_t raw_value;

    if (temperature_deci_c == NULL)
    {
        return -1;
    }

    if (pt100_read_raw_channel(dev, channel, &raw_value) != 0)
    {
        return -3;
    }

    if (pt100_parse_raw_to_deci_c(raw_value, temperature_deci_c) != 0)
    {
        return -4;
    }

    return 0;
}

返回码区分了三种失败:-1 参数错、-3 总线读取失败、-4 解析失败(含传感器故障)。调用方可以根据返回码做不同处理——比如总线失败可能是暂时性的,重试即可;传感器故障则需要上报告警。

多通道批量读取与 INT16_MIN 哨兵

单通道读取在多通道场景下效率低——4 通道要发 4 次 Modbus 请求。驱动提供了批量读取接口,一次 FC04 请求读全部通道:

int pt100_read_all_deci_c(const pt100_device_t *dev, int16_t *temperature_deci_c)
{
    uint16_t raw_values[16];
    uint8_t index;
    uint8_t has_fault = 0U;

    if ((dev == NULL) || (temperature_deci_c == NULL))
    {
        return -1;
    }

    if (dev->config.channel_count > (uint8_t)(sizeof(raw_values) / sizeof(raw_values[0])))
    {
        /* 通道数超出本地缓冲,配置异常。 */
        return -2;
    }

    if (pt100_read_all_raw(dev, raw_values) != 0)
    {
        return -3;
    }

    for (index = 0U; index < dev->config.channel_count; index++)
    {
        if (pt100_parse_raw_to_deci_c(raw_values[index], &temperature_deci_c[index]) != 0)
        {
            temperature_deci_c[index] = INT16_MIN;
            has_fault = 1U;
        }
    }

    return (has_fault != 0U) ? -4 : 0;
}

这里有个关键设计:部分通道故障不能让整批读取失败。即使某一通道返回 0xFFFF,其他通道的有效数据仍然要写进输出数组。所以循环里对每个通道单独解析,失败的通道写入 INT16_MIN 作为标记,同时置 has_fault 标志,最后返回 -4 告诉调用方”有通道故障,但数据已尽量填充”。

为什么用 INT16_MIN 而不是 0

这是最容易踩坑的点。合法温度 0.0℃ 解析出来就是 0,如果用 0 表示”无效”,就无法区分”测到 0℃“和”没测到”。INT16_MIN = -32768 在物理上对应 -3276.8℃,和 PT100 模块的故障哨兵 0xFFFF 解析结果重合,但这里作为应用层哨兵有不同语义:

  • 原始值 0xFFFF:模块自报传感器故障(断线、损坏)
  • INT16_MIN:应用层”本轮无有效采样”的统一标记,可能是传感器故障,也可能是总线读取失败、通道号越界等

上游 pt100_read_all_celsius 据此转换成浮点无效值:

for (index = 0U; index < dev->config.channel_count; index++)
{
    if (temperature_deci_c[index] == INT16_MIN)
    {
        temperature_c[index] = PT100_INVALID_TEMPERATURE_C;
    }
    else
    {
        temperature_c[index] = (float)temperature_deci_c[index] / 10.0f;
    }
}

上行编码器也用 INT16_MIN 判断是否输出 null

/* PT100 原始值是 *10 ℃,格式化为 1 位小数(如 23.1)。 */
static int fmt_pt100(char *buf, int buf_sz, int16_t raw)
{
    int whole;
    int frac;
    int neg = 0;
    int val = (int)raw;

    if (raw == INT16_MIN)
    {
        return rt_snprintf(buf, buf_sz, "null");
    }

    if (val < 0) { neg = 1; val = -val; }
    whole = val / 10;
    frac  = val % 10;
    return rt_snprintf(buf, buf_sz, "%s%d.%d", neg ? "-" : "", whole, frac);
}

这样 JSON 里的温度数组能准确表达”某通道无效”,Flutter 端收到 null 就显示”---“,收到 23.1 就显示具体温度,收到 0.0 也照常显示 0.0——三种状态泾渭分明。

多从站统一处理

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

/* 现场两块 PT100 模块的通道数。与 main.c 保持一致。 */
#define APP_PT100_SLAVE4_CHANNELS    2U
#define APP_PT100_SLAVE5_CHANNELS    4U
#define APP_PT100_CHANNELS_TOTAL     (APP_PT100_SLAVE4_CHANNELS + APP_PT100_SLAVE5_CHANNELS)

应用层测量结构体里用两个独立数组存放:

typedef struct
{
    /* ... 其他字段 ... */
    int16_t pt100_4_values[APP_PT100_SLAVE4_CHANNELS];
    int16_t pt100_5_values[APP_PT100_SLAVE5_CHANNELS];
    /* ... */
} app_measurement_t;

两块模块用各自的 pt100_device_t 实例,配置不同的 slave_addrchannel_count,但调用同一套驱动接口。上行编码器把两组温度拼接进同一个 temp 数组:

for (i = 0; i < (int)APP_PT100_SLAVE4_CHANNELS; ++i)
{
    fmt_pt100(t_s[1 + i], sizeof(t_s[1 + i]), m->pt100_4_values[i]);
}
for (i = 0; i < (int)APP_PT100_SLAVE5_CHANNELS; ++i)
{
    fmt_pt100(t_s[1 + APP_PT100_SLAVE4_CHANNELS + i],
              sizeof(t_s[0]),
              m->pt100_5_values[i]);
}

t_s[0] 是 PXW 温压一体的温度,t_s[1..2] 是从站 4 的 2 通道,t_s[3..6] 是从站 5 的 4 通道,t_s[7..8] 是流量计温度。9 个温度点拼成上行 JSON 的 temp 数组。

valid 位图的跨从站聚合

上行 JSON 里有个 valid 字段是位图,bit5 表示 PT100 是否有效。两块模块任一通道非 INT16_MIN 就置位:

for (i = 0; i < (int)APP_PT100_SLAVE4_CHANNELS; ++i)
{
    if (m->pt100_4_values[i] != INT16_MIN) { v |= (1U << 5); break; }
}
if ((v & (1U << 5)) == 0U)
{
    for (i = 0; i < (int)APP_PT100_SLAVE5_CHANNELS; ++i)
    {
        if (m->pt100_5_values[i] != INT16_MIN) { v |= (1U << 5); break; }
    }
}

从站 4 全故障时再查从站 5,任一有效即认为 PT100 整体有效。这种”或”聚合避免了单模块故障导致整个 PT100 位被清零。

完整数据流

从 Modbus 总线到上行 JSON 的完整链路:

PT100 模块
  │  16 位寄存器(符号位+绝对值,或 0xFFFF 故障)

Modbus_ServiceReadRegisters (FC04)
  │  uint16_t raw_values[N]

pt100_parse_raw_to_deci_c
  │  int16_t temperature_deci_c  (0.1℃ 单位,故障写 INT16_MIN)

app_measurement_t.pt100_4_values / pt100_5_values
  │  int16_t 数组,跨线程共享

fmt_pt100
  │  JSON 字符串 "23.1" 或 "null"

uplink JSON "temp":[23.1,null,22.9,...]

每一层都有明确的无效值表示:

  • 寄存器层0xFFFF(模块故障)
  • 解析层INT16_MIN(统一哨兵)
  • 浮点层9999.9f(显示无效)
  • JSON 层null(上行协议)

层与层之间通过显式判断转换,不依赖隐式约定,维护时不容易出错。

小结

  • 符号位编码:PT100 用 0x8000 作符号位、低 15 位作绝对值,不是补码,必须显式解析
  • 故障哨兵0xFFFF 是模块自报故障,解析前先检查,避免误判为 -3276.7℃
  • INT16_MIN 哨兵:应用层”无有效采样”的统一标记,和合法的 0 值区分开
  • 通道映射:1-based 通道号 + input_reg_base 偏移,边界检查用 channel_count 适配不同模块
  • 批量读取:一次 FC04 读全部通道,部分故障不影响其他通道数据
  • 分层无效值:寄存器 0xFFFF → 解析 INT16_MIN → 浮点 9999.9 → JSON null,每层显式转换

后续阅读