//-------------------------
时隔许久~ 下面做rtt应用方面的一些总结:如创建线程和启动,消息队列、信号量、互斥量、事件、软件定时器、邮箱的实现和应用
//---------------------------
1.创建线程和启动
根据之前的介绍,这里要做的就是定义线程控制块、线程栈以及线程函数,然后扔到rt_thread_init里面初始化一下(初始化了控制块和栈帧内的数据,栈的sp返回给控制块),然后调用rt_thread_startup把线程挂到就绪列表(即优先级链表数组)并开启调度——以上是静态内存的做法,即控制块和线程栈的空间都是我们自己找的;另外也可采用动态内存的方式:在SRAM中娶一个大数组,把她交给rtt,之后我们不用自己创建控制块和线程栈,而是调用rt_thread_create,由系统从那个大数组中分配空间给我们的控制块和栈。在下面这个OS启动过程中所执行的一个函数中我们可以清楚的看到二者的区别:
void rt_application_init(void)
{
rt_thread_t tid;
//动态内存
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
//静态内存
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_stack, sizeof(main_stack), RT_MAIN_THREAD_PRIORITY, 20);
#endif
rt_thread_startup(tid);
}
这里根据是否定义了RT_USING_HEAP来决定动态还是静态创建main_thread
对于启动流程,常见有两种:一是在main中先创建好所有的线程,然后启动OS开始调度;另一种是创建一个启动线程,启动线程负责一个个创建其他线程(启动线程在工作说明已经启动OS开始调度了),之后自行结束绳命。两种流程感觉区别不大,而rtt选择的方式是:系统上电后执行启动文件中的reset_handler,【程序最后面跳到_main,正常情况下,_main初始化完堆和栈会跳到main,但rtt使用了编译器的$Sub$和$Super$进行了扩展,在main之前执行了$Sub$$main进行了硬件、定时器、调度器等的初始化,并创建启动线程、开启调度,然后由启动线程跳到main】 —— so,rtt偏向于第二种启动方式
另外,上面中括号括起来的部分根据编译器的不同会存在差异:
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void);
/* RT-Thread components initialization */
rt_components_init();
//编译器不同,走不同的路
/* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
但总归是从reset_handler跳到某一个地方,进行一些预处理,然后再跳回主线程,只需知道某些初始化在main之前就已经搞定了
2.消息队列
struct rt_messagequeue
{
struct rt_ipc_object parent;
void *msg_pool; /**池子 */
rt_uint16_t msg_size; /**每条消息大小 */
rt_uint16_t max_msgs; /**最大消息数量 */
rt_uint16_t entry; /**已经进来的消息个数 */
void *msg_queue_head; /**消息队列的头指针*/
void *msg_queue_tail; /**消息队列的尾指针*/
void *msg_queue_free; /**空闲链表指针*/
};
里面算是有俩队列(链表):消息链表和空闲链表,有消息需要写入(即发送)时就从空闲链表上摘一个下来挂到消息链表上(这里平常的操作是挂到尾部,符合FIFO,另外rtt提供发送紧急消息rt_mq_urgent,这时就是挂到链表头部,线程取的时候就会先取到这个紧急消息)。
动态内存时使用rt_mq_create创建消息队列(主要工作是申请(mq->msg_size + sizeof(struct rt_mq_message)) * mq->max_msgs大小的msg_pool),而静态内存时使用rt_mq_init,之后都需要把msg_pool改造成空闲链表:
for (temp = 0; temp < mq->max_msgs; temp ++)
{
head = (struct rt_mq_message *)((rt_uint8_t *)mq->msg_pool +
temp * (mq->msg_size + sizeof(struct rt_mq_message)));
head->next = mq->msg_queue_free;
mq->msg_queue_free = head;
}
其中的rt_mq_message里边只有一个指向自己类型的指针,用于把单个节点连接成链表
下面看一下消息的具体发送过程:
rt_err_t rt_mq_send(rt_mq_t mq, void *buffer, rt_size_t size)
{
register rt_ubase_t temp;
struct rt_mq_message *msg;
/* 发送的内容不能大于一条消息的大小 */
if (size > mq->msg_size)
return -RT_ERROR;
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* 找到空闲链表,摘一个下来 */
msg = (struct rt_mq_message *)mq->msg_queue_free;
/* 消息队列满了 */
if (msg == RT_NULL)
{
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
/* move free list pointer */
mq->msg_queue_free = msg->next; //更新空闲链表头
/* enable interrupt */
rt_hw_interrupt_enable(temp);
/* the msg is the new tailer of list, the next shall be NULL */
msg->next = RT_NULL;
/* 把要发送的内容复制过来 */
rt_memcpy(msg + 1, buffer, size);
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* link msg to message queue */
if (mq->msg_queue_tail != RT_NULL)
{
/* 新生产的msg挂到消息链表尾部 */
((struct rt_mq_message *)mq->msg_queue_tail)->next = msg;
}
/* set new tail */
mq->msg_queue_tail = msg;
/* if the head is empty, set head */
if (mq->msg_queue_head == RT_NULL)
mq->msg_queue_head = msg;
/* increase message entry */
mq->entry ++;
/* 如果有阻塞在这个消息队列上的线程,唤醒表头那个,并进行调度 */
if (!rt_list_isempty(&mq->parent.suspend_thread))
{
rt_ipc_list_resume(&(mq->parent.suspend_thread));
/* enable interrupt */
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
mq->parent.suspend_thread是消息队列她爹里面的一个双向链表,用于挂阻塞在自己身上的线程们(爹说:想娶她不?排队!),而线程过来娶亲可以按照FIFO,也可以按照优先级(往阻塞链表上插的时候比较优先级找到自己的位置)。
下面看接收的实现:
rt_err_t rt_mq_recv(rt_mq_t mq,
void *buffer,
rt_size_t size,
rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
struct rt_mq_message *msg;
rt_uint32_t tick_delta;
/* initialize delta tick */
tick_delta = 0;
/* get current thread */
thread = rt_thread_self();
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* 队列里面没有消息且自己不想等待,返回错误码 */
if (mq->entry == 0 && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_ETIMEOUT;
}
/* 队列里没消息,但是还想等等,
这里用的是while,所以想要出去,
要么是等到了消息,要么是超时或者发生错误 */
while (mq->entry == 0)
{
/* reset error number in thread */
thread->error = RT_EOK;
/* 这里表示等待时间结束了,最终也没有等到她,于是默默离开 */
if (timeout == 0)
{
/* enable interrupt */
rt_hw_interrupt_enable(temp);
thread->error = -RT_ETIMEOUT;
return -RT_ETIMEOUT;
}
/* 先把自己挂到她爹给的阻塞链表上 */
rt_ipc_list_suspend(&(mq->parent.suspend_thread),
thread,
mq->parent.parent.flag);
/* 等待时间>0,设置定时器时间,定好闹钟,开始睡觉 */
if (timeout > 0)
{
/* 存一下当前的tick,后面用来更新timeout */
tick_delta = rt_tick_get();
/* 设置时间并开启定时器,到期后会把线程再放回就绪链表 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
/* enable interrupt */
rt_hw_interrupt_enable(temp);
/* 前面把自己挂起来了,所以执行一次调度 */
rt_schedule();
/* 貌似是消息队列被删除后强制唤醒的状况,不知道还有没有其他情况会改error的值 */
if (thread->error != RT_EOK)
{
/* return error */
return thread->error;
}
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* 更新等待时间剩余值,如果是定时器到期唤醒,则剩余值变成0,
如果是是中途唤醒,那么timeout更新后还剩一些,如果mq->entry还是0
的话(即仍然没有消息可以拿),就用剩下的时间再睡一觉 */
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
}
}
/* 有消息可以直接拿;或者是在while中等到了消息 —— 终于等到你 */
msg = (struct rt_mq_message *)mq->msg_queue_head;
/* move message queue head */
mq->msg_queue_head = msg->next;
/* reach queue tail, set to NULL */
if (mq->msg_queue_tail == msg)
mq->msg_queue_tail = RT_NULL;
/* decrease message entry */
mq->entry --;
/* enable interrupt */
rt_hw_interrupt_enable(temp);
/* 复制消息 */
rt_memcpy(buffer, msg + 1, size > mq->msg_size ? mq->msg_size : size);
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* put message to free list */
msg->next = (struct rt_mq_message *)mq->msg_queue_free;
mq->msg_queue_free = msg;
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
在rt_ipc_list_suspend的实现中我们可以看到线程首先把自己suspend(即从就绪列表中移除),然后根据参数flag的取值决定是按FIFO还是优先级方式插入阻塞链表。
rt_inline rt_err_t rt_ipc_list_suspend(rt_list_t *list,
struct rt_thread *thread,
rt_uint8_t flag)
{
/* suspend thread */
rt_thread_suspend(thread);
switch (flag)
{
case RT_IPC_FLAG_FIFO:
rt_list_insert_before(list, &(thread->tlist));
break;
case RT_IPC_FLAG_PRIO:
{
struct rt_list_node *n;
struct rt_thread *sthread;
/* find a suitable position */
for (n = list->next; n != list; n = n->next)
{
sthread = rt_list_entry(n, struct rt_thread, tlist);
/* find out */
if (thread->current_priority < sthread->current_priority)
{
/* insert this thread before the sthread */
rt_list_insert_before(&(sthread->tlist), &(thread->tlist));
break;
}
}
/*
* not found a suitable position,
* append to the end of suspend_thread list
*/
if (n == list)
rt_list_insert_before(list, &(thread->tlist));
}
break;
}
return RT_EOK;
}
接收消息和等待的过程在程序中做了较详细的注释,之后的几个通信组件实现发送和接收的方法大体上都是这个套路,之后挑一些独特的地方进行介绍。
//-----(注:以下引号中的内容&图copy自RTT的官方文档)-----
3.信号量 & 互斥量
信号量可以是多值或二值型,就是赋值多少而已,没有任何差异。当信号量初始化为1时,看起来就跟互斥量很像了,但 —— “互斥量和信号量不同的是:拥有互斥量的线程拥有互斥量的所有权,互斥量支持递归访问且能防止线程优先级翻转;并且互斥量只能由持有线程释放,而信号量则可以由任何线程释放。” —— “在信号量中,因为已经不存在实例,线程递归持有会发生主动挂起(最终形成死锁)”,所以三个不同点:所有权、递归、优先级提升。
优先级翻转简述:设优先级A>B>C,if 在AB挂起期间,C获得资源M并运行,A醒来之后抢占C,if A也需要M,则又会挂起换成C运行,这时if B醒来了会抢占C,间接的也就相当于抢占了比她优先级高的A —— 不河里
解决方法:优先级继承—— “提高某个占有某种资源的低优先级线程的优先级,使之与所有等待该资源的线程中优先级最高的那个线程的优先级相等,然后执行,而当这个低优先级线程释放该资源时,优先级重新回到初始设定。因此,继承优先级的线程避免了系统资源被任何中间优先级的线程抢占。”
4.事件
“1)事件只与线程相关,事件间相互独立:每个线程可拥有 32 个事件标志,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件;
2)事件仅用于同步,不提供数据传输功能;
3)事件无排队性,即多次向线程发送同一事件 (如果线程还未来得及读走),其效果等同于只发送一次。”
5.邮箱
与之前几个ipc组件不同的是,邮箱实现了阻塞发送:
rt_err_t rt_mb_send_wait(rt_mailbox_t mb,
rt_uint32_t value,
rt_int32_t timeout)
{
struct rt_thread *thread;
register rt_ubase_t temp;
rt_uint32_t tick_delta;
/* initialize delta tick */
tick_delta = 0;
/* get current thread */
thread = rt_thread_self();
/* disable interrupt */
temp = rt_hw_interrupt_disable();
//邮箱满了,而且不想等待
if (mb->entry == mb->size && timeout == 0)
{
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
//邮箱满了,但是想在这里等一等
while (mb->entry == mb->size)
{
/* reset error number in thread */
thread->error = RT_EOK;
//等待时间到了,仍然没地方,默默离开
if (timeout == 0)
{
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return -RT_EFULL;
}
//把自己挂到发送的阻塞线程上
rt_ipc_list_suspend(&(mb->suspend_sender_thread),
thread,
mb->parent.parent.flag);
//根据timeout设置和开启定时器,然后就睡下等闹钟
if (timeout > 0)
{
tick_delta = rt_tick_get();
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
/* enable interrupt */
rt_hw_interrupt_enable(temp);
//线程挂起了,执行一次调度
rt_schedule();
//醒了之后先看看自己的状态对不对
if (thread->error != RT_EOK)
{
return thread->error;
}
/* disable interrupt */
temp = rt_hw_interrupt_disable();
/* 更新timeout ,如果是定时器到期唤醒,则剩余值变成0(或者<0),
之前在消息队列那里写过,如果是是中途唤醒,就用剩下的时间再睡一觉,
不过现在突然想不到什么情况下会被中途唤醒而且邮箱还是满的,
毕竟上面已经判断过thread->error了*/
if (timeout > 0)
{
tick_delta = rt_tick_get() - tick_delta;
timeout -= tick_delta;
if (timeout < 0)
timeout = 0;
}
}
//走到这里说明可以发送了
mb->msg_pool[mb->in_offset] = value;
/* increase input offset */
++ mb->in_offset;
if (mb->in_offset >= mb->size)
mb->in_offset = 0;
/* increase message entry */
mb->entry ++;
//如果有人等着收邮箱的话,唤醒表头,并调度
if (!rt_list_isempty(&mb->parent.suspend_thread))
{
rt_ipc_list_resume(&(mb->parent.suspend_thread));
/* enable interrupt */
rt_hw_interrupt_enable(temp);
rt_schedule();
return RT_EOK;
}
/* enable interrupt */
rt_hw_interrupt_enable(temp);
return RT_EOK;
}
说是有点不一样,其实跟阻塞接收没啥区别,只不过现在需要两条阻塞链表了