如何自动为结构中的字节添加填充以修复 C 中的寄存器对齐?

How can I automatically add padding to bytes in a struct to fix register alignment in C?

提问人:Andrew Cline 提问时间:3/27/2020 最后编辑:Andrew Cline 更新时间:3/28/2020 访问量:624

问:

乍一听,这听起来像是一个奇怪的问题,我明白为什么。我的问题有点难以解释,但希望我能很好地解释。

编辑:填充可能已经在CPU中完成,但是当保存到HMI本地存储寄存器时,此填充会丢失,因此对齐关闭并且HMI无法正确显示数据。

我有一个正在编程的 HMI。在HMI上创建页面的软件主要是拖放式的。这一点很重要,因为用户无法访问宏中的变量,只有在将数据保存到本地内存后,他们才能将数据输入到这些拖放对象中。

我有一个连接到 HMI 的传感器,传感器具有只能通过多寄存器读取访问的内存“块”。这意味着我需要一个宏来读取这些数据并将其临时保存在 HMI 中,以便您可以在单击“发送”后将其发送回去之前对其进行编辑

拖放元素只能访问 16 位寄存器。传入的数据具有中间抛出的单个字节。

我创建了一个结构体,并将数据接收到结构体中,并将其写入HMI寄存器。下面是 C 语言中的代码:

struct configStruct { // 72 Bytes or 36 words/registers long
    char description[32]; // char string
    float c1CE_A; // coefficient 1_A
    float c1CE_B; // coefficient 1_B
    float c1CE_C; // coefficient 1_C
    char c1useLogFit; // Single byte that is 1 if "Log Fit" is enabled, and 0 if disabled.
    float c2CE_A;
    float c2CE_B;
    float c2CE_C;
    char c2useLogFit;
    float c3CE_A;
    float c3CE_B;
    float c3CE_C;
    char c3useLogFit;
    char dummyByte;
};

int MacroEntry()
{

    struct configStruct *dataPtr, data1;

    ReadData(ProductConfig_PLC,1,2305,36,(void*)&data1); // read data from sensor and save in struct

    // Something needs to happen here that converts the bytes into words so the data lines up with the 16-bit registers in the HMI

    WriteData(ProductConfig_HMI,0,2305,36,(void*)&data1); // write temporary data to HMI from struct

    return 0;
}

(ProductConfig_PLC/HMI 在软件的选项卡中定义。它有读/写地址等选项)

该功能工作正常,以及 。问题在于,当宏将结构数据保存到 HMI 的 16 位寄存器时,字节会偏离对齐方式。我最终将我的浮子拆分在寄存器之间。我知道是这种情况,因为前三个浮动显示良好。另一个在单个字节 don't 之后浮动。ReadDataWriteData

(另外,我已经使用数组和移动字节解决了这个问题,但这非常乏味,我还有很多事情要做)

有没有办法将单字节变量转换为单词,以便在将它们写入 HMI 时正确对齐?如果这些寄存器中有其他数据,我将无法访问这些字节。

我也知道我可以创建另一个具有适当大小变量的结构并复制所有内容。然而,这成为一个问题,因为传感器中的一些“块”包含数百个变量,手动发送和接收都会很乏味,而且会让人麻木。

以下是数据定义:

The Product Code Calibration Database starts at modbus address 0x900
repeats for each Product Code (currently 0 – 49). Each product code
calibration table is 72 bytes/36 words long. To calculate the start address for each
product code: address = 0x900 + (36 * Product Code number)
Data Definition:
Product Code Description – character string (32 bytes)
Constituent 1 Coefficient A – IEEE float (4 bytes)
Constituent 1 Coefficient B – IEEE float (4 bytes)
Constituent 1 Coefficient C – IEEE float (4 bytes)
Constituent 1 Use Log Fit - 8 bit integer (1 byte) 1= use log fit
Constituent 2 Coefficient A – IEEE float (4 bytes)
Constituent 2 Coefficient B – IEEE float (4 bytes)
Constituent 2 Coefficient C – IEEE float (4 bytes)
Constituent 2 Use Log Fit - 8 bit integer (1 byte) 1= use log fit
Constituent 3 Coefficient A – IEEE float (4 bytes)
Constituent 3 Coefficient B – IEEE float (4 bytes)
Constituent 3 Coefficient C – IEEE float (4 bytes)
Constituent 3 Use Log Fit - 8 bit integer (1 byte) 1= use log fit

