23.1 操作系統概述
之前的實驗都是利用單片機實現某個單一功能,但是有時候需要在兩個功能同時運行,這時就需要引入操作系統的概念,操作系統(Operating System,簡稱OS)是一種管理電腦硬件與軟件資源的程序,同時也是計算機系統的內核與基礎,操作系統大致包括5種功能:進程管理,作業管理,存儲管理,設備管理與文件管理。
23.1.1 操作系統分類
操作系統有三種基本類型:多道程序系統,分時系統,實時系統,最初操作系統是不支持這種微型單片機的運行的,隨著科技的發展才產生了針對于這種M系列內核的嵌入式操作系統,常見的嵌入式操作系統有FreeeRTOS,uCos,uC-Linux(一種Linux精簡版本),在STM32中一般運用FreeRTOS和uCos這兩種系統,Linux由于必須有內存才能運行,一般Linux系統需要大約200M的存儲空間才能裝下,我們這里采用uCos-II系統為例來進行嵌入式操作系統的移植實驗。
23.1.2 uCOS簡介
uCos系統最早出自于1992年美國嵌入式專家Jean J.Labrosse發表在《嵌入式系統編程》上的,并在該雜志的BBS上發布了源碼,發展到現在uCos-III已經出來,但是目前使用最廣泛的還是uCos-II,本單元我們采用uCos-II來進行介紹。
uCos-II是一個可以基于ROM運行的,可裁剪的,搶占式,實時多任務內核,采用C語言進行編寫,這是一種專門為計算機的嵌入式應用設計的,CPU硬件相關部分采用匯編語言編寫,執行效率高,占用空間小,最小內核可編譯至2Kbyte,uCos-II體系結構如下圖所示。
從上圖可以發現,我們移植系統的時候,只需要修改os_cpu.h,os_cpu_a.asm和os_cpu.c等三個文件即可,其中其中:os_cpu.h,進行數據類型的定義,以及處理器相關代碼和幾個函數原型;os_cpu_a.asm,是移植過程中需要匯編完成的一些函數,主要就是任務切換函數;os_cpu.c,定義一些用戶HOOK函數。
圖中定時器的作用是為UCOS-II提供系統時鐘節拍,實現任務切換和任務延時等功能。這個時鐘節拍由OS_TICKS_PER_SEC(在os_cfg.h中定義)設置,一般我們設置uCos-II的系統時鐘節拍為1ms~100ms,具體根據你所用處理器和使用需要來設置。我們利用STM32F1的SYSTICK定時器來提供UCOS-II時鐘節拍。
uCos-II早期版本只支持64個任務,但是從2.80版本開始,支持任務數提高到255個,不過對我們來說一般64個任務都是足夠多了,一般很難用到這么多個任務。uCos-II保留了最高4個優先級和最低4個優先級的總共8個任務,用于拓展使用,但實際上,uCos-II一般只占用了最低2個優先級,分別用于空閑任務(倒數第一)和統計任務(倒數第二),所以剩下給我們使用的任務最多可達255-2=253個(V2.91)。
所謂的任務,其實就是一個死循環函數,該函數實現一定的功能,一個工程可以有很多這樣的任務(最多255個),uCos-II對這些任務進行調度管理,讓這些任務可以并發工作(不是同時工作,并發只是各任務輪流占用CPU,而不是同時占用,任何時候還是只有1個任務能夠占用CPU),這就是uCos-II最基本的功能。
uCos-II的任何任務都是通過一個叫任務控制塊(TCB)的東西來控制的,每個任務管理塊有3個最重要的參數:1,任務函數指針;2,任務堆棧指針;3,任務優先級;任務控制塊就是任務在系統里面的身份證(uCos-II通過優先級識別任務)
在uCos-II中,使用CPU的時候,優先級高(數值小)的任務比優先級低的任務具有優先使用權,即任務就緒表中總是優先級最高的任務獲得CPU使用權,只有高優先級的任務讓出CPU使用權(比如延時)時,低優先級的任務才能獲得CPU使用權。uCos-II不支持多個任務優先級相同,也就是每個任務的優先級必須不一樣。任務的調度其實就是CPU運行環境的切換
uCos-II的每個任務都是一個死循環。每個任務都處在以下5種狀態之一的狀態下,這5種狀態是:睡眠狀態、就緒狀態、運行狀態、等待狀態(等待某一事件發生)和中斷服務狀態。
(1)睡眠狀態:任務在沒有被配備任務控制塊或被剝奪了任務控制塊時的狀態。
(2)就緒狀態:系統為任務配備了任務控制塊且在任務就緒表中進行了就緒登記,任務已經準備好了,但由于該任務的優先級比正在運行的任務的優先級低,還暫時不能運行,這時任務的狀態叫做就緒狀態。
(3)運行狀態:該任務獲得CPU使用權,并正在運行中,此時的任務狀態叫做運行狀態。
(4)等待狀態:正在運行的任務,需要等待一段時間或需要等待一個事件發生再運行時,該任務就會把CPU的使用權讓給別的任務而使任務進入等待狀態。
(5)中斷服務狀態:一個正在運行的任務一旦響應中斷申請就會中止運行而去執行中斷服務程序,這時任務的狀態叫做中斷服務狀態。
uCos-II任務的5個狀態轉換關系如圖
23.1.3 uCOS-II中與任務相關的函數
(1)創建進程:OSTaskCreate
函數原型:OSTaskCreate( void( *task )( void *pd ), void *pdata, OS_STK *ptos, INTU prio )
函數參數:
task:指向任務代碼的指針
pdata:任務開始執行時,傳遞給任務的參數的指針
ptos:分配給任務的堆棧的棧頂指針
prio:分配給任務的優先級
每個任務都有自己的堆棧,堆棧必須申明為OS_STK類型,并且由連續的內存空間組成。可以靜態分配堆棧空間,也可以動態分配堆棧空間。
(2)刪除進程
函數原型:INT8U OSTaskDel( INT8U prio )
函數參數:
prio:進程的優先級,該函數是通過任務優先級來實現任務刪除的
(3)請求刪除進程
函數原型:INT8U OSTaskDelReq( INT8U prio )
函數參數:
prio:進程的優先級
(4)修改進程優先級
函數原型:INT8U OSTaskChangePrio( INT8U oldprio, INT8U newprio )
函數參數:
oldprio:進程的源優先級
newprio:進程的新優先級
(5)進程掛起
函數原型:INT8U OSTaskSuspend( INT8U prio )
函數參數:
prio:進程的優先級
任務掛起和任務刪除有點類似,任務掛起只是將被掛起任務的就緒標志刪除,并做任務掛起記錄,并沒有將任務控制塊任務控制塊鏈表里面刪除,也不需要釋放其資源,而任務刪除則必須先釋放被刪除任務的資源,并將被刪除任務的任務控制塊也給刪了。被掛起的任務,在恢復后可以繼續運行。
(6)恢復進程
函數原型:INT8U OSTaskResume( INT8U prio )
函數參數:
prio:進程的優先級
23.2 uCos-II移植
我們將下載好的uCOS-II的源代碼解壓出來如下圖所示。
23.2.1 在工程中添加相應的文件
(1)在工程目錄下建立UCOSII文件夾,并在該文件夾內新建三個文件夾CONFIG,CORE和PORT
(2)將除了os_cfg_r.h和os_dbg_r.c這兩個文件以外的所有文件全部復制到CORE文件夾下
(3)在CONFIG文件夾中新建includes.h文件和os_cfg.h文件
(4)在PORT文件夾中新建os_cpu.h,os_cpu_a.asm,os_cpu_c.c這3個文件
(5)在工程中添加這三個目錄下的文件,如下圖所示。
注:不要把ucos-ii.c文件添加到UCOS-CORE分組中,否則會提示有重復定義錯誤。
23.2.2 文件修改
我們編譯工程后可以發現報了11個錯誤,但都是同一個錯誤,如下圖所示。
我們在移植的時候并沒有發現這個文件,那是因為我們并沒有用到這個文件,這個文件是在ucos-ii.h文件中引用的,我們跳轉到這個文件將其屏蔽掉。
注 :我們可以發現在修改的時候,文件雖然可以打開,但是修改不了,這是因為我們下載的源碼都被設置成了只讀模式,在工程中只讀文件會有一個鑰匙的標志,這就需要我們將文件的只讀屬性去掉即可。
去掉只讀屬性之后,我們會發現項目中的文件上鑰匙標志消失了,如下圖所示。
此時,我們就可以對文件內容進行修改了。打開ucos_ii.h文件,屏蔽44行的文件引用,如下圖所示。
此時會發現報更多的錯誤,此時我們進行新建文件的修改。
(1)os_cpu_a.asm文件詳解
①這部分代碼主要用于定義外部變量,IMPORT表示這是一個外部變量,不是在本程序內定義的,EXPORT則表示這些函數位于該文件內,供其他文件調用,類似于C語言中的extern關鍵字。
IMPORT OSRunning
IMPORT OSPrioCur
IMPORT OSPrioHighRdy
IMPORT OSTCBCur
IMPORT OSTCBHighRdy
IMPORT OSIntNesting
IMPORT OSIntExit
IMPORT OSTaskSwHook
EXPORT OSStartHighRdy
EXPORT OSCtxSw
EXPORT OSIntCtxSw
EXPORT OS_CPU_SR_Save
EXPORT OS_CPU_SR_Restore
EXPORT PendSV_Handler
②EQU和C語言中的define關鍵字一樣,用于宏定義,定義了一些寄存器的地址
NVIC_INT_CTRL EQU 0xE000ED04 ;中斷控制寄存器
NVIC_SYSPRI2 EQU 0xE000ED20 ;系統優先級寄存器
NVIC_PENDSV_PRI EQU 0xFFFF0000 ;PendSV中斷和系統節拍中斷
NVIC_PENDSVSET EQU 0x10000000 ;觸發軟件中斷的值
PRESERVE8
AREA |.text|, CODE, READONLY
THUMB
③OS_CPU_SR_Save和OS_CPU_SR_Restore是用于開關中斷的匯編函數,通過給PRIMASK寫1來關閉中斷,寫0來開啟中斷,這里也可以使用CPS指令來快速開關中斷
OS_CPU_SR_Save
MRS R0, PRIMASK ;讀取PRIMASK到R0,R0為返回值
CPSID I ;PRIMASK=1,關中斷(NMI和硬件FAULT可以響應)
BX LR ;返回
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;讀取R0到PRIMASK中,R0為參數
BX LR ;返回
④OSStartHighRdy是由OSStart()調用,用來開啟多任務,如果多任務開啟失敗就會進入OSStartHang函數中
OSStartHighRdy
LDR R4, =NVIC_SYSPRI2 ;設置PendSV優先級
LDR R5, =NVIC_PENDSV_PRI
STR R5, [R4]
MOV R4, #0 ;設置PSP=0
MSR PSP, R4
LDR R4, =OSRunning ;設置OSRunning=1
MOV R5, #1
STRB R5, [R4]
;切換到最高優先級的任務
LDR R4, =NVIC_INT_CTRL ;R4=NVIC_INT_CTRL
LDR R5, =NVIC_PENDSVSET ;R5=NVIC_PENDSVSET
STR R5, [R4]
CPSIE I ;開啟所有中斷
OSStartHang
B OSStartHang ;死循環
⑤這兩個函數都用于任務切換,它們的本質都是觸發PendSV中斷,具體切換過程在PendSV的中斷函數中進行,其中OSCtxSw是任務級切換,OSIntCtxSw是中斷級切換,是從中斷退出時切換到一個任務中,從中斷切換到任務的過程中,CPU的寄存器入棧工作已經完成。
OSCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;觸發PendSV異常
LDR R5, =NVIC_PENDSVSET
STR R5, [R4] ;向NVIC_INT_CTRL寫入NVIC_PENDSVSET觸發PendSV中斷
POP {R4, R5}
BX LR
OSIntCtxSw
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ;觸發PendSV異常
LDR R5, =NVIC_PENDSVSET
STR R5, [R4] ;向NVIC_INT_CTRL寫入NVIC_PENDSVSET觸發PendSV中斷
POP {R4, R5}
BX LR
NOP
⑥這部分代碼才是真正的任務切換函數,通過觸發PendSV中斷來進入該函數內進行任務切換
PendSV_Handler
CPSID I ;任務切換過程中必須關閉所有中斷
MRS R0, PSP ;如果在用PSP堆棧,則可以忽略保存寄存器
CBZ R0, PendSV_Handler_Nosave ;如果PSP為0就轉移到PendSV_Handler_Nosave
SUBS R0, R0, #0x20 ;R0-=20H
STM R0, {R4-R11}
LDR R1, =OSTCBCur
LDR R1, [R1]
STR R0, [R1]
PendSV_Handler_Nosave
PUSH {R14} ;保存R14的值
LDR R0, =OSTaskSwHook ;調用OSTaskSwHook()
BLX R0
POP {R14}
LDR R0, =OSPrioCur
LDR R1, =OSPrioHighRdy
LDRB R2, [R1]
STRB R2, [R0]
LDR R0, =OSTCBCur
LDR R1, =OSTCBHighRdy
LDR R2, [R1]
STR R2, [R0]
LDR R0, [R2] ;R0作為新任務的SP
LDM R0, {R4-R11} ;從堆棧中恢復R4-R11
ADDS R0, R0, #0x20
MSR PSP, R0 ;用新任務的SP加載PSP
ORR LR, LR, #0x04 ;確保LR的bit2為1,返回后使用進程堆棧
CPSIE I ;開啟所有中斷
BX LR ;中斷返回
end
(2)os_cpu.h文件詳解
①這部分主要用于定義一些數據類型,其中重點關注OS_STK這個數據類型,我們在定義任務堆棧的時候就是該類型數據,這是一個32位的數據類型,按字節算的話實際堆棧大小是我們定義的4倍。
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned short INT16U;
typedef signed short INT16S;
typedef unsigned int INT32U;
typedef signed int INT32S;
typedef float FP32;
typedef double FP64;
typedef unsigned int OS_STK;
typedef unsigned int OS_CPU_SR;
②這部分代碼定義了堆棧的增長方向,任務機切換的宏定義OS_TASK_SW,如果OS_CRITICAL_METHOD被定義為3的話那么進出臨界段的宏定義分別為OS_ENTER_CRITICAL和OS_EXIT_CRITICAL,這兩個函數都是用匯編語言編寫的
//OS_CRITICAL_METHOD = 1 :直接使用處理器的開關中斷指令來實現宏
//OS_CRITICAL_METHOD = 2 :利用堆棧保存和恢復CPU的狀態
//OS_CRITICAL_METHOD = 3 :利用編譯器擴展功能獲得程序狀態字,保存在局部變量cpu_sr
#define OS_CRITICAL_METHOD 3 //進入臨界段的方法
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
#endif
void OSCtxSw(void);
void OSIntCtxSw(void);
void OSStartHighRdy(void);
void OSPendSV(void);
#if OS_CRITICAL_METHOD == 3u
OS_CPU_SR OS_CPU_SR_Save(void);
void OS_CPU_SR_Restore(OS_CPU_SR cpu_sr);
#endif
OS_CPU_EXT INT32U OSInterrputSum;
(3)sys.h文件修改
添加關于條件編譯的定義,在文件中添加以下代碼即可。
#define SYSTEM_SUPPORT_OS 1
當宏定義為1的時候,編譯器在編譯的時候會只編譯滿足條件的代碼,當為0時,這部分代碼不會被編譯。
(4)delay.c文件修改
①添加Sys_Tick中斷服務函數與函數定義
#include "includes.h"
//支持UCOSII
#ifdef OS_CRITICAL_METHOD
#define delay_osrunning OSRunning //OS是否運行標記,0,不運行;1,在運行
#define delay_ostickspersec OS_TICKS_PER_SEC //OS時鐘節拍,即每秒調度次數
#define delay_osintnesting OSIntNesting //中斷嵌套級別,即中斷嵌套次數
#endif
//systick中斷服務函數,使用OS時用到
void SysTick_Handler()
{
//OS開始跑了,才執行正常的調度處理
if( delay_osrunning==1 )
{
OSIntEnter() ; //進入中斷
OSTimeTick() ; //調用ucos的時鐘服務程序
OSIntExit() ; //觸發任務切換軟中斷
}
}
②時鐘初始化函數修改
void SysTick_Init( u8 SYSCLK )
{
#if SYSTEM_SUPPORT_OS
u32 reload;
#endif
SysTick->CTRL &= ~( 1<<2 ) ; //SYSTICK使用外部時鐘源
fac_us = SYSCLK/8 ; //fac_us都需要使用
#if SYSTEM_SUPPORT_OS
reload = SYSCLK/8 ; //每秒鐘的計數次數,單位為K
reload *= 1000000/delay_ostickspersec ; //根據delay_ostickspersec設定溢出時間
fac_ms = 1000/delay_ostickspersec ; //代表OS可以延時的最少單位
SysTick->CTRL |= 1<<1 ; //開啟SYSTICK中斷
SysTick->LOAD = reload ; //每1/delay_ostickspersec秒中斷一次
SysTick->CTRL |= 1<<0 ; //開啟SYSTICK
#else
fac_ms = ( u16 )fac_us*1000 ; //代表每個ms需要的systick時鐘數
#endif
}
首先要做根據UCOSII中定義的OS_TICKS_PER_SEC來計算出SysTick的裝載值reload,開啟SysTick中斷,將reload值寫進SysTick的LOAD寄存器中,最后開啟SysTick,開啟SysTick后還要編寫其中斷服務函數。
③微秒級別延時函數
void delay_us( u16 nus )
{
#if SYSTEM_SUPPORT_OS
u32 ticks, told, tnow, tcnt=0 ;
u32 reload = SysTick->LOAD ; //LOAD的值
ticks = nus*fac_us ; //需要的節拍數
OSSchedLock() ; //禁止調度,防止打斷us延時
told = SysTick->VAL ; //剛進入時的計數器值
while( 1 )
{
tnow = SysTick->VAL ;
if( tnow!=told )
{
//這里注意一下SYSTICK是一個遞減的計數器
if( tnow
④毫秒級別延時函數
void delay_ms( u16 nms )
{
#if SYSTEM_SUPPORT_OS
//如果OS已經在跑了,并且不是在中斷里面(中斷里面不能任務調度)
if( ( delay_osrunning==1 )&&( delay_osintnesting==0 ) )
{
//延時的時間大于OS的最少時間周期
if( nms>=fac_ms )
OSTimeDly( nms/fac_ms ) ; //UCOSII延時
nms %= fac_ms ; //延時太短,采用普通方式延時
}
delay_us( ( u32 )( nms*1000 ) ) ; //普通方式延時
#else
u32 temp ;
SysTick->LOAD = ( u32 )nms*fac_ms ; //時間加載(SysTick->LOAD為24bit)
SysTick->VAL = 0x00 ; //清空計數器
SysTick->CTRL = 0x01 ; //開始倒數
do
{
temp = SysTick->CTRL ;
}while( ( temp&0x01 )&&!( temp&( 1<<16 ) ) ) ; //等待時間到達
SysTick->CTRL = 0x00 ; //關閉計數器
SysTick->VAL = 0x00 ; //清空計數器
#endif
}
(5)usart1.c文件修改
①添加頭文件定義
#if SYSTEM_SUPPORT_OS
#include "includes.h"
#endif
②修改串口中斷服務函數
void USART1_IRQHandler()
{
#if SYSTEM_SUPPORT_OS
OSIntEnter() ;
#endif
//接收到數據
if( USART1->SR&( 1<<5 ) )
{
if( USART1->DR=='\\n' )
{
USART1_Data.Len = USART1_Rx_Count ;
USART1_Rx_Count = 0 ;
USART1_Data.State = 1 ;
}
USART1_Data.Buffer[ USART1_Rx_Count ] = USART1->DR ;
USART1_Rx_Count ++ ;
}
#if SYSTEM_SUPPORT_OS
OSIntExit() ;
#endif
}
23.3 實驗例程
例程:利用移植完成的ucos-ii系統新建兩個任務,并且在兩個任務中打印自定義的任務名稱。
#include "sys.h"
#include "delay.h"
#include "usart1.h"
#include "includes.h"
/****************************************************
Name :Task01
Function :任務1
Paramater :None
Return :None
****************************************************/
#define TASK01_PRIO 7 //設置任務優先級
#define TASK01_SIZE 64 //設置任務堆棧大小
OS_STK TASK01_STK[ TASK01_SIZE ] ; //任務堆棧
void Task01( void *pdata )
{
while( 1 )
{
printf( "Task1 Run\\r\\n" ) ;
delay_ms( 1000 ) ;
}
}
/****************************************************
Name :Task02
Function :任務2
Paramater :None
Return :None
****************************************************/
#define TASK02_PRIO 6 //設置任務優先級
#define TASK02_SIZE 64 //設置任務堆棧大小
OS_STK TASK02_STK[ TASK02_SIZE ] ; //任務堆棧
void Task02( void *pdata )
{
while( 1 )
{
printf( "Task2 Run\\r\\n" ) ;
delay_ms( 2000 ) ;
}
}
/****************************************************
Name :Start
Function :開始任務
Paramater :None
Return :None
****************************************************/
#define START_PRIO 10 //開始任務的優先級設置為最低
#define START_SIZE 64 //設置任務堆棧大小
OS_STK START_STK[ START_SIZE ] ; //任務堆棧
void Start( void *pdata )
{
OS_CPU_SR cpu_sr=0 ;
pdata = pdata ;
OS_ENTER_CRITICAL() ; //進入臨界區(無法被中斷打斷)
OSTaskCreate( Task01, ( void * )0, ( OS_STK* )&TASK01_STK[ TASK01_SIZE-1 ], TASK01_PRIO ) ;
OSTaskCreate( Task02, ( void * )0, ( OS_STK* )&TASK02_STK[ TASK02_SIZE-1 ], TASK02_PRIO ) ;
OSTaskSuspend( START_PRIO ) ; //掛起起始任務
OS_EXIT_CRITICAL() ; //退出臨界區(可以被中斷打斷)
}
/****************************************************
Name :Main
Function :主函數
Paramater :None
Return :None
****************************************************/
int main()
{
STM32_Clock_Init( 9 ) ; //系統時鐘設置
SysTick_Init( 72 ) ; //延時初始化
USART1_Init( 72, 115200 ) ; //串口初始化為115200
OSInit() ;
OSTaskCreate( Start, ( void * )0, ( OS_STK * )&START_STK[ START_SIZE-1 ], START_PRIO ) ; //創建起始任務
OSStart() ;
while( 1 ) ;
}
將程序下載進單片機,打開串口助手可以看到以下的效果。
通過時間可以看出,Task2的任務2s打印一次數據,Task1的任務1s打印一次數據,和我們程序所寫一致,所以說明UCOS-II系統移植成功。
-
操作系統
+關注
關注
37文章
6747瀏覽量
123201 -
計算機系統
+關注
關注
0文章
281瀏覽量
24089 -
軟件資源
+關注
關注
0文章
2瀏覽量
5633
發布評論請先 登錄
相關推薦
評論