本帖最后由 DKENNY 于 2024-5-20 20:51 编辑
#申请原创# @21小跑堂
## 前言
上篇介绍了 mbed rtos 的一些基本知识,以及线程的同步机制,接下来我们继续讲讲 mbedos 的线程间的通讯以及在中断服务函数中的使用。
## 3、mbedos 线程间通讯
mbed-rtos 提供了多种方式的线程间通讯,包括队列(Queue)、邮件(Mail)两种,另外还提供了一个辅助类内存池(MemoryPool,用于 Queue 中的数据存储),它们提供的主要方法如下:
类名
| 方法
| 用途
| Queue
| template<typename Queue()
T,
uint32_t
queue_sz>
| 构造函数,构造大小为 queue_sz,类 型为 T 的队列
| osStatus put(T* data, uint32_t millisec=0)
| 把 T 类型的数据放入队列
| osEvent get(uint32_t millisec=osWaitForever)
| 获取队列信息, 取出的数据包含在 osEvent 对象中
| MemoryPool
| template<typename MemoryPool()
T,
uint32_t
pool_sz>
| 构造函数,构造大小为 pool_sz,类型 为 T 的内存池
| T* alloc(void)
| 从内存池中分配存储 T 类型的内存块, 返回的是内存地址,如果分配不成功 则返回 NULL
| T* calloc(void)
| 从内存池中分配存储 T 类型的内存块 并把它填充为 0,返回的是内存地址, 如果分配不成功则返回 NULL
| osStatus free(T *block)
| 把 block 指向的内存释放给内存池
| Mail
| template<typename Mail()
T,
uint32_t
queue_sz>
| 构造函数,构造大小为 queue_sz,类 型为 T 的邮件队列
| T* alloc(uint32_t millisec=0)
| 在 Mail 中分配存储 T 类型的内存块, 返回的是内存地址,如果分配不成功 则返回 NULL
| T* calloc(uint32_t millisec=0)
| 从系统中分配存储 T 类型的内存块并 把它填充为 0,返回的是内存地址,如 果分配不成功则返回 NULL
| osStatus put(T *mptr)
| 把 T 类型的数据放入邮件队列中
| osEvent get(uint32_t millisec=osWaitForever)
| 获取队列信息,取出的数据包含在 osEvent 对象中
| osStatus free(T *mptr)
| 把 mptr 指向的内存释放给系统
|
从以上的方法列表可以看出,Queue 和 Mail 本质上是一样的,不同的是 Queue 需要额外的内存池,而 Mail 可以从系统中分配内存,另外,从使用方式上来说,都是基于先进先 出的队列方式,其中 1 个线程产生数据,另外 1 个线程使用数据,如下面的示意图:
下面是 Queue 方式单独使用的例子:
#include "mbed.h"
#include "rtos.h"
Queue<uint32_t, 255> queue;
void send_thread()
{
uint32_t i = 0;
while (true)
{
i++;
queue.put(&i);
ThisThread::sleep_for(1s);
}
}
int main (void)
{
Thread thread;
thread.start(&send_thread);
while (true)
{
osEvent evt = queue.get();
if (evt.status == osEventMessage)
{
uint32_t* v = (uint32_t *)evt.value.p;
printf("Value id %u. \n", *v);
}
}
}
Queue 方式独立使用的缺点是只能传递简单类型的数据,对于结构类型的数据就无能为力了,我们必须借助 MemoryPool 的帮助,只在 Queue 队列中传递指针即可。 在Mbed OS中,MemoryPool和Queue都是用于实现任务间通信的机制,但它们的设计和实现方式略有不同,这导致了它们对数据类型的限制也不同。
1. **MemoryPool:** MemoryPool是一个用于分配和管理内存块的机制,它可以用于动态地分配内存,然后在不同的任务之间传递这些内存块。因为MemoryPool本质上只是管理一块块内存,而不关心内存中存储的具体内容,所以它能够传递任何类型的数据,包括结构体。
2. **Queue:** Queue是一个先进先出(FIFO)的数据结构,用于在任务之间传递数据。但是,Queue在实现时需要考虑数据的复制和管理,因此它对传递的数据类型有一定的限制。在Mbed OS中,Queue通常只能传递简单的数据类型,如整数、浮点数、指针等,因为它需要在队列中保存数据的拷贝,并且要确保数据的复制和释放过程是安全和高效的。传递复杂的数据类型,如结构体或者对象,可能会涉及到深拷贝和析构函数的调用,这会增加系统的复杂性和开销,因此通常不建议在Queue中传递复杂的数据类型。
综上所述,MemoryPool和Queue在设计和实现上有所区别,导致了它们对传递数据类型的限制不同。MemoryPool能够传递任何类型的数据,而Queue通常只能传递简单的数据类型,以确保系统的高效和安全。
示例代码如下:
#include "mbed.h"
#include "rtos.h"
#include "UnbufferedSerial.h"
typedef struct
{
uint32_t length;
char str[255];
} message_t;
MemoryPool<message_t, 16> mpool;
Queue<message_t, 16> queue;
UnbufferedSerial pc(USBTX,USBRX);
/* Send Thread */
void send_thread ()
{
int32_t i =-1;
char serialdata[255];
char key;
while (true)
{
pc.read(&key, 1);
serialdata[++i] = key;
if (serialdata[i]=='\n' || i==255)
{
message_t *message = mpool.alloc();
message->length = i;
memcpy(message->str,serialdata,i);
queue.put(message);
i=-1;
}
ThisThread::yield();
}
}
int main (void)
{
Thread thread;
thread.start(&send_thread);
while (true)
{
osEvent evt = queue.get();
if (evt.status == osEventMessage)
{
message_t *message = (message_t*)evt.value.p;
pc.write("User input length is %d.\r\n",message->length);
for (uint8_t i=0;i<message->length;i++)
{
pc.write(&message->str[i],1);
}
pc.write("\r\n", 2);
mpool.free(message);
}
ThisThread::yield();
}
}
这段代码实现了一个基于Mbed OS的简单串口通讯应用,其中包括一个发送线程和一个接收线程。主要功能如下:
1. **初始化**:定义了一个消息结构体`message_t`,内含消息长度和消息内容数组。创建了一个内存池`mpool`和一个消息队列`queue`,用于存储消息。初始化了一个串口对象`pc`,用于串口通讯。
2. **发送线程** (`send_thread`):该线程负责从串口读取用户输入字符,将字符存入缓冲区`serialdata`中。当接收到换行符或达到最大长度时,将数据封装成消息并放入消息队列中。
3. **主函数** (`main`):在主函数中,启动发送线程。主循环中不断检查消息队列是否有消息到达。如果有消息到达,就从队列中取出消息并通过串口发送消息内容,最后释放消息所占用的内存。
总体来说,这段代码展示了如何利用Mbed OS的特性,如内存池和消息队列,实现了一个简单的串口通讯应用。发送线程负责接收用户输入并封装成消息,接收线程则负责从消息队列中获取消息并发送消息内容。通过这种方式实现了线程间的通讯和数据交换。
这里有必要了解一下线程内的变量作用域,如果你把上面 send_thread 函数中的 i 和 serialdata 变量定义到 while(true)里面,你就会发现应用工作不正常了,输出的字符长度都是 0,这是因为线程重入后,变量在 while 循环中被重新初始化了。
以上功能如果用 Mail 通讯机制就会更加方便,因为 Mail 整合了 Queue 和 MemoryPool 的功能,工作示意图如下:
## 4、mbed-rtos 在中断服务程序中的应用
mbed-rtos 提供的同步机制和通讯机制同样也可以应用在中断服务程序中,但考虑到中断服务程序必须尽快返回,所以互斥锁机制不能用,而且所以涉及到需要等待的场合必须立刻返回,我们来看下面的示例代码,其效果和采用多线程方式实现是一样的:
#include "mbed.h"
#include "rtos.h"
#include "UnbufferedSerial.h"
typedef struct
{
uint32_t length;
char str[255];
} message_t;
Mail<message_t, 16> queue;
UnbufferedSerial pc(USBTX,USBRX);
int32_t bufindex =-1;
char serialdata[255];
void serialhandler ()
{
char key;
pc.read(&key,1);
serialdata[++bufindex]=key;
if (serialdata[bufindex]=='\n' || bufindex==255)
{
message_t *message = queue.alloc();
message->length = bufindex;
memcpy(message->str,serialdata,bufindex);
queue.put(message);
bufindex=-1;
}
}
int main (void)
{
pc.attach(&serialhandler);
while (true)
{
osEvent evt = queue.get();
if (evt.status == osEventMail)
{
message_t *message = (message_t*)evt.value.p;
pc.write("User input length is %d.\r\n",message->length);
for (uint8_t i=0;i<message->length;i++)
{
pc.write(&message->str[i],1);
}
pc.write("\r\n",2);
queue.free(message);
}
ThisThread::yield();
}
}
这段代码实现了一个基于Mbed OS的串口通讯应用,包括了一个串口数据处理函数和一个主循环。主要功能如下:
1. **初始化**:定义了一个消息结构体`message_t`,包含消息长度和消息内容数组。创建了一个邮箱`queue`,用于存储消息。初始化了一个串口对象`pc`,用于串口通讯。还定义了一个缓冲区`serialdata`和一个索引`bufindex`用于暂存串口接收的数据。
2. **串口数据处理函数** (`serialhandler`):该函数通过串口中断处理接收到的字符,将字符存入缓冲区`serialdata`中。当接收到换行符或达到最大长度时,将数据封装成消息并放入邮箱中。
3. **主函数** (`main`):在主函数中,通过`pc.attach(&serialhandler)`将串口数据处理函数与串口对象关联。主循环中不断检查邮箱是否有消息到达。如果有消息到达,就从邮箱中取出消息并通过串口发送消息内容,最后释放消息所占用的内存。
总体来说,这段代码实现了一个简单的串口通讯应用,通过邮箱实现了消息的传递和处理。串口数据处理函数负责接收用户输入并封装成消息,主循环则负责处理接收到的消息并发送消息内容。通过这种方式实现了串口数据的异步处理和通讯。
当然,不同的中断服务程序之间也可以用 Queue 或 Mail 实现通讯,但需要注意的是, 在使用这两者的 get 函数时一定要立即返回,示例代码如下:
#include "mbed.h"
#include "rtos.h"
#include "UnbufferedSerial.h"
typedef struct
{
uint32_t length;
char str[255];
} message_t;
Mail<message_t, 16> queue;
UnbufferedSerial pc(USBTX,USBRX);
int32_t bufindex =-1;
Ticker tick;
char serialdata[255];
void serialhandler ()
{
char key;
pc.read(&key, 1);
serialdata[++bufindex]=key;
if (serialdata[bufindex]=='\n' || bufindex==255)
{
message_t *message = queue.alloc();
message->length = bufindex;
memcpy(message->str,serialdata,bufindex);
queue.put(message);
bufindex=-1;
}
}
void serialout()
{
osEvent evt = queue.get(0);
if (evt.status == osEventMail)
{
message_t *message = (message_t*)evt.value.p;
pc.write("User input length is %d.\n",message->length);
for (uint8_t i=0;i<message->length;i++)
{
pc.write(&message->str[i], 1);
}
pc.write("\r\n", 2);
queue.free(message);
}
}
int main (void)
{
pc.attach(&serialhandler);
tick.attach(&serialout,1s);
while(true);
}
这段代码与之前的代码相比,主要区别在于引入了一个定时器对象`tick`,并定义了一个定时器中断处理函数`serialout`。主要的实现功能包括:
1. **定时器中断处理函数** (`serialout`):该函数定时从邮箱中获取消息,并将消息内容通过串口发送。与之前的代码相比,这里使用了定时器来周期性地检查邮箱中是否有消息到达,而不是在主循环中不断地轮询。
2. **主函数** (`main`):在主函数中,通过`pc.attach(&serialhandler)`将串口数据处理函数与串口对象关联。使用`tick.attach(&serialout, 1s)`将定时器中断处理函数与定时器对象关联,设定定时器的周期为1秒。然后进入一个无限循环,等待程序运行。
这段代码的功能与之前的代码相似,都是实现了一个串口通讯应用,通过邮箱来异步处理串口接收到的消息。不同之处在于使用了定时器来周期性地处理消息发送,而不是在主循环中轮询邮箱状态。这样可以更有效地利用系统资源,避免了主循环中的忙等待,提高了系统的响应速度。
## 结语
在Mbed OS的世界里,RTOS(实时操作系统)是不可或缺的一部分。它提供了一个强大的基础,使得开发者能够轻松构建复杂的嵌入式系统,并在物联网(IoT)领域发挥作用。通过Mbed RTOS,开发者可以利用其轻量级、高效性以及丰富的功能特性,实现多任务处理、同步通信、定时调度等功能,从而构建出稳定、可靠的物联网设备和嵌入式系统。
Mbed RTOS的应用范围非常广泛。它可以用于各种物联网设备的开发,如传感器节点、智能家居、工业控制设备等。同时,它也适用于嵌入式系统领域,能够帮助开发者管理系统中的多个任务,实现任务间的协作和通信,提高系统的稳定性和可靠性。另外,Mbed RTOS还可以应用于实时控制系统、嵌入式网络设备等领域,满足实时性要求较高的应用场景。
在Mbed OS的生态系统中,RTOS是连接硬件和应用程序的桥梁,为开发者提供了一个高效、稳定的开发平台。通过Mbed RTOS,开发者可以专注于应用程序的开发,而无需过多关注底层的系统细节。这种简单易用的开发体验,使得Mbed OS成为了物联网设备和嵌入式系统开发的首选平台之一。
总的来说,Mbed RTOS在Mbed OS的生态系统中扮演着至关重要的角色。它为开发者提供了一个强大的基础,使得他们能够轻松构建、部署和管理物联网设备和嵌入式系统。
本次分享就到这里了,本人也是一个初入操作系统的小白,如有问题,欢迎各位讨论交流。
|
衔接上文,介绍mbedos 线程间的通讯方式以及实现演示,并跟随了中断中的应用,文末对两篇文章进行总结梳理,结构完整。