☰
ZYNQ上FreeRTOS实战:Vitis 2023.2从工程创建到调试全流程
2026/9/25 5:46:28 网站建设 项目流程

1. 为什么ZYNQ上跑FreeRTOS值得单独拿出来说

ZYNQ这颗芯片有意思的地方在于它把ARM Cortex-A9硬核和FPGA可编程逻辑塞到了一起。很多人拿到ZYNQ开发板之后,第一反应是跑裸机程序点个灯,再进一步就是跑Linux。但实际做工业控制、电机驱动、多轴运动控制这类场景的时候,Linux的实时性往往不够看,裸机又很难管理多个并发任务。这时候FreeRTOS就成了一个非常务实的选择——它足够轻量,调度确定性好,中断延迟可控,而且Xilinx的Vitis工具链对它的支持已经相当成熟。

我这次要聊的是在Vitis 2023.2下面,从零开始创建一个ZYNQ的FreeRTOS工程,一直到下载调试、串口输出、任务运行验证的完整流程。整个过程我会把踩过的坑、工具链的脾气、以及一些不太容易在官方文档里找到的细节都摊开来讲。不管你是刚接触ZYNQ的新手,还是从STM32那边转过来想试试FreeRTOS的老手,这篇内容应该都能让你少走一些弯路。

Vitis 2023.2这个版本相比早期的SDK有了不小的变化,很多操作逻辑和界面布局都重新设计了。网上不少教程还是基于2018、2019版本的SDK来写的,直接照着做会在Vitis里找不到对应的菜单。所以我会尽量把Vitis 2023.2的实际操作路径讲清楚,包括创建平台工程、配置BSP、添加FreeRTOS组件、写任务代码、设置调试配置这些环节。

2. 开发环境搭建与工程创建前的准备

2.1 Vitis 2023.2的安装要点

Vitis 2023.2的安装包体积不小,完整安装大概需要100GB以上的磁盘空间。如果你只做ZYNQ 7000系列的裸机和FreeRTOS开发,安装的时候可以只勾选Zynq-7000相关的器件支持,这样能省下不少空间。安装过程中有一个容易忽略的点:Vitis 2023.2默认会同时安装Vivado,如果你只需要Vitis,可以在安装器里取消Vivado的勾选,但要注意某些平台工程的创建仍然依赖Vivado生成的XSA文件。

安装完成之后,建议先确认一下环境变量是否配置正确。在Windows下,Vitis会往系统PATH里添加一些工具路径,但有时候安装器不会自动做这件事。你可以打开命令行,输入xsct看看能不能找到这个命令。如果提示找不到,需要手动把Vitis安装目录下的bin文件夹加到PATH里。

注意:Vitis 2023.2对Windows版本有要求,Windows 10需要是较新的补丁版本,Windows 11基本没问题。如果你在虚拟机里跑,内存建议至少16GB,否则综合和编译的时候会非常卡。

2.2 获取XSA硬件描述文件

在Vitis里创建平台工程之前,你需要先有一个XSA文件。这个文件是Vivado导出的硬件描述,里面包含了ZYNQ的PS端配置信息,比如DDR型号、时钟频率、外设使能情况、MIO分配等等。如果你手里只有开发板而没有现成的XSA,需要先在Vivado里创建一个Block Design,配置好ZYNQ Processing System,然后导出XSA。

对于常见的ZYNQ 7020开发板,PS端的配置一般包括:DDR3容量1GB、UART1接MIO48/49作为串口调试口、SD卡接MIO40-45、以太网接MIO16-27、QSPI Flash接MIO1-6。这些配置在Vivado的ZYNQ PS配置界面里都能找到对应的选项。配置完成之后,Generate Output Products,然后Export Hardware,记得勾选Include bitstream。

提示:如果你暂时没有硬件板子,也可以用Vivado创建一个最小系统的XSA,只使能UART和DDR,这样也能在Vitis里跑FreeRTOS工程,只是没法下载到实际硬件上验证。

2.3 创建工作空间的注意事项

Vitis的工作空间路径尽量不要包含中文和空格,这是很多Xilinx工具的通病。我一般会在D盘或者E盘根目录下建一个vitis_ws文件夹作为工作空间。另外,工作空间所在的磁盘最好有足够的剩余空间,因为编译过程中会产生大量的中间文件,一个中等规模的FreeRTOS工程编译下来,临时文件可能占到几个GB。

创建工程的时候,Vitis 2023.2会让你选择工程类型。这里要选Platform Project,先创建平台工程,然后再基于平台创建Application Project。这个流程和早期的SDK不太一样,SDK里可以直接创建应用工程然后关联硬件平台,Vitis里必须先把平台工程建好。

