【发布时间】:2021-01-13 16:16:50
【问题描述】:
我有一个嵌入式项目,我正在实现一个库来处理 2 个微处理器之间的串行通信。我有一个结构,可以为随着时间的推移可能会添加的数据包提供一种可扩展的方法。
需要有不同的数据包携带可变长度的信息。每种类型的数据包由接收它的处理器以不同的方式解释。
只要有一个字节进入,就会执行读取。当一个完整的数据包到来时,调用消费者函数来尽快处理它。
我想解释我的方法,并就其弱点以及如何改进它获得一些反馈。
目前的方法:
- 创建一个显示数据成员和长度的结构。
#define PASSWORD_PACKET_BYTES 5
typedef union
{
uint8_t PasswordBuffer[PASSWORD_PACKET_BYTES];
struct
{
uint32_t Password;
uint8_t CRC;
};
}password_packet_t;
- 创建一个包含所有类型数据包的联合和一个 uint_8 缓冲区。
- 创建一个结构,用于保存读取的字节、正在读取的当前数据包类型(联合标记)、转义状态。
#define DISPLAY_PACKET_BYTES 16
typedef struct
{
union
{
uint8_t SerialBuffer[DISPLAY_PACKET_BYTES];
password_packet_t PasswordPacket;
};
struct
{
uint8_t CurrentOperation;
uint8_t BytesRead;
bool EscapeNext;
};
}disp_receive_packet_t;
- 创建一个显示不同类型数据包值的枚举(还包含转义字符的值)。
enum
{
RECEIVE_NO_OPERATION = 0xA0,
ACK,
PASSWORD,
VERSION_REQUEST,
RECEIVE_ESCAPE,
}DISP_RECEIVE_OPERATIONS;
功能的实现: - 每当读取一个字节时,检查它是否是一个新操作,如果是,则更改联合标记。 -Else 如果转义字符值设置转义标志。 -Else 将数据推送到缓冲区,增加读取的字节数。 还为当前操作调用检查完成函数(switch 语句)。 如果完成,则处理数据包并重置控制结构。 如果没有,什么都不会发生。
void DispRead1B(void)
{
static uint8_t ReadByte;
ReadByte = (uint8_t)UARTCharGet(DISP_BASE);
if(DisplayPacket.EscapeNext)
{
DisplayPacketPushByte(ReadByte);
DisplayPacket.EscapeNext = false;
}
else
{
if(ReadByte == RECEIVE_ESCAPE)
{
DisplayPacket.EscapeNext = true;
}
else if((ReadByte < RECEIVE_ESCAPE) && (ReadByte > RECEIVE_NO_OPERATION))
{
DisplayPacketChangeOperation(ReadByte);
}
else
{
DisplayPacketPushByte(ReadByte);
}
}
}
void DisplayPacketChangeOperation(uint8_t Operation)
{
DisplayPacket.CurrentOperation = Operation;
DisplayPacketResetBytesRead();
DisplayPacket.EscapeNext = false;
DisplayPacketCheckOperationFinished();
}
void DisplayPacketPushByte(uint8_t Value)
{
DisplayPacket.SerialBuffer[DisplayPacket.BytesRead] = Value;
DisplayPacketIncrementBytesRead();
DisplayPacketCheckOperationFinished();
}
void DisplayPacketIncrementBytesRead(void)
{
DisplayPacket.BytesRead += 1;
if(DisplayPacket.CurrentOperation == RECEIVE_NO_OPERATION)
{
DisplayPacket.BytesRead = 0;
}
}
bool DisplayPacketCheckOperationFinished(void)
{
bool PacketFinished = false;
switch (DisplayPacket.CurrentOperation)
{
case ACK:
PacketFinished = DisplayPacketCheckAckFinished(); break;
case VERSION_REQUEST:
PacketFinished = DisplayPacketCheckVersionRequestFinished(); break;
case PASSWORD:
PacketFinished = DisplayPacketCheckPasswordFinished(); break;
default:
return false;
}
if(PacketFinished)
{
DisplayPacketChangeOperation(RECEIVE_NO_OPERATION);
}
return PacketFinished;
}
bool DisplayPacketCheckAckFinished(void)
{
if(DisplayPacket.BytesRead == ACK_PACKET_BYTES)
{
if(CheckSysFlagNotSet(KeepAliveAckReceived))
{
UpdateSysFlag(KeepAliveAckReceived,true);
StartAliveTimer();
}
else
{
UpdateSysFlag(ConnectionError,false);
ReloadAliveTimer();
}
return true;
}
return false;
}
bool DisplayPacketCheckVersionRequestFinished(void)
{
if(DisplayPacket.BytesRead == VER_REQ_PACKET_BYTES)
{
UpdateSysFlag(FirmwareVerRequested,true);
return true;
}
return false;
}
bool DisplayPacketCheckPasswordFinished(void)
{
if(DisplayPacket.BytesRead == PASSWORD_PACKET_BYTES)
{
PasswordPacket = DisplayPacket.PasswordPacket;
UpdateSysFlag(PasswordReceived,true);
return true;
}
return false;
}
使用这种方法不需要序列化,因为联合会处理它。此外,如果需要备份数据副本,复制数据包的语法也很简单。 IE。 PacketType = CommUnion.PacketType;
添加新数据包时。创建一个结构并将其添加到联合中,添加新的枚举字段,并添加一个 case 到 checkfinished switch 语句中,该语句调用该数据包的函数。
这需要库的源代码在我不喜欢的多行上进行更改。
有没有办法以更简单的方式实现类似的功能,需要外部更改而编译的通信结构可以保持不变?
第二种方法(使用函数指针):
- 摆脱联合。控制结构仍然存在,因为我们需要跟踪当前的数据包类型和读取的字节数、转义等。
- 摆脱枚举方法来创建数据包值。创建函数来为不同的数据包注册检查完成的函数,并像枚举方法一样为它们分配值。 register 函数是一个函数指针数组,用于检查函数。
- 在启动时,所有数据包结构都被初始化。(主程序或项目特定模块的更改不在库中。)
- 然后使用注册功能注册所有数据包。这样,数据包的数量和转义字符的值可以在运行时计算(如果编译器很智能,则可以在编译时计算)。
- 当您压入一个数据字节时,检查完成函数指针用于调用该函数。该函数序列化(或调用此类函数)并在数据包完全接收时处理数据包。
这种方法的好处是创建了数据包的实例(无论哪种方式都需要这样做)并调用寄存器。我觉得需要改变的东西更少,这应该减少人为错误(理想情况下)。
如果有人愿意与我讨论这两种方法的优缺点,或者向我指出一种更精简的做事方式,我将不胜感激。
【问题讨论】:
-
总的来说,结构和联合是危险的,因为填充。如果您希望使用 struct/union 来反映数据协议,则必须确保没有填充/对齐问题。在较大的 MCU(32 位)上,这通常意味着使用
#pragma pack或类似的非标准扩展名禁用结构的填充。这样的代码不能在编译器之间移植。如果需要 100% 可移植的标准 C,那么就没有办法编写序列化/反序列化例程。 -
至于您的 struct/union/enum 问题,如果没有具体的代码示例,很难理解您的意思。或者至少是结构/联合声明。请不要描述代码,而是发布它。或者它的简化版本。不编译但类似于 C 的伪代码也可以。
-
我们使用的是 8 位和 32 位处理器。所有的数据包都被设计成首先在它们最大的数据成员中进行数据包,然后再变小。一切都是 32,16 或 8 位变量。结构以确认没有填充的方式组织。最后的填充是可以的,因为每种类型的数据包都有 1 或 2 个实例。我将尝试添加代码示例,但哪一部分令人困惑?第一种还是第二种方法?
-
我认为结构/联合声明本身可能解释了很多。可能还有选择数据包类型的代码。
-
我为第一个实现添加了代码。第二个处于构思阶段,所以我实际上还没有它的代码。希望这有助于解释它。
标签: c serialization embedded uart