开放原子训练营第三期:RT-Thread 学习有感

news2024/12/22 20:45:59

介绍

前几天有幸收到C站的训练营学习邀请,了解到这两天即将举行的开放原子 RTT 训练营。博主算是一名嵌入式方向的小白,主要还是在裸机上进行开发,但对嵌入式的操作系统和实时系统很感兴趣。在这次学习训练营中借助一些学习示例,我对RTThread这款国产实时操作系统有了深入的了解。因此我的这篇总结博文也是主要围绕讲解的几个示例而展开的,至于 RTT 的详细介绍博主现在确实了解不多,因此可能要等日后学习再加以补充~

RT-Thread 是什么?

RT-Thread是一个C语言编写的实时操作系统(RTOS),针对嵌入式系统而设计,可以运行于多种架构。它具有实时性、微内核、轻量级、可裁剪等特点,支持多线程和时间片轮转调度等特性。相较于一般的裸机开发,即使使用了中断一次也只能先执行一个任务。RT-Thread 的引入允许分时等操作,同时进行操作。

环境搭建

教程来自:RT-Thread 环境搭建 https://atomgit.com/joyce/train-note
rtthread-nano atomGit 源码仓库:https://atomgit.com/OpenAtomFoundation/rtthread-nano

下载 RT-Thread Studio 后,安装对应包。

一个是 rt-thread 4.1.0 源码,一个是 st 固件包 stm32f411-st-nucleo。这次使用的开发板是 stm32f411,把 stm32f103c8t6 作为调试器。

image-20230424002612063

然后双击 dpinst_amd64.exe 安装 stlink 驱动。

新建工程

本次培训讲解到的几个示例分别为:LED、按键、自动初始化机制、自定义msh命令、多线程、线程间通信、定时器、消息队列以及结合前面功能的摩斯电码包。因此我们需要新建一个项目,并在项目中下载一个现成的摩斯电码代码包以便开发时使用。

LED
morse
按键
自动初始化机制
自定义msh命令
多线程
线程间通信
定时器
消息队列

新建 RTT 项目,基于开发板:stm32f411-st-nucleo。

安装好后双击 rt-thread-settings,可以搜索软件包并给当前项目安装。搜索 morse 并添加。

如果安装错误是很常见的问题,我们只需要手动下载对应的包进行安装,再重新添加即可。

image-20230424004948953

示例1:LED 间歇闪烁

嵌入式少不了点灯环节。这里主要是叫我们熟悉在 RTThread Studio 上的代码编写。

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>

/* defined the LED0 pin: PA5 */
#define USER_LED_PIN               GET_PIN(A, 5)

int main(void)
{
    int count = 1;
    /* set LED0 pin mode to output */
    rt_pin_mode(LED0_PIN, PIN_MODE_OUTPUT);//输出模式

    while (count++)
    {
        rt_pin_write(LED0_PIN, PIN_HIGH);
        rt_thread_mdelay(500);
        rt_pin_write(LED0_PIN, PIN_LOW);
        rt_thread_mdelay(500);
    }
    return RT_EOK;	//代表线程挂起成功
}

编译构建代码:点击红色框里的按钮。

下载代码:点击黄色框里的按钮。

打开对应终端:点击蓝色框里的按钮。

image-20230424012021429

示例2:按键

按键功能类似,定义按键引脚,设置上拉输入模式,下降沿触发函数在终端输出,并启用。

#define USER_KEY GET_PIN(C, 13) // GET_PIN(H,4)

void irq_callback()
{
    rt_kprintf("RT-Thread!\r\n");
}

int main(void)
{
    rt_pin_mode(USER_KEY, PIN_MODE_INPUT_PULLUP);
    rt_pin_attach_irq(USER_KEY, PIN_IRQ_MODE_RISING_FALLING, irq_callback, RT_NULL);
    rt_pin_irq_enable(USER_KEY, PIN_IRQ_ENABLE);
    return 0;
}

示例3:自动初始化机制

板子上电时会触发一些函数。

初始化顺序宏接口描述
1INIT_BOARD_EXPORT(fn)非常早期的初始化,此时调度器还未启动
2INIT_PREV_EXPORT(fn)主要是用于纯软件的初始化、没有太多依赖的函数
3INIT_DEVICE_EXPORT(fn)外设驱动初始化相关,比如网卡设备
4INIT_COMPONENT_EXPORT(fn)组件初始化,比如文件系统或者 LWIP
5INIT_ENV_EXPORT(fn)系统环境初始化,比如挂载文件系统
6INIT_APP_EXPORT(fn)应用初始化,比如 GUI 应用