3. FreeRTOS工程的核心配置与BSP定制

3.1 平台工程创建与硬件信息导入

打开Vitis 2023.2之后,选择File -> New -> Platform Project。给平台工程起个名字,比如zynq_freertos_platform。在下一步里,选择Create a new platform from hardware (XSA),然后浏览到你之前导出的XSA文件。Vitis会自动解析XSA里的硬件信息,包括PS端的配置和外设列表。

解析完成之后,你会看到一个硬件概览界面,里面列出了CPU核、外设、内存映射等信息。这里要确认一下UART外设是否已经使能,因为后面FreeRTOS的串口输出要靠它。如果XSA里没有使能UART,需要回到Vivado重新配置再导出。

平台工程创建好之后,右键点击平台工程,选择Build Project。这一步会生成BSP相关的文件,包括硬件描述的头文件和链接脚本。编译完成之后,平台工程就可以被应用工程引用了。

3.2 应用工程创建与FreeRTOS模板选择

接下来创建应用工程。File -> New -> Application Project。选择刚才创建的平台工程作为硬件平台。在工程类型选择界面,Vitis 2023.2会列出几个模板,包括Empty Application (C)、Hello World、FreeRTOS Hello World、FreeRTOS Tickless Demo等。

对于第一次接触ZYNQ FreeRTOS的开发者,我建议先选FreeRTOS Hello World模板。这个模板会自动帮你把FreeRTOS的源码、配置文件、以及一个简单的任务创建代码都准备好。你可以先编译下载运行,确认整个工具链和硬件都没问题,然后再基于这个模板修改成自己的业务代码。

如果你选的是Empty Application,那么需要手动在BSP里使能FreeRTOS。具体操作是:右键应用工程 -> Board Support Package Settings -> 在Overview里找到freertos10_xilinx,把它勾选上。然后BSP会重新生成,FreeRTOS的源码会被添加到BSP工程里。

3.3 BSP关键参数配置

FreeRTOS在ZYNQ上运行,有几个BSP参数需要特别关注。打开BSP Settings之后,找到freertos10_xilinx这一项,展开后可以看到以下关键配置:

  • configTICK_RATE_HZ:系统节拍频率,默认是100Hz,也就是每10ms一个tick。对于大多数控制任务来说,100Hz够用了。如果你需要更精细的时间控制,可以调到1000Hz,但要注意tick中断的开销会相应增加。
  • configTOTAL_HEAP_SIZE:FreeRTOS堆大小,默认是65536字节。如果你创建的任务比较多,或者任务里用了较大的局部变量,这个值需要调大。ZYNQ 7020有1GB DDR,堆开到几MB完全没问题。
  • configUSE_PREEMPTION:是否使用抢占式调度,默认是1。除非你有特殊的调度需求,否则保持默认即可。
  • configUSE_TIME_SLICING:同优先级任务是否按时间片轮转,默认是1。
  • configMAX_PRIORITIES:最大优先级数量,默认是8。如果你的任务优先级层次比较多,可以适当调大。
  • configUSE_MUTEXES:是否使用互斥量,默认是1。
  • configUSE_COUNTING_SEMAPHORES:是否使用计数信号量,默认是1。

这些参数在FreeRTOSConfig.h文件里也能改,但通过BSP Settings改的好处是,Vitis会自动帮你生成对应的宏定义,不容易出错。改完BSP参数之后,记得重新Build平台工程和应用工程。

实操心得:configTOTAL_HEAP_SIZE这个值我建议一开始就设大一点,比如256KB。因为FreeRTOS的堆是在BSP的链接脚本里分配的,如果后期发现不够用再改,需要重新编译整个BSP和所有依赖的工程,比较费时间。

4. FreeRTOS任务代码编写与调试实操

4.1 任务创建与串口输出的完整代码

FreeRTOS Hello World模板生成的代码里,已经包含了一个简单的任务创建示例。但那个示例比较简单,我把它扩展一下,加上串口输出和两个不同优先级的任务,方便观察调度行为。

