我看小智的文档里面使用了操作系统Freertos,但他这个地方没有使用消息队列来接受手机端的数据,他是通过自己写了一个数组缓冲区来接收的,这两个一样的,使用消息队列可能会方便一点。
第一种:环形缓冲区
数据结构:采用了两个结构体套接的形式,第一个结构体用于存放接受的数据,是一个字节数最大为MAX_ONELINE_BYTE的数组,别名是ble_rx_buffer_t,第二个结构体是用于多少个数据,写指针,读指针,还有剩余指针一个四个,别名是ble_rx_t。
// 1. 单个数据包的缓冲结构 // 用于存放一行具体的数据内容 typedef struct{ uint8_t buffer[MAX_ONELINE_BYTE]; }ble_rx_buffer_t; typedef struct{ // 数据池:静态分配的环形数组,存放所有接收到的数据包 ble_rx_buffer_t printer_buffer[MAX_LINE]; uint32_t r_index; uint32_t w_index; uint32_t left_line; }ble_rx_t; ble_rx_t g_ble_rx; xHandler=NULL;//申请的互斥量的句柄
成员变量 | 类型 | 作用 | 形象比喻 |
printer_buffer | 数组 | 仓库货架。真正存放数据的地方,大小为 MAX_LINE 行。 | 一排固定的储物柜。 |
r_index | 索引 | 取货口。告诉读取任务,下一次该从数组的哪个位置拿数据。 | 仓库管理员记录“下一个该取哪个柜子”。 |
w_index | 索引 | 存货口。告诉写入中断,下一次该把数据放到数组的哪个位置。 | 仓库管理员记录“下一个空柜子在哪里”。 |
left_line | 计数 | 空位统计。记录当前还剩多少个空位。 | 仓库门口的“剩余空位计数器”。 |
xHandler | 句柄 | 钥匙(锁)。确保同一时间只有一个人(中断或任务)能操作仓库。 | 仓库大门的钥匙,防止两人同时进出撞到一起。 |
🔄 工作原理图解
这个结构体通过 r_index 和 w_index 的移动,将线性的数组变成了环形的逻辑结构:
写入时:数据存入 printer_buffer[w_index],然后 w_index 加 1。如果 w_index 超过 MAX_LINE,则归零(回到数组开头)。
读取时:数据从 printer_buffer[r_index] 取出,然后 r_index 加 1。如果 r_index 超过 MAX_LINE,则归零。
判满:通过 left_line 判断。如果 left_line 为 0,说明 w_index 追上了 r_index,队列满了。
逻辑:
- 数据结构设计(环形缓冲区)
- 核心变量:g_ble_rx 结构体包含 printer_buffer(数据池)、r_index(读指针)、w_index(写指针)和 left_line(剩余空间计数)。
- 原理:利用数组模拟队列。写指针 w_index 负责在尾部写入,读指针 r_index 负责在头部读取。当指针到达数组末尾时,会自动回绕到开头(if(w_index >= MAX_LINE) w_index = 0),形成闭环,无需移动内存数据,效率极高。
- 写入操作 (write_to_printbuffer - 中断上下文):
- 中断保护:使用 xSemaphoreTakeFromISR 获取互斥锁。这是为了防止在写入过程中被读任务打断,或者被其他中断打断,保证数据完整性。
- 边界检查:检查 left_line 防止缓冲区溢出(满了就不写了),检查 length 防止单次写入过长。
- 数据拷贝:使用 memcpy 将数据存入 w_index 指向的位置。
- 指针推进:写入完成后,w_index 加 1,left_line 减 1。
- 上下文切换:如果锁操作导致高优先级任务被阻塞,通过 portYIELD_FROM_ISR 触发上下文切换,确保系统实时响应。
- 就是在中断里面的申请的xHigherPriorityTaskWoken的这个,假如蓝牙在接受数据的时候来了更高的优先级任务,这个xHigherPriorityTaskWoken会变为pdTRUE,在中断结尾的时候,用if条件检查这个数据,可以跳转更高优先级任务
BaseType_t xHigherPriorityTaskWoken = pdFALSE;// portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
- 读取操作 (read_to_printer - 任务上下文):
- 阻塞/超时机制:使用 xSemaphoreTake(..., 10)。任务会尝试获取锁,最多等待 10 个系统节拍。如果超时(说明总线忙),则返回 NULL,防止任务死锁。
- 判空与读取:获取锁后,检查 left_line(这里逻辑稍微有点绕,实际是看是否有数据可读)。如果有数据,取出 r_index 指向的数据指针,然后 r_index 加 1。
- 资源释放:读取完毕后,必须调用 xSemaphoreGive 释放锁,并更新 left_line(释放空间)。
- 并发控制(Mutex):
- 核心作用:xHandler 互斥锁是保护神。它确保了 r_index 和 w_index 这两个共享资源在同一时间只能被一个执行流(中断或任务)修改,彻底杜绝了“读写竞争”导致的数据错乱。
portYIELD_FROM_ISR函数说明:
1. 在 FreeRTOS 中,中断的优先级高于任何任务。当一个中断发生时,CPU 会暂停当前任务,转而去执行中断服务程序(ISR)。
中断中的操作:ISR 通常会通过 xSemaphoreGiveFromISR 或 xQueueSendFromISR 等函数来通知某个任务“有事情发生了”。
2. 唤醒高优先级任务:这些 ...FromISR 函数有一个关键参数 pxHigherPriorityTaskWoken。如果一个优先级比当前被中断任务更高的任务正在等待这个信号,那么该高优先级任务就会被唤醒,并且 pxHigherPriorityTaskWoken 会被设置为 pdTRUE。
中断返回的抉择:ISR 执行完毕后,CPU 需要决定返回到哪里。
3. 不使用 portYIELD_FROM_ISR:CPU 会无条件返回到被中断的那个低优先级任务,继续执行它,直到下一个调度点(如系统节拍)才检查并切换到高优先级任务。这造成了响应延迟。
4. 使用 portYIELD_FROM_ISR(xHigherPriorityTaskWoken):这个宏会检查 xHigherPriorityTaskWoken 的值。如果为 pdTRUE,它会强制进行一次上下文切换,让 CPU 直接去执行那个刚刚被唤醒的高优先级任务,而不是返回原来的低优先级任务。
队列的话比较方便,就是创建一个消息队列来接受,Write和Read队列的函数文档都有