在嵌入式开发中,自定义通信协议是指开发者根据具体应用的需求,设计并实现一个专门的协议,用于在嵌入式系统与外部设备(如上位机、传感器、其他微控制器等)之间进行数据交换。这种协议通常定义了数据的格式、传输规则、错误检测机制等,确保通信的正确性与可靠性。
自定义通信的目的
简化通信:通过明确的数据格式和规则,确保嵌入式系统与其他设备之间能够顺利、准确地交换数据。
提高效率:根据实际需求设计协议,使通信更加高效、低功耗、节省带宽。
解决兼容性问题:避免使用标准协议时可能出现的兼容性问题,特别是当设备数量、数据类型等不确定时。
增加可控性:在嵌入式开发中,通过自定义协议可以精确控制数据的传输方式和格式。
自定义hex协议
我们在使用 USART 与上位机通信时,有时标准的 USART 协议不能满足我们的功能需求,故我们需要在 USART 的基础上定义我们自己的通信协议;
我们首先规定,PC 与 MCU 通信过程中,先发送高位的字节(0x1213456 先发送 12),以小端方式存储(具体需要验证当前环境的存储方式);
以平衡小球为例,我们需要定义如下规则:
设置小球位置
帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | |
---|---|---|---|---|---|---|
字节数 | 2 | 1 | 1 | 2 | 1 | 1 |
默认值 | 0xAA 0xAA | 0xB0 | 0x02 | 上位机传入 | Add(8) | 0xBB |
小球的位置可以从 0ms ~ 2000+ms,所以我们这里用两个字节存放小球返回的位置;
Add(8):对命令位、数据长度和数据位进行累加后取低 8 位。
设置 PID 相关参数
帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | |
---|---|---|---|---|---|---|
字节数 | 2 | 1 | 1 | 12 | 1 | 1 |
默认值 | 0xAA 0xAA | 0xB1 | 0x0C | 上位机传入 | Add(8) | 0xBB |
数据位:数据位包含P、I、D三个参数,每个参数 4 个字节所以一共 12 个字节
- P: 4 个字节,float 类型。P 值
- I: 4 个字节,float 类型。I 值
- D: 4 个字节,float 类型。D 值
获取当前小球位置
帧头 | 命令位 | 数据长度 | 数据 | 校验位 | 帧尾 | |
---|---|---|---|---|---|---|
字节数 | 2 | 1 | 1 | 2 | 1 | 1 |
默认值 | 0xAA 0xAA | 0xF0 | 0x02 | MPU 返回 | Add(8) | 0xBB |
获取当前 PID 相关参数
帧头 | 命令位 | 数据长度 | 数据位 | 校验位 | 帧尾 | |
---|---|---|---|---|---|---|
字节数 | 2 | 1 | 1 | type(1) | P/I/D(4) | 1 | 1 |
默认值 | 0xAA 0xAA | 0xE0 | 0x05 | MPU 返回 | Add(8) | 0xBB |
数据位包含一个 type 用于区分参数(P、I、D三个参数),后 4 个字节是前面 type 所返回的具体数值
0x00 : Kp
0x01:Ki
0x02:Kd
float 的内存表示
因为 PC 在与 MCU 通信时会涉及到小数的传输,所以这里需要先了解下 float 在内存中是如何表示的:在 C 语言中,float 通常以 IEEE 754 单精度浮点数格式存储,占 4字节(32位)。
PC 在与 MCU 通信时我们如果直接传输一个小数比如 12.1231,MCU 就需要通过字符串来接收这个指令,虽然简单但是会造成数据位的大小不固定,协议是需要规定数据位大小的。(12.1 是 4 个字节,12.1213 是 7 个字节)
所以我们这里采取将 float 转为 十六进制码进行发送,这样 MCU 只需要通过 char[4] 大小的数组接收并组装为 float 即可。(12.1 是 4 个字节,12.1213 还是 4 个字节)
实现
1 | // USART 接收处理 |
环形队列
PC 通过 USART 与 MCU 通信过程中通常会产生如下一些问题:
MCU 的接收处理速度跟不上 PC 的发送速度,未完整处理接收到的数据,从而导致丢包或者粘包的问题;因此我们引入了环形队列这一数据结构进行处理;
环形队列是一种特殊的队列数据结构,它使用固定长度的数组实现队列的功能,并通过逻辑上的“首尾相接”形成一个环形的结构。
特点
队列的最后一个元素的下一个位置可以与第一个元素相连,形成一个循环结构。
当队尾指针到达数组末尾时,如果还有空余空间,可以绕回到数组的开头继续存储数据。
为了区分队列是“空”还是“满”,通常会保留一个空闲位置或者使用额外的标志位。
实现方法
假设用一个固定长度为 N 的数组来实现环形队列:
- 队头指针(head):指向队列中第一个元素的位置。
- 队尾指针(tail):指向下一个可以插入的位置(注意,不是最后一个元素)。
队列的基本操作
入队(enqueue):
- 检查队列是否已满: (tail + 1) % N == head 若成立,则队列已满。
- 插入数据后,将 tail 指针向前移动: tail = (tail + 1) % N
出队(dequeue):
- 检查队列是否为空: tail == head 若成立,则队列为空。
- 删除数据后,将 head 指针向前移动: head = (head + 1) % N
判断队列是否为空:
tail == head
判断队列是否已满:
(tail + 1) % N == head
计算队列中元素个数:
size = (tail − head + N) % N
实现
1 |
|
问题
一、环形队列中为什么要预留一个空位
环形队列中的数据是通过头指针(head)和尾指针(tail)来管理的:
- 空队列:当 head == tail 时,队列为空。
- 满队列:如果不预留空位,当所有缓冲区都填满时,head 和 tail 也会变成相等,这会导致无法区分“队列满”和“队列空”这两种状态。
因此,为了简化逻辑并明确状态,环形队列中通常会留出一个空位作为缓冲,这样就可以通过以下规则来判断队列是否满或空:
- 队列空:head == tail
- 队列满:(tail + 1) % size == head
二、同时使用 DMA 和 RBEN 中断时的数据竞争问题