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;
}
关键三步:
raw_value & 0x7FFFU取低 15 位作为绝对值raw_value & 0x8000U判断符号位- 负温度时取相反数,正温度直接赋值
举例:
| 原始值 | 符号位 | 绝对值 | 解析结果(0.1℃) | 物理温度 |
|---|---|---|---|---|
0x00E7 | 0 | 231 | +231 | +23.1℃ |
0x80E7 | 1 | 231 | -231 | -23.1℃ |
0x0000 | 0 | 0 | 0 | 0.0℃ |
0x8000 | 1 | 0 | 0(负零) | 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_addr 和 channel_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→ JSONnull,每层显式转换