FreeRTOS_同步互斥与通信_环形buffer、队列_学习笔记

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读队列时,他再起来继续工作

队列中有三个东西:

  1. 环形buffer(传送带)
  2. senderlist:想要发送但被阻塞的任务(睡着的A)
  3. 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;
	}
}

上述读写环形缓冲区的操作,一直尝试读取红外接收器的信号,效率很低。
因此,对红外遥控器的控制进行改进:

  1. 创建队列
  2. 在挡球板platform_task任务中读队列
  3. 在红外遥控器的中断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)实现写队列

  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值