嵌入式系统的串行通信包组织
Serial Communication Packet Organization for an Embedded System
我有一个嵌入式项目,我正在实现一个库来处理 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 如果转义字符值设置转义标志。
-否则将数据推送到缓冲区,增加读取的字节数。
还为当前操作(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。数据包类型 = CommUnion.PacketType;
添加新数据包时。创建了一个结构并将其添加到联合中,添加了新的枚举字段,并添加了调用此数据包函数的检查完成的 switch 语句的 case。
这需要库的源代码更改多行,我不喜欢。
有没有办法以更简单的方式实现类似的功能,需要外部更改,而编译的通信结构可以保持不变?
心目中的第二种方法(使用函数指针):
- 摆脱工会。控制结构仍然存在,因为我们需要跟踪当前数据包类型和字节读取、转义等。
- 摆脱枚举方法来创建数据包值。创建函数来为不同的数据包注册检查完成的函数,并像枚举方法一样为它们赋值。寄存器函数是一个函数指针数组,用于检查函数。
- 启动时所有数据包结构都被初始化。(主程序或项目特定模块的更改不在库中。)
- 然后使用register函数注册所有数据包。这样可以在运行时(如果编译器很聪明,则可以在编译时)计算数据包的数量和转义字符的值。
- 当您压入一个数据字节时,Check finished 函数指针用于调用该函数。此函数序列化(或调用此类函数。)并在数据包被完全接收时处理该数据包。
此方法的好处是创建数据包的实例(需要以任何一种方式完成)并调用注册。我觉得需要更改的东西更少,应该可以减少人为错误(理想情况下)。
如果有人愿意与我讨论这两种方法的优点或缺点,或者向我指出一种更精简的做事方式,我将不胜感激。
数据结构设计
对于并集,您将不得不强制它按位 (#pragma pack) 以确保数据正确。有了这个修复,我认为联合方法工作得很好,因为我已经研究过类似的实现
但还有其他事情我会担心。
可变长度数据包
There needs to be distinct packets that carry variable length information. Each type of packet is interpreted differently by the processor receiving it.
如果忽略尾随位,则实现固定长度的数据包会容易得多。如果您使用可变数据包长度,具体取决于您使用的外围设备,但我假设是 UART,您将配置驱动程序以能够确定何时完全接收数据包(我假设您的 EscapeNext 标志)。否则,您将不知道收到的数据是否有效。
使用转义标志,您必须确保您的主要负载永远不会有相同的字节数据,否则您的驱动程序将错误地假设数据包已被完全接收。
您可以使用某种类型的外设接收超时来处理可变长度,这种超时会在位之间经过太多时间时触发,但是仍然有许多变量可以使用该类型的实现
** 编辑
如果你打算仍然使用可变长度,那么你肯定需要让你的数据包的一部分代表数据包的长度。
使用 CRC 与使用奇偶校验
要确认数据包的整体完整性,您需要使用 CRC。 CRC 可以确认您的数据包的整体有效性,因为您可能不知道您是否在整个数据包中丢失了一个字节。
奇偶校验仅在字节级别使用,因为外设不关心它需要发送的数据是否是垃圾,所以它不知道它是否把整个数据包搞砸了。
提示:固定长度的数据包 = 更少的调试时间
使用更快的波特率和固定长度的数据包的好处是不那么复杂,因此调试时间更少。就数据包传输时间而言,这很可能会超过使用可变长度数据包所带来的好处。
在 921k 波特率下,您应该能够每 8 或 9 微秒传输一个字节。
如果您处理的字节少于 10 个字节,那么每个数据包将节省 0.1 毫秒或更少。
提示:DMA
我不知道您使用的是什么 MCU 或库,但理想情况下,您可以使用固定长度的数据包设置 DMA 并配置 DMA Rx Complete 中断。 DMA 将允许您将字节移动到另一个内存位置,并在它已满时通知您(同样取决于 MCU)。所以要监控的东西更少,但前期配置更多。
我有一个嵌入式项目,我正在实现一个库来处理 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 如果转义字符值设置转义标志。 -否则将数据推送到缓冲区,增加读取的字节数。 还为当前操作(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。数据包类型 = CommUnion.PacketType;
添加新数据包时。创建了一个结构并将其添加到联合中,添加了新的枚举字段,并添加了调用此数据包函数的检查完成的 switch 语句的 case。
这需要库的源代码更改多行,我不喜欢。
有没有办法以更简单的方式实现类似的功能,需要外部更改,而编译的通信结构可以保持不变?
心目中的第二种方法(使用函数指针):
- 摆脱工会。控制结构仍然存在,因为我们需要跟踪当前数据包类型和字节读取、转义等。
- 摆脱枚举方法来创建数据包值。创建函数来为不同的数据包注册检查完成的函数,并像枚举方法一样为它们赋值。寄存器函数是一个函数指针数组,用于检查函数。
- 启动时所有数据包结构都被初始化。(主程序或项目特定模块的更改不在库中。)
- 然后使用register函数注册所有数据包。这样可以在运行时(如果编译器很聪明,则可以在编译时)计算数据包的数量和转义字符的值。
- 当您压入一个数据字节时,Check finished 函数指针用于调用该函数。此函数序列化(或调用此类函数。)并在数据包被完全接收时处理该数据包。
此方法的好处是创建数据包的实例(需要以任何一种方式完成)并调用注册。我觉得需要更改的东西更少,应该可以减少人为错误(理想情况下)。
如果有人愿意与我讨论这两种方法的优点或缺点,或者向我指出一种更精简的做事方式,我将不胜感激。
数据结构设计
对于并集,您将不得不强制它按位 (#pragma pack) 以确保数据正确。有了这个修复,我认为联合方法工作得很好,因为我已经研究过类似的实现
但还有其他事情我会担心。
可变长度数据包
There needs to be distinct packets that carry variable length information. Each type of packet is interpreted differently by the processor receiving it.
如果忽略尾随位,则实现固定长度的数据包会容易得多。如果您使用可变数据包长度,具体取决于您使用的外围设备,但我假设是 UART,您将配置驱动程序以能够确定何时完全接收数据包(我假设您的 EscapeNext 标志)。否则,您将不知道收到的数据是否有效。
使用转义标志,您必须确保您的主要负载永远不会有相同的字节数据,否则您的驱动程序将错误地假设数据包已被完全接收。
您可以使用某种类型的外设接收超时来处理可变长度,这种超时会在位之间经过太多时间时触发,但是仍然有许多变量可以使用该类型的实现
** 编辑 如果你打算仍然使用可变长度,那么你肯定需要让你的数据包的一部分代表数据包的长度。
使用 CRC 与使用奇偶校验
要确认数据包的整体完整性,您需要使用 CRC。 CRC 可以确认您的数据包的整体有效性,因为您可能不知道您是否在整个数据包中丢失了一个字节。
奇偶校验仅在字节级别使用,因为外设不关心它需要发送的数据是否是垃圾,所以它不知道它是否把整个数据包搞砸了。
提示:固定长度的数据包 = 更少的调试时间
使用更快的波特率和固定长度的数据包的好处是不那么复杂,因此调试时间更少。就数据包传输时间而言,这很可能会超过使用可变长度数据包所带来的好处。 在 921k 波特率下,您应该能够每 8 或 9 微秒传输一个字节。 如果您处理的字节少于 10 个字节,那么每个数据包将节省 0.1 毫秒或更少。
提示:DMA
我不知道您使用的是什么 MCU 或库,但理想情况下,您可以使用固定长度的数据包设置 DMA 并配置 DMA Rx Complete 中断。 DMA 将允许您将字节移动到另一个内存位置,并在它已满时通知您(同样取决于 MCU)。所以要监控的东西更少,但前期配置更多。