首页 > 代码库 > 智能家居系统-软件设计

智能家居系统-软件设计

1 智能家居远程控制系统的软件实现 

1.1 基于uC/OS-II的中央控制器的软件设计

1.1.1 uC/OS-II系统移植

本设计使用uC/OS-II操作系统,uC/OS-II是一个源码公开、可移植、可固化、可剪裁和抢占式的实时多任务操作系统,uC/OS-II的大部分源码是用标准ANSI C编写,并且编程规范,可读性很高,内核中只有少量的与硬件相关的代码使用汇编语言编写,总共200余行,移植非常方便[37]。uC/OS-II软件体系结构如图5-1所示。移植工作主要包括以下几个方面的内容:

1)修改OS_CPU.H中常量、数据类型和宏;

2)用汇编语言改写OS_CPU_A.ASM中的四个函数 ;

3)用C语言改写OS_CPU_C.C中的几个简单函数;

 技术分享

 

图5-1 uC/OS-I 软硬件体系结构图

uC/OS-II处理器无关的代码提供uCOS-II的系统服务,移植uCOS II的主要工作就是处理器和编译器相关代码以及BSP(Board Support Package)的编写。uCOS II大部分代码是使用ANSI C语言书写的,因此uCOS-II的可移植性较好。uC/OS-II 系统移植只需要使用C和汇编语言写一些处理器相关的代码[20]。uC/OS-II 系统移植工作主要步骤:

(1)OS_CPU.H的修改

1)进出临界区相关代码

前后台系统靠中断来实现实时任务,而操作系统使用任务调度来保证实时性,在调度过程中,不能被打断,因此必须关中断。

不同的CPU有不同的中断管理方法,为了便于移植,uC/OS-II定义了两个宏来表示中断开关,它们是:

OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()。

uC/OS-II系统有三种方法来实现进出临界区代码,OS_CPU.H中的常数OS_CRITICAL_METHOD规定了其实现方法[37]

本系统选择的是方法3,即定义:

#define  OS_CRITICAL_METHOD   3

使用方法3时,需要状态寄存器的值保存在局部变量cpu_sr中,因此需要定义两个函数分别进行状态寄存器的保存与恢复,这两个函数是:

#define  OS_ENTER_CRITICAL()  {cpu_sr = OS_CPU_SR_Save();}//进入临界区,保存CPU寄存器。

#define  OS_EXIT_CRITICAL()    {OS_CPU_SR_Restore(cpu_sr);}//退出临界区,恢复CPU寄存器。

这两个函数在文件os_cpu_a.asm中用汇编语言来实现的,具体实现代码如下:

OS_CPU_SR_Save              ;存储和恢复CPU状态寄存器SR

    MRS     R0, PRIMASK     ;保存全局中断标志

    CPSID   I                                  ;关中断

    BX      LR

OS_CPU_SR_Restore

    MSR     PRIMASK, R0             ;恢复全局中断标志

    BX      LR

2)堆栈的增长方向定义

不同的处理器的堆栈生长方向不同,有向上生长的,也有向下生长的,向上增长入栈由内存的低地址向高地址;向下增长入栈由内存的高地址向低地址。不同的增长方向下,堆栈栈顶也不同:

向上增长:ptos为TaskStk[0]

向下增长:ptos为TaskStk [SIZE-1]

可以根据OS_CFG.H中的常数OS_STK_GROWTH作为选择开关,使用户可通过定义该常数的值来选择相应的代码段,STM32向下增长,定义如下:

#define  OS_STK_GROWTH        1 

3)数据类型定义

对于不同的CPU、不同的开发环境,同样的数据结构可能其类型是不同的,为了便于系统在不同的环境下移植,uC/OS-II定义了系统使用的数据类型。

定义方法是使用typedef关键字进行声明,如下:

typedef unsigned char BOOLEAN; /*布尔变量,主要用在二值变量定义  */

typedef unsigned char  INT8U;   /*定义INT8U为8位无符号整形     */

typedef unsigned short INT16U;  /*定义INT16U为16位无符号整形    */

typedef unsigned int   INT32U;  /*定义INT32U为32位无符号整形   */

typedef unsigned int   OS_STK;  /*定义OS_STK为32位堆栈定义变量*/

.........

(2)OS_CPU_A.ASM的修改

1)系统的启动

OSStartHighRdy()启动最高优选级任务,由OSStart调用,系统启动前,至少需要创建一个任务,否则系统会崩溃。