其中 INIT_APP_EXPORT 里挂载我们自己想在板子上电时运行的函数。

#include <rtthread.h>

int export_app(void)
{
    rt_kprintf("export_app RT-Thread!\r\n");//板子启动时在终端输出这句话
    return 0;
}
INIT_APP_EXPORT(export_app);//挂载函数

int main(void){
    return 0;
}

正确运行后,启动板子时就会输出 export_app RT-Thread!。

示例4:自定义 msh 命令

rt-thread 系统的终端叫做 FinalSH,也支持用户写入一些自己的 shell 命令。比如我们写一个命令,用户输入 hello 终端输出 hello RT-Thread! 。

void hello(void)
{
    rt_kprintf("hello RT-Thread!\n");
}

MSH_CMD_EXPORT(hello , say hello to RT-Thread);//挂载函数

第二个参数是 description 命令的描述。比如我们打开 win cmd 输入 help,可以看到很多命令和他们的描述:

image-20230424012701856

我们在 FinalSH 里输入 help,也可以看到我们自己定义的这个 msh 命令机器描述:hello say hello to RT-Thread

示例5:线程管理

这里要求我们写一个双线程的代码,优先级2>1,看看两者执行情况。导出到 msh 命令中。

#include <rtthread.h>

#define THREAD_PRIORITY         25
#define THREAD_STACK_SIZE       512
#define THREAD_TIMESLICE        5

static rt_thread_t tid1 = RT_NULL;

/* 线程 1 的入口函数 */
static void thread1_entry(void *parameter)
{
    rt_uint32_t count = 0;

    while (1)
    {
        /* 线程 1 采用低优先级运行,一直打印计数值 */
        rt_kprintf("thread1 count: %d\n", count ++);
        rt_thread_mdelay(500);
    }
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *param)
{
    rt_uint32_t count = 0;

    /* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
    for (count = 0; count < 10 ; count++)
    {
        /* 线程 2 打印计数值 */
        rt_kprintf("thread2 count: %d\n", count);
    }
    rt_kprintf("thread2 exit\n");
    /* 线程 2 运行结束后也将自动被系统脱离 */
}

