我正在开发基于微控制器的软件项目。 该项目的一部分是二进制协议的解析器。 协议是固定的,不能更改。 PC充当"主人"并且主要传输命令,这些命令必须由微控制器板的" slave"执行。
协议数据由硬件通信接口接收,例如, UART,CAN或以太网。 那不是问题。
在接收到帧的所有字节(4-10,取决于命令)之后,它们被存储在类型为" uint8_t cmdBuffer [10]"的缓冲区中。并设置一个标志,表示现在可以执行该命令。 帧的第一个字节(cmdBuffer [0])包含命令,帧的其余部分是命令的参数,根据命令的不同,数字和大小可能不同。 这意味着,有效载荷可以通过多种方式进行解释。对于每个可能的命令,数据字节都会改变它们的含义。
我不想要有太多丑陋的操作,而是自我记录代码。 所以我的方法是:
示例:
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
}
}
我发现有两个问题:
首先,根据微控制器架构,byteorder可以是intel或motorola,用于2或4字节参数。 这应该不是什么大问题。协议本身使用网络字节顺序。在目标控制器上,可以使用宏来更正订单。
主要问题:我的tyepdef结构中可能有填充字节。我使用gcc,所以我可以在我的typedef中添加一个" packed" -attribute。其他编译器为此提供了编译指示。但是,在32位机器上,打包结构将导致更大(和更慢)的机器代码。好吧,这也可能不是问题。但是我听说,访问未对齐的内存时可能会出现硬件故障(例如,在ARM体系结构上)。
有很多命令(大约50个),所以我不想将cmdBuffer作为一个数组访问 我认为"结构方法"与"数组方法相比,提高了代码的可读性;
所以我的问题:
此致 lugge
答案 0 :(得分:3)
通常,由于填充,结构对于存储数据协议是危险的。对于可移植代码,您可能希望避免使用它们。因此,保持原始数据阵列仍然是最好的主意。您只需要根据收到的命令对其进行不同的解释。
此场景是需要某种多态性的典型示例。不幸的是,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这样的东西可以在解释器函数中处理。当然,您可以更改函数原型,也许您需要返回一些内容或传递更多参数等。
请注意,上述示例最适合所有命令导致相同类型的操作。如果你需要根据命令做完全不同的事情,其他方法更合适。
答案 1 :(得分:2)
恕我直言,这是一个肮脏的黑客。当移植到具有不同对齐要求,不同可变大小,不同类型表示(例如,大端/小端)的系统时,代码可能会中断。或者甚至在相同的系统上但不同版本的编译器/系统头/无论如何。
我认为它不会违反严格的别名,只要相关字节构成有效的表示。
我只是编写代码以明确定义的方式读取数据,例如
bool extract_A(PayloadA_t *out, uint8_t const *in)
{
out->foo = in[0];
out->bar = read_uint32(in + 1, 4);
// ...
}
这可能比" hack"版本,它取决于您的要求是否更喜欢维护头痛,或者那些额外的微秒。
答案 2 :(得分:1)
以相同的顺序回答您的问题:
这种方法很常见,但我知道提到这种技术的任何一本书都被称为肮脏黑客。你自己拼出了这些理由:实质上它是非常不可移植的,或者需要大量的预处理器才能让它变得便携。
严格别名规则:查看What is the strict aliasing rule?的最高投票答案
我所知道的唯一替代解决方案是如您自己提到的那样明确编写反序列化代码。这实际上可以像下面这样可读:
uint8_t *p = buffer;
struct s;
s.field1 = read_u32(&p);
s.field2 = read_u16(&p);
予。 E.我会使读取函数将指针向前移动反序列化的字节数。
答案 3 :(得分:1)
这是一个肮脏的黑客。我在这个解决方案中遇到的最大问题是内存对齐,而不是字节顺序或结构打包。
内存对齐问题是这样的。某些微控制器(如ARM)要求多字节变量与某些存储器偏移对齐。也就是说,2字节半字必须在偶数存储器地址上对齐。并且4字节字必须在4的倍数的存储器地址上对齐。这些对齐规则不是由串行协议强制执行的。因此,如果您只是将串行数据缓冲区转换为打包结构,则各个结构成员可能没有正确的对齐方式。然后,当您的代码尝试访问未对齐的成员时,将导致对齐错误或未定义的行为。 (这就是编译器默认创建一个未打包的结构的原因。)
关于字节顺序,当您的代码访问压缩结构中的成员时,听起来像是建议更正字节顺序。如果您的代码多次访问压缩结构成员,则每次都必须更正字节顺序。当首次从串行端口接收数据时,仅更正字节序一次会更有效。这是不将数据缓冲区简单地转换为压缩结构的另一个原因。
当您收到命令时,您应该将每个字段分别解析为一个解压缩的结构,其中每个成员都正确对齐并具有正确的字节顺序。然后您的微控制器代码可以最有效地访问每个成员如果正确完成,此解决方案也更加便携。
答案 4 :(得分:0)
是的,这是内存对齐的问题。
您正在使用哪种控制器?
只需声明结构以及以下语法
即可__attribute__(packed)
可能会解决您的问题。
或者您可以尝试通过地址访问变量作为引用,而不是按值引用。