在Zynq平台上使用uCOS [原创www.cnblogs.com/helesheng]

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);
EMIO初始化代码

随后还可看启动操作系统的函数:

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 };
MainTask输出的代码

四、在调试中解决的几个问题

在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的堆栈应该问题不大。 

posted @ 2023-05-04 15:52  helesheng  阅读(498)  评论(0编辑  收藏  举报