OSStartHighRdy                       ;启动最高优选级的任务

 LDR     R0, =NVIC_SYSPRI14       ; PendSV中断地址

 LDR     R1, =NVIC_PENDSV_PRI    ;设置PendSV优选级为最低

 STRB    R1, [R0]                   ;写入PendSV优选级

 MOVS    R0, #0                    ; 设置PSP

 MSR     PSP, R0                    ; PSP=0,R0内容加载到PSP

 LDR     R0, =OSRunning             

 

 MOVS   R1, #1

 STRB    R1, [R0]

 LDR   R0, =NVIC_INT_CTRL ;装载中断控制器状态寄存器ICSR地址

 LDR     R1, =NVIC_PENDSVSET     ;ICSR:bit28=1,

;悬起PendSV中断

 STR     R1, [R0]

 CPSIE   I                           ; 开系统中断

OSStartHang

 B       OSStartHang      ; 程序不会运行到此处

2)任务切换

任务的切换过程就是通过任务就绪表找到优先级最高的任务,切换过程和中断过程有些类似,中断自动保存寄存器,而任务切换由软件进行上下文环境保存和程序跳转。任务切换时保存当前任务的上下文环境到该任务的私有堆栈中,然后复制最高优先级任务私有堆栈的数据到CPU的工作寄存器中,然后执行新的任务代码段,实现了任务的切换。在uC/OS-II 中,任务级切调用OS_Sched(),然后调OS_TASK_SW(),OS_TASK_SW()是OSCtxSw()的宏声明。

OSCtxSw

   LDR     R0, =NVIC_INT_CTRL;中断控制器状态寄存器ICSR地址

   LDR     R1, =NVIC_PENDSVSET; 中断状态寄存器ICSR

   STR     R1, [R0]           ; ICSR:bit28=1,悬起PendSV中断

   BX      LR                 ;LR--返回

uC/OS-II是可剥夺式实时内核,在中断退出时,要进行优选级查询,如果中断使新的高优选级任务进入就绪状态,则要触发任务调度。作为可剥夺内核,uC/OS-II在中断服务程序的最后会调用 OSIntExit()函数检查任务就绪状态,看是否有高优选级任务进入就绪状态,如果有就调用OSIntCtxSw()函数,uC/OS-II使用OSIntCtxSw()进行中断级任务切换[38]

3)PendSV中断——PendSV_Handler

OSCtxSw()虽然是任务级切换的核心函数,但是STM32的硬件架构决定了PendSV中断处理才是任务切换的处理函数,STM32采用Cortex-M3硬件内核,当Cortex-M3进入异常服务例程时,自动压栈了R0-R3,R12,LR(R14,连接寄存器),PSR(程序状态寄存器)和PC(R15),并且在返回时自动弹出。

(3)OS_CPU_C.C的修改

OS_CPU_C.C文件包含几个钩子函数和堆栈初始化函数,但主要工作只需要修改 OSTaskStkInit()函数,该函数由任务创建函数OSTaskCreate()和OSTaskCreateExt()来调用[63],在任务建立时就被调用以初始化任务的堆栈,使用堆栈来模拟处理器的寄存器[37],初始化后STM32寄存器如图5-2所示。

 技术分享

       图5-2 处理器寄存器初始值

堆栈主要是模拟CPU寄存器,系统中需要移植函数OSTaskStkInit(),其原型为:

OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)

1.1.2 uC/OS-II多任务处理

uCOS-II最多可支持64个任务,其内核为占先式,总是执行就绪态的优先级最高的任务,并支持Semaphore (信号量)、Mailbox (邮箱)、MessageQueue(消息队列)等多种进程间通信机制。每一个任务由三部分组成,任务控制块,任务的私有堆栈、任务代码。

1)系统时钟节拍设置

时钟节拍为系统的任务调度和定时服务提供同步时钟,时钟节拍常数为:OS_TICKS_PER_SEC,系统中定义OS_TICKS_PER_SEC为100,那么通过CPU的硬件定时器中断,可以将操作系统的时钟频率设置为每秒100Hz,即每秒进行100次任务调度,系统“心跳”为10ms。

 技术分享

图5-3 任务通过SysTick进行轮转调度

