长期读者,首次发布。
我通过蓝牙在传感器和基站之间建立了串行链路。蓝牙连接非常不可靠,而且会像你不会相信的那样丢弃数据包。我用这个作为积极的一面,并将设计一个强大的串行协议,可以在一个糟糕的链接中幸存下来。
我只是想从人们那里得到一些想法,因为我是办公室里唯一的嵌入式开发人员。
计划是使用字节填充来创建具有起始(STX)和结束(ETX)字节、索引号和CRC的数据包。当STX、ETX和DLE字符出现时,我计划使用转义字符(DLE)。这部分都很清楚,这里说的是应该做的代码
static void SendCommand(struct usart_module * const usart_module, uint16_t cmd,
uint8_t *data, uint8_t len)
{
//[STX] ( { [IDX] [CMD_H] [CMD_L] [LEN] ...[DATA]... } [CRC] ) [ETX] // Data in {} brackets is XORed together for the CRC. // Data in () is the packet length
static uint8_t idx;
uint8_t packet[256];
uint8_t crc, packetLength, i;
if (len > 250)
return;
packetLength = len + 5;
packet[0] = idx++;
packet[1] = (cmd >> 8);
packet[2] = (cmd & 0x00FF);
packet[3] = packetLength;
crc = 0;
for (i = 0; i <= 3; i++)
{
crc ^= packet[i];
}
for (i = 0; i < len; i++)
{
packet[4 + i] = data[i];
crc ^= data[i];
}
packet[4 + len] = crc;
// Send Packet
uart_putc(usart_module, STX);
for (i = 0; i < packetLength; i++)
{
if ((packet[i] == STX) || (packet[i] == ETX) || (packet[i] == DLE))
{
uart_putc(usart_module, DLE);
}
uart_putc(usart_module, packet[i]);
}
uart_putc(usart_module, ETX);
}
所以这会发送一个数据包,但现在我需要添加一些代码来跟踪数据包,并自动重新发送失败的数据包,这就是我需要一些想法帮助的地方。
我有几个选择;-最简单的方法是,分配一个由256个数据包组成的庞大阵列,每个数据包都有足够的空间来存储一个数据包,发送数据包后,将其放入缓冲区,如果在x段时间后我没有收到ACK,则再次发送。然后,如果我确实收到了ACK,请从数组中删除该记录,使条目为空,这样我就知道它收到得很好。
问题是,如果我使用256字节的最坏情况数据包大小,以及256个实例,那就是64K的RAM,而我没有(即使我有,那也是一个糟糕的想法)
-下一个想法是,创建一个所有数据包的链表,并使用malloc命令等动态分配内存,删除已确认的数据包,保留未确认的数据,这样我就知道在x时间后重新发送哪个数据包。
我的问题是整个malloc的想法。说实话,我从来没有用过它,我也不喜欢在内存有限的嵌入式环境中使用它。也许这只是我的愚蠢,但我觉得这为我不需要的一大堆其他问题打开了大门。
-潜在的解决方案是,像上面提到的那样,为所有数据包创建一个链表,但在fifo中创建它们,并移动所有记录以将它们保存在fifo。
例如。发送数据包1,将数据包放入fifo
发送数据包2,将数据包包入fifo
接收数据包1的NACK,不执行任何操作
发送数据包3,将数据包放入fifo
接收对数据包2的ACK,将包2设置为在fifo中0x00
接受对数据包3的ACK,将数据包放入fifo中
fifo中没有更多空间,遍历并将所有数据拖到前面,因为数据包2和3所在的位置现在是空的。
可以让他们实时洗牌,但我们需要在收到每个数据包后洗牌一整张fifo,这似乎是很多不必要的工作?
我想让你们中的一个人说:"天哪,Ned,这还不错,但如果你只是做xyz,那么这会为你节省大量的工作、RAM或复杂性"之类的话。
只想让几个人从中跳出想法,因为这就是你通常获得最佳解决方案的方式。我喜欢我的解决方案,但我觉得我错过了什么,或者可能过于复杂了?不确定。。。我只是对这个想法没有百分之一百的满意,我不认为,只是想不出更好的方法。
您不需要使用malloc。只需静态地分配所有数据包,可能是作为一个结构数组。但是不要在运行时使用数组来遍历数据包。相反,扩展你的链表想法。创建两个列表,一个用于等待ACK的数据包,另一个用于空闲(即可用)数据包。启动时,将每个数据包添加到空闲列表中。发送数据包时,请将其从空闲列表中删除,并将其添加到等待确认列表中。使等待确认列表双重链接,以便您可以从列表中间删除数据包并将其返回到空闲列表。
如果你的数据包大小变化很大,并且你想用更少的内存支持更多的数据包,那么你可以为不同大小的数据包创建多个空闲列表。例如,最大大小空闲列表包含最大的数据包,而经济大小空闲列表则包含较小的数据包。只要知道将数据包返回到哪个空闲列表,等待ACK列表就可以同时保持两种大小。这可以通过在数据包结构中添加标志来知道。
typedef enum PacketSizeType
PACKET_SIZE_MAX = 0,
PACKET_SIZE_ECONOMY
} PacketSizeType;
typedef struct PacketBase{
PacketBase * next;
PacketBase * prev;
PacketSizeType type;
uint8_t data[1]; // a place holder (must be last)
} PacketBase;
typedef struct PacketMax
{
PacketBase base; // inherit from PacketBase (must be first)
uint8_t data[255];
} PacketMax;
typedef struct PacketEconomy
{
PacketBase base; // inherit from PacketBase (must be first)
uint8_t data[30];
} PacketEconomy;
PacketMax MaxSizedPackets[100];
PacketEconomy EconomySizedPackets[100];
Packet *free_list_max;
Packet *free_list_economy;
Packet *awaiting_ack_list;
初始化代码应该在两个数组中循环,将base.type
成员设置为MAX
或ECONOMY
,并将数据包添加到适当的空闲列表中。发送代码从适当大小的空闲列表中获取数据包,并将其移动到等待ACK列表。等待ACK列表可以包含这两种类型的分组,因为它们都继承自PacketBase
。ACK处理程序代码应该检查base.type
以将数据包返回到适当的空闲列表。
几个注意事项。
- 您需要考虑订购。无序交货可以接受吗
- 你在乎重复的数据包吗
- 必须在超时的情况下检测丢失的ACK
我的猜测是,您关心数据包交付顺序,不想要重复的数据包。
一种基本的方法是发送一个带有序列号的数据包,在一个时间段内等待ACK。如果收到ACK,请按顺序发送下一个数据包。如果您没有得到ACK(考虑超时和NAK等效),则重新发送。重复
然后,数据结构问题就变成了当有未处理的数据包时在应用程序中排队。
如果你想处理多个未处理的数据包,你需要一个更复杂的数据结构和一种进行选择性ACK的方法。有很多文献,但一个好的起点是Tannenbaum的《计算机网络》。