介绍
前几天有幸收到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 作为调试器。
然后双击 dpinst_amd64.exe 安装 stlink 驱动。
新建工程
本次培训讲解到的几个示例分别为:LED、按键、自动初始化机制、自定义msh命令、多线程、线程间通信、定时器、消息队列以及结合前面功能的摩斯电码包。因此我们需要新建一个项目,并在项目中下载一个现成的摩斯电码代码包以便开发时使用。
新建 RTT 项目,基于开发板:stm32f411-st-nucleo。
安装好后双击 rt-thread-settings,可以搜索软件包并给当前项目安装。搜索 morse 并添加。
如果安装错误是很常见的问题,我们只需要手动下载对应的包进行安装,再重新添加即可。
示例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; //代表线程挂起成功
}
编译构建代码:点击红色框里的按钮。
下载代码:点击黄色框里的按钮。
打开对应终端:点击蓝色框里的按钮。
示例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:自动初始化机制
板子上电时会触发一些函数。
初始化顺序 | 宏接口 | 描述 |
---|---|---|
1 | INIT_BOARD_EXPORT(fn) | 非常早期的初始化,此时调度器还未启动 |
2 | INIT_PREV_EXPORT(fn) | 主要是用于纯软件的初始化、没有太多依赖的函数 |
3 | INIT_DEVICE_EXPORT(fn) | 外设驱动初始化相关,比如网卡设备 |
4 | INIT_COMPONENT_EXPORT(fn) | 组件初始化,比如文件系统或者 LWIP |
5 | INIT_ENV_EXPORT(fn) | 系统环境初始化,比如挂载文件系统 |
6 | INIT_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,可以看到很多命令和他们的描述:
我们在 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
其中 inc 是头文件,src 是 .c 文件,samples 文件夹里是 FinalSH 对应的提示信息。
正确运行后,就可以通过按键输入 morse 码啦!
心得体会
在此之前博主的开发一直是基于裸机的,只是后台的死循环+前台的中断,并没有怎么接触操作系统的层面。因此接触 RT-Thread 的初次学习后最主要的感觉就是把课内纸上谈兵的操作系统拉到了现实的实践中,也能更真实地踏入这个领域。这也很大程度上归功于这款系统的轻量,安装方便,不需要大量资源即可运行的特点。
因为最近报名的嵌入式芯片系统大赛也需要使用 RTT 系统,所以博主还会利用手头的板子进行后续的深入学习,先尝试理解内核的基本概念和操作流程,到掌握线程、信号量、定时器等基本API的使用方法,以及亲自实现线程调度、通信、互斥、同步等代码,加深对嵌入式操作系统的理解。