#include <stdio.h> #include "xparameters.h" #include "xil_printf.h" #include "FreeRTOS.h" #include "task.h" #define TASK1_PRIORITY (tskIDLE_PRIORITY + 2) #define TASK2_PRIORITY (tskIDLE_PRIORITY + 1) #define TASK_STACK_SIZE (configMINIMAL_STACK_SIZE * 2) static void vTask1(void *pvParameters) { (void)pvParameters; while (1) { xil_printf("Task1 is running, tick count: %lu\r\n", xTaskGetTickCount()); vTaskDelay(pdMS_TO_TICKS(1000)); } } static void vTask2(void *pvParameters) { (void)pvParameters; while (1) { xil_printf("Task2 is running, tick count: %lu\r\n", xTaskGetTickCount()); vTaskDelay(pdMS_TO_TICKS(500)); } } int main(void) { xil_printf("FreeRTOS on ZYNQ started.\r\n"); xTaskCreate(vTask1, "Task1", TASK_STACK_SIZE, NULL, TASK1_PRIORITY, NULL); xTaskCreate(vTask2, "Task2", TASK_STACK_SIZE, NULL, TASK2_PRIORITY, NULL); vTaskStartScheduler(); while (1) { // 正常情况下不会执行到这里 } return 0; }

这段代码里,Task1的优先级比Task2高,Task1每1000ms打印一次,Task2每500ms打印一次。因为Task1优先级更高,所以每次Task1从阻塞中恢复时,会抢占Task2。实际串口输出的顺序会反映出这个调度行为。

4.2 编译配置与链接脚本调整

在Vitis里编译FreeRTOS工程,需要注意几个地方。首先,确保应用工程的C/C++ Build Settings里,Include路径包含了BSP的include目录。这个一般Vitis会自动配置好,但如果你手动改过工程结构,可能需要检查一下。

其次,链接脚本里的堆栈配置。FreeRTOS有自己的堆管理,但main函数运行之前,系统还是需要一个初始的栈。这个栈的大小在链接脚本里定义,一般是_STACK_SIZE。如果FreeRTOS的任务栈开得比较大,而链接脚本里的栈太小,可能会在启动调度器之前就出问题。

另外,ZYNQ的DDR地址映射要注意。FreeRTOS的代码和数据默认是链接到DDR里的,地址从0x00100000开始。如果你在Vivado里配置的DDR起始地址不是这个,需要在链接脚本里对应修改。

注意:Vitis 2023.2在编译FreeRTOS工程时,有时候会报undefined reference to _exit之类的错误。这是因为FreeRTOS的某些移植层需要实现_exit、_sbrk等系统调用。解决办法是在工程里添加一个syscalls.c文件,或者直接在BSP里使能enable_printf相关的选项。

4.3 下载调试与串口验证

编译通过之后,就可以下载到板子上验证了。把ZYNQ开发板的JTAG口和下载器连好,UART口和电脑连好。在Vitis里右键应用工程,选择Run As -> Launch Hardware。Vitis会自动完成以下步骤:配置FPGA、下载ELF文件到DDR、设置PC指针到main函数入口、启动CPU。

下载完成之后,打开串口调试助手,选择对应的COM口,波特率设成115200,数据位8,停止位1,无校验。按一下开发板上的复位键,你应该能在串口助手里看到类似下面的输出:

FreeRTOS on ZYNQ started. Task1 is running, tick count: 0 Task2 is running, tick count: 0 Task2 is running, tick count: 500 Task1 is running, tick count: 1000 Task2 is running, tick count: 1000 Task2 is running, tick count: 1500 Task1 is running, tick count: 2000 ...

从输出里可以看到,Task1和Task2的打印顺序和tick count完全符合优先级调度的预期。Task1每1000个tick打印一次,Task2每500个tick打印一次。当两个任务同时就绪时,Task1先执行。

4.4 调试模式下查看任务状态

Vitis 2023.2的调试器支持FreeRTOS的任务感知调试。在Debug模式下,打开Debug视图,可以看到一个FreeRTOS Task List的窗口,里面列出了当前所有任务的名称、优先级、状态、栈使用情况等信息。这个功能对于排查任务栈溢出、优先级反转等问题非常有用。

要启用这个功能,需要在调试配置里勾选Enable FreeRTOS Task Aware Debugging。然后重新启动调试会话,就能在Debug视图里看到任务列表了。如果某个任务的栈使用量接近100%,说明栈开小了,需要调大TASK_STACK_SIZE。

实操心得:FreeRTOS的栈溢出检测有两种模式,configCHECK_FOR_STACK_OVERFLOW设为1或2。设为1只检查栈指针是否越界,速度较快;设为2还会在栈末尾填充魔术字,检查魔术字是否被覆盖,更可靠但稍慢。我一般设为2,配合任务感知调试,基本不会出现栈溢出导致的花式崩溃。

5. 常见问题排查与避坑指南

5.1 下载时不识别芯片怎么办

这是Vitis用户遇到频率最高的问题之一。现象是点击下载按钮之后,Vitis提示找不到目标芯片,或者JTAG链扫描不到设备。这个问题通常有几个原因:

