繁体   English   中英

使用强制转换来访问像结构一样的字节数组?

[英]Use casts to access a byte-array like a structure?

我正在研究基于微控制器的软件项目。 该项目的一部分是二进制协议的解析器。 协议是固定的,不能更改。 PC充当“主设备”并主要发送命令,这些命令必须由“从设备”即微控制器板执行。

协议数据由硬件通信接口接收,例如UART,CAN或以太网。 那不是问题。

在接收到帧的所有字节(4-10,取决于命令)之后,将它们存储在“uint8_t cmdBuffer [10]”类型的缓冲区中并设置标志,表示现在可以执行该命令。 帧的第一个字节(cmdBuffer [0])包含命令,帧的其余部分是命令的参数,根据命令的不同,数字和大小可能不同。 这意味着,有效载荷可以通过多种方式进行解释。 对于每个可能的命令,数据字节都会改变它们的含义。

我不希望有太多丑陋的操作,而是自我记录代码。 所以我的方法是:

  • 我为每个命令创建了一个“typedef struct”
  • 确定命令后,指向cmdBuffer的指针将转换为我的新typedef的指针
  • 通过这样做,我可以访问命令的参数作为结构成员,避免数组访问中的幻数,参数的位操作> 8位,并且它更容易阅读

例:

typedef struct
{
    uint8_t commandCode;
    uint8_t parameter_1;
    uint32_t anotherParameter;
    uint16 oneMoreParameter;
}payloadA_t;

//typedefs for payloadB_t and payloadC_t, which may have different parameters

void parseProtocolData(uint8_t *data, uint8_t length)
{
  uint8_t commandToExecute;

  //this, in fact, just returns data[0]
  commandToExecute = getPayloadType(data, length);

  if (commandToExecute == COMMAND_A)
  {
    executeCommand_A( (payloadA_t *) data);
  }
  else if (commandToExecute == COMMAND_B)
  {
    executeCommand_B( (payloadB_t *) data);
  }
  else if (commandToExecute == COMMAND_C)
  {
    executeCommand_C( (payloadC_t *) data);
  }
  else
  {
    //error, unknown command
  }
}

我看到两个问题:

  • 首先,根据微控制器架构,字节顺序可以是2或4字节参数的intel或motorola。 这应该不是什么大问题。 协议本身使用网络字节顺序。 在目标控制器上,可以使用宏来更正顺序。

  • 主要问题:我的tyepdef结构中可能有填充字节。 我正在使用gcc,所以我可以在我的typedef中添加一个“packed”属性。 其他编译器为此提供了编译指示。 但是,在32位机器上,打包结构将导致更大(和更慢)的机器代码。 好吧,这也可能不是问题。 但是我听说,访问未对齐的内存时可能会出现硬件故障(例如,在ARM体系结构上)。

有很多命令(大约50个),所以我不想访问cmdBuffer作为一个数组我认为“结构方法”增加了代码的可读性,与“数组方法”相反

所以我的问题:

  • 这种方法可以,还是只是一个肮脏的黑客?
  • 有没有编译器可以依赖“严格别名规则”并使我的方法不起作用的情况?
  • 有更好的解决方案吗? 你会如何解决这个问题?
  • 这可以保持,至少一点,便携?

问候,lugge

通常,由于填充,结构对于存储数据协议是危险的。 对于可移植代码,您可能希望避免使用它们。 因此,保持原始数据阵列仍然是最好的主意。 您只需要根据收到的命令以不同的方式解释它。

这种情况是需要某种多态性的典型示例。 不幸的是,C没有对该OO功能的内置支持,因此您必须自己创建它。

最好的方法取决于这些不同类型数据的性质。 由于我不知道,我只能以这种方式提出建议,它可能适用于您的具体情况,也可能不是最佳选择:

typedef enum
{
  COMMAND_THIS,
  COMMAND_THAT,
  ... // all 50 commands

  COMMANDS_N // a constant which is equal to the number of commands
} cmd_index_t;


typedef struct
{
  uint8_t      command;        // the original command, can be anything
  cmd_index_t  index;          // a number 0 to 49
  uint8_t      data [MAX];     // the raw data
} cmd_t;

然后,第一步是在收到命令后,将其转换为相应的索引。

// ...receive data and place it in cmdBuffer[10], then:
cmd_t cmd;
cmd_create(&cmd, cmdBuffer[0], &cmdBuffer[1]);