STM32微控制器有一个嘀嗒定时器,使用该定时器完成系统精确定时中断服务。调用库函数SysTick_Config(SystemFrequency / 100) , 函数的参数就是systick重装定时器的值,并且自动打开中断,将中断设为最低的优先级,时钟设为HCLK即系统时钟72mhz,并重置计数寄存器开始计数。SystemFrequency为每秒72,000,000,所以SystemFrequency / 100就是1/100秒,也就是10ms。

void  OS_CPU_SysTickInit (void)
{
      /* SYSTICK分频--时钟节拍为:OS_TICKS_PER_SEC       */
     if ( SysTick_Config(SystemCoreClock / OS_TICKS_PER_SEC) )
    {
         while (1);
    }  
}

该函数的中断处理函数如下,调用OSTimeTick()函数完成所有计时服务和时间更新相关操作。

2)任务的堆栈分配[62]

堆栈作用的就是用来保存局部变量,从本质上讲也就是将CPU寄存器的值保存到RAM中。在uC/OS中,每一个任务都有一个独立的任务堆栈。uC/OS-II的在建立任务函数中要对新建任务的堆栈进行初始化,OSTaskCreate()和OSTaskCreateExt()通过调用OSTaskStkInit(),初始化任务的栈结构[63]。堆栈初始化函数原型是:

OS_STK *OSTaskStkInit (void (*task)(void *pd), void *p_arg, OS_STK *ptos, INT16U opt);

堆栈的建立过程:

typedef unsigned int   OS_STK;      

#define    TASK_STK_SIZE      512

OS_STK TaskStartStk[TASK_STK_SIZE];

以上3句语句即定义一个数组作为任务堆栈,堆栈长度512*4 = 2K Bytes。

本设计中使用了5个用户任务,还有一个系统任务创建任务,一个有6个任务,因此开辟6个堆栈结构。堆栈空间的大小根据任务需求设定,一是计算任务本身的需求,如局部变量、函数调用等,二是计算最多中断嵌套层数,确定最终需要保存的寄存器、中断服务程序中局部变量,然后得出任务的堆栈大小。本系统使用的堆栈如下:

/********************系统任务堆栈************************/

#define  APP_TASK_START_STK_SIZE             64u

#define MyTaskLED1_STK_SIZE                           128u

#define MyTaskLCD_STK_SIZE                             256u

#define MyTaskZigBee_STK_SIZE                        256u

#define MyTaskGPRS_STK_SIZE                           256u

#define MyTaskWIFI_STK_SIZE                            256u

 

OS_STK         App_TaskStartStk[APP_TASK_START_STK_SIZE];

OS_STK               TaskLED1Stk[MyTaskLED1_STK_SIZE - 1];

OS_STK               TaskLCDStk[MyTaskLCD_STK_SIZE - 1];

OS_STK               TaskZigBeeStk[MyTaskZigBee_STK_SIZE - 1];

OS_STK               TaskGPRSStk[MyTaskGPRS_STK_SIZE - 1];

OS_STK               TaskWIFIStk[MyTaskWIFI_STK_SIZE - 1];

3)任务创建与优先级分配

根据任务的重要性,对所有的用户任务分配优选级,从0到63,但是最低2个和最高2个已经分配给操作系统,因此用户可以分配的优选级还有60个。优选级号越低,优选级越高,在抢占式调度中,被调度的可能性越大,因此一般把根据任务的耗时时间和实时性要求,给任务分配不同的优选级,同时考虑到后续升级的可能性,两个不同任务的优选级之间需要隔几个数。

优先级分配:

#define  APP_TASK_START_PRIO                  2u  

#define  MyTaskLED1_PRIO                          4u

#define  MyTaskLCD_PRIO                           7u

#define  MyTaskZigBee_PRIO                       16u

#define  MyTaskGPRS_PRIO                        13u

#define  MyTaskWIFI_PRIO                          10u

uC/OS系统中,一般先建立一个任务,用它来创建其他任务,本文任务创建5个任务,具体如下:

void  AppTaskCreate (void)
{
      OSTaskCreate( TaskLED1,
               (void*) 0,
               &TaskLED1Stk[MyTaskLED1_STK_SIZE - 1],
                MyTaskLED1_PRIO
                );

      OSTaskCreate( TaskLCD,
               (void*) 0,
               &TaskLCDStk[MyTaskLCD_STK_SIZE - 1],
                MyTaskLCD_PRIO
               );

      OSTaskCreate( TaskGPRS,
                (void*) 0,
                &TaskGPRSStk[MyTaskGPRS_STK_SIZE - 1],
                MyTaskGPRS_PRIO
                );

      OSTaskCreate( TaskWIFI,
               (void*) 0,
               &TaskWIFIStk[MyTaskWIFI_STK_SIZE - 1],
               MyTaskWIFI_PRIO
               );

      OSTaskCreate( TaskZigBee,
               (void*) 0,
               &TaskZigBeeStk[MyTaskZigBee_STK_SIZE - 1],
               MyTaskZigBee_PRIO
               );   
}

