学习目标
- 理解操作系统功能
- 学会使用RTX51操作系统
学习内容
操作系统
我们认知的操作系统
我们认知的操作系统更多的是通过PC机或者手机来进行理解的,例如Window系统,Android系统。
由于这些商业系统属于高度集成化后的产物,我们会发现操作系统这个概念很复杂、很宽泛,无法理解和去实现。
目前大家已知的操作系统功能大致如下:
- 进程管理:管理程序的执行,包括进程的创建、终止、调度和通信等。
- 内存管理:负责管理内存的分配和回收,保证每个程序都能获得足够的内存空间。
- 文件系统管理:为应用程序提供文件的读写和管理功能,包括文件的创建、打开、关闭、删除等。
- 设备管理:管理计算机系统中的各种设备,包括输入输出设备、网络设备、存储设备等。
- 安全管理:保护系统的安全性,防止病毒、黑客等对系统进行攻击和破坏。
- 用户界面:提供用户与计算机系统进行交互的方式,包括图形用户界面和命令行界面等。
- 网络通信:管理计算机系统的网络通信,包括数据传输、协议处理、连接管理等。
- 错误处理:监测和处理各种错误和异常情况,保证系统的稳定性和可靠性。
操作系统是计算机系统中的核心软件,为用户和应用程序提供各种服务和支持,使得计算机系统能够高效、稳定地运行。
以上是我们认知中的商业操作系统的功能概述,每一块单独拿出来,都是一个复杂的内容,而且这些最终这些还要集合到一起去。
最小的操作系统
其实操作系统的复杂程度和硬件功能的集成度相关,内存、存储、CPU性能、是否有网络设备、是否有输入输出设备等等这些条件。
由于集成度不同,通用性也就不同,但是作为一个操作系统,必然会提供以下功能:
- 任务管理
- 任务调度
- 任务间通信
所有操作系统必须提供这三种特定功能,有的只是叫法不同。
任务管理、任务调度和任务通讯是单片机最小操作系统中的核心功能,下面分别介绍一下:
- 任务管理:最小操作系统通常支持多任务,任务管理就是对多任务的管理。它包括任务的创建、销毁和切换。任务创建时需要分配任务所需要的资源,例如堆栈、全局变量等;任务销毁时需要释放这些资源。任务切换时需要保存任务的上下文并切换到下一个任务的上下文。
- 任务调度:任务调度是指按照一定的规则从就绪队列中选择一个任务并将处理器分配给它执行。最小操作系统通常采用时间片轮转的方式进行任务调度,每个任务被分配一个时间片,当时间片用完后,处理器被分配给下一个任务。
- 任务通讯:任务通讯是指多个任务之间进行信息交换。最小操作系统中通常采用消息队列的方式进行任务通讯,一个任务发送消息,另一个任务接收消息。任务通讯还可以采用信号量、邮箱等方式实现。
需要注意的是,最小操作系统的功能非常简单,只能满足一些基本的需求,例如任务管理、任务调度和任务通讯。如果需要更加复杂的功能,例如文件系统、网络协议栈等,则需要使用更加完善的操作系统。
RTX51系统
RTX51是Keil公司推出的用于8051系列单片机的实时操作系统(RTOS)。它提供了任务管理、任务调度、任务通讯、定时器、信号量、邮箱等实时操作系统的基本功能,并且与Keil公司的C51编译器紧密集成,能够方便地进行开发和调试。
RTX51支持多任务并发执行,通过任务管理器实现任务的创建、删除、挂起、恢复、优先级调整等功能。任务调度器能够根据任务的优先级、时间片轮转等算法进行任务调度,保证系统中各个任务都能得到适当的执行机会。任务通讯机制包括消息队列、信号量、邮箱等,能够方便地实现任务之间的数据共享和同步。
除了任务管理、任务调度、任务通讯等基本功能之外,RTX51还提供了定时器、中断服务程序等功能,能够方便地实现定时器、PWM、ADC等应用。同时,RTX51还提供了对Flash和EEPROM的编程支持,能够方便地实现在线升级等功能。
总之,RTX51是一款成熟、可靠、易用的实时操作系统,能够大大简化单片机的开发过程,提高开发效率和可靠性。
RTX51 包含两个版本:
- RTX51 Tiny
- RTX51 Full
RTX51 Tiny是一个非常小型的实时操作系统,具有基本的任务调度功能,包括任务优先级和时间片轮转等。RTX51 Tiny适用于基于51系列单片机的应用程序,特别是对于小型和简单的应用程序,因为它不需要太多的RAM和ROM资源。
注意: RTX51采用的是Timer0实现的任务切换,因此不可以使用
timer0
,包括其中断函数也不可以声明! 否则系统不可用!
RTX51 Full则是一个功能更为强大的实时操作系统,它不仅支持基本的任务调度功能,还提供了更多的RTOS特性,例如信号量、邮箱、消息队列、事件标志和互斥量等,使得它更加适合于需要更高级RTOS特性的应用程序。
RTX51 Tiny是一个轻量级的RTOS,适用于简单的应用程序,而RTX51 Full则提供了更多的RTOS特性,适用于更为复杂的应用程序。
RTX51 Real-Time Kernel
RTX51 is a real-time kernel for the 8051 family of microcontrollers that is designed to solve two problems common to embedded programs.
- Multitasking: several operations must execute simultaneously.
Real-time control: operations must execute within a defined period of time.
RTX51 Tiny环境搭建
新建一个项目
- 打开keil安装目录,来到
C51\RtxTiny2\SourceCode
目录,拷贝Conf_tny.A51
和RTX51TNY.LIB
到项目中。
- 在项目中添加一个Group,名称自己定义,我们在这里定义为
OS
,将添加的两个文件加入到group中
- 打开配置,来到
Target
中,将Operating system
修改为RTX-51 Tny
// P5.3 闪烁 void sys_init() { P5M1 &= ~0x08; P5M0 &= ~0x08; }
// 这里函数名可随意, 建议不要使用start, 会和I2C.h里的Start冲突 void mainstart() _task 0 { sys_init(); // 创建任务 1 os_create_task(1); // 结束任务 0 os_delete_task(0); }
void task0() _task 1 { while(1) { P53 = 1; os_wait1(K_TMO);
P53 = 0;
os_wait1(K_TMO);
}
}
- 不再有main函数
- 代码入口为标记为 `_task_ 0`的函数。
- `_task_`标记的函数,表示这个是独立的任务,多个task可以同时执行。
<a name="qwYtF"></a>
### 库函数与RTX51
开发过程中,会将库函数环境和OS环境进行整合,整合的过程需要用到两者特性,并且不产生冲突。
1. 新建项目
2. 在项目目录中新建`User`目录、`LIb`目录、`OS`目录。
- User目录存放`main.c`等和业务相关的文件
- `Lib`目录存放库函数目录
- `OS`目录存放操作系统文件
3. 在项目中添加Group,依次对应新建的几个目录,将对应的文件加入到各自的group中。
4. keil中配置include path,将以上几个目录配置进去。
5. 修改`config.h`文件,内容如下:
```c
#ifndef __CONFIG_H
#define __CONFIG_H
//========================================================================
// 主时钟定义
//========================================================================
#define MAIN_Fosc 24000000L //定义主时钟
//#define MAIN_Fosc 22118400L //定义主时钟
//#define MAIN_Fosc 12000000L //定义主时钟
//#define MAIN_Fosc 11059200L //定义主时钟
//#define MAIN_Fosc 5529600L //定义主时钟
#define NULL 0
//========================================================================
// 头文件
//========================================================================
#include "STC8H.H"
#include "RTX51TNY.H"
#include <intrins.h>
#include <stdlib.h>
#include <stdio.h>
//========================================================================
// 类型定义
//========================================================================
typedef unsigned char u8; // 8 bits
typedef unsigned int u16; // 16 bits
typedef unsigned long u32; // 32 bits
typedef signed char int8; // 8 bits
typedef signed int int16; // 16 bits
typedef signed long int32; // 32 bits
typedef unsigned char uint8; // 8 bits
typedef unsigned int uint16; // 16 bits
typedef unsigned long uint32; // 32 bits
//===================================================
#define TRUE 1
#define FALSE 0
//===================================================
#define Priority_0 0 //中断优先级为 0 级(最低级)
#define Priority_1 1 //中断优先级为 1 级(较低级)
#define Priority_2 2 //中断优先级为 2 级(较高级)
#define Priority_3 3 //中断优先级为 3 级(最高级)
#define ENABLE 1
#define DISABLE 0
#define SUCCESS 0
#define FAIL -1
//===================================================
#define I2C_Mode_Master 1
#define I2C_Mode_Slave 0
#define PIE 0x20 //1: 比较结果由0变1, 产生上升沿中断
#define NIE 0x10 //1: 比较结果由1变0, 产生下降沿中断
#define PWMA 128
#define PWMB 129
#define FALLING_EDGE 1 //产生下降沿中断
#define RISING_EDGE 2 //产生上升沿中断
//===================================================
//========================================================================
// 外部函数和变量声明
//========================================================================
#endif
- include加入OS的支持
RTX51的延时问题
示例代码
#include "config.h"
#include "GPIO.h"
#include <stdio.h>
#include "delay.h"
#define LED_SW P45
#define LED1 P27
#define LED2 P26
void GPIO_config(void) {
GPIO_InitTypeDef GPIO_InitStructure; //结构定义
GPIO_InitStructure.Pin = GPIO_Pin_5; //指定要初始化的IO,
GPIO_InitStructure.Mode = GPIO_PullUp; //指定IO的输入或输出方式,GPIO_PullUp,GPIO_HighZ,GPIO_OUT_OD,GPIO_OUT_PP
GPIO_Inilize(GPIO_P4, &GPIO_InitStructure);//初始化
GPIO_InitStructure.Pin = GPIO_Pin_7; //指定要初始化的IO,
GPIO_InitStructure.Mode = GPIO_PullUp; //指定IO的输入或输出方式,GPIO_PullUp,GPIO_HighZ,GPIO_OUT_OD,GPIO_OUT_PP
GPIO_Inilize(GPIO_P2, &GPIO_InitStructure);//初始化
GPIO_InitStructure.Pin = GPIO_Pin_3; //指定要初始化的IO,
GPIO_InitStructure.Mode = GPIO_PullUp; //指定IO的输入或输出方式,GPIO_PullUp,GPIO_HighZ,GPIO_OUT_OD,GPIO_OUT_PP
GPIO_Inilize(GPIO_P5, &GPIO_InitStructure);//初始化
}
void sys_init() {
GPIO_config();
EA = 1;
}
// 这里函数名可随意, 建议不要使用start, 会和I2C.h里的Start冲突
void main_start() _task_ 0 {
sys_init();
LED_SW = 0;
// 创建任务 1
os_create_task(1);
// 创建任务 2
os_create_task(2);
// 结束任务 0
os_delete_task(0);
}
void task_1() _task_ 1 {
P53 = 1;
while(1) {
P53 = !P53;
//os_wait2(K_TMO, 200);
delay_ms(250);
delay_ms(250);
delay_ms(250);
delay_ms(250);
}
}
void task_2() _task_ 2 {
LED1 = 0;
while(1) {
LED1 = !LED1;
os_wait2(K_TMO, 200);
}
}
两个任务,一个延时会卡住cpu执行,一个不会。
我们可以适当调节延时时间,通过逻辑分析仪来分析。delay的延时会影响到系统的延时。
我们在非必要的情况下,尽量少使用卡住cpu的延时操作。或者在任务执行过程中,任务中的延时要求不需要那么高。否则两者会影响。
K_TMO与K_IVL的区别
调用os_wait()
/ os_wait2()
指定K_TMO
/ K_IVL
参数都能让任务进入waiting
状态,然后等待一段时间后恢复到ready
状态,K_TMO
和K_IVl
的区别如下:
1、计算的起点:K_TMO
是以当前调用wait / wait2
的时间为起点,K_IVL
是以上一次任务结束为起点。
2、是否包含任务本身执行时间:K_TMO
不包含,K_IVl
包含。
通过一个时序图说明情况,如下图,有3个任务,分别是task_0/1/2
,假设3个任务的自身执行时间1ms
- task_0没有调用任何阻塞API。
- task_1使用K_TMO参数等待3ms超时。
- task_2使用K_IVL等待3ms间隔。
并假设调度器先按照task_0、task_1、task_2的顺序调度任务:
task_0先执行1ms,SysTick=1
task_1执行1ms,然后等待3ms超时,SysTick=2
task_2执行1ms,然后等待3ms间隔,SysTick=3
task_0执行2ms,此时只有task_0可执行,故连续执行2ms,SysTick=5
task_2执行1ms,注意,由于K_IVL包含任务自身执行时间,离上一次task_2结束已经过去2ms,所以接下来要执行task_2,SysTick=6
task_1执行1ms,task_1等待的时间已经到了并且超时,所以执行task_1,SysTick=7
task_0执行1ms,SysTick=8
task_2执行1ms,离上一次task_2结束已经过去2ms,接下来执行task_2,SysTick=9
……
能够观察到,K_TMO是超时的意思,它能够保证一定会等到足够的时间,但是不一定准确,时间轴上可能是不连续的,比如上图而言,task_1只在SysTick=11的时候准确等待了3ms,而前面SysTick=6的时候等待了4ms。
K_IVL保证间隔的时间是准确的,在时间轴上是连续的。对于code = x(ms),wait() / wait2() = y(ms),调用API后,K_TMO将在 >=(x+y) 的时间执行完成,K_IVL将在 y 时间执行完成。
官网文档:https://developer.arm.com/documentation/ka002383/latest
:::info 总结如下:
- 如果想从现在起等待一段时间,用K_TMO
- 如果想做周期性动作,用K_IVL :::
K_SIG信号等待
通过os_wait实现信号等待
void task_2() _task_ 2 {
while(1) {
printf("task 2 start wait\r\n");
os_wait1(K_SIG);
printf("task 2 end wait\r\n");
}
}
以上是一个任务在执行,通过os_wait1(K_SIG)
进行信号等待,直到有信号来才能执行下一句。
我们可以通过在一些事件中来发送信号:
os_send_signal(2);
练习
- 采用RTX51 Tiny,通过按键来切换开发板功能。