事情是这样的,项目需要一个串口采集 1032 协议电压的功能。在实现中还是遇到不少问题,由于是第一次使用,遂做下一些记录。
报文内容 #
使用串口发送 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 就可以。
细心的朋友可能发现了,我是使用 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
寻找正确的 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 的大小端序判断,这样可用性和可移植性会更强。