点击此处链接到 HMI 数据表

感谢您的帮助。

c 结构 cpu 寄存器 plc

评论

4赞 Lundin 3/27/2020
你是在问为什么结构体有填充吗?请参阅为什么结构的 sizeof 不等于每个成员的 sizeof 和?。至于如何解决这个问题......您可以使用非标准功能(如 )打包数据,但前提是您的 CPu 实际上可以读取未对齐的数据。#pragma pack
1赞 Armali 3/27/2020
@Andrew Cline - 不确定您的描述是否包含错误的假设。您如何知道来自传感器的数据长度为 72 字节或 36 字/寄存器?传感器接口文档的链接将按顺序排列。您如何知道 HMI 的数据长度为 72 字节或 36 字/寄存器?HMI文档的链接是有序的。
1赞 Andrew Cline 3/27/2020
@Armali除非在数据表中,否则我不会指定它。它被确认为 36,因为传感器只发送整个块。如果这是错误的,HMI 将不会验证数据,我会收到错误。
1赞 Andrew Cline 3/27/2020
@MawgsaysreinstateMonica 跨平台是什么意思?在谷歌搜索之后,封装似乎也是 C++ 的事情。这是严格的 C.,单一用途不是一个足够狭窄的搜索词,所以不知道那是什么。
2赞 Lundin 3/27/2020
网络数据与填充无关。填充由本地系统的编译器完成,以满足本地对齐要求。只需在你喜欢的调试器中查看结构内存映射即可。

答:

1赞 chux - Reinstate Monica 3/28/2020 #1

C11 允许对齐控制。

也许让许多成员结盟 - 任何时候,限制性更强的成员都可能跟随限制性较小的成员。

#include <stdalign.h>

struct configStruct {
    char description[32];
    alignas (short) float c1CE_A; // coefficient 1_A
    float c1CE_B; // coefficient 1_B
    float c1CE_C; // coefficient 1_C
    char c1useLogFit;
    alignas (short) float c2CE_A;
    float c2CE_B;
    float c2CE_C;
    char cc2useLogFit;
    alignas (short) float c3CE_A;
    float c3CE_B;
    float c3CE_C;
    char c3useLogFit;
    char dummyByte;
};

评论

0赞 Andrew Cline 3/28/2020
这似乎正是我所需要的,但是,HMI 软件附带的编译器无法识别这些函数或找到 stdalign.h。HMI 似乎正在运行 ARM linux 内核。我可以下载较新的 C11 编译器并替换旧的编译器吗?(当然是在备份之后)
0赞 chux - Reinstate Monica 3/28/2020
@AndrewCline IDK。机会偏爱有准备的头脑
0赞 Andrew Cline 3/28/2020
我不知道这意味着什么,但我认为这意味着,“尝试一下,找出答案”。我现在正在做的:)
1赞 dbush 3/28/2020 #2

大多数编译器将填充添加到 a 中以达到对齐目的。假设 a 为 4 个字节,则结构的实际布局如下所示:structfloat

struct configStruct { 
    char description[32];
    float c1CE_A; 
    float c1CE_B; 
    float c1CE_C; 
    char c1useLogFit; 
    char dummy[3];    // padding bytes
    float c2CE_A;
    float c2CE_B;
    float c2CE_C;
    char c2useLogFit;
    char dummy[3];    // padding bytes
    float c3CE_A;
    float c3CE_B;
    float c3CE_C;
    char c3useLogFit;
    char dummyByte;
    char dummy[2];    // padding bytes
};

所以你的 72 字节结构实际上是 80 字节。

