ZYNQ上FreeRTOS实战:Vitis 2023.2从工程创建到调试全流程 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_SIZEFreeRTOS堆大小默认是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里又没有有效的bitstreamJTAG可能会被占用。把启动模式拨到JTAG模式再试。5.2 串口没有输出怎么排查串口没输出也是高频问题。排查思路可以按以下顺序来确认串口线接的是PS端的UART不是PL端的。ZYNQ开发板上通常有两个串口一个是PS UART一个是CP210x之类的USB转串口芯片。要接PS UART那个。确认串口调试助手的波特率是115200。Vitis的FreeRTOS模板默认用的是115200如果你改过BSP里的UART配置波特率可能不一样。确认XSA里使能了UART外设。如果Vivado里没使能UARTVitis里也不会有对应的驱动。在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配合一个高精度的计时器来实现。这个监控任务在项目后期调优的时候能帮你快速定位资源瓶颈。