ModBus 报文解析实战
事情是这样的,项目需要一个串口采集 1032 协议电压的功能。在实现中还是遇到不少问题,由于是第一次使用,遂做下一些记录。
ModBus 模拟量 Hex
ModBus 模拟量 Float
报文内容
使用串口发送 ModBus 报文时,需要解析收到的报文
01 03 04 00 00 41 40 CB 93
这是请求报文:
- 01:设备地址,表示要访问的 Modbus 设备的地址为 1。
- 03:功能码,表示要读取保持寄存器。
- 00 00:起始地址,表示要读取的保持寄存器的起始地址为 0。
- 00 02:寄存器数量,表示要读取的保持寄存器数量为 2。
- C4 0B:CRC 校验码,用于验证报文的正确性。
返回报文:
- 01:设备地址,表示返回的报文是来自地址为 1 的 Modbus 设备。
- 03:功能码,表示返回的报文是读取保持寄存器的响应报文。
- 04:字节数,表示返回的数据字节数为 4。
- 00 00:寄存器值,表示起始地址为 0 的第一个保持寄存器的值。
- 41 40:寄存器值,表示起始地址为 1 的第二个保持寄存器的值。
- CB 93:CRC 校验码,用于验证报文的正确性。
解析返回报文
由于我只需要采集单路 ModBus 所以我的请求报文是固定的。因此我的返回报文的头部内容也是固定的。
所以我们需要解析的数据就是
00 00 41 40
注意:这是 IEEE 754 浮点数
这就是我们需要的电压数据。其实很简单,只需要做一个十六进制转 float 就可以。
12 转 hex
细心的朋友可能发现了,我是使用 41400000 转换的,这是为什么呢?
这是因为我的 float 格式为 CDAB(这是等到最后才发现的)
C 语言代码实现
使用共用体进行类型转换
1 2 3 4 5 6 7 8 9 10 11
| #include <stdio.h>
typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t;
int main(int argc, char** argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40};
return 0; }
|
首先要明确的是,直接使用强制类型转换是不行的。直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <stdio.h> #include <string.h>
typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t;
typedef union { uint32_t u32; float f; } float_union_t;
static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; }
int main(int argc, char **argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; float value = utils_get_float_at(rsp, 0); printf("value = %f\n", value);
return 0; }
|
输出结果如下:
1 2 3
| pureos@pureos:~$ gcc 1.c -o 1 pureos@pureos:~$ ./1 value = 3.015625
|
可以看到我们按照正常的字节序转换出来的浮点数是错误的,将这个值转换为十六进制为:
40 41 00 00
转 Hex
寻找正确的 12.000000
所以正确的12.000000
应该是多少呢?
使用以下函数查看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <stdio.h> #include <string.h>
typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t;
typedef union { uint32_t u32; float f; } float_union_t;
static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; }
static inline void utils_set_float_at(void *data, int pos, float value) { *((uint32_t *)((uint8_t *)(data) + pos)) = *(uint32_t *)(&value); }
int main(int argc, char **argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; float value = utils_get_float_at(rsp, 0); printf("value = %f\n", value);
uint8_t float_to_hex_buf[4]; float float_num = 12.000000; bzero(float_to_hex_buf, sizeof(float_to_hex_buf)); utils_set_float_at(float_to_hex_buf, 0, float_num);
printf("float %f to hex = ", float_num); for (int i = 0; i < 4; i++) { printf("%.02hx ", float_to_hex_buf[i]); } printf("\n");
return 0; }
|
代码输出结果:
1 2 3 4
| pureos@pureos:~$ gcc 1.c -o 1 pureos@pureos:~$ ./1 value = 3.015625 float 12.000000 to hex = 00 00 40 41
|
现在已经非常清晰了,在我当前的环境中,需要将每位寄存器上的数据位互换位置。也就是两两之间互换。
原始数据:00 00 41 40
正确数据:00 00 40 41
然后将 uint8 的数据转为 uint16,这个时候就可以获取正确的数据了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include <stdio.h> #include <string.h>
typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t;
typedef union { uint32_t u32; float f; } float_union_t;
static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; }
static inline void utils_set_float_at(void *data, int pos, float value) { *((uint32_t *)((uint8_t *)(data) + pos)) = *(uint32_t *)(&value); }
int main(int argc, char **argv) { uint8_t rsp[] = {0x00, 0x00, 0x41, 0x40}; float value = utils_get_float_at(rsp, 0); printf("value = %f\n", value);
uint8_t float_to_hex_buf[4]; float float_num = 12.000000; bzero(float_to_hex_buf, sizeof(float_to_hex_buf)); utils_set_float_at(float_to_hex_buf, 0, float_num);
printf("float %f to hex = ", float_num); for (int i = 0; i < 4; i++) { printf("%.02hx ", float_to_hex_buf[i]); } printf("\n");
uint16_t dest[4]; bzero(dest, sizeof(dest)); int rc = 2; int offset = 1; for (int i = 0; i < rc; i++) { dest[i] = (rsp[(i << 1)] << 8) | rsp[offset + (i << 1)]; }
for (int i = 0; i < 2; i++) { printf("%.04hx ", dest[i]); } printf("\n");
value = utils_get_float_at(dest, 0); printf("value = %f\n", value);
return 0; }
|
输出为:
1 2 3 4 5 6
| pureos@pureos:~$ gcc 1.c -o 1 pureos@pureos:~$ ./1 value = 3.015625 float 12.000000 to hex = 00 00 40 41 0000 4140 value = 12.000000
|
可以看到,我们已经正确解析出了电压值。
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| #include <stdio.h> #include <string.h>
typedef unsigned char uint8_t; typedef unsigned short int uint16_t; typedef unsigned int uint32_t;
typedef union { uint32_t u32; float f; } float_union_t;
static inline float utils_get_float_at(void *data, int pos) { uint8_t __attribute__((aligned(4))) tmp[4] = {0}; memcpy(tmp, (uint8_t *)data, 4); uint32_t lw = *((uint32_t *)(((uint8_t *)(tmp)) + pos)); float_union_t fu = {.u32 = lw}; return fu.f; }
static int check_cpu() { union w { int a; char b; } c; c.a = 1; return (c.b == 1); }
static void reverse(uint16_t data[], int num) { int i, j; uint16_t temp;
for (i = 0, j = num - 1; i < j; i++, j--) { temp = data[i]; data[i] = data[j]; data[j] = temp; } }
int main(int argc, char **argv) { int rc = 2; int offset = 1; uint8_t rsp[] = {0x01, 0x03, 0x04, 0x00, 0x00, 0x41, 0x40, 0xCB, 0x93}; uint16_t dest[2];
bzero(dest, sizeof(dest)); for (int i = 0; i < rc; i++) { dest[i] = (rsp[offset + 2 + (i << 1)] << 8) | rsp[offset + 3 + (i << 1)]; }
if (!check_cpu()) { reverse(dest, 2); }
float value = utils_get_float_at(dest, 0); printf("value = %f\n", value);
return 0; }
|
最后还加入了一个对 cpu 的大小端序判断,这样可用性和可移植性会更强。