智能家居系统-软件设计
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了智能家居系统-软件设计相关的知识,希望对你有一定的参考价值。
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=(uint8_t *)OSQPend(ZIGBEE_Q,250,&err);//timeout= 2.5s. if(err!=OS_ERR_NONE) { ...//用户代码 } else { ...//用户代码 OSQFlush(WIFI_Q); //清空消息队列 } } }
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 == WIFI_RX_FRAME_HEADER_0) { Uart4WiFi_RxState = WIFI_RX_STATE_SD1; //符合条件则跳转 .....//用户函数 } else //否则从新开始 { Uart4WiFi_RxState = WIFI_RX_STATE_SD0; } break; } ...... } //end of switch } //end of function
以上是关于智能家居系统-软件设计的主要内容,如果未能解决你的问题,请参考以下文章