Skip to main content

ModBus 报文解析实战

·1571 words·4 mins
Kydin
Author
Kydin
自由のために戦え
Table of Contents

事情是这样的,项目需要一个串口采集 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 语言代码实现
#

使用共用体进行类型转换
#

#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;
}

首先要明确的是,直接使用强制类型转换是不行的。直接看代码:

#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;
}

输出结果如下:

pureos@pureos:~$ gcc 1.c -o 1
pureos@pureos:~$ ./1
value = 3.015625

可以看到我们按照正常的字节序转换出来的浮点数是错误的,将这个值转换为十六进制为:

40 41 00 00

转 Hex

寻找正确的 12.000000
#

所以正确的12.000000 应该是多少呢?

使用以下函数查看一下:

#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;
}

代码输出结果:

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,这个时候就可以获取正确的数据了

#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;
}

输出为:

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

可以看到,我们已经正确解析出了电压值。

完整代码
#

#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 的大小端序判断,这样可用性和可移植性会更强。