/* 线程示例 */
int thread_sample(void)
{
    /* 创建线程 1,名称是 thread1,入口是 thread1_entry*/
    tid1 = rt_thread_create("thread1",
                            thread1_entry, RT_NULL,
                            THREAD_STACK_SIZE,
                            THREAD_PRIORITY, THREAD_TIMESLICE);

    /* 如果获得线程控制块,启动这个线程 */
    if (tid1 != RT_NULL)
    rt_thread_startup(tid1);

    /* 初始化线程 2,名称是 thread2,入口是 thread2_entry */
    rt_thread_init(&thread2,
                    "thread2",
                    thread2_entry,
                    RT_NULL,
                    &thread2_stack[0],
                    sizeof(thread2_stack),
                    THREAD_PRIORITY - 1, THREAD_TIMESLICE);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(thread_sample, thread sample);

如上,线程1的代码是循环无限输出 1,2,3,4,5……线程2是输出0-9.

先创建并启动线程1,再创建并启动线程2,会发现虽然线程2晚启动,但是由于 PRIORITY 值比1小,优先级高,所以终端输出是先输出2的0-9,再开始输出1.

如果用延时函数延时创建线程2,可以看到1先输出了一段时间后被2打断,2执行完后继续输出1.

示例6:定时器

单片机很重要的一个内容:中断定时器。

#include <rtthread.h>

/* 定时器的控制块 */
static rt_timer_t timer1;
static rt_timer_t timer2;
static int cnt = 0;

/* 定时器 1 超时函数 */
static void timeout1(void *parameter)
{
    rt_kprintf("periodic timer is timeout %d\n", cnt);

    /* 运行第 10 次,停止周期定时器 */
    if (cnt++ >= 9)
    {
        rt_timer_stop(timer1);
        rt_kprintf("periodic timer was stopped! \n");
    }
}

/* 定时器 2 超时函数 */
static void timeout2(void *parameter)
{
    rt_kprintf("one shot timer is timeout\n");
}

int timer_sample(void)
{
    /* 创建定时器 1  周期定时器 */
    timer1 = rt_timer_create("timer1", timeout1,
                             RT_NULL, 10,
                             RT_TIMER_FLAG_PERIODIC);//10ms调用timeout1一次。周期定时就是一段时间后停止

    /* 启动定时器 1 */
    if (timer1 != RT_NULL)
        rt_timer_start(timer1);

    /* 创建定时器 2 单次定时器 */
    timer2 = rt_timer_create("timer2", timeout2,
                             RT_NULL, 30,
                             RT_TIMER_FLAG_ONE_SHOT);//30ms调用timeout2一次

    /* 启动定时器 2 */
    if (timer2 != RT_NULL)
        rt_timer_start(timer2);
    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(timer_sample, timer sample);

示例7:消息队列

线程2给线程1发消息,发20次,第八次是紧急消息,其他都是普通消息。线程1接收20次后停止接收。线程2发送20次后停止发送。

#include <rtthread.h>

/* 消息队列控制块 */
static struct rt_messagequeue mq;
/* 消息队列中用到的放置消息的内存池 */
static rt_uint8_t msg_pool[2048];

ALIGN(RT_ALIGN_SIZE)
static char thread1_stack[1024];
static struct rt_thread thread1;
/* 线程 1 入口函数 */
static void thread1_entry(void *parameter)
{
    char buf = 0;
    rt_uint8_t cnt = 0;

    while (1)
    {
        /* 从消息队列中接收消息 */
        if (rt_mq_recv(&mq, &buf, sizeof(buf), RT_WAITING_FOREVER) == RT_EOK)
        {
            rt_kprintf("thread1: recv msg from msg queue, the content:%c\n", buf);
            if (cnt == 19)
            {
                break;
            }
        }
        /* 延时 50ms */
        cnt++;
        rt_thread_mdelay(50);
    }
    rt_kprintf("thread1: detach mq \n");
    rt_mq_detach(&mq);
}

ALIGN(RT_ALIGN_SIZE)
static char thread2_stack[1024];
static struct rt_thread thread2;
/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
    int result;
    char buf = 'A';
    rt_uint8_t cnt = 0;

    while (1)
    {
        if (cnt == 8)
        {
            /* 发送紧急消息到消息队列中 */
            result = rt_mq_urgent(&mq, &buf, 1);
            if (result != RT_EOK)
            {
                rt_kprintf("rt_mq_urgent ERR\n");
            }
            else
            {
                rt_kprintf("thread2: send urgent message - %c\n", buf);
            }
        }
        else if (cnt >= 20) /* 发送 20 次消息之后退出 */
        {
            rt_kprintf("message queue stop send, thread2 quit\n");
            break;
        }
        else
        {
            /* 发送消息到消息队列中 */
            result = rt_mq_send(&mq, &buf, 1);
            if (result != RT_EOK)
            {
                rt_kprintf("rt_mq_send ERR\n");
            }

            rt_kprintf("thread2: send message - %c\n", buf);
        }
        buf++;
        cnt++;
        /* 延时 5ms */
        rt_thread_mdelay(5);
    }
}

/* 消息队列示例的初始化 */
int msgq_sample(void)
{
    rt_err_t result;

    /* 初始化消息队列 */
    result = rt_mq_init(
        &mq,
        "mqt",
        &msg_pool[0],     /* 内存池指向 msg_pool */
        1,                /* 每个消息的大小是 1 字节 */
        sizeof(msg_pool), /* 内存池的大小是 msg_pool 的大小 */
        RT_IPC_FLAG_PRIO  /* 如果有多个线程等待,优先级大小的方法分配消息 */
    );

    if (result != RT_EOK)
    {
        rt_kprintf("init message queue failed.\n");
        return -1;
    }

    rt_thread_init(&thread1,
                   "thread1",
                   thread1_entry,
                   RT_NULL,
                   &thread1_stack[0],
                   sizeof(thread1_stack), 25, 5);
    rt_thread_startup(&thread1);

    rt_thread_init(&thread2,
                   "thread2",
                   thread2_entry,
                   RT_NULL,
                   &thread2_stack[0],
                   sizeof(thread2_stack), 25, 5);
    rt_thread_startup(&thread2);

    return 0;
}

/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(msgq_sample, msgq sample);

示例9:morse 摩斯电码

这里只需要使用下载好的摩斯电码包即可。

代码使用方式:输入 morse_shell_example 可以查看 morse 码示例。根据摩斯电码表按下按键(如:快按一下是e,短按+长按是a)可以输入对应的字母,实际上是程序打印在终端的效果。

代码包下载地址:https://github.com/zhkag/morse

image-20230424030239432

其中 inc 是头文件,src 是 .c 文件,samples 文件夹里是 FinalSH 对应的提示信息。

正确运行后,就可以通过按键输入 morse 码啦!

心得体会

在此之前博主的开发一直是基于裸机的,只是后台的死循环+前台的中断,并没有怎么接触操作系统的层面。因此接触 RT-Thread 的初次学习后最主要的感觉就是把课内纸上谈兵的操作系统拉到了现实的实践中,也能更真实地踏入这个领域。这也很大程度上归功于这款系统的轻量,安装方便,不需要大量资源即可运行的特点。

因为最近报名的嵌入式芯片系统大赛也需要使用 RTT 系统,所以博主还会利用手头的板子进行后续的深入学习,先尝试理解内核的基本概念和操作流程,到掌握线程、信号量、定时器等基本API的使用方法,以及亲自实现线程调度、通信、互斥、同步等代码,加深对嵌入式操作系统的理解。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/455438.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Linux】 1.2基本指令-part 2

文章目录 07. man指令08.cp指令(copy)&#xff08;重要&#xff09;09.mv指令(move)&#xff08;重要&#xff09;10.cat指令- 补充&#xff1a;echo 与 >11.more指令12.less指令&#xff08;重要&#xff09;13.head指令14.tail指令- 补充&#xff1a;管道 07. man指令 ma…

CV不存在了?体验用Segment Anything Meta分割清明上河图

目录 1 Facebook抠图神器2 本地版SAM配置3 Web版SAM体验4 总结 1 Facebook抠图神器 在图像处理与计算机视觉领域&#xff0c;图像分割(image segmentation)是在像素级别将一个完整图像划分为若干具有特定语义区域(region)或对象(object)的过程。每个分割区域是一系列拥有相似特…

IJKPLAYER源码分析-常用API

前言 本文简要介绍IJKPLAYER的几个常用API&#xff0c;以API使用的角度&#xff0c;来审视其内部运作原理。这里以iOS端直播API调用切入。 调用流程 init 创建播放器实例后&#xff0c;会先调用init方法进行初始化&#xff1a; - (IJKFFMediaPlayer *)init {self [super ini…

《深入浅出Embedding》随笔

ChatGPT的核心运行机制或许是Transformer&#xff0c; ChatGPT的核心数据机制或许就是嵌入&#xff08;Embedding&#xff09;了。什么是Embedding呢&#xff1f;了解Embedding可以为我们的软件研发工作带来哪些帮助呢&#xff1f;鉴于此&#xff0c;老码农阅读了《深入浅出Emb…

前端切图仔入门Docker,三分钟上线自己的博客平台

依稀记得2022年趁某平台优惠买了台云服务器&#xff0c;周未准备安装MySQL数据库&#xff0c;两天时间卡在MySQL环境配置上&#xff0c;实在是折磨一气之下把服务器给退了。 要是我早一点学会Docker&#xff0c;我的博客估计已上线一年啦&#xff01;前端切图仔学会Docker&…

AI魔法秀:D-ID助你打造视频虚拟数字人

随着ChatGPT的各种玩法&#xff0c;写文章、做PPT、编程、修bug等&#xff0c;大家都玩的不亦说乎&#xff0c;可以说真的给家人们提升的效率很高&#xff0c;最近个人尝试着制作一个虚拟数字人&#xff0c;也是一个玩法&#xff0c;可以帮助很多中小企业做企业文化宣讲或者是产…

闲谈【Stable-Diffusion WEBUI】的插件:美不美?交给AI打分

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;咖啡店艺术评价&#xff08;Cafe Aesthetic&#xff09; &#xff08;零&#xff09;前言 本篇主要提到了WEBUI的Cafe Aesthetic插件&#xff0c;这是一个相对独立的插件&#xff0c;单独标签页&#xff0c;…

周杰伦代言的蕉下,3年半广告宣传费超10亿,全靠营销?

五一假期将至&#xff0c;各地即将迎来旅游小热潮&#xff0c;不少游客也开始为自己的出行准备攻略。随着露营、徒步等城市户外运动的兴起&#xff0c;防晒理念“再度升温”&#xff0c;靠卖小黑伞起家的蕉下瞄准年轻世代消费者&#xff0c;又在疯狂收割“防晒焦虑”。 去年4月…

【JavaWeb】JavaScript

1、JavaScript 介绍 Javascript 语言诞生主要是完成页面的数据验证。因此它运行在客户端&#xff0c;需要运行浏览器来解析执行 JavaScript 代码。 JS 是 Netscape 网景公司的产品&#xff0c;最早取名为 LiveScript;为了吸引更多 java 程序员。更名为 JavaScript。 JS 是弱…

排序 - 冒泡排序(Bubble Sort)

文章目录 冒泡排序介绍冒泡排序实现复杂度和稳定性冒泡排序时间复杂度冒泡排序稳定性 代码实现核心&注意结尾 每日一道算法提高脑力&#xff0c;今天是第一天&#xff0c;来个最简单的算法–冒泡排序。 冒泡排序介绍 它是一种较简单的排序算法。它会遍历若干次要排序的数列…

对话庄表伟老师-文字实录

我内心有一套价值观&#xff0c;有一套世界观&#xff0c;它是一个完整的整体&#xff0c;无论我做任何的事情&#xff0c;工作也好、生活也好、学习也好、去做社区也好、或者是结识朋友也好、去聊天也好&#xff0c;背后的价值观在内心都是一整套的&#xff0c;互相之间是不会…

Python学习之简易图片浏览器

俗话说实践是学习最有效的方法。最近在学习python&#xff0c;于是就参考着各类文章&#xff0c;也倒腾了一个简易图片浏览器&#xff0c;效果图如下&#xff1a; 整个浏览器分为左右两侧&#xff0c;左侧是地址栏图片文件列表&#xff1b;右侧则是图片显示区域。 左侧地址栏有…

数据结构,二叉搜索树的详解

&#x1f9d1;‍&#x1f4bb;作者&#xff1a;程序猿爱打拳&#xff0c;Java领域新星创作者&#xff0c;阿里云社区博客专家。 &#x1f5c3;️文章收录于&#xff1a;数据结构与算法 &#x1f5c2;️JavaSE的学习&#xff1a;JavaSE &#x1f5c2;️MySQL数据库的学习: MySQL…

Python基础之类

一&#xff1a;什么是类 类即类别/种类&#xff0c;是面向对象分析和设计的基石&#xff0c;如果多个对象有相似的数据与功能&#xff0c;那么该多个对象就属于同一种类。有了类的好处是&#xff1a;我们可以把同一类对象相同的数据与功能存放到类里&#xff0c;而无需每个对象…

【AI绘画】Midjourney的使用及程序示例

Midjourney 1.背景2.Midjourney的原理3.Midjourney的使用方法4.Midjourney的示例代码 1.背景 Midjourney 是一款基于深度学习的图像转换工具&#xff0c;其可以将一张图像转换成具有不同风格的图像&#xff0c;例如将一张照片转换成卡通风格的图像。Midjourney 基于 TensorFlow…

Jetson Nano一步到位打开USB摄像头(Rosmaster小车)

背景&#xff1a;我用的rosmaster r2小车配的摄像头是Astra pro&#xff0c;也就是下图这款&#xff1a; 1. 支持的摄像头 Jetson开发包有多个用于连接相机的接口&#xff0c;包括USB、以太网和MIPI CSI-2。流行的相机是现成的支持&#xff0c;而Jetson生态系统合作伙伴支持广…

基于树莓派的OpenEuler基础实验二

文章目录 基于树莓派的OpenEuler基础实验二一、ROS中间件介绍1. ROS话题通信与服务通信2. 常见的ROS终端命令 二、中间件基础实验1. ROS的移植2. ROS的安装和环境配置3. 第一个ROS实践之开启小海龟4. ROS话题实践1&#xff09;ROS工作区与软件包的创建2&#xff09;ROS的话题通…

可能是最强的Python可视化神器,建议一试

数据分析离不开数据可视化&#xff0c;我们最常用的就是Pandas&#xff0c;Matplotlib&#xff0c;Pyecharts当然还有Tableau&#xff0c;看到一篇文章介绍Plotly制图后我也跃跃欲试&#xff0c;查看了相关资料开始尝试用它制图。 Plotly Plotly是一款用来做数据分析和可视化的…

《商用密码应用与安全性评估》第二章政策法规2.4商用密码应用安全性评估工作

商用密码应用安全性评估体系发展历程 第一阶段&#xff1a;制度奠基期&#xff08;2007年11月至2016年8月&#xff09; 第二阶段&#xff1a;再次集结期&#xff08;2016年9月至2017年4月&#xff09; 第三阶段&#xff1a;体系建设期&#xff08;2017年5月至2017年9月&…

【Vue3】vue3中的watchEffect使用及其他的API

目录 一&#xff0c;watchEffect 二&#xff0c;生命周期 三&#xff0c;什么是hooks? 四&#xff0c;toRef 五&#xff0c;其他组合式API 5.1shallowReactive&shallowRef 5.2readonly&shallowReadonly 5.3.toRaw&markRaw 5.4自定义Ref-customRef ​5.5pr…