3. 信号读取与写入
任务目标
本案例实现一个最小可用的信号读写模块:从总线报文(模拟数据帧)中按信号名读取物理值,并将控制信号按指定值写入新的总线报文。通过该模块,掌握HIL测试中信号级操作的基本模式——信号与报文的映射、字节序处理、物理值与原始值的转换。
实现方案
定义Signal数据结构描述信号在报文中的位置(起始字节、起始位、位宽)与转换参数(缩放因子、偏移量)。定义Message结构承载原始字节。核心函数read_signal从字节数组中提取并转换为物理值,write_signal将物理值转换为原始值并写入字节数组的对应位域。使用CAN FD扩展帧的经典布局作为示例,但逻辑适用于任何基于位偏移的信号映射。
完整代码
#include <cstdint>
#include <cstring>
#include <stdexcept>
#include <string>
#include <vector>
struct Signal {
std::string name;
uint8_t start_byte; // 起始字节索引(0-based)
uint8_t start_bit; // 起始位(0-7,LSB为0)
uint8_t bit_length; // 信号位宽
double factor; // 缩放因子:物理值 = 原始值 * factor + offset
double offset;
};
struct Message {
uint32_t id;
std::vector<uint8_t> data; // 原始字节,长度固定为8(CAN FD)
};
// 提取位域原始值(小端序,LSB优先排列)
uint64_t extract_raw(const Message& msg, const Signal& sig) {
if (sig.start_byte + (sig.bit_length + sig.start_bit + 7) / 8 > msg.data.size())
throw std::out_of_range("Signal exceeds message length");
uint64_t raw = 0;
for (uint8_t i = 0; i < sig.bit_length; ++i) {
uint8_t global_bit = sig.start_byte * 8 + sig.start_bit + i;
uint8_t byte_idx = global_bit / 8;
uint8_t bit_idx = global_bit % 8;
uint8_t bit = (msg.data[byte_idx] >> bit_idx) & 0x01;
raw |= (uint64_t)bit << i;
}
return raw;
}
// 写入位域原始值
void inject_raw(Message& msg, const Signal& sig, uint64_t raw_value) {
if (sig.start_byte + (sig.bit_length + sig.start_bit + 7) / 8 > msg.data.size())
throw std::out_of_range("Signal exceeds message length");
uint64_t mask = (sig.bit_length == 64) ? ~0ULL : ((1ULL << sig.bit_length) - 1);
raw_value &= mask;
for (uint8_t i = 0; i < sig.bit_length; ++i) {
uint8_t global_bit = sig.start_byte * 8 + sig.start_bit + i;
uint8_t byte_idx = global_bit / 8;
uint8_t bit_idx = global_bit % 8;
uint8_t bit = (raw_value >> i) & 0x01;
if (bit)
msg.data[byte_idx] |= (1 << bit_idx);
else
msg.data[byte_idx] &= ~(1 << bit_idx);
}
}
// 读取信号物理值
double read_signal(const Message& msg, const Signal& sig) {
uint64_t raw = extract_raw(msg, sig);
return static_cast<double>(raw) * sig.factor + sig.offset;
}
// 写入信号物理值
void write_signal(Message& msg, const Signal& sig, double physical_value) {
double raw_d = (physical_value - sig.offset) / sig.factor;
if (raw_d < 0 || raw_d >= (sig.bit_length >= 64 ? 1e19 : (double)(1ULL << sig.bit_length)))
throw std::out_of_range("Physical value out of signal range");
uint64_t raw = static_cast<uint64_t>(raw_d + 0.5); // 四舍五入
inject_raw(msg, sig, raw);
}
// 最小验证入口
int main() {
// 定义信号:车速,位于字节2的bit0起,12位宽,factor=0.1,offset=0
Signal speed{"VehicleSpeed", 2, 0, 12, 0.1, 0.0};
// 定义信号:发动机状态,位于字节0的bit4起,2位宽,factor=1,offset=0
Signal eng_state{"EngineState", 0, 4, 2, 1.0, 0.0};
// 构造原始报文(模拟从总线接收)
Message rx{0x123, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}};
// 手动设置:车速原始值=250(物理25.0),发动机状态原始值=2
inject_raw(rx, speed, 250);
inject_raw(rx, eng_state, 2);
// 读取验证
double v = read_signal(rx, speed);
double e = read_signal(rx, eng_state);
// 写入新值:车速设为33.3,发动机状态设为1
Message tx = rx; // 基于接收报文构造发送报文
write_signal(tx, speed, 33.3);
write_signal(tx, eng_state, 1.0);
// 回读验证
double v_new = read_signal(tx, speed);
double e_new = read_signal(tx, eng_state);
// 断言检查
bool pass = (v == 25.0) && (e == 2.0) && (v_new == 33.3) && (e_new == 1.0);
return pass ? 0 : 1;
}
编译与运行
g++ -std=c++11 -o signal_rw signal_rw.cpp
./signal_rw
echo $? # 输出0表示验证通过
验证方法
程序通过main中的断言完成自检:先构造已知原始值的报文,读取后确认物理值正确;再写入新物理值并回读比对。若所有断言成立,进程退出码为0;否则为1。该验证覆盖了位提取、位注入、物理量转换的完整闭环。
关键说明
- 信号布局采用小端序、LSB优先排列,与CANoe/CANalyzer的DBC默认位序一致。
extract_raw与inject_raw是核心位操作函数,支持跨字节信号(如12位车速跨越字节2和字节3)。- 写入时对物理值执行范围检查并四舍五入取整,避免溢出位域。
- 实际HIL工程中,
Message的收发由总线接口卡驱动(如Vector XL API),本案例聚焦信号映射逻辑,可直接嵌入上层调用。