4)任务通信与同步

嵌入式系统中的各个任务都是以并发的方式运行,经常需要互相无冲突地访问同一个共享资源,或者竞争使用外设,程序运行中甚至有时还要互相限制和制约,才保证任务的顺利运行。所以,系统必须具有完备的同步和通信机制,uC/OS-II使用信号量、消息邮箱和消息队列来实现任务间通信,在uC/OS-II中,这些都被称为事件,由事件控制块来记录和控制,事件控制块是一个叫OS_EVENT的数据结构。其中事件类型有6种:

#define  OS_EVENT_TYPE_UNUSED      0

#define  OS_EVENT_TYPE_MBOX        1

#define  OS_EVENT_TYPE_Q             2

#define  OS_EVENT_TYPE_SEM          3

#define  OS_EVENT_TYPE_MUTEX       4

#define  OS_EVENT_TYPE_FLAG         5

 

本系统中,使用了4个消息队列,声明如下:

OS_EVENT *GPRS_Q;

OS_EVENT *WIFI_Q;

OS_EVENT *ZIGBEE_Q;

OS_EVENT *LCD_Q;

GPRS_Q  =OSQCreate(&GPRSCmdTab[0],25);   //GPRS消息队列

WIFI_Q   =OSQCreate(&WIFICmdTab[0],25);   //WIFI消息队列

ZIGBEE_Q =OSQCreate(&ZIGBEECmdTab[0],25); //ZIGBEE的消息队列

LCD_Q   =OSQCreate(&LCDCmdTab[0],10);     //LCD消息队列

消息队列在系统中传递消息,可以发送和接收一个消息指针,例如,在ZigBee驱动函数下,调用OSQPend等待消息队列的处理如下:

void TaskZigBee (void *p_arg)
{                
    for(;;)
    { 
        ZigBee_data=http://www.mamicode.com/(uint8_t *)OSQPend(ZIGBEE_Q,250,&err);//timeout= 2.5s.>

5)任务调度

uC/OS-II 规定各个任务的优先级必须不同,在一个系统中,如果某个任务的优先级号越小,那么这个任务的优先级越高。uC/OS-II是基于任务优先级抢占式任务调度法的,有任务切换和中断返回切换两种调度方法,任务的优先级号就是任务编号。基于优先级的调度法指,CPU总是让处在就绪态的优先级最高的任务先运行。内核在任务调度时,某个任务被调度的条件有两个:条件1,该任务此时已处于就绪状态;条件2,该任务的优先级最高。

uC/OS-II任务的调度是由调度器完成的。所谓调度器实际上是一个函数 OSShed(),此函数通过搜索任务就绪表来获得最高优先级的就绪任务,任务就绪状态由OSRdyTbl和OSRdyGrp记录,就绪的任务在任务就绪表中设置其标志位,退出就绪的任务在就绪表中撤消其标志位[37]。对于最高优先级任务的查找,uC/OS-II利用哈希表来定位最高优先级的就绪任务,算法简洁,效率极高[38]。uC/OS-II任务就绪表如图5-4所示。

 技术分享

图5-4 uC/OS-II任务就绪表示意图

OSRdyTbl和OSRdyGrp共同实现任务优选级调度,当OSRdyTbl[i]中的任何一位是1时,OSRdyGrp的第i位置1,i的范围是0到7。内核主要是通过操作 OSRdyTbl[]和 OSRdyGrp 这两个数组来实现任务的调度。某个任务就绪时,将该任务放入就绪表,其算法的实现代码为:

OSRdyGrp            |= OSMapTbl[prio >> 3];  // OSRdyGrp置位

OSRdyTbl[prio >> 3] |= OSMapTbl[prio & 0x07];   // OSRdyTbl置位

其中,OSMapTbl[8] 是屏蔽字,用于限制OSRdyTbl[]数组的元素下标在0到7之间,见表5.1。

表5.1 OSMapTbl []数值

Index

Bit Mask

0

0000 0001

1

0000 0010

2

0000 0100

3

0000 1000

4

0001 0000

5

0010 0000

6

0100 0000

7

1000 0000

当任务挂起或者退出,则该任务的优选级号则会在就绪表中删除,其算法实现代码如下:

  if((OSRdyTbl [prio>>3] &= OSMapTbl[prio&0x07]) = = 0) // OSRdyTbl

     OSRdyGrp &= OSMapTbl[prio>>3];                // OSRdyGrp

uC/OS-II任务调度的第一件事件就是找到任务就绪表中的最高优先级任务,然后将其在就绪表中位置置0,算法和查找最高优选级相同。

uC/OS-II总是运行进入就绪态任务中优先级最高的那一个,由调度器决定应该运行的任务。在uC/OS-II系统调度中,任务级的调度是由函数OSSched()完成的,实现任务切换,中断级的调度是由函数OSIntExt()完成的,实现可剥夺式任务调度。

1.1.3 uC/OS-II驱动设计

本系统中,主控MCU面对的外设主要是UART接口,有WIFI、ZigBee、LCD、协控制器,如何保证实时性是一个必须考虑的问题。由于系统采用了统一的数据协议,因此,用户函数主要任务就是与串口交互。

为了提高系统实时性,采用了中断模式,STM32有5个串口,其中串口1作ISP下载和系统状态监控,串口2、3、4、5与外设模块相连接,完成系统数据交互,串口设置:波特率15200,数据位8,停止位1。系统上层协议如表5.2所示。嵌入式系统中断服务函数开发注意事项:

1)中断服务函数不能返回一个值;