第一,下载器驱动没装好。在Windows设备管理器里看一下,如果下载器对应的设备有黄色感叹号,说明驱动有问题。需要手动安装Vitis安装目录下的驱动,路径一般在Vitis\2023.2\data\xicom\cable_drivers\nt64下面。

第二,开发板没有上电,或者JTAG线没插紧。这个听起来很基础,但确实有很多人栽在这上面。确认开发板的电源指示灯亮着,JTAG排线方向正确。

第三,Vivado或Vitis的hw_server进程卡死了。打开任务管理器,把所有hw_server相关的进程结束掉,然后重新启动Vitis。

第四,ZYNQ的启动模式设置不对。如果开发板的启动模式拨码开关设成了QSPI启动,而Flash里又没有有效的bitstream,JTAG可能会被占用。把启动模式拨到JTAG模式再试。

5.2 串口没有输出怎么排查

串口没输出也是高频问题。排查思路可以按以下顺序来:

  • 确认串口线接的是PS端的UART,不是PL端的。ZYNQ开发板上通常有两个串口,一个是PS UART,一个是CP210x之类的USB转串口芯片。要接PS UART那个。
  • 确认串口调试助手的波特率是115200。Vitis的FreeRTOS模板默认用的是115200,如果你改过BSP里的UART配置,波特率可能不一样。
  • 确认XSA里使能了UART外设。如果Vivado里没使能UART,Vitis里也不会有对应的驱动。
  • 在main函数最开始加一句xil_printf("test\r\n"),确认程序确实跑到了main。如果这句都没有输出,说明程序可能卡在启动阶段了。

5.3 FreeRTOS任务跑不起来的原因

有时候工程编译下载都没问题,但任务就是跑不起来,串口只打印了启动信息就没了。这种情况一般是vTaskStartScheduler()没有成功启动调度器。可能的原因包括:

  • 堆空间不足,xTaskCreate返回了errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY。检查configTOTAL_HEAP_SIZE是否够大。
  • 中断向量表配置错误。FreeRTOS需要把SysTick和SVC中断指向自己的处理函数。如果BSP里的中断配置被改过,可能导致调度器启动失败。
  • 栈溢出。如果main函数的栈太小,在调用vTaskStartScheduler()之前就溢出了,程序会跑飞。

5.4 常见问题速查表

问题现象可能原因排查方法
下载时提示找不到芯片驱动未安装、JTAG线松动、hw_server卡死检查设备管理器、重新插拔JTAG、结束hw_server进程
串口无输出接错串口、波特率不对、UART未使能确认接PS UART、检查波特率115200、检查XSA配置
任务不运行堆不足、中断向量错误、栈溢出调大configTOTAL_HEAP_SIZE、检查BSP中断配置、启用栈溢出检测
编译报错undefined reference缺少系统调用实现添加syscalls.c或使能BSP的printf选项
调试时看不到任务列表未启用Task Aware Debugging在调试配置里勾选对应选项

避坑技巧:如果你在Vitis里同时打开了多个工程,编译的时候可能会因为依赖关系混乱而报一些莫名其妙的错误。我一般会先Clean所有工程,然后按平台工程 -> BSP工程 -> 应用工程的顺序依次Build,这样基本不会出问题。

6. 从FreeRTOS工程延伸到实际项目的一些经验

FreeRTOS在ZYNQ上跑通之后,下一步通常是要和PL端的逻辑打交道。比如通过AXI GPIO读取外部信号,通过AXI DMA搬运数据,或者通过中断响应PL端的事件。这些操作在FreeRTOS环境下和在裸机环境下有一些区别,主要是中断优先级和任务优先级的配合问题。

ZYNQ的中断控制器(GIC)支持中断优先级分组,FreeRTOS的configMAX_API_CALL_INTERRUPT_PRIORITY决定了哪些中断可以调用FreeRTOS的API。如果PL端的中断优先级设得比这个值还高,那么在中断服务函数里调用xSemaphoreGiveFromISR之类的API就会出问题。我一般会把PL端的中断优先级设在中等偏下的位置,确保不会干扰FreeRTOS的调度。

另外,FreeRTOS的堆管理策略也值得根据项目特点选一下。默认的heap_4支持内存释放和碎片合并,适合动态创建删除任务的场景。如果你的任务都是静态创建的,用heap_1更简单也更安全。在BSP Settings里可以切换heap的实现方式。

最后说一个实际调试中很有用的技巧:在FreeRTOS里加一个低优先级的监控任务,定期打印每个任务的栈使用量和CPU占用率。栈使用量可以通过uxTaskGetStackHighWaterMark获取,CPU占用率可以用vTaskGetRunTimeStats配合一个高精度的计时器来实现。这个监控任务在项目后期调优的时候能帮你快速定位资源瓶颈。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询