FreeRTOS_同步互斥与通信_概念_学习笔记
信号量、互斥量的本质是队列,队列的本质是加强版环形缓冲区
5 FreeRTOS数据传输的方法-环形buffer、队列
如果我有两个任务TaskA和TaskB,他俩可以同时运行。想要在他们之间传递数据,可以用一个全局变量实现。
但是用全局变量传递数据,一次只能传递1个数据,利用率很低,此外还可能出错
数据传输方法 | 数据个数 | 互斥措施 | 阻塞-唤醒 |
---|---|---|---|
全局变量 | 1个 | 无 | 无 |
环形缓冲 | 多个 | 无 | 无 |
队列 | 多个 | 有 | 有 |
5.1 环形buffer概念
如果只需要沟通两个任务,且不考虑互斥和唤醒,就可以利用环形缓冲区
环形缓冲区就是个数组,需要指定读位置r和写位置w。
写时,如果w越界了,就要让他从0开始往后找
int buf[8];
int r=0, w=0; //r和w表示下一个读/写位置
//写操作:
if(w+1 != r)
{
buf[w] = val;
w++;
if(w == 8){w = 0};
}
//读操作:
if(r != w)
{
val = buf[r];
r++;
if(r == 8){r = 0};
}
能不能创建一个全局变量num来统计buf中的元素值,用num==8来判断是否满呢?这又涉及到全局变量的问题,有两个任务会对这个变量进行修改,有赋值过程中跳到别的任务的风险。
在上述方法中,写操作只能修改写位置,读操作只能修改读位置,没有任何一个变量能被多个任务修改,这就能避免出错。
5.2 队列概念与本质
队列中,数据读写的本质就是环形缓冲区,在此基础上增加了互斥机制、阻塞-环形机制。
如果队列不传输数据,只调整数据个数,他就是信号量(semaphore)
如果信号量中,限定数据个数最大为1,他就是互斥量(mutex)
举个例子:
流水线(环形buffer)两边有工人A和B,A(发送者)负责把产品放到流水线上,B(接收者)负责把产品从流水线上拿走进行下一步加工。
对于B,他执行的是读队列操作。如果流水线上没有产品,就睡一会(阻塞)。等到闹钟响了,或A写队列时(唤醒),他再起来继续工作。
对于A,他执行的是写队列操作。如果流水线放满了,就睡一会。等到闹钟响了,或B读队列时,他再起来继续工作
队列中有三个东西:
- 环形buffer(传送带)
- senderlist:想要发送但被阻塞的任务(睡着的A)
- receiverlist:想要接收但被阻塞的任务(睡着的B)
读队列的流程
创建任务B,他是就绪态,被放在在ReadyList中。B中执行读队列操作时,如果队列为空,任务B就会被阻塞,从ReadyList中删除,放到Queue.ReceiverList和DelayedLsit中。
即:一个就绪任务,如果读队列时阻塞了,会从就绪列表中删除,放到队列接收列表和等待列表里。
唤醒的情况1:
当任务A写队列时,会访问Queue.ReceiverList,如果非空,就会唤醒里面的第一个任务。将任务B从Queue.ReceiverList和DelayedLsit中删除,重新放入ReadyList中。
唤醒的情况2:
在每个tick中断,判断任务B是否超时(超时的时间由读操作的参数TickType_t指定),如果超时就唤醒。
5.3 队列函数
5.3.1 队列创建与删除
动态分配:
QueueHandle_t xQueueCreate(长度, 大小);
长度表示能放几个数据,大小表示每个数据的大小,以字节为单位。
创建成功返回句柄,失败返回NULL。
静态分配:
QueueHandel_t xQueueCreateStatic(长度, 大小, uint8_t数组, SQT结构体);
跟静态创建函数差不多。
复位:将队列回复为初始状态
xQueueReset(队列);
删除:
void vQueueDelete(队列);
只能删除动态创建的队列,并释放内存。
5.3.2 写队列
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
一般记住
xQueueSendToFront(队列, 数据指针, 等待时间)
xQueueSendToBack(队列, 数据指针, 等待时间)
即可
5.3.3 读队列
读队列跟pop一样,读完了以后回吧这个数据移除
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait );
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
一般记住
xQueueReceive(队列, 数据指针, 等待时间)
5.3.4 查询
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
5.3.5 窥视
会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。
/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(
QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueuePeekFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
);
5.3.6 覆盖
当队列长度为1时,可以覆盖数据
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(
QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(
QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
5.4 实验
5.4.1 原始程序
原始程序是一个利用红外遥控器控制挡球板的打砖块游戏,
xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
while (1)
{
game1_draw();//让球运动到下一个位置,并判断是否发生触碰
vTaskDelay(50);
}
其中,挡球板任务如下:
static void platform_task(void *params)
{
byte platformXtmp = platformX;
uint8_t dev, data, last_data;
// Draw platform
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
while (1)
{
/* 读取红外遥控器 */
if (0 == IRReceiver_Read(&dev, &data))
{
if (data == 0x00)
{
data = last_data;
}
if (data == 0xe0) /* Left */
{
btnLeft(); //左移
}
if (data == 0x90) /* Right */
{
btnRight(); //右移
}
last_data = data;
// Hide platform
draw_bitmap(platformXtmp, g_yres - 8, clearImg, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
// Move platform 根据移动情况改变x坐标
if(uptMove == UPT_MOVE_RIGHT)
platformXtmp += 3;
else if(uptMove == UPT_MOVE_LEFT)
platformXtmp -= 3;
uptMove = UPT_MOVE_NONE;
// Make sure platform stays on screen
if(platformXtmp > 250)
platformXtmp = 0;
else if(platformXtmp > g_xres - PLATFORM_WIDTH)
platformXtmp = g_xres - PLATFORM_WIDTH;
// Draw platform 并且重新绘制挡球板
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
platformX = platformXtmp;
}
}
}
现在获取红外遥控的值是通过IRReceiver_Read实现,其读环形缓冲区:
int IRReceiver_Read(uint8_t *pDev, uint8_t *pData)
{
if (isKeysBufEmpty())
return -1;
*pDev = GetKeyFromBuf();
*pData = GetKeyFromBuf();
return 0;
}
而写环形缓冲区则由IRReceiver_IRQ_Callback中断服务程序实现
void IRReceiver_IRQ_Callback(void)
{
uint64_t time;
static uint64_t pre_time = 0;
/* 1. 记录中断发生的时刻 */
time = system_get_ns();
/* 一次按键的最长数据 = 引导码 + 32个数据"1" = 9+4.5+2.25*32 = 85.5ms
* 如果当前中断的时刻, 举例上次中断的时刻超过这个时间, 以前的数据就抛弃
*/
if (time - pre_time > 100000000)
{
g_IRReceiverIRQ_Cnt = 0;
}
pre_time = time;
g_IRReceiverIRQ_Timers[g_IRReceiverIRQ_Cnt] = time;
/* 2. 累计中断次数 */
g_IRReceiverIRQ_Cnt++;
/* 3. 次数达标后, 解析数据, 放入buffer */
if (g_IRReceiverIRQ_Cnt == 4)
{
/* 是否重复码 */
if (isRepeatedKey())
{
/* device: 0, val: 0, 表示重复码 */
PutKeyToBuf(0);
PutKeyToBuf(0);
g_IRReceiverIRQ_Cnt = 0;
}
}
if (g_IRReceiverIRQ_Cnt == 68)
{
IRReceiver_IRQTimes_Parse();
g_IRReceiverIRQ_Cnt = 0;
}
}
上述读写环形缓冲区的操作,一直尝试读取红外接收器的信号,效率很低。
因此,对红外遥控器的控制进行改进:
- 创建队列
- 在挡球板platform_task任务中读队列
- 在红外遥控器的中断IRISR中写队列
5.4.2 改进需求:读环形缓冲区->读队列
将读写环形缓冲区改成读写队列。
void game1_task(void *params)
{...
/* 创建队列:平台任务从里面读到红外数据,... */
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data));
xTaskCreate(platform_task, "platform_task", 128, NULL, osPriorityNormal, NULL);
while (1)
{
game1_draw();//让球运动到下一个位置,并判断是否发生触碰
vTaskDelay(50);
}
...}
QueueHandle_t g_xQueuePlatform; /* 挡球板队列 */
static void platform_task(void *params)
{
byte platformXtmp = platformX;
uint8_t dev, data, last_data;
struct input_data idata; //用来存放数据
// Draw platform
draw_bitmap(platformXtmp, g_yres - 8, platform, 12, 8, NOINVERT, 0);
draw_flushArea(platformXtmp, g_yres - 8, 12, 8);
while (1)
{
/* 读取红外遥控器 */
//if (0 == IRReceiver_Read(&dev, &data))
if (pdPASS == xQueueReceive(g_xQueuePlatform, &idata, portMAX_DELAY))
{
data = idata.val;
if (data == 0x00)
{
data = last_data;
}
g_xQueuePlatform = xQueueCreate(10, sizeof(struct input_data))创建了一个队列,并在挡球板任务中使用xQueueReceive读队列。
5.4.3 改进需求:写环形缓冲区->写队列
extern QueueHandle_t g_xQueuePlatform; // 挡球板队列 声明外部变量
void IRReceiver_IRQ_Callback(void)
{
uint64_t time;
static uint64_t pre_time = 0;
struct input_data idata; // 定义输入数据
/* 1. 记录中断发生的时刻 */
time = system_get_ns();
/* 一次按键的最长数据 = 引导码 + 32个数据"1" = 9+4.5+2.25*32 = 85.5ms
* 如果当前中断的时刻, 举例上次中断的时刻超过这个时间, 以前的数据就抛弃
*/
if (time - pre_time > 100000000)
{
g_IRReceiverIRQ_Cnt = 0;
}
pre_time = time;
g_IRReceiverIRQ_Timers[g_IRReceiverIRQ_Cnt] = time;
/* 2. 累计中断次数 */
g_IRReceiverIRQ_Cnt++;
/* 3. 次数达标后, 解析数据, 放入buffer */
if (g_IRReceiverIRQ_Cnt == 4)
{
/* 是否重复码 */
if (isRepeatedKey())
{
/* device: 0, val: 0, 表示重复码 */
//PutKeyToBuf(0);
//PutKeyToBuf(0);
/* 写队列 */
idata.dev = 0;
idata.val = 0;
xQueueSendToBackFromISR(g_xQueuePlatform, &idata, NULL);//中断里不允许等待
g_IRReceiverIRQ_Cnt = 0;
}
}
if (g_IRReceiverIRQ_Cnt == 68)
{
IRReceiver_IRQTimes_Parse();
g_IRReceiverIRQ_Cnt = 0;
}
}
使用xQueueSendToBackFromISR(g_xQueuePlatform, &idata, NULL)实现写队列