...

void cmd_create (cmd_t* cmd, uint8_t command, uint8_t* data)
{
   cmd->command = command;
   memcpy(cmd->data, data, MAX);

   switch(command)
   {
     case THIS: cmd->index = COMMAND_THIS; break;
     case THAT: cmd->index = COMMAND_THAT; break;
     ... 
   }
}

一旦索引0到N意味着您可以实现查找表。 每个这样的查找表可以是函数指针的数组,其确定数据的特定解释。 例如:

typedef void (*interpreter_func_t)(uint8_t* data);

const interpreter_func_t interpret [COMMANDS_N] =
{
  &interpret_this_command,
  &interpret_that_command,
  ...
};

使用:

interpret[cmd->index] (cmd->data);

然后,您可以为不同的任务创建类似的查找表。

   initialize [cmd->index] (cmd->data);
   interpret  [cmd->index] (cmd->data);
   repackage  [cmd->index] (cmd->data);
   do_stuff   [cmd->index] (cmd->data);
   ...

对不同的体系结构使用不同的查找表。 像endianess这样的东西可以在解释器函数中处理。 您当然可以更改函数原型,也许您需要返回一些内容或传递更多参数等。

请注意,上述示例最适合所有命令导致相同类型的操作。 如果你需要根据命令做完全不同的事情,其他方法更合适。

恕我直言,这是一个肮脏的黑客。 当移植到具有不同对齐要求,不同变量大小,不同类型表示(例如,大端/小端)的系统时,代码可能会中断。 或者甚至在相同的系统上但不同版本的编译器/系统头/无论如何。

我不认为它违反严格的别名,只要相关的字节形成有效的表示。

我只想编写代码以明确定义的方式读取数据,例如

bool extract_A(PayloadA_t *out, uint8_t const *in)
{
    out->foo = in[0];
    out->bar = read_uint32(in + 1, 4);
    // ...
}

这可能比“hack”版本运行稍慢,它取决于您的要求是否更喜欢维护头痛,或者那些额外的微秒。

以相同的顺序回答您的问题:

  • 这种方法很常见,但我知道提到这种技术的任何一本书仍然被称为肮脏的黑客。 你自己拼出了这些理由:实质上它是高度不可移植的,或者需要大量的预处理器魔法才能使它变得便携。

  • 严格别名规则:请参阅最高投票答案什么是严格别名规则?

  • 我所知道的唯一替代解决方案是如您自己提到的那样明确编写反序列化代码。 这实际上可以像这样非常易读:

uint8_t *p = buffer; struct s; s.field1 = read_u32(&p); s.field2 = read_u16(&p);

IE我将使读取函数将指针向前移动反序列化字节的数量。

  • 如上所述,您可以使用预处理器来处理不同的字节序和结构打包。

这是一个肮脏的黑客。 我在这个解决方案中遇到的最大问题是内存对齐而不是字节顺序或结构打包。

内存对齐问题是这样的。 某些微控制器(如ARM)要求多字节变量与某些存储器偏移对齐。 也就是说,2字节半字必须在偶数存储器地址上对齐。 并且4字节字必须在4的倍数的存储器地址上对齐。这些对齐规则不是由串行协议强制执行的。 因此,如果您只是将串行数据缓冲区转换为打包结构,则各个结构成员可能没有正确的对齐方式。 然后,当您的代码尝试访问未对齐的成员时,将导致对齐错误或未定义的行为。 (这就是编译器默认创建未打包结构的原因。)

关于字节顺序,听起来你提议在代码访问压缩结构中的成员时更正字节顺序。 如果您的代码多次访问压缩结构成员,则每次都必须更正字节顺序。 当首次从串行端口接收数据时,仅更正字节序一次会更有效。 这是不简单地将数据缓冲区转换为压缩结构的另一个原因。

当您收到命令时,您应该将每个字段分别解析为一个解压缩的结构,其中每个成员都正确对齐并具有正确的字节顺序。 然后您的微控制器代码可以最有效地访问每个成员 如果正确完成,此解决方案也更便于携带。

是的,这是内存对齐的问题。

你在使用哪种控制器?

只需声明结构以及以下语法,

__attribute__(packed)

可能会解决你的问题。

或者您可以尝试通过地址而不是按值引用来访问变量作为引用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM