内存池同消息队列一样,可以通过它实现线程间的数据传输。
有人说,有了消息队列,咱还要设计一个内存池干嘛,吃饱了没事干啊~,别说,还真不是!
确实,我们可以设计消息队列(Message Queue)来进行线程间的大量数据传输,从而实现线程间通信(Inter-Thread-Communication)。但别忘了,消息队列存在以下缺点:
针对上述问题,一种解决方案是创建一块静态内存池,用来存放传输的数据,然后将该内存池的地址通过消息队列传输,从而实现“零移动”的数据传输。换句话说,数据本身没有移动,移动的是数据对象的地址。内存池的优点如下:
可见,内存池很好地弥补了消息队列的不足。当然,要实现数据通信,得内存池+消息队列。
啥是内存池?咱先来看看官方对内存池的介绍:
Memory Pools are fixed-size blocks of memory that are thread-safe. They operate much faster than the dynamically allocated heap and do not suffer from fragmentation. Being thread-safe, they can be accessed from threads and ISRs alike. A Memory Pool can be seen as a linked list of available (unused) memory blocks of fixed and equal size. Allocating memory from a pool (using osMemoryPoolAlloc) simply unchains a block from the list and hands over control to the user. Freeing memory to the pool (using osMemoryPoolFree) simply rechains the block into the list.
抓重点,内存池有以下特点:
共享内存是线程之间数据交换的一种基本模型,而内存池正是采用了这一思想,即获得地址即可访问!
要使用内存池,得先知道RTX为用户提供的相关定义和函数接口,下面简要介绍~
类型
osMemoryPoolAttr_t
: 内存池属性结构体osMemoryPoolId_t
: 内存池句柄函数
osMemoryPoolNew
osMemoryPoolId_t osMemoryPoolNew(uint32_t block_count, uint32_t block_size,const osMemoryPoolAttr_t* attr)
// 输入
* block_count : 内存池中内存块的最大个数
* block_size : 每个内存块的字节数
* attr : 内存池属性,默认为NULL
// 输出
* 内存池ID
// 注意
* 不能在ISR中调用该函数
osMemoryPoolGetName
const char* osMemoryPoolGetName(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池名字
osMemoryPoolAlloc
void* osMemoryPoolAlloc(osMemoryPoolId_t mp_id,uint32_t timeout)
// 输入
* mp_id: 内存池ID
* timeout: 超时设置
// 输出
* 分配的内存块地址或者NULL
osMemoryPoolFree
osStatus_t osMemoryPoolFree(osMemoryPoolId_t mp_id,void* block)
// 输入
* mp_id : 内存池ID
* block : 待释放的内存块地址c
// 输出
* 函数执行后的状态码: osOK、osErrorParameter、osErrorResource
osMemoryPoolGetCapacity
uint32_t osMemoryPoolGetCapacity(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池允许的最大内存块数量
osMemoryPoolGetBlockSize
uint32_t osMemoryPoolGetBlockSize(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存块的大小
osMemoryPoolGetCount
uint32_t osMemoryPoolGetCount(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池中已经被使用的内存块个数
osMemoryPoolGetSpace
uint32_t osMemoryPoolGetSpace(osMemoryPoolId_t mp_id)
// 输入
* mp_id : 内存池ID
// 输出
* 内存池中可用的内存块个数
osMemoryPoolDelete
osStatus_t osMemoryPoolDelete(osMemoryPoolId_t mp_id)
// 输入
* mp_id :内存池ID
// 输出
* 函数执行后的状态码
// 注意
* 不能在ISR中调用该函数
案例是最好的学习方式,这里笔者介绍一个玩具案例,具体步骤如下。
功能:实现两个线程,一个线程用来不断读取按键的状态,并将按键的状态封装到一个结构体中,然后通过内存池和消息队列将该数据发送给另一个线程;另一个线程负责接收该数据,并根据该数据来点亮对应的LED灯。
* KEY1 键按下后松开, 红灯闪烁。
* KEY2 键按下后松开, 绿灯闪烁。
根据功能,我们需要定义两个线程,相应地有两个线程函数;还有一个内存池,包括相应的内存块数据结构体;最后还需要定义一个消息队列。这里需要说明一下通常我们是将内存池和消息队列结合起来使用。内存池负责保存复杂对象本身,然后通过将该对象的地址传入消息队列,实现线程间通信。
// 定义两个线程
osThreadId_t led; // LED灯
osThreadId_t key; // 按键
void thread_led(void* arg); // LED灯线程函数
void thread_key(void* arg); // 按键线程函数
osMemoryPoolId_t mp_id; // 内存池
typedef struct {
// 内存块数据结构体
uint8_t red;
uint8_t green;
}memory_block_t;
osMessageQueueId_t mq_id; // 消息队列
我们在主线程app_main
的线程函数中创建我们的线程,内存池,和消息队列,并执行一些按键和GPIO的初始化函数。
void app_main (void *arg) {
// 外设初始化
LED_GPIO_Config();
Key_GPIO_Config();
// 内存池 + 消息队列
mp_id = osMemoryPoolNew(16,sizeof(memory_block_t),NULL); // 内存池负责保存对象
mq_id = osMessageQueueNew(16,sizeof(memory_block_t*),NULL); // 消息队列负责传递对象的地址
// 两个线程
key = osThreadNew(thread_key,NULL,NULL);
led = osThreadNew(thread_led,NULL,NULL);
osThreadExit(); // 任务完成,退出
}
我们的执行函数,当然是两个线程函数了。首先我们来看管理按键的线程函数。
void thread_key(void* arg)
{
memory_block_t* mb_led; // 内存块指针
while(1){
// 请求一个内存块
mb_led = (memory_block_t*)osMemoryPoolAlloc(mp_id,osWaitForever);
// 扫描按键,按键按下返回 1 ,否则0
if( Key_Scan(GPIOA,GPIO_Pin_0) == KEY_ON)
mb_led->red = 0; // 红灯亮
else
mb_led->red = 1; // 红灯灭
if(Key_Scan(GPIOC,GPIO_Pin_13) == KEY_ON)
mb_led->green = 0; // 绿灯亮
else
mb_led->green = 1; // 绿灯灭
// 将数据传入消息队列
osMessageQueuePut(mq_id,&mb_led,NULL,osWaitForever);
osDelay(100);
}
}
然后,再来看管理LED灯的线程函数。
void thread_led(void* arg)
{
memory_block_t* mb_led; // 内存块指针
while(1){
osMessageQueueGet(mq_id,&mb_led,NULL,osWaitForever);
LED1(mb_led->red);
LED2(mb_led->green);
// 完成一次传输,释放该内存块
osMemoryPoolFree(mp_id,mb_led);
}
}
具体的GPIO,按键配置啥的,就不讲了,那不是本文的范畴,你们没问题的~
由于按下按键,松开后LED灯才会亮,所以为了抓怕这个实验现象,我拍了几十张,才成功~
KEY1键按下:
KEY2键按下:
要实现线程之间的数据传输,非内存池+消息队列莫属了,话说这两个结合在一起是真的好用。任何复杂数据结构都可以封装成结构体,并将其保存在内存池中。需要传输该复杂对象的数据吗?不需要!咱不传递该对象本身,我们通过消息队列来传递该对象的地址,有了地址就有了数据本身。传输一个地址,相对传输对象本身,开销还是挺小的。
当然了,本文只是简单介绍了内存池+消息队列的使用,并实现了一个玩具案例。更多的细节啊啥的,需要参考官方资料,以及实际的项目需求,希望对大家有所帮助,任何疑问,欢迎留言,谢谢~
☞官方资料