本文目录
W800上手 Part.2 AOS开发入门
1.AOS简介
AOS不是一个Operating System,而是YoC规范里定义的一套统一RTOS API接口,用于简化开发流程,提高可移植性。当然,AOS提供了默认的Rhino内核集成,笔者就直接采用Rhino内核,没有折腾切换到FreeRTOS内核上。
2.RTOS
Rhino本质上仍旧是一个RTOS,这里先简单讲一下RTOS中比较重要的概念。
2.1 RTOS简介
RTOS(Real Time Operating System)是操作系统的一种,与我们常用的操作系统不同,RTOS旨在为嵌入式设备提供一套完整的任务管理,其功能比之我们常见的Windows、Mac OS之流简单了不少,也更底层。好处就是它的性能开销非常低,实时性很高。
2.2 Task与调度
操作系统(Operating System)本质上是一套用来管理计算机硬件资源的软件,现代操作系统的资源调度一般以进程(Process)为单位,而在RTOS中,这个概念被称为 Task。
由于在OS之中,运行的Task通常远多于CPU的线程数量,OS在管理系统资源时通常采用 时间切片 + 轮询调度 (Windows采用的方案为抢占式调度),Linux中的调度算法为epoll,而绝大多数RTOS为了精简,采用了Round-Robin调度算法,至于优先为哪个进程分配资源(这里主要指CPU时间),取决于在创建Task时为Task设定的优先级(Priority)。For example:
void TaskA(void* privData);
void TaskB(void* privData);
int main(void){
aos_task_t* TaskAHandlePtr;
aos_task_t* TaskBHandlePtr;
aos_task_new_ext(TaskAHandlePtr, "TaskA", TaskA, NULL, 1024, 0);
aos_task_new_ext(TaskBHandlePtr, "TaskB", TaskB, NULL, 1024, 1);
return 0;
}
其中aos_task_new_ext的函数定义为:
aos_task_new_ext(aos_task_t* task, const char* name, (*function)(void*), void* privData, int stackSize, int priority);
那么,在调度过程中,如果TaskA和TaskB同时试图执行指令,CPU时间将优先分配给TaskA(Priority值越小,优先级越高)。优先级系统是一套非常有用的系统,可以将更多的资源分配给比较急迫需要资源的Task,不过在使用的时候需要合理的分配优先级,否则很容易出现卡死在某一个Task中无法跳出的情况。
如果对于不需要优先级系统的项目,我们可以使用更为简单的aos_task_new()来创建等优先级任务
aos_task_new(const char* name, (*function)(void*), void* privData, int stackSize);
2.3 锁与信号量
在RTOS开发之中,我们不可避免的需要在多个Task中访问同一个资源,为了避免脏读(Dirty Read)和脏写(Dirty Write)造成的数据错误,我们需要约定一套标识(Flag)用于确定自己能否访问某一特定资源,在大多数情况下,这么一套标识系统被称为锁(Lock)或信号量(Semaphore)
2.3.1 锁
在RTOS之中我们用的通常是互斥锁(Mutex),像自旋锁(SpinLock)之流这里不做讲解。
所谓互斥锁,本质上是一种独占锁,同一时间只能被一个Task获取。For example:
static uint8_t g_var_example = 0;
static aos_mutex_t g_mutex_example;
void TaskA(void* privData){
...
aos_mutex_lock(&g_mutex_example, AOS_WAIT_FOREVER);
g_var_example = 1;
aos_mutex_unlock(&g_mutex_example);
...
}
void TaskB(void* privData) {
...
aos_mutex_lock(&g_mutex_example, AOS_WAIT_FOREVER);
g_var_example = 2;
aos_mutex_unlock(&g_mutex_example);
...
}
int main(void) {
int retVal = 0;
retVal |= aos_mutex_new(&g_mutex_example);
if(retVal != 0) {
LOGD("MAIN", "Error: Can't Create Mutex");
} else {
retVal |= aos_task_new("TaskA", TaskA, NULL, 1024);
retVal |= aos_task_new("TaskB", TaskB, NULL, 1024);
if (retVal != 0) {
LOGD("MAIN", "Error: Can't Create Tasks");
}
}
return retVal;
}
在这段代码执行完毕之后,g_var_example的值为多少,g_var_example又是如何变化的呢?
由于C++代码是顺序执行的,我们首先创建了TaskA,这时TaskA获取了锁,并将g_var_example设置为1,而后释放锁,由于RTOS时间切片的调度方式,这个流程并不是连贯完成的,我们假设TaskB在TaskA获取锁之后被创建,TaskB试图获取锁,但此时的锁已经被TaskA获取,TaskB无法获取到锁,开始进入WAIT,直到TaskA将锁释放,TaskB才获取到锁,并将g_var_example设置到2。所以整体的流程大概是这样:
flowchart LR
g_var_example=0 --TaskA--> g_var_example=1 --TaskB--> g_var_example=2
2.3.2 信号量
这里说的信号量指的是计数信号量,二进制信号量退化为互斥锁。所谓信号量的功能类似于锁,但信号量提供了更为高级的同步操作功能。所谓计数信号量跟锁的区别在于,信号量有一个引用计数器功能,当引用计数器为0时,无法被任何Task获取,当引用计数器大于0时,可以被获取。
For Example:
typedef struct {
uint8_t Bit0 : 1;
uint8_t Bit1 : 1;
uint8_t Bit2 : 1;
uint8_t Bit3 : 1;
uint8_t Bit4 : 1;
uint8_t Bit5 : 1;
uint8_t Bit6 : 1;
uint8_t Bit7 : 1;
} FlagGroup_8Bit_t;
aos_sem_t g_sem_example;
FlagGroup_8Bit_t g_flag_example = {0};
void TaskA(void* privData) {
...
if (aos_sem_is_valid(&g_sem_example)) {
aos_sem_wait(&g_sem_example, AOS_WAIT_FOREVER);
g_flag_example.Bit0 = 1;
aos_sem_signal(&g_sem_example);
} else {
LOGD("TaskA", "ERROR: sem is not valid");
}
...
}
void TaskB(void* privData) {
...
if(aos_sem_is_valid(&g_sem_example)) {
aos_sem_wait(&g_sem_example, AOS_WAIT_FOREVER);
g_flag_example.Bit1 = 1;
aos_sem_signal(&g_sem_example);
} else {
LOGD("TaskB", "ERROR: sem is not valid");
}
...
}
int main(void) {
int retVal = 0;
retVal |= aos_sem_new(&g_sem_example, 8);
if (retVal != 0) {
LOGD("MAIN", "ERROR: create sem failed");
} else {
retVal |= aos_task_new("TaskA", TaskA, NULL, 1024);
retVal |= aos_task_new("TaskB", TaskB, NULL, 1024);
if (retVal != 0) {
LOGD("MAIN", "ERROR: create tasks failed");
}
}
return retVal;
}
为了理解这个示例,我们先看看aos_sem_new的定义
aos_sem_new(aos_sem_t* sem, int count);
第二个参数count,就是我们前文提到的引用计数器,在这个示例里,我们指定的引用计数为8,故而最多可以有8个Task同步获取信号量,我们只获取了两个,故而这里是同步操作的。在使用信号量需要特别注意不要同时写同一个变量,否则会引起非常严重的数据冲突问题。
2.4 事件与微服务
2.4.1 事件
在裸机编程之中,我们使用NVIC来“抢占”系统CPU时间,而在RTOS之中,我们通常使用事件(Event)来唤醒(Notify)某一Task该执行特定操作了。本质上Event的出现就是为了在各个进程之中传递信息,而不用占用大量CPU时间用于轮询标志位。
For example:
#define Bit0 0x01
#define Bit1 0x02
#define Bit3 0x04
#define Bit4 0x08
#define Bit5 0x10
#define Bit6 0x20
#define Bit7 0x40
aos_event_t g_event_example;
void TaskA(void* privData) {
unsigned int originEventFlag;
...
aos_event_get(&g_event_example, Bit7 & 0xFF, OR_CLEAR, &originEventFlag,
RHINO_WAIT_FOREVER);
// Doing Something Here
aos_event_set(&g_event_example, Bit6 & 0xFF, OR);
...
}
void TaskB(void* privData) {
unsigned int originEventFlag;
...
aos_event_get(&g_event_example, Bit6 & 0xFF, OR_CLEAR, &originEventFlag,
RHINO_WAIT_FOREVER);
// Doing Something Here
...
}
int main(void) {
int retVal = 0;
retVal |= aos_event_new(&g_event_example, 0);
if (retVal != 0) {
LOGD("MAIN", "ERROR: can't create event");
} else {
retVal |= aos_task_new("TaskA", TaskA, NULL, 1024);
retVal |= aos_task_new("TaskB", TaskB, NULL, 1024);
if (retVal != 0) {
LOGD("MAIN", "ERROR, can't create tasks");
} else {
aos_event_set(&g_event_example, Bit7 & 0xFF, OR);
}
}
return retVal;
}
这个示例的执行流程应该非常明了,首先是TaskA和TaskB都被创建,但此时的g_event_example并没有被set任何事件,故而TaskA和TaskB均被挂起,而后main中将g_event_example的Bit7置位,此时TaskA被Notify,开始执行自己的逻辑,而后TaskA将g_event_example的Bit6置位,此时TaskB被Notify,开始执行逻辑。
2.4.2 微服务
微服务(uService)是一个YoC组件,并非RTOS本身提供的功能,其本质上依然是事件,但其对事件进行了自己的包装,将其包装为支持RPC请求/应用的服务。
本质上微服务的出现还是为了管理复杂的状态,其RPC风格的调用可以提高代码可读性。
For example:
#define uServiceEventExample0 0x01
#define uServiceEventExample1 0x02
void uServiceEventCallBack(uint32_t eventID, const void* data, void* context) {
switch(eventID) {
case uServiceEventExample0: {
// Do something here
break;
}
case uServiceEventExample1: {
// Do something here
break;
}
default: {
LOGD("uService", "ERROR: unknown Event ID");
break;
}
}
}
int main(void) {
int retVal = 0;
retVal |= event_service_init(NULL);
if (retVal == 0) {
retVal |= event_subscribe(uServiceExample0, NULL);
retVal |= event_subscribe(uServiceExample1, NULL);
if (retVal != 0) {
LOGD("MAIN", "ERROR: can't subscribe uService events");
} else {
retVal |= event_publish(uServiceEventExample0, NULL);
retVal |= event_publish(uServiceEventExample1, NULL);
if (retVal != 0) {
LOGD("MAIN", "ERROR: can't publish uService events");
}
}
} else {
LOGD("MAIN", "ERROR: can't initialize uService");
}
return retVal;
}
uService跟Event管理的最大区别就在于,uService采用回调(Callback)处理,而Event则是以Task为单位,本质上是uService在初始化之后创建了一个自己的Task,并通过subscribe来管理多个Callback,在Event被publish后,即调用相对应的Callback。