2)中断服务函数不能传递参数;

3)中断服务函数应该短而有效率的,在ISR中大量运算是不明智的。

4)避免在中断内使用标准IO库函数调用,如printf函数。

表5.2 网关与上位机通信协议

帧头

长度

客户端

编号

消息号

设备类

设备号

功能号

扩展位

校验

帧尾

7E 9A

06

xx

01

xx

xx

xx

xx

xx

5A 3E

交互实例:

上位机发:7e 9a 06 02 01 F1 02 00 00 FC 5a 3e 

说明:2号WiFi设备请求2号机温度查询

下位机回:7E 9A 06 02 01 F1 02 12 03 11 5A 3E 

说明:2号WiFi设备回复2号机温度+18.03℃

为了实现数据协议的分析和处理,同时减小中断开销,采用了状态机编程方法,串口数据在中断函数中进行状态机跳转,当出现最终状态时,表明接收到的数据符合协议标准,创立软件标志。有限状态机思想广泛应用于硬件控制电路设计,也是软件上常用的一种处理方法。它把复杂的控制逻辑分解成有限个稳定状态,在每个状态上判断事件,变连续处理为离散数字处理。

状态机模式编程优势:

1、状态机可以减少许多条件语句;

2、用状态进入和退出动作实现有保证的初始化和清除;

3、使用和维护都较简单;

4、轻易地改变状态机拓扑,系统升级方便;

5、提高了运行时效率和更小的内存占用;

下面以WIFI通信函数说明系统基于状态机的编程思路:

1)建立状态转换标志

对数据协议的帧头帧尾进行定义,便于编程,提高代码的可读性。

#define  WIFI_RX_FRAME_HEADER_0        0x7E      //帧头0

#define  WIFI_RX_FRAME_HEADER_1        0x9A      //帧头1

#define  WIFI_RX_FRAME_ED_0             0x5A       //帧尾0

#define  WIFI_RX_FRAME_ED_1             0x3E       //帧尾1

定义状态机跳转标志,程序按照状态进行转换。

#define  WIFI_RX_STATE_SD0              0   

#define  WIFI_RX_STATE_SD1              1   

#define  WIFI_RX_STATE_LEN              2   

#define  WIFI_RX_STATE_DATA             3   

#define  WIFI_RX_STATE_CHKSU           4   

#define  WIFI_RX_STATE_ED0              5   

#define  WIFI_RX_STATE_ED1              6 

2)根据逻辑关系编写状态跳转函数

void  Uart4WiFiDataRxHandler (INT8U rx_data)
{
    switch (Uart4WiFi_RxState)          //状态机状态标志
    {
        case WIFI_RX_STATE_SD0:    //状态0 ,起始状态                    
        {
          if (rx_data =http://www.mamicode.com/= WIFI_RX_FRAME_HEADER_0)  >

 

智能家居系统-软件设计