做 Qt 上位机开发时,常常会遇到这样的疑问:设备到底需要我发送
"01 03 00 00",还是发送0x01 0x03 0x00 0x00?
假设你在代码中定义:
cpp
QString str = "01 03 00 00";
这只是一个字符串。如果直接发送出去,硬件线路上实际传输的二进制字节是:
cpp
30 31 20 30 33 20 30 30 20 30 30
对应关系:
cpp
'0' = 0x30 | '1' = 0x31 | ' ' = 0x20
但从协议文档来看,设备真正期望收到的往往是 01 03 00 00 这 4 个字节。
一、概念厘清:Hex、ASCII 与字节
1. Hex 到底是什么?
Hex(十六进制)本质上只是二进制数据的显示与表达方式,并不是一种独立的编码类型。
cpp
十进制:1 | 十六进制:0x01 | 二进制:0000 0001
对于数值 0x41:
-
它既可以表示整数
65; -
在 ASCII 表中也可以映射为字符
'A'。
核心误区:0x41 ≠ "0x41"。
-
0x41是 1 个原始字节; -
而
"0x41"是 4 个 ASCII 字符:'0'、'x'、'4'、'1',对应字节为30 78 34 31。
2. ASCII 又是什么?
ASCII 是将物理字节映射为可视文本的字符编码标准。
| 字符 | ASCII 十六进制 | 字符 | ASCII 十六进制 |
|---|---|---|---|
'A' |
0x41 |
'a' |
0x61 |
'B' |
0x42 |
'0' |
0x30 |
'C' |
0x43 |
'1' |
0x31 |
cpp
QByteArray data1 = "ABC"; // 实际字节:41 42 43
QByteArray data2;
data2.append(char(0x41));
data2.append(char(0x42));
data2.append(char(0x43)); // 实际字节:41 42 43
两者发送到总线上完全一致,但它们在逻辑上的语义完全不同。
二、Qt 核心数据类型映射
在工业通信开发中,最关键的三个数据类型职责清晰明确:
cppQString ────────► [ 文本层 ] 例:"HELLO"(用于 UI 控件显示) │ ▼ QByteArray ───────► [ 字节层 ] 例:48 45 4C 4C 4F(物理通信传输) │ ▼ char ──────────► [ 元素层 ] 例:0x48(单个字节存储)
通信层与协议层必须统一以 QByteArray 为核心。切忌把所有数据都直接用 QString 处理。
三、常用编码转换技巧与 API 规范
1. ASCII / 文本 与 QByteArray 转换
文本转字节(TX)
cpp
QString text = "HELLO";
// Latin1 编码(单字节字符)
QByteArray dataLatin1 = text.toLatin1(); // 48 45 4C 4C 4F
// UTF-8 编码(多字节字符)
QString cnText = "你好";
QByteArray dataUtf8 = cnText.toUtf8(); // UTF-8 下中文占多字节
注意:QString::length() 代表字符数,而 QByteArray::size() 代表实际字节数,二者在处理非纯 ASCII 字符串时不一定相等。
字节转文本(RX)
cpp
QByteArray data = "HELLO";
// 确认收到的是文本协议时方可转换
QString text1 = QString::fromLatin1(data);
QString text2 = QString::fromUtf8(data);
重要原则:只有确定收到的是纯文本协议时,才使用 QString 转换。
对于二进制协议,绝不能先转 QString 再解析,而应该直接按字节偏移截取:
cpp
// 假设二进制接收:01 03 02 00 64
quint8 address = static_cast<quint8>(data[0]); // 0x01
quint8 function = static_cast<quint8>(data[1]); // 0x03
quint16 value = (static_cast<quint8>(data[3]) << 8) |
static_cast<quint8>(data[4]); // 0x0064 -> 100
2. Hex 字符串与真实字节互转(关键场景)
工业调试软件用户常在输入框填入 Hex 文本,例如 "01 03 00 00 00 02",需通过 QByteArray::fromHex() 还原为真正的物理字节发送。
Hex 文本转物理字节(TX)
Hex 文本 "01 03 FF A0" ──[ QByteArray::fromHex() ]──► 物理二进制 01 03 FF A0
cpp
QByteArray hexStr = "01 03 00 00 00 02";
QByteArray sendData = QByteArray::fromHex(hexStr); // 转换后变为真正的 6 个字节
serial.write(sendData); // 正确发往串口
物理字节转 Hex 文本(日志输出 / RX 展示)
cpp
QByteArray rxBuffer;
rxBuffer.append(char(0x01));
rxBuffer.append(char(0x03));
rxBuffer.append(char(0x00));
rxBuffer.append(char(0x64));
// 加空格分隔并转大写,提升日志可读性
QString logText = rxBuffer.toHex(' ').toUpper();
qDebug() << "[RX]" << logText; // 输出: [RX] 01 03 00 64
四、高级开发坑点与避坑策略
1. ASCII 协议与二进制协议对比
ASCII 协议(文本流) : "TEMP=25.6\r\n" ──► 可读性好、占用空间大二进制协议(字节流) : AA 55 01 03 00 64 ──► 数据紧凑、解析高效、无编码歧义
2. char 类型符号问题(极易踩坑)
在 C++/Qt 中,QByteArray 的元素类型是 char,其是否有符号(signed / unsigned)取决于编译器与平台实现。
cpp
QByteArray data;
data.append(char(0xFF));
// ❌ 错误:在很多平台 char 为 signed,0xFF 会变负数 -1
char rawVal = data[0];
// ✅ 正确:显式强转为 quint8 (0~255)
quint8 byteVal = static_cast<quint8>(data[0]); // 255
3. 大小端序(Endianness)
组合十六进制显示只代表字节顺序,不代表数据高低位组合逻辑:
cpp
// 接收到两个字节:12 34
// 1. 大端模式(Big-Endian / 网络字节序) -> 0x1234
quint16 beVal = (static_cast<quint8>(data[0]) << 8) | static_cast<quint8>(data[1]);
// 2. 小端模式(Little-Endian) -> 0x3412
quint16 leVal = static_cast<quint8>(data[0]) | (static_cast<quint8>(data[1]) << 8);
五、工程化推荐实现
1. Hex 工具类封装
为避免在项目各处重复编写转化与去空格逻辑,推荐封装全局命名空间:
cpp
namespace HexUtils {
/**
* @brief 将带有空格/无空格的 Hex 字符串转为真实 QByteArray
*/
inline QByteArray fromHex(const QString& text) {
QByteArray hex = text.toLatin1();
hex.replace(" ", ""); // 自动清除空格
return QByteArray::fromHex(hex);
}
/**
* @brief 将物理字节数组格式化为带空格的大写 Hex 字符串
*/
inline QString toHex(const QByteArray& data) {
return QString::fromLatin1(data.toHex(' ').toUpper());
}
} // namespace HexUtils
2. 上位机分层数据流模型
成熟的工控上位机系统应当将通信解耦,通信层只传递 QByteArray,展示层再转 QString

六、速查对账表与核心总结
| 操作需求 | API / 实现方式 | 发送/得到示例 |
|---|---|---|
| 发送文本 | QString::toLatin1() |
"A" → 0x41 |
| 发送二进制指令 | QByteArray::fromHex("01 03") |
"01 03" → 0x01 0x03 |
| 格式化输出日志 | data.toHex(' ').toUpper() |
0x01 0x03 → "01 03" |
| 提取无符号单字节 | static_cast<quint8>(data[i]) |
防止 0xFF 溢出变 -1 |
一句话要领:
ASCII "01 03" 发送的是字符 30 31 20 30 33;Hex "01 03" 还原后发送的是真正字节 01 03。保持通信逻辑全程采用 QByteArray,只在 UI 呈现时转 QString,即可避开绝大多数上位机通信 Bug。