uCOS是我個人熟悉和喜歡的操作系統,從最早的C51到後來的LPC2000和STM32,uCOS-II或uCOS-III都是我進行產品開發的首選的實時操作系統。但卻從未嘗試過在全可編程片上系統(APSoC)上使用過uCOS,這幾天心血來潮想來試試看。卻發現採用Zynq + uCOS的工程師並不多,網上雖然有一些介紹文章,但照着操作做後依然存在這樣那樣的問題。這篇博文將我使用Zynq + uCOS方案時遇到的問題羅列一下,供後來者參考。由於也是淺嘗而止,如果用這個方案開發產品,估計還會有大量的問題,以後遇到再逐漸補齊吧,也歡迎網友們在評論區指出和補充。
以下原創內容歡迎網友轉載,但請註明出處: https://www.cnblogs.com/helesheng
一、在SDK中配置Zynq-7000上的uCOS的開發環境
uCOS的開發商Micrium公司(現已被Silicon Labs公司收購)已經幫助大家將uCOS-II和uCOS-III移植到了常見的所有品牌處理器包括Zynq-7000上。大家可以到Silicon Labs公司的網站的相關連接(https://www.silabs.com/developers/micrium)上直接下載。但下載需要有Silicon Labs公司的賬號,不知爲什麼我用QQ郵箱和學校教工的郵箱都沒法收到Silicon Labs的確認郵件只好作罷。在GitHub上找到一個網友分享的鏈接:https://github.com/suisuisi/zynq_guide/tree/main/ucos,下載到了一個mirium的壓縮包ucos_v1_45.7z,在此對不知名的朋友表示感謝。
下載並解壓後得到如下圖所示的目錄結構。
在Zynq的軟件開發工具SDK中可以配置uCOS的開發環境,從而在SDK中新建應用時直接生成uCOS-II或uCOS-III的應用工程。具體方法是,在SDK的主菜單中單擊Xilinx->Repositories,在Local Repositories欄中導入剛纔解壓的uCOS開發包。但需要注意的是,指定的路徑必須到壓縮包中的../ucos/文件夾下,如下圖所示,否則SDK將找不到新建工程的模板。
二、建立一個驗證ucos實時操作系統的簡單的Zynq硬件系統
做一個ucos簡單的多任務演示系統,不同任務控制不同GPIO點亮不同的LED,方便觀察。我用的開發板是PYNQ-Z2,MIO沒有連接到足夠的LED,用EMIO來連接LED。具體步驟如下:
1、新建Vivado工程,創建Block Design,在Block Design中添加ZYNQ7 Processing System IP核。
2、配置ZYNQ7 Processing System IP核。我直接使用了PYNQ-Z2預置的配置文件pynq_revC.tcl。
加載預置的配置文件後,需要手動將官方配置文件沒有引出的EMIO引出2個。完成下圖所示的配置後,單擊OK在Block Design界面中就可以找到ZYNQ處理器模塊上的GPIO端子了。
3、爲EMIO添加輸出連接端口。並通過約束文件將這兩個EMIO連接到PYNQ-Z2上的LED上。
#LED_PS
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports {GPIOA_tri_io[0]}]
set_property -dict {PACKAGE_PIN M14 IOSTANDARD LVCMOS33} [get_ports {GPIOA_tri_io[1]}]
4、綜合併產生二進制配置文件,並將其導出。
三、在SDK中開發uCOS代碼
1、啓動SDK開發環境,選擇新建應用工程,此時彈出的新建工程嚮導如下圖所示。
和之前的新建應用工程最大的不同是OS Platform下拉菜單中多了一個ucos的選項。選擇ucos選項,並輸入工程名稱後單擊Next,進入新建工程模板選擇窗口。在下圖所示的工程模板選擇窗口中選擇的Micrium uC/OS-II Hello World或Micrium uC/OS-III Hello World工程模板。
2、修改ucos工程模板創建工程的標準輸入/輸出設備
不知什麼原因,Micrium的模板並未將標準的輸入輸出設備指定爲調試的uart0口。這將導致直接編譯運行該工程模板後無法看到輸出的Hello World。修改起來也非常容易:雙擊剛纔新建的應用工程的板級支持包(BSP)工程的配置文件system.mss(在左側導航窗口的bsp工程下)。在彈出的下圖界面中單擊Modify this BSP’s Setting。
在隨後彈出的配置窗口如下圖所示,在左側選擇ucos_standalone。在右側stdin/stdout(標準輸入/輸出)設備都選擇爲ps7_uart_0。
3、在工程模塊框架下編寫任務代碼
嵌入式系統中,實時操作系統所起到的最核心作用就是管理和分配系統中的各種資源,尤其是嵌入式系統最爲重要的資源:CPU的時間。uCOS以“任務”作爲CPU時間分配的基本對象。程序員在開發單個uCOS任務時,最重要的“心法”就是:認爲本任務獨佔CPU。
爲實現上述控制兩個獨立的LED採用不同頻率閃爍的目標,我設計採用兩個任務各自控制一個LED,它們各自按照自己的節奏延時和刷新EMIO狀態。兩個任務的代碼如下:
1 //控制LED0的亮滅. 2 void TaskLed(void *pdata) 3 { 4 while(1) 5 { 6 XGpioPs_WritePin(&psGpioInstancePtr, 54, 1);//EMIO的第0位輸出1 7 OSTimeDly(300); 8 XGpioPs_WritePin(&psGpioInstancePtr, 54, 0);//EMIO的第0位輸出0 9 OSTimeDly(300); 10 } 11 } 12 //控制LED1的亮滅. 13 void TaskLed1(void *pdata) 14 { 15 while(1) 16 { 17 XGpioPs_WritePin(&psGpioInstancePtr, 55, 1);//EMIO的第1位輸出1 18 OSTimeDly(200); 19 XGpioPs_WritePin(&psGpioInstancePtr, 55, 0);//EMIO的第1位輸出0 20 OSTimeDly(200); 21 } 22 }
上面代碼中的關鍵是OSTimeDly();函數,它是uCOS提供的系統函數。該函數“告訴”操作系統:當前任務要延時固定的時鐘節拍(Tick)時間,可以在這段時間內將CPU讓給其他任務來使用,延遲節拍數到達後本任務再通過“競爭上崗”繼續運行。和OSTimeDly();類似的系統函數還有OSTimeDlyHMSM();它們的區別在於OSTimeDly();參數的單位是時鐘節拍數(Ticks),而OSTimeDlyHMSM();的單位則是時、分、秒。這裏兩個任務分別按照200個Ticks和300個Ticks的間隔切換LED的亮滅狀態。
接下來查看一下每個Ticks的時長:打開本工程的板級支持包(bsp)工程xxx_bsp下include文件夾下的os_cfg.h,並在其中搜索宏OS_TICKS_PER_SEC,它定義了每秒鐘內時鐘節拍的數量。缺省情況下,這個宏被配置爲1000,即每個Tick的時長爲1ms。兩個任務中的延遲也就分別是200ms和300ms。個人覺得,這個時間片長度對於運行速度爲650MHz的Cortex-A9內核還是有點偏長,實際應用中可以考慮適當增加OS_TICKS_PER_SEC的數量。
4、編寫任務配套的初始化代碼
和其他所有uCOS開發一樣,我們也需要爲每個任務完成設置優先級、分配堆棧和創建任務等工作。代碼如下所示:(注意:這些代碼有問題,關於解決和改正的辦法,將在本博文後續部分介紹)
1 //設置任務優先級 2 #define LED_TASK_Prio 3 3 #define LED1_TASK_Prio 5 4 //設置任務堆棧大小 5 #define LED_STK_SIZE 64 6 #define LED1_STK_SIZE 64 7 //任務堆棧 8 OS_STK TASK_LED1_STK[LED_STK_SIZE]; 9 OS_STK TASK_LED_STK[LED1_STK_SIZE];
在main函數中補充EMIO初始化的代碼:
1 XGpioPs_Config* GpioConfigPtr; 2 int xStatus; 3 //EMIO的初始化 4 GpioConfigPtr = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID); 5 if(GpioConfigPtr == NULL) 6 return XST_FAILURE; 7 xStatus = XGpioPs_CfgInitialize(&psGpioInstancePtr,GpioConfigPtr, GpioConfigPtr->BaseAddr); 8 //EMIO的輸入輸出設置 9 XGpioPs_SetDirectionPin(&psGpioInstancePtr, 54,1); 10 XGpioPs_SetDirectionPin(&psGpioInstancePtr, 55,1); 11 //使能EMIO輸出 12 XGpioPs_SetOutputEnablePin(&psGpioInstancePtr, 54,1); 13 XGpioPs_SetOutputEnablePin(&psGpioInstancePtr, 55,1);
隨後還可看啓動操作系統的函數:
1 UCOSStartup(MainTask);
在MainTask任務中添加兩個LED任務的啓動函數:
1 //初始化任務 2 OSTaskCreate(TaskLed, (void * )0, (OS_STK *)&TASK_LED_STK[LED_STK_SIZE-1], LED_TASK_Prio); 3 OSTaskCreate(TaskLed1, (void * )0, (OS_STK *)&TASK_LED1_STK[LED1_STK_SIZE-1], LED1_TASK_Prio);
另外在MainTask中保留不斷輸出任務的代碼,起作用是每隔1秒中輸出一遍啓動時間。
1 int i=0; 2 while (DEF_TRUE) { 3 OSTimeDlyHMSM(0, 0, 1, 0); 4 printf("%d seconds from main task start.\r\n",i); 5 i++; 6 };
四、在調試中解決的幾個問題
在SDK中編譯代碼後將FPGA的配置文件和PS的程序下載到Zynq器件中,運行程序後會發下TaskLed1任務無法正常運行,返回代碼中查找問題。
1、任務優先級重新分配
由於TaskLed1任務無法正常運行,我首先想到的是任務優先級分配的問題。首先查看工程xxx_bsp下include文件夾下的os_cfg.h中的宏OS_LOWEST_PRIO,發現該宏定義的最低優先級爲63。我自己編寫代碼中定義的優先級LED_TASK_Prio和LED1_TASK_Prio分別爲3和5,遠遠沒有達到這個極限,說明問題不在這裏。
隨後檢查模板中定義的唯一一個任務MainTask的優先級,進入模板中啓動這個任務的函數UCOSStartup(MainTask);。其中創建該任務的函數中定義的優先級爲UCOS_START_TASK_PRIO,其值居然爲5!也就是說我隨意定義的任務優先級LED1_TASK_Prio居然和模板中使用的唯一一個優先級是重複的——這就是TaskLed1任務無法正常運行的原因。
2、任務堆棧的修改
不幸的是修改TaskLed1任務的優先級數後,發現這兩個任務居然都不能正常運行了。重新對照模板中提供的MainTask代碼,來檢查自己編寫的代碼,發現這個任務的堆棧深度居然爲784,遠大於我分配給TaskLed和TaskLed1兩個任務的64!
我嘗試將TaskLed和TaskLed1兩個任務的堆棧分配爲1024後,代碼如下:
1 //設置任務優先級 2 #define LED_TASK_Prio 3 3 #define LED1_TASK_Prio 4 4 //設置任務堆棧大小 5 #define LED_STK_SIZE 1024 6 #define LED1_STK_SIZE 1024 7 //任務堆棧 8 OS_STK TASK_LED1_STK[LED_STK_SIZE]; 9 OS_STK TASK_LED_STK[LED1_STK_SIZE];
修改後的代碼就正常了。分析原因,應該是EMIO操作函數XGpioPs_WritePin(&psGpioInstancePtr, 54, 1);佔用的內存較多。但幸運的是,Zynq系統使用DDR內存系統往往達到100MB以上甚至數GB,爲每個任務分配數KB的堆棧應該問題不大。