嵌入式系统的串行通信包组织

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;
#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。

这需要库的源代码更改多行,我不喜欢。

有没有办法以更简单的方式实现类似的功能,需要外部更改,而编译的通信结构可以保持不变?

心目中的第二种方法(使用函数指针):

此方法的好处是创建数据包的实例(需要以任何一种方式完成)并调用注册。我觉得需要更改的东西更少,应该可以减少人为错误(理想情况下)。

如果有人愿意与我讨论这两种方法的优点或缺点,或者向我指出一种更精简的做事方式,我将不胜感激。

数据结构设计

对于并集,您将不得不强制它按位 (#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)。所以要监控的东西更少,但前期配置更多。