如果你使用的是 gcc,你可以声明要打包的结构体:

struct configStruct __attribute__((packed)) { 
    char description[32];
    float c1CE_A; 
    float c1CE_B; 
    float c1CE_C; 
    char c1useLogFit; 
    float c2CE_A;
    float c2CE_B;
    float c2CE_C;
    char c2useLogFit;
    float c3CE_A;
    float c3CE_B;
    float c3CE_C;
    char c3useLogFit;
    char dummyByte;
};

这将消除填充。

如果没有,则需要手动反序列化数据以处理它,因为它将序列化回字节缓冲区以将其写入另一端。

如何执行此操作的示例如下:

void deserialize(char *indata, struct configStruct *data)
{
    char *p = indata;
    int offset = 0;
    memcpy(data->description, p + offset, sizeof(data->description));
    offset += sizeof(data->description);
    memcpy(&data->c1CE_A, p + offset, sizeof(data->c1CE_A));
    offset += sizeof(data->c1CE_A);
    memcpy(&data->c1CE_B, p + offset, sizeof(data->c1CE_B));
    offset += sizeof(data->c1CE_B);
    memcpy(&data->c1CE_C, p + offset, sizeof(data->c1CE_C));
    offset += sizeof(data->c1CE_C);
    memcpy(&data->c1useLogFit, p + offset, sizeof(data->c1useLogFit));
    offset += sizeof(data->c1useLogFit);
    // etc.
}

void serialize(struct configStruct *data, char *outdata)
{
    char *p = outdata;
    int offset = 0;
    memcpy(p + offset, data->description, sizeof(data->description));
    offset += sizeof(data->description);
    memcpy(p + offset, &data->c1CE_A, sizeof(data->c1CE_A));
    offset += sizeof(data->c1CE_A);
    memcpy(p + offset, &data->c1CE_B, sizeof(data->c1CE_B));
    offset += sizeof(data->c1CE_B);
    memcpy(p + offset, &data->c1CE_C, sizeof(data->c1CE_C));
    offset += sizeof(data->c1CE_C);
    memcpy(p + offset, &data->c1useLogFit, sizeof(data->c1useLogFit));
    offset += sizeof(data->c1useLogFit);
    // etc.
}

int MacroEntry()
{

    char indata[72], outdata[72];
    struct configStruct data;

    ReadData(ProductConfig_PLC,1,2305,36,(void*)indata); 
    deserialize(indata, &data);

    // work with "data"

    serialize(&data, outdata);
    WriteData(ProductConfig_HMI,0,2305,36,(void*)outdata); // write temporary data to HMI from struct

    return 0;
}

评论

0赞 chux - Reinstate Monica 3/28/2020
在某个地方有一个技巧,可以附加成员名称的行号,使其没有重复的成员 - 就像这里一样。还可以帮助用户代码避免使用这些成员。另外,我认为允许某些位大小的无名成员。dummy
0赞 dbush 3/28/2020
@chux-ReinstateMonica:我只是用虚拟成员作为正在发生的事情的例子。声明的结构与传入的二进制数据匹配,因此显式添加填充无济于事。要么需要打包结构,要么需要反序列化。
0赞 Andrew Cline 3/28/2020
@dbush反序列化似乎是关键术语。这一切都是序列化的,无论 CPU 在填充方面做什么,我不知道,但 WriteData 函数会忽略任何隐式填充,无论如何它都会进入序列化的本地内存
0赞 Andrew Cline 3/28/2020
@dbush,我需要在将数据发送到 HMI 本地寄存器之前对其进行“反序列化”。宏只是获取数据并将其保存在 HMI 上的临时内存中以进行编辑。宏不会将数据发送回 HMI,因为 HMI 必须是一个单独的宏。我有 2 个发送和接收数据的按钮。
0赞 dbush 3/28/2020
@AndrewCline 看起来 write 函数完全采用 read 函数返回的内容。如果你不接触数据,你可能会得到相同的数据推送,但如果你尝试按原样修改结构,那就是你遇到问题的时候。