尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MicroBlaze Bootloader完全指南:从原理到上电自启

MicroBlaze Bootloader完全指南:从原理到上电自启 第一次用MicroBlaze做板上软件时我差点被一个现象搞自闭在Vivado里生成比特流烧进去再用Vitis把程序下载到板子上功能测试一切正常但只要把电源断掉再上电程序就彻底“失忆”串口一个字符都不吐。相信不少刚接触FPGA的兄弟也被这个问题折磨过。这事儿的根子在于MicroBlaze是个软核处理器它本身没有片内Flash程序存到哪、上电后从哪取第一条指令全部要靠外部电路和引导代码来定。换句话说没有Bootloader的MicroBlaze就像一个刚睡醒就失忆的人必须有人把他拽回正轨。这篇文章我按Vivado 2021.1的实际操作顺序把MicroBlaze Bootloader从原理、Block Design搭建、Vitis工程编写到Flash固化、MCS生成和常见故障排查完整讲一遍。内容偏实战适合刚入门FPGA嵌入式开发、或者已经能把Hello World跑起来但不知道“怎么固化”的新手也适合被启动异常卡住好几天的兄弟对照排查。1. 为什么MicroBlaze必须有一个Bootloader软核的“失忆”问题1.1 软核处理器与MCU的存储区别用惯了STM32这类MCU的人很容易默认“程序下载进去就能上电自启”。MCU内部有Flash复位后CPU直接从固定地址读代码这是硬件决定好的。MicroBlaze完全不同它是由FPGA查找表和BRAM临时“拼”出来的处理器比特流bitstream下载进去之后FPGA里才出现一个CPU但CPU拿来当内存的BRAM是易失的断电即清空CPU自身也没有任何非易失程序区。所以在MicroBlaze上至少有两份“文件”要区分清楚一份是描述硬件电路用的比特流烧进FPGA配置Flash另一份是MicroBlaze要执行的程序也就是elf或bin。新手阶段用JTAG下载elf本质上是把程序临时塞进BRAM或DDR调试期间能用一断电就没了。要让板子上电后自动跑业务必须有一个“引导者”先把程序从外部Flash读进RAM再跳转过去这个引导者就是Bootloader。1.2 Bootloader解决的第一个问题上电后程序从哪来MicroBlaze复位后从哪个地址取指是在Vivado里配置的绝大多数系统把复位向量Reset Vector设为0x00000000。问题来了0x00000000这个地址上如果挂的是BRAM那么上电瞬间BRAM里必须已经有一段可执行代码这段代码不可能凭空出现只能通过比特流在烧写的时候一起初始化进去。也就是说我们平时生成的普通比特流只包含“硬件电路”而包含Bootloader的比特流还要把Bootloader的机器码作为BRAM初值一起固化进去。这就是MicroBlaze Bootloader最核心的链路上电 - FPGA完成配置 - BRAM里已经有Bootloader - CPU从0x0执行Bootloader - Bootloader初始化DDR和Flash控制器 - 从外部Flash读取应用镜像 - 拷贝到内存 - 跳转到应用入口。每一步都缺一不可。1.3 启动介质与Flash分区规划外部非易失存储最常用的是板载QSPI Flash因为它既可以存FPGA配置比特流也可以存MicroBlaze的应用镜像Vivado和Vitis对它的支持也最完善。有些板卡用SD卡或者eMMC原理一样只是驱动和速度不同。QSPI Flash规划分区时必须提前想清楚不能随手乱写。我的习惯是单独划一个区域给版本信息和App镜像区域Flash地址范围示例用途00x000000 - 0x1FFFFFFPGA配置比特流由Vivado烧写10x200000 - 0x5FFFFFMicroBlaze应用镜像App.bin20x600000 - 0x6FFFFF版本号、CRC、升级标志等元数据30x700000 - 0x7FFFFF备份镜像用于FOTA失败回滚注意QSPI Flash擦除是按扇区来的常见扇区大小是4KB或64KBApp镜像写入时会发生“整扇区擦除”如果元数据区和App区挨得太近擦除时可能被误伤。规划分区时建议在关键边界上留出至少一个扇区的余量。2. Vivado 2021.1中的工程搭建与关键参数这一步错了后面全白搭2.1 安装与License、版本匹配的坑Vivado 2021.1对应的开发套件已经集成Vitis从这一代开始传统SDK被Vitis取代。很多人装了Vivado之后发现没有Vitis多半是安装时没有勾选Vitis组件需要重新运行安装程序补装。安装路径不要出现中文和空格否则后边综合、仿真和Vitis工程经常出莫名其妙的问题这是老生常谈但每次都会有人踩。License方面MicroBlaze和大部分常用IPAXI UARTlite、AXI Quad SPI、BRAM Controller等在WebPACK免费版本里就能用不需要破解或者折腾奇怪的证书。只要Vivado能正常安装并加载LicenseBlock Design里添加MicroBlaze一般不会弹授权错误。如果提示某些IP缺少License优先检查License版本和Vivado版本是否匹配而不是急着找其他途径。2.2 Block Design最小化系统设计打开Vivado 2021.1新建工程选择目标FPGA型号然后创建Block Design。一个能跑Bootloader的最小系统至少需要这几个IPMicroBlaze处理器核性能_level默认或选合适档位Bootloader对性能不敏感选最低档够用Clocking Wizard把板载时钟转成MicroBlaze和AXI总线的时钟Processor System Reset用来同步复位和锁定时钟非常重要LMB BRAM Controller Block Memory Generator挂在MicroBlaze本地存储器总线上作为启动用的BRAMAXI UARTlite串口打印调试信息强烈建议必须有AXI Quad SPI读写QSPI Flash用如果应用比较大还要加DDR控制器MIG或者一块大容量的BRAM。加完IP后Run Block Automation和Run Connection Automation让Vivado自动连线。很多人偷懒直接全自动结果MicroBlaze的本地存储器总线没连上BRAM复位向量指向0x0但0x0上没有任何存储上电直接跑飞这种问题后面查起来很费劲。2.3 地址分配与MicroBlaze向量配置双击MicroBlaze核在配置界面里有几个参数必须认真看Reset Vector和Exception Vector。默认Reset Vector是0x00000000Exception Vector一般在0x00000010附近。如果BRAM挂在0x0那么CPU复位后自然从BRAM启动这正是Bootloader要呆的地方。Exception Vector默认也在0x0附近后面讲中断时会说这个位置和Bootloader的冲突问题这里先不展开。在Address Editor里我建议的地址规划是这样的外设/存储地址范围说明LMB BRAM0x00000000 - 0x00007FFF32KB放BootloaderAXI Quad SPI0x40000000 - 0x4000FFFFFlash控制器寄存器AXI UARTlite0x40600000 - 0x4060FFFF串口寄存器DDR如果用MIG0x80000000 起始放App和运行数据这里有个新手容易犯的错把BRAM放在0x0的同时又把AXI外设也放在0x0附近Vivado地址编辑器里会有冲突提示。Bootloader运行区必须独占低地址段而且BRAM容量不能只图大因为BRAM同时承担着Bootloader代码和初始数据选个16KB到32KB就够用太大反而挤占Block RAM资源。2.4 生成Bitstream并导出XSABlock Design里点Validate Design通过后先Generate Output Products再跑Implementation最后Generate Bitstream。这个过程在Vivado 2021.1里很成熟只要工程配置没错基本一次过。如果implement design过程中报错变红优先看时序约束和时钟是否真的锁定尤其检查Clocking Wizard输入的板载时钟频率是否和开发板一致。比特流生成后File - Export Hardware勾选Include bitstream导出XSA文件。这个XSA是后续Vitis工程的基础里面包含了硬件描述、比特流、MicroBlaze配置和内存映射信息。一个非常重要的文件是.mmi文件它记录了哪个BRAM对应elf的哪个段位置一般在工程目录下的project.runs/impl_1/design_1_wrapper.mmi。后面把Bootloader注入比特流时必须用到这个文件建议先把它保存好。3. Bootloader启动链路拆解从Flash读取到跳转的全过程3.1 一个极简的Bootloader工作流程很多人一听到Bootloader就想到U-Boot那种庞然大物其实MicroBlaze的Bootloader可以非常精简核心就四件事初始化必要外设QSPI控制器、DDR、UART从Flash的约定偏移地址读取应用镜像把镜像拷贝到App的链接地址比如DDR跳转过去让App接管CPU如果加上工程化一点的设计还可以在镜像头部放一个结构体包含魔数Magic Number、镜像长度、CRC校验值、版本号。Bootloader先读头部校验通过再搬移。这个设计非常值得做因为没有校验的裸搬运Flash数据错一位就是随机跑飞排查起来极其痛苦。我的建议版Bootloader流程如下上电复位 - 串口打印 Bootloader v1.0 - 初始化QSPI Flash - 读取Flash偏移0x600000处的镜像头部魔数/长度/CRC - 校验头部和镜像CRC - 如果错误串口打印错误码停在while(1) - 如果通过把镜像从Flash搬到0x80000000 - 刷新/关闭Cache - 打印 Jump to App at 0x80000000 - 通过函数指针跳转3.2 核心API与内存拷贝为什么BRAM里的Bootloader不能“太胖”在Vitis 2021.1的standalone BSP里Flash读写最常见的是xilisf库核心函数类似XilSpiFlash_Init、XilSpiFlash_Read、XilSpiFlash_Write、XilSpiFlash_Erase。不同BSP版本可能名称有差异如果你板子上的BSP中没有这些函数就用AXI Quad SPI的裸驱动XSpi完成同样的事情重点是理解流程而不是背API。代码骨架大致是这样#include xil_printf.h #include xil_cache.h #include xilisf.h /* 根据BSP实际库调整头文件 */ #define APP_FLASH_OFFSET 0x200000 #define APP_HEADER_OFFSET 0x600000 #define APP_DEST_ADDR 0x80000000 #define APP_MAX_SIZE 0x400000 typedef struct { u32 magic; /* 魔数比如0xA5A5A5A5 */ u32 length; /* 镜像长度 */ u32 crc; /* 镜像CRC */ u32 version; /* 版本号 */ } app_header_t; int bootloader_main(void) { app_header_t header; xil_printf(Bootloader v1.0\r\n); /* 初始化QSPI和DDR具体调用以BSP为准 */ if (XilSpiFlash_Init() ! XST_SUCCESS) { xil_printf([FATAL] SPI Flash init failed\r\n); while(1); } /* 读镜像头部 */ XilSpiFlash_Read(APP_HEADER_OFFSET, header, sizeof(header)); if (header.magic ! 0xA5A5A5A5) { xil_printf([ERROR] invalid magic: 0x%08X\r\n, header.magic); while(1); } /* 读应用镜像到DDR */ XilSpiFlash_Read(APP_FLASH_OFFSET, (u8*)APP_DEST_ADDR, header.length); /* 跳转前刷新Cache */ Xil_DCacheDisable(); void (*app_entry)(void) (void (*)(void))APP_DEST_ADDR; xil_printf(Jump to App at 0x%08X\r\n, APP_DEST_ADDR); app_entry(); return 0; /* 正常不会回到这里 */ }代码里有个容易被忽略的点Xil_DCacheDisable()。如果MicroBlaze开启了数据CacheDDR里的数据被CPU读过之后会留在Cache里Flash读操作把新数据写入DDR后Cache里可能还是旧数据跳转过去执行的就是乱码指令。所以要么禁用Cache要么严格按照Xil_DCacheFlush一类的API在写入后刷新。因为Bootloader是活在BRAM里的BRAM资源非常有限代码切忌堆大功能。printf重定向、浮点打印、文件系统统统不要碰。串口打印只用xil_printf它比标准printf精简得多。3.3 跳转前的三件套Cache、中断、外设状态我见过太多人栽在跳转这一步明明Flash里的镜像读出来了CRC也对了一跳转就死。大部分原因不是跳转本身而是跳转前的系统状态没有收拾干净。第一件事是Cache。前面已经说了数据Cache要刷新或禁用指令Cache也要视情况处理。很多版本的BSP默认不使能指令Cache但如果开启了App入口的前几条指令可能被Cache命中到旧内容。第二件事是中断。Bootloader阶段如果使能了中断跳转到App之前一定要关中断并且最好把中断控制器的全局使能也关掉。否则跳转瞬间外部设备一个中断过来CPU会跑去执行Bootloader里的中断服务函数而那时Bootloader的堆栈和上下文可能已经被App覆盖直接跑飞。第三件事是外设状态。Bootloader用过的QSPI控制器、UART、DMA如果App里还要用最好在Bootloader阶段把自己初始化过的外设恢复成默认状态或者明确在文档里告诉App的开发者“这个外设已经由Bootloader初始化过”。否则App在初始化外设时面对的是一个半初始化状态配置起来容易出怪问题。3.4 把串口当成Bootloader的“仪表盘”没有屏幕没有调试器的裸板调试串口打印就是唯一的眼睛。Bootloader里每一阶段的关键动作都打印一行信息看起来像是“浪费程序空间”实际上能帮你省下大量排查时间。我自己的习惯是上电后先打印Bootloader版本和编译时间然后打印Flash ID、镜像头部信息魔数、长度、CRC、版本搬运过程中按比例打印进度跳转前打印目标地址。一旦板子跑不起来串口输出能直接告诉你卡在哪个环节——是Flash初始化失败还是镜像长度读出来是0还是魔数不对。配合后面讲的排查表基本能做到一眼定位。4. 实操用Vitis 2021.1写出并注入Bootloader4.1 创建Platform、Bootloader和App三个工程打开Vitis 2021.1设置工作空间然后创建一个Platform工程选之前导出的XSA。Platform生成后里面有硬件规格和standalone BSP的基础框架。接下来创建两个Application ProjectBootloader工程BSP选择standalone语言选CApp工程同样选standalone可以先选Hello World模板Bootloader工程里链接脚本必须把代码和堆栈放在BRAM0x00000000 - 0x00007FFFApp工程必须把代码放在App的实际运行地址比如DDR的0x80000000。这些在右键工程的lscript.ld里修改。很多人跳转失败查来查去最后发现就是链接脚本没改App编译出来还是链接在0x0又被Bootloader所在BRAM地址覆盖了这是最典型的低级错误。4.2 Bootloader代码框架与关键函数实现在Bootloader工程的main.c里按上一节的骨架把代码填进去。如果你用的Vitis版本里有现成的Bootloader模板可以直接拿来改但模板一般比较复杂新手反而看不懂。我更推荐空工程手敲一遍几十行代码就能跑理解也更透。有一点需要提醒MicroBlaze的BSP在生成时可能会默认使能一些库和驱动增加BSP体积。Bootloader工程里用不到的驱动模块比如网络、DMA尽量在BSP设置里关掉减少库函数占用BRAM也减少初始化时间。BSP的standalone_bsp里有很多Enable选项按需开关即可。QSPI Flash的初始化细节不同板卡差异比较大。以常见的Digilent Arty/A7系列为例板载QSPI Flash连接在AXI Quad SPI控制器的SPI0通道上BSP默认就能识别。如果是自己画的板子务必确认Flash的型号、命令集是否被BSP支持比如Winbond W25Q系列、Macronix MX25L系列市面上主流型号基本都支持但小众型号可能没有对应驱动。4.3 App工程链接脚本让应用落在“正确的地图坐标”App工程编译出elf之后代码段地址必须和Bootloader跳转的目标地址一致。如果App链接在0x0而Bootloader跳去0x80000000那跑起来的必然是垃圾数据。修改方法在Vitis的App工程里打开lscript.ld查看Memory Regions。如果DDR基地址是0x80000000把text、data、bss、heap、stack都放到DDR区域。这里的堆栈大小根据业务需求定但别忽略MicroBlaze没有MMU栈溢出不会报错只会悄无声息地覆盖相邻数据程序表现为“随机死机”。建议裸机环境下栈大小至少设为1KB复杂工程再加。App工程可以不需要任何Bootloader相关代码甚至不需要知道Bootloader长什么样只要保证三个约定就行了链接地址和运行地址一致、入口函数是main、中断向量表和Bootloader约定的位置一致。前两个是通用规则第三个后面细说。4.4 用updatemem把Bootloader熔进BitstreamBootloader代码写好后需要把它“熔”进比特流让BRAM在上电时就有Bootloader的机器码。Vivado提供了updatemem命令在Vitis的终端里或者Vivado Tcl Console里都能调用updatemem \ --meminfo design_1_wrapper.mmi \ --bit design_1_wrapper.bit \ --data bootloader.elf \ --proc microblaze_0 \ --out design_1_wrapper_boot.bit参数含义参数说明--meminfo指定MMI文件路径就是Vivado工程impl_1目录里那个.mmi--bit原始比特流--dataBootloader的elf文件--procMicroBlaze实例名要和Block Design里的名字完全一致--out输出的新比特流文件名这里有两个高频坑一是MMI文件必须来自你当前综合实现的工程不能拿别的版本或者网上下载的凑数否则BRAM地址映射对不上二是--proc名字写错最常见的是少了下划线或者大小写不对updatemem会报错找不到处理器。命令执行成功后会生成design_1_wrapper_boot.bit烧录这个比特流之后BRAM里就有Bootloader了这才是“上电自启”的完整形态。5. 上板烧录与Flash固化MCS生成、偏移地址与失败链路排查5.1 生成App.bin的步骤与注意事项App工程编译后的elf并不能直接让Bootloader搬运Bootloader一般按二进制镜像处理所以要把elf转成bin。在Vitis里可以用MicroBlaze交叉编译工具链的objcopymb-objcopy -O binary app.elf app.bin如果你的环境变量里没有mb-objcopy在Vitis的BSP或工具链目录里也能找到路径大致是Vitis安装目录/gnu/microblaze/nt64/bin/mb-objcopy.exe。转出来的app.bin大小就是应用实际二进制大小烧写之前可以看一眼如果bin文件大小是0或者只有几十字节多半是链接脚本或编译出了问题。生成bin之后再算一下CRC填到镜像头部里。这一步建议写一个小脚本自动完成因为以后每次编译都要重新生成手动算错一次就够难受的。5.2 两种固化方式Vitis Program Flash与Vivado生成MCS拿到app.bin之后要把它烧到QSPI Flash的约定偏移地址对应Bootloader里读的那个偏移比如0x200000。两个常用方式方式一Vitis的Program Flash在Vitis菜单里选Xilinx - Program Flash会弹出对话框。你需要选择Hardware Platform当前XSA对应的PlatformConfiguration选择Flash类型比如qspi-x4-singleImage Fileapp.binOffset0x200000必须和Bootloader读的地址一致点击Program后Vitis会通过JTAG先加载一个Flash烧写小程序到MicroBlaze可执行的位置然后用这个小程序把镜像写入QSPI Flash。整个过程串口上可能会打印进度。这个方式适合“我就是要更新App”的日常开发。方式二Vivado生成MCS后烧写如果你希望把“FPGA配置比特流 App镜像”一次性写进Flash可以用Vivado菜单Tools - Generate Memory Configuration File。在对话框里Memory Type选择SPI Flash对应的型号和格式比如MCSInterface选择SPIx1或SPIx4要和板卡实际连接一致Load Bitstream Files选择包含Bootloader的那个比特流design_1_wrapper_boot.bitLoad Data Files选择app.bin并设置偏移比如0x200000勾选Fill Flash With 0xFF把未使用区域清成0xFF生成MCS后用Hardware Manager连接板卡右键FPGA设备 - Add Configuration Memory Device选中Flash型号把MCS烧进去。这个方法的好处是一步到位坏处是每次改App都要重新生成MCS重新擦写整个Flash效率不如Vitis Program Flash高。我的经验是产品量产用MCS开发调试用Program Flash两不耽误。5.3 上电启动失败的排查链路附对照表Bootloader最折磨人的是“上电后什么反应都没有”下面这张表是我排过很多次之后总结的基本能覆盖大部分情况现象可能原因排查方法上电后串口完全没有输出比特流没有Flash或Flash里是旧比特流检查FPGA配置LED或用JTAG下载设计确认串口有乱码波特率/时钟配置不对确认UART波特率确认MicroBlaze时钟频率有Bootloader打印但报Flash初始化失败Flash型号不匹配或SPI接口配置错读Flash ID和芯片手册核对有打印且Flash正常但魔数错误app.bin根本没烧进Flash偏移写错用Flash读回工具看0x600000处内容魔数正确搬运完成但一跳转就死链接地址不一致或Cache没处理检查App链接脚本跳转前禁用CacheApp能跑但一开中断就死中断向量表和Bootloader冲突修改MicroBlaze异常向量地址或先验证无中断版本排查时记住一条原则把Bootloader的串口打印当成“进度条”一步步确认走到了哪里。串口没有任何输出问题大概率在FPGA配置或者时钟复位串口有输出但停在某一步问题就在那一步之后。5.4 Flash擦写的工程化提醒QSPI Flash的写操作前必须先擦除擦除按扇区不能只擦几个字节。Bootloader里如果做了App升级功能写Flash前要处理好“擦除范围不能越过扇区边界”这个问题。比如你只想升级App区结果不小心把整个Flash都擦掉了连FPGA配置比特流都清了板子就直接变砖只能重新用JTAG烧。更稳妥的做法是把“当前正在执行的代码”和“新写入的代码”放在不同扇区先写备份区校验成功后再更新主区。这个思路放到MicroBlaze Bootloader里也是一样一级Bootloader固化在BRAM里不会丢但它负责从Flash选主区还是备份区加载App这样即使升级失败上电还能从备份区启动。6. 实际项目里踩到的坑与工程化建议6.1 复位信号、外部按键与Processor System Reset开发板上通常有个复位按键直接连到FPGA的全局复位引脚。如果这个按键信号没有处理直接拉进Processor System Reset IP很容易出现“上电10次里有两三次起不来”的诡异问题。原因是机械开关和电源上电时序会产生毛刺处理器在复位释放的瞬间可能采到不确定状态这就是FPGA课程里常说的复位信号亚稳态。正确的做法是让Processor System Reset IP来处理复位同步和时钟锁定板级只要保证复位信号源干净即可。如果板子上复位信号质量太差加一个简单RC滤波或者专门的复位芯片。这个小问题排查起来极其隐蔽因为大多数时候正常偶尔起不来重烧一遍又好了。6.2 中断向量重定位App一开中断就死的常见原因MicroBlaze的异常向量和中断向量地址在Vivado的硬件配置里是固定的默认就在0x0附近的低地址区域。Bootloader把BRAM的0x0区域占掉了如果App也想用默认的中断向量两者就会冲突。结果就是App的main函数执行得好好的第一个中断到来时CPU跳去0x0附近执行读到的却是Bootloader的代码段直接死机。解决方案有两个方向一是在Block Design的MicroBlaze配置里把Exception Vector改到DDR的高地址区域比如0x80000010让App的中断向量表落在DDR里二是App自己不做中断向量重定位而是通过BSP的中断控制器驱动来注册处理函数。两个方案都能跑但建议新手先做出一版不带中断的BootloaderApp等跳转链路完全稳定了再单独处理中断向量重定位的问题否则两个问题混在一起很难排查。6.3 升级Bootloader自身的两条路BRAM里的Bootloader是随着比特流一起固化的要改Bootloader必须重新生成比特流、重新烧FPGA配置Flash。如果产品已经交付现场没有JTAGBootloader出bug就只能返厂这是最被动的局面。工程上常用两级Bootloader方案破解这个问题一级Bootloader仍然固化在BRAM里只做最小工作——从Flash读取二级Bootloader到RAM并跳转二级Bootloader放在Flash可写区域功能是完整的镜像校验和App升级它可以通过UART、网口或USB升级自己。这样一级Bootloader永远不变二级Bootloader和应用都可以在线升级。这个方案对新手来说可能有点远但设计Flash分区时一定要想着“以后可能要加二级Bootloader”预留好空间别把Flash塞得满满当当。6.4 这套启动思路如何迁移到其他平台MicroBlaze的Bootloader原理放到Zynq、RISC-V软核、NIOS II甚至STM32上都是相通的。核心要回答三个问题复位向量指向哪、镜像放在哪、如何完成跳转。Zynq有硬件BootROM会先把FSBL加载到OCM和MicroBlaze从BRAM启动本质是一个套路NIOS II也是软核同样要处理上电引导STM32的Bootloader则是在Flash里做程序区和App区划分。我个人的体会是在FPGA上把MicroBlaze这套Bootloader完整做一遍对整个嵌入式体系的理解都会上一个台阶。过去只知道“下程序进去就能跑”做完之后才明白上电引导、地址链接、中断向量、Cache一致性、Flash分区这些真正决定系统可靠性的东西是怎么一环扣一环配合起来的。最后分享一个小建议刚开始做的时候别急着上完整工程先用“Bootloader串口打印 从Flash搬个小数组 跳转到一个LED闪烁程序”这种最小组合验证通路全部跑通之后再逐步往里面加中断、CRC、OTA这些功能。串口打印就是最廉价的调试器多打一行字往往能少熬一个通宵。
返回列表