W800上手 Part.2 AOS开发入门

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。

发表回复