CMSIS_OS中osMailPut 和 osMessagePut 的问题
1. 背景
为了屏蔽不同OS之间的差别,ARM公司开发了一套OS接口--CMSIS_OS。
在使用STM32 cube生成的free rtos工程中,遇到一些问题。
问题1:osMessageGet 和 osMessagePut 发送和接收队列(结构体,数组等)。
问题2:osMailGet 和 osMailPut发送和接收队列(结构体,数组等)。
2. 问题分析( osMessagePut 和osMessageGet 为例)
osStatus osMessagePut (osMessageQId queue_id, uint32_t info, uint32_t millisec) { portBASE_TYPE taskWoken = pdFALSE; TickType_t ticks; ticks = millisec / portTICK_PERIOD_MS; if (ticks == 0) { ticks = 1; } if (inHandlerMode()) { if (xQueueSendFromISR(queue_id, &info, &taskWoken) != pdTRUE) { return osErrorOS; } portEND_SWITCHING_ISR(taskWoken); } else { if (xQueueSend(queue_id, &info, ticks) != pdTRUE) { return osErrorOS; } } return osOK; }
发送函数本质是调用xQueueSendFromISR 和xQueueSend。这个函数调用
BaseType_t xQueueGenericSendFromISR( QueueHandle_t xQueue, const void * const pvItemToQueue, BaseType_t * const pxHigherPriorityTaskWoken, const BaseType_t xCopyPosition )
继续调用
( void ) prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );
最终调用
( void ) memcpy( ( void * ) pxQueue->u.pcReadFrom, pvItemToQueue, ( size_t ) pxQueue->uxItemSize );
所以本质就是memcpy,把用户需要发送的数据拷贝到队列的内部内存保存。
osMessagePut (osMessageQId queue_id, uint32_t info, uint32_t millisec)接口上,传入的只能是一个数据或指针,所以传入数组或结构体时候,就必须以指针方式传入。
对于osMessageGet ,通用关键的也是memcpy,不过包装后,有一点非常特殊。
和free rtos接口的调用时经过xQueueReceiveFromISR(queue_id, &event.value.v, &taskWoken)。
那么传入的产生就是一个指针,而后面的memcpy会往这个指针里拷贝内容。
所以如果创建的消息长度大于4个字节,指针自加,就会把队列里面的数据拷贝到栈空间的某一个未知的位置。
osEvent osMessageGet (osMessageQId queue_id, uint32_t millisec) { portBASE_TYPE taskWoken; TickType_t ticks; osEvent event; event.def.message_id = queue_id; event.value.v = 0; if (queue_id == NULL) { event.status = osErrorParameter; return event; } taskWoken = pdFALSE; ticks = 0; if (millisec == osWaitForever) { ticks = portMAX_DELAY; } else if (millisec != 0) { ticks = millisec / portTICK_PERIOD_MS; if (ticks == 0) { ticks = 1; } } if (inHandlerMode()) { if (xQueueReceiveFromISR(queue_id, &event.value.v, &taskWoken) == pdTRUE) { /* We have mail */ event.status = osEventMessage; } else { event.status = osOK; } portEND_SWITCHING_ISR(taskWoken); } else { if (xQueueReceive(queue_id, &event.value.v, ticks) == pdTRUE) { /* We have mail */ event.status = osEventMessage; } else { event.status = (ticks == 0) ? osOK : osEventTimeout; } } return event; }
那么问题出现了,结构体或数组,必须以指针的方式传入,然后指针方式读出。
若生产者比消费者频率高,如何保证一包数据没有被接收前不能在发送端覆盖了。
本来这个工作free rtos已经做了,但是经过这么一封装之后,free rtos保护的只是一个指针而已。
实际的发送端,怎么保证源数据空间不被新数据覆盖,就需要用户考虑了。
好在cmsis考虑了这个情况。例子来源 https://os.mbed.com/handbook/CMSIS-RTOS
typedef struct { float voltage; /* AD result of measured voltage */ float current; /* AD result of measured current */ uint32_t counter; /* A counter value */ } message_t; osPoolDef(mpool, 16, message_t); osPoolId mpool; osMessageQDef(queue, 16, message_t); osMessageQId queue; void send_thread (void const *args) { uint32_t i = 0; while (true) { i++; // fake data update message_t *message = (message_t*)osPoolAlloc(mpool); message->voltage = (i * 0.1) * 33; message->current = (i * 0.1) * 11; message->counter = i; osMessagePut(queue, (uint32_t)message, osWaitForever); osDelay(1000); } } osThreadDef(send_thread, osPriorityNormal, DEFAULT_STACK_SIZE); int main (void) { mpool = osPoolCreate(osPool(mpool)); queue = osMessageCreate(osMessageQ(queue), NULL); osThreadCreate(osThread(send_thread), NULL); while (true) { osEvent evt = osMessageGet(queue, osWaitForever); if (evt.status == osEventMessage) { message_t *message = (message_t*)evt.value.p; printf("\nVoltage: %.2f V\n\r" , message->voltage); printf("Current: %.2f A\n\r" , message->current); printf("Number of cycles: %u\n\r", message->counter); osPoolFree(mpool, message); } } }
发送端:osPoolAlloc,从pool中申请空间。也就是说队列不在保存数据,只保存数据指针。
如果有N份数据,原来是N份保存在队列中,现在队列中只申请N个指针的空间。
发送数据时候,从pool中拿到一个空间,保存数据,然后发送指针给messageQ。
接收时候:从队列中取到指针的地址,然后使用数据。因为此数据在pool有空间,并且不会被覆盖,所以不需要在做拷贝到本地变量中。
数据使用完成之后,就可以释放空间,就是osPoolFree。
问题在哪里:
1)看发送,先osPoolAlloc,后osMessagePut。如果队列空间不够,在osMessagePut是可以设置阻塞时间的。
而osPoolAlloc没有阻塞时间,那么就会直接返回错误,上面的例子是官方的例子,就没有考虑申请空间失败的情况。
如果返回了错误,那么后面的osMessagePut超时机制就是空架子了,反正执行不到这里。
2)看接收,osMessageGet之后,队列计数就会减1,以便于发生端数据入队。
如果有数据等待入队,那么这里不会入队,因为靠osPoolFree释放pool后才会入队。所以时序要求高的应用下需要特别注意。
3)上面分析的同样的问题,申请队列时候,不能用sizeof队列大小作为itemSize。
上面的官方例子有bug,osMessageQDef(queue, 16, message_t); 这样申请会导致接收队列时候,free rtos按照itemSize大小拷贝内容到evt.value.p地址。
正确的方法是osMessageQDef(queue, 16, uing32_t)。
实际上这种用法下,已经不需要向队列拷贝具体的内容了,对列发数据前申请空间,收数据后释放空间。
osMailGet 和 osMailPut的问题和上面的messageQ一样,另外,还发现一个更严重的问题。
osMailPut没有做超时时间,实现里面调用的是xQueueSend(queue_id->handle, &mail, 0)。
本以为发送队列满的时候靠申请数据空间来做超时等待,查看源码发现根本没有。
osStatus osMailPut (osMailQId queue_id, void *mail) { portBASE_TYPE taskWoken; if (queue_id == NULL) { return osErrorParameter; } taskWoken = pdFALSE; if (inHandlerMode()) { if (xQueueSendFromISR(queue_id->handle, &mail, &taskWoken) != pdTRUE) { return osErrorOS; } portEND_SWITCHING_ISR(taskWoken); } else { if (xQueueSend(queue_id->handle, &mail, 0) != pdTRUE) { return osErrorOS; } } return osOK; }
因为在调用put之前,会先申请数据空间。而osMailAlloc调用osPoolAlloc,根本没有使用超时参数millisec。
void *osMailAlloc (osMailQId queue_id, uint32_t millisec) { (void) millisec; void *p; if (queue_id == NULL) { return NULL; } p = osPoolAlloc(queue_id->pool); return p; }
3. 其他问题
osMessagePut可以通过传递复杂数据结构的指针,来实现任务间通信。
不过这些数据结构应该是动态分配的,传入参数是pool空间内存的指针,不是任务中的局部变量和全局变量的指针,否则就hardfault。
调用是 message_t *message = (message_t*)osPoolAlloc(mpool); 和osMessagePut(queue, (uint32_t)message, osWaitForever);
入口是osStatus osMessagePut (osMessageQId queue_id, uint32_t info, uint32_t millisec);
内部调用是xQueueSend(queue_id, &info, ticks) != pdTRUE)
这里调用关系复杂,使用局部变量和全局变量的指针频频出错,原因还未知。
4. 附录
期待ST出面解决,Will come back to you soon..