TencentOS浅学过程记录

TencentOS浅学过程记录

  • 前言
  • 一、RTOS
  • 二、学习资料来源
  • 三、初步学习过程中的疑难问题解决
    • 任务调度以及轮询时间片
    • 消息队列与邮箱队列
    • 互斥锁
    • 任务中为什么一定要加while(1)循环
    • 内存管理
  • 三、实操问题解决
    • 使用TencentOS中的shell(即**CLI命令行**)
    • RTOS新手学习一定要注意**堆栈大小**
    • 使用动态内存时记得要去tos_config.h文件中修改配置
    • 提醒

前言

大三下生产实习作业,基于LoRaWAN的物联网应用,刚好有一个选题是关于RTOS的,之前对于RTOS也有过一些了解,但是一直没有真正的学习使用过,刚好借这个机会学习一波。(这里主要分享一下我在对于腾讯TencentOS学习使用过程中的一些收获与问题解决过程)

一、RTOS

实时操作系统的概念这里不再赘述。相较于之前顺序执行的裸机编程,RTOS多任务执行以及阻塞机制使得程序执行效率更高,一些没有必要执行的代码可以利用阻塞机制使它仅在必要的时候执行,任务重要性可以按照优先级分配,整体看起来就是我们想要哪些代码执行,哪些代码不执行都是可以控制调节的,这样可以使重要的任务一直得到有效执行,这样程序执行效率有很大提高。

二、学习资料来源

当前开源的RTOS非常多,刚开始我有了解到RT-Thread,Freertos,TencentOS,uCOS等等,但是对于新手大佬推荐RT-Thread比较多,毕竟是国产开源,相关资料手册等等都是中文编写,目前使用也比较广泛,各处学习网站也有非常多的问题解决博客。当然TencentOS也是中文资料,只不过目前使用没几年使用量不是特别大,除过腾讯开源库中的资料其他可借鉴的博客也比较少。至于为什么选择TencentOS,作业时间留的有点短,杰杰大佬说TencentOS源码实现非常精简,没有特别复杂的数据结构在里面,对于新手接触RTOS,简单了解体验RTOS中的一些任务通信机制非常友好好上手(当然C语言基础得不错)(不同公司开源的RTOS内部任务同步以及通信机制大同小异),作业时间只有一个月,TencentOS正合我意。
我在学习过程中主要借鉴的学习资料有:

  1. 腾讯TencentOS Github开源库
  2. _杰杰_大佬的TencentOS专栏
  3. TencentOS QQ官方技术交流群:684946161

以上以Github开源库为主要资料,杰杰大佬的专栏写于2019年左右,但是目前TencentOS在一些方面更新了很多,总之就是许多代码不是以前的代码了,但是大同小异,杰杰大佬专栏中对于邮箱,信号量,互斥锁,事件等等的讲解非常有助于对TencentOS中相关任务机制的理解。由于更新比较多,学习者在学习过程中还是有必要阅读一下相关源码的(会C就可以,源码参考杰杰大佬的专栏会相对比较好理解)。
GitHub中资料比较齐全
TencentOS浅学过程记录_第1张图片
其他的在CSDN上也是零零散散能找到多少是多少。

三、初步学习过程中的疑难问题解决

这里主要指在运行github提供的内核开发指南中的例程源码时,尝试性的修改代码产生的一系列问题以及对一些概念的理解。

任务调度以及轮询时间片

1.任务调度:每一次任务调度即进入knl_sched()的时候,系统会去查找就绪态任务中优先级最高的任务。如果优先级最高的任务不止一个(相同优先级的任务会按顺序挂载在同一个列表中),系统会在同一优先级任务列表中选取第一个进行执行;

2.轮询时间片处理:当进入轮训时间片处理程序时,系统会获取当前任务以及当前任务的优先级,然后判断该优先级列表中是否还有同优先级的任务处于就绪态,如果有就按照轮询时间片机制,轮询时间片数值依次减少。如果没有同优先级任务处于就绪状态,系统将不再执行轮询时间片机制。(这也就是说轮询时间片仅在当任务就绪列表中同一个优先级的任务有两个以上时才会起作用,否则是不会起作用的);在轮询时间片起作用的情况下,当一个任务执行时间超出轮询时间片的时间设定后,系统会把相同优先级任务列表中第一个任务调换到最后一个,那么下一次系统调度即执行knl_sched()时取最高优先级的第一个任务(原先的任务插到了列表中的最后一个,这个时候已经实现了任务被轮询时间片强行切到了另外一个任务)
代码注释简单如下:

//任务调度函数
__KNL__ void knl_sched(void)
{
    TOS_CPU_CPSR_ALLOC();
    if (unlikely(!tos_knl_is_running())) {
        return;
    }
    if (knl_is_inirq()) {
        return;
    }
    if (knl_is_sched_locked()) {
        return;
    }
    TOS_CPU_INT_DISABLE();
    k_next_task = readyqueue_highest_ready_task_get(); //取就绪态列表中最高优先级列表中的第一个任务
    if (knl_is_self(k_next_task)) {//如果任务没有发生切换(即正在执行的任务就是当前就绪态列表中优先级最高的那个任务),则不用设置上下文转换
        TOS_CPU_INT_ENABLE();
        return;
    }
    cpu_context_switch();//如果切换到新的任务,那么需要进行上下文转换
    TOS_CPU_INT_ENABLE();
}
void SysTick_Handler(void)
{
      HAL_IncTick();
      if(tos_knl_is_running())
      {
        tos_knl_irq_enter();
        tos_tick_handler();//时间片操作在这个里面
        tos_knl_irq_leave();
      }
}
__API__ void tos_tick_handler(void)
{
    if (unlikely(!tos_knl_is_running())) {
        return;
    }
    tick_update((k_tick_t)1u);
#if TOS_CFG_TIMER_EN > 0u && TOS_CFG_TIMER_AS_PROC > 0u//软件定时器模块
    soft_timer_update();
#endif

#if TOS_CFG_ROUND_ROBIN_EN > 0u
    robin_sched(k_curr_task->prio);//执行轮询时间片
#endif
}
//时间片执行函数
__KNL__ void robin_sched(k_prio_t prio)//传入当前运行任务的优先级
{
    TOS_CPU_CPSR_ALLOC();
    k_task_t *task;
    TOS_CPU_INT_DISABLE();
    task = readyqueue_first_task_get(prio);  //获取就绪态任务中该优先级列表中的第一个任务
    if (!task || knl_is_idle(task)) {
        TOS_CPU_INT_ENABLE();
        return;
    }
    if (readyqueue_is_prio_onlyone(prio)) { //查找就绪态任务中该优先级列表中是否只有这一个任务,如果只有一个,就不需要使用轮询时间片
        TOS_CPU_INT_ENABLE();
        return;
    }
    if (knl_is_sched_locked()) {
        TOS_CPU_INT_ENABLE();
        return;
    }
    if (task->timeslice > (k_timeslice_t)0u) { //如果就绪态任务中同一优先级就绪列表中不止一个任务,那么按照轮询时间片机制来处理
        --task->timeslice;//时间片数值减1
    }
    if (task->timeslice > (k_timeslice_t)0u) {
        TOS_CPU_INT_ENABLE();
        return;
    }
    readyqueue_move_head_to_tail(k_curr_task->prio);  //任务执行时间超过轮训时间片设定时间,当前运行的任务会被移动到该优先级列表中的尾部,使得其他任务移到前面,下次再进行任务调度时便实现了任务切换(即时间片起作用)
    task = readyqueue_first_task_get(prio); //这时当前任务已经移动到了列表尾部,这里提取列表中的第一个,下面对该任务进行时间片数值设置
    if (task->timeslice_reload == (k_timeslice_t)0u) {
        task->timeslice = k_robin_default_timeslice;
    } else {
        task->timeslice = task->timeslice_reload;
    }
    TOS_CPU_INT_ENABLE();
    knl_sched();//执行任务调度
}

总结:这个功能很鸡肋,大部分时间没什么用,而且这是一种强行切换,如果你的任务中存在互斥锁,强行切换导致互斥锁没有来的及释放,一定会出问题。(总之这是一种强行切换,一般情况下只有在任务中不加os_delay()时才会用到,而且在切换时不对原先任务中的互斥锁等同步机制进行应急处理)。感觉没什么用。

消息队列与邮箱队列

TencentOS里面提供了消息队列,邮箱队列以及优先级消息队列,优先级邮箱队列。底层实现主要是两个分别是环形队列和优先级队列来实现,这两个区别就是,环形队列按照先入先出,优先级队列写入时会记录写入的优先级,弹出时会按照优先级高低先后弹出。另外就是消息队列和邮箱队列的区别,消息队列仅传递指针,邮箱队列可以传递较大的内存块,底层实现都是环形队列,只不过消息队列存入的是指针即一个32位的地址,邮箱队列可以存入一个结构体中的所有数据。

uint8_t msg_pool[MESSAGE_MAX * sizeof(void *)];//消息队列存储池MESSAGE_MAX*地址占内存大小(更小)

typedef struct mail_st {
    char   *message;
    int     payload;
} mail_t;
uint8_t mail_pool[MAIL_MAX * sizeof(mail_t)];//邮箱队列存储池MAIL_MAX*结构体占内存大小(更大)

我的理解:邮箱队列可以保证数据存入邮箱,数据没有被接收之前不会丢失,但是消息队列中的地址数据未被接收之前如果该地址指向的内存数据发生变化,那么接收到之后的数据可能并不是你想传递的数据。(如果数据被更改时间间隔足够长的话可以使用)。这个时候我觉得消息队列还挺鸡肋的,我还没有想到一个消息队列的合理利用场景,纠结于传送地址,但地址数据发生改变不就乱了,希望大佬看到可以指点一下。这块指出一下,不同的RTOS如腾讯还有FreeRTOS中的同步及通信机制原理上差不多但是使用时还是有区别的,比如FreeRTOS中的消息队列和TencentOS中的邮箱一样传递的是数据不是指针。

互斥锁

互斥锁中有一个优先级翻转的概念,优先级翻转会对程序中任务执行产生很大的危害,高优先级的任务得不到有效执行,举一个简单的优先级翻转的例子:有四个任务A,B,C,优先级分别为1,2,3,并且A与C在争夺互斥锁,某一时刻A,B均处于阻塞态,任务C获取到互斥锁,此时锁处于闭锁状态。接下来A任务到达就绪态,因为优先级高抢断C执行但是获取不到锁进入阻塞态。这是B到达就绪态,由于B不需要互斥锁且优先级高于C,所以B抢断C执行。在这个过程中A想要执行就必须等待B执行完之后切换到C,C执行完才能轮到A。然而A是优先级最高的任务,这样会使得系统中优先级最高的任务无法得到有效执行,严重的话会导致程序崩溃。

TencentOS采用了优先级继承策略,当任务A申请锁的时候,发现锁在C的手里且C的优先级低于A的优先级,那么立马将C的优先级提高到和A等同的优先级,那么C执行过程中就不会被B打断,即A只要等C执行完就可以执行了,不用等待B的执行。这样一定程度上削弱了优先级翻转的危害。

当信号量为1的时候也可以当做互斥锁用,但是由于信号量没有设计优先级继承的东西,所有有可能造成优先级翻转。

任务中为什么一定要加while(1)循环

在RTOS初学过程中,有些时候就忘记写while(1)了,执行后发现咋突然就停下来了,甚是奇怪。每个任务也是一个函数,切换任务时会保存上一个任务的上下文。如果没有while(1),程序执行完最后一句就结束了,函数执行结束后函数里面的变量等等都会被释放掉,该任务就被直接销毁了,只有让任务执行函数处于循环当中,函数不会结束,任务才不会被销毁。任务才能够在调度时保存上下文,下次从原先离开的地方继续执行。这也就是我们在使用过程中看到任务函数只在while(1)内部执行,切换任务从哪里离开,下次就从哪里开始。

内存管理

TencentOS中有两种内存管理,分别是静态内存管理和动态内存管理。
静态内存是在全局变量区开辟一块待分配的内存,可以对这片内存进行管理(申请释放定长内存块)。
动态内存是指在堆区申请释放定长内存块。
内存泄漏:申请的内存块使用完之后一定要释放,并且申请内存的获得的首地址不能丢失,相关内存泄漏的知识可以参考其他博文。
动态内存管理参考示例:

typedef struct
{
    uint16_t rec_len;
    uint8_t rec_Buf[RECEIVELEN];
} usart_recv_mail;//接收邮箱消息的结构体
uint8_t mail_lphuart1_pool[MAIL_MAX * sizeof(usart_recv_mail)];//邮箱内存区存储串口数据
void entry_inf_get_from_lorawan_task(void *arg)
{
  k_err_t err;
  usart_recv_mail recdata;
  size_t mail_size;
  while(1)
  {
    usart_recv_mail *p = tos_mmheap_alloc(sizeof(usart_recv_mail));
    //在堆区申请一个usart_recv_mail大小的内存,p指向分配的内存的起始地址,这个指针不能丢失(即p只能指向这块内存的首地址),如果指向其他地方,这块内存就会发生内存泄漏得不到利用。
    err = tos_mail_q_pend(&mail_lphuart1_inf, p, &mail_size, TOS_TIME_FOREVER);//等待邮箱中的消息,将等待到的消息存储到p指向的内存区
    if (err == K_ERR_NONE)
    {
      TOS_ASSERT(mail_size == sizeof(usart_recv_mail));
      /* 等待lpuart1产生中断 */
      if(UART_TO_LRM_RECEIVE_FLAG)
      {
          UART_TO_LRM_RECEIVE_FLAG = 0;
          usart2_send_data((*p).rec_Buf,(*p).rec_len);//因为p就是usart_recv_mail结构体类型,所以可以像结构体一样使用
      }
    }
    tos_mmheap_free(p);//使用完之后记得一定要释放内存
    tos_task_delay(500);//任务睡眠
  }
}

总结:按照我的理解,动态内存的使用是在当原先任务堆栈大小设置的较小,而实际任务中需要使用更大的内存,也就是说,原先任务堆栈大小不能满足程序运行的需要,为了程序正常运行,我们需要从堆区开辟一块动态内存来使得程序正常运行,程序运行完之后把内存释放掉用于其他。

三、实操问题解决

这块主要是我在完成生产实习作业过程中即实际开始自己码代码过程中遇到的疑难问题解决。

使用TencentOS中的shell(即CLI命令行)

命令行类似一种对话框,基本逻辑就是PC端通过串口输入命令,单片机破解命令执行对应的功能函数。TencentOS自带shell中包括了help(查询有哪些命令,分别是哪些功能)、ps(每个任务的堆栈大小以及实际运行中的堆栈深度),还有free(查询程序内存使用情况)。我们也可以添加自己的命令对单片机进行控制。(总之使用起来非常方便)
shell文件位置(开源库中的文件)
TencentOS浅学过程记录_第2张图片
TencentOS浅学过程记录_第3张图片
shell中的文件全部移进去,包含一些头文件啥的,反正都在github下载的这些文件中,找一下就有。
最后总共就这么多就够了。
TencentOS浅学过程记录_第4张图片

TencentOS的shell是用字符输入输出队列配合信号量实现的。简单来说就是你在串口助手界面输入一个字符,shell代码会将获取到的字符塞入到字符队列中去,同时信号量加一。另一边任务函数会根据信号量的多少进行字符循环获取保存并回传给PC,信号量为零则进行阻塞。当任务函数检测到有\n,\r时则会跳出循环,将之前保存的字符与命令库中的命令进行对比,如果对比正确则执行响应函数,对比错误就回传"command not found\r\n"
核心代码如下:

__API__ void tos_shell_input_byte(uint8_t data)
{
    if (tos_chr_fifo_push(&SHELL_CTL->shell_rx_fifo, data) == K_ERR_NONE) {//把获取到的字符添加到字符队列中去
        tos_sem_post(&SHELL_CTL->shell_rx_sem);//同时释放一个信号量
    }
}//这个函数是要放到串口接收中断中去的
//shell任务执行函数
__STATIC__ void shell_parser(void *arg)
{
    int rc;
    shell_prompt();
    while (K_TRUE) {
        rc = shell_readline();//按照信号量多少循环读取字符并回传给PC,信号量为0则阻塞,直到检测到回车或换行才会跳出,rc指接收到的字符个数
        if (rc > 0) {
            shell_cmd_process();//如果接收到字符就进行命令对比并执行相关函数
        }
        shell_prompt();//返回PC一个回车换行
    }
}//这个任务没有睡眠,仅仅根据信号量来控制执行与阻塞。

效果:
TencentOS浅学过程记录_第5张图片

RTOS新手学习一定要注意堆栈大小

任务程序运行都是在分配的堆栈内存中运行的,如果分配的堆栈内存不够,程序就会直接暴毙(进入硬件错误中断)。所以新手在调试程序过程中可以尝试:
1.硬件错误中断中加入printf函数用于提醒你进入了硬件中断(这个时候首先考虑堆栈内存不够问题)TencentOS浅学过程记录_第6张图片
2.如果直接导入了shell文件,可以通过"ps"命令直接进行获取当前任务的吃栈深度。一般所有的任务堆栈大小尽量先设置的大一些,然后ps命令看一下最大吃栈深度有多深,最后根据最大吃栈深度调整任务堆栈大小(单片机内存还是要省着用的)
TencentOS浅学过程记录_第7张图片
stk size代表设定的堆栈大小stk depth代表程序实际运行时所使用到的最大栈深度
3.TencentOS提供了,获取最大吃栈深度的函数

k_err_t tos_task_stack_draught_depth(k_task_t *task, int *depth);
//获取一个任务栈的最大吃水深度

使用动态内存时记得要去tos_config.h文件中修改配置

可以把最初给的大小根据实际情况改大一点,否则后边的大内存申请会有问题。

#define TOS_CFG_MMHEAP_DEFAULT_POOL_SIZE        0xA00	// 配置TencentOS tiny默认动态内存池大小

提醒

代码中有详细注释

你可能感兴趣的:(腾讯云,物联网,云计算,经验分享)