004-ModBus 报文解析实战

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++) {
/* shift reg hi_byte to temp OR with lo_byte */
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;
}

// true is little endian, flase is big endian
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++) {
/* shift reg hi_byte to temp OR with lo_byte */
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 的大小端序判断,这样可用性和可移植性会更强。


004-ModBus 报文解析实战
https://kydins.com/posts/f2557fcc.html
作者
Kydin
发布于
2023年10月8日
许可协议