ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DAPLink 任意格式固件烧录:pyOCD/OpenOCD 实战指南

DAPLink 任意格式固件烧录:pyOCD/OpenOCD 实战指南 1. 先弄清楚 DAPLink 到底替我们省了什么事做嵌入式这行桌面上最不缺的就是下载器J-Link、ST-Link、CMSIS-DAP、串口 ISP、USB DFU每一种背后都有一段驱动装不上的血泪史。DAPLink 这个开源调试方案能被这么多人长期留在手边核心原因不是它性能最强而是它把“调试器”和“U盘”这两件事塞进了同一块小板子里。插上它电脑里同时多出虚拟串口、可拖拽的存储盘、CMSIS-DAP 调试接口一个硬件三份用途出门带一根线就够。标题里说的“下载任意格式固件”一句话就能讲明白开发中你拿到的固件可能是 .hex可能是 .bin也可能是 .elf 或者 .s19甚至是一段需要指定偏移量的裸数据。DAPLink 本体在拖拽模式下只认少数几种格式但把它接进 pyOCD 或 OpenOCD 的工具链之后这些格式都能被解析、转换、再写成统一的烧录动作。这篇文章要打通的就是这条链路无论手里拿到什么格式的固件都能通过 DAPLink 稳定地写进目标芯片。适合谁看如果你刚买了一块基于 STM32F103C8T6 的 DAPLink 小板插上电脑只看到一个 U 盘图标不知道下一步该干什么那这篇就是给你写的如果你已经在用但经常撞上“下载成功却不运行”“识别不到设备”“校验不过”这些情况后面第五节可以直接跳到排查部分。整套流程不依赖特定商业软件纯命令行环境就能跑通。1.1 为什么它成了我的常驻调试器DAPLink 本质上是一套跑在调试器单片机上的固件实现了 CMSIS-DAP 协议。这个协议是 ARM 主导的开放标准被 Keil、IAR、pyOCD、OpenOCD、各类 IDE 广泛支持。也就是说只要目标芯片支持 SWD 或 JTAG不管是 STM32、GD32、Nordic、NXP 还是各种国产 M0/M3/M4 内核DAPLink 都能直接拉过来用不像某些厂商专用调试器那样绑定自家芯片。成本也是一大原因。以 STM32F103C8T6 为蓝本的 DAPLink 小板物料成本压得很低自己打板焊接也能搞定。固件本身开源后期升级、改 VID/PID、加自定义功能都有据可查。我自己的做法是常备两块一块刷 HID 模式放在包里兼容性最好插到别人电脑上大概率免驱另一块刷 WinUSB 模式放在工位上速度明显更快批量烧录时省时间。这里有个容易被忽略的点不同来源的 DAPLink 固件功能集合不完全一样。有些只做了调试口和拖拽盘没引出串口有些串口的 TX/RX 和调试口共用引脚用的时候要改跳线。拿到手第一件事是把它插到电脑上看看设备管理器里到底出现了几个设备而不是急着往目标板上下固件。1.2 “任意格式”这四个字的真实边界先把话说清楚DAPLink 不是什么万能格式翻译器。它的核心工作只有一件事把一段数据按地址写进目标芯片的 Flash。所以“任意格式”能否成立取决于这个格式能不能被解析成“起始地址 数据流”这样的序列。Intel HEX 和 Motorola S-record 自带地址记录解析出来就能直接写纯 bin 文件里没有任何地址信息必须由你在命令行里手动指定烧录基地址elf 和 axf 除了数据还带符号表和段信息工具链能自动识别哪些段属于 Flash、哪些属于 RAM。格式是否自带地址典型来源烧录时的处理方式.hex是Keil、IAR、GCC 导出直接解析地址段写入.bin否编译器 objcopy 输出必须指定基地址如 0x08000000.elf / .axf是含段信息GCC、Keil 链接产物工具按段自动落位.s19 / .srec是部分老工具链、汽车电子直接解析地址段写入.uf2是拖拽模式生成的产物仅在 DAPLink 盘内流转真正需要动脑的是 bin 这类无地址格式。以 STM32F103C8T6 为例它的主 Flash 从 0x08000000 开始容量 64KB也就是到 0x0800FFFF 结束RAM 从 0x20000000 开始共 20KB。如果一份 bin 是从 Keil 里直接烧录出来的它隐含的基地址就是 0x08000000但如果这份 bin 是某个 Bootloader 的升级包需要写到 0x08008000 之后就必须在烧录命令里显式指定偏移。地址填错芯片看起来烧写成功了实际跑起来就是一片空白或者直接硬件异常。2. 硬件挑选与 USB 三件套的取舍DAPLink 的硬件形态五花八门从拇指大小的裸板到集成在开发板上的调试模块都有。选型这件事没有标准答案关键是看你手上要烧的目标芯片和你的使用场景。给 STM32 烧固件和给一颗 3.3V 供电的低功耗蓝牙 SoC 烧固件对调试器的供电能力、接口电平、复位引脚的要求都不一样。这一节把常见的几种形态和它们对应的使用场景理一理。2.1 手边几种 DAPLink 方案怎么挑最常见的三类第一类是基于 STM32F103C8T6 的自制小板便宜、资料多、固件好找缺点是 USB 全速接口速度一般HID 模式下烧写几十 KB 的固件还能接受上百 KB 就有点慢了。第二类是基于 LPC11U35 或 ATSAMD21 的方案原生 USB 控制器性能更好价格略高。第三类是开发板自带的调试模块比如某些官方评估板会提供一个可切换为 DAPLink 模式的接口好处是省一根线坏处是它的引脚定义固定用起来不够灵活。我个人的判断标准很简单日常调试选 HID 模式为主的小板插上就能用兼容性最好需要批量烧写或者烧大固件时换成 WinUSB 模式的板子。如果你的项目对调试时序有要求比如需要在芯片刚上电的极短时间内 halt 住内核那还要看这块 DAPLink 是否把 nRESET 引脚引出来了。很多便宜的小板只引出 SWCLK、SWDIO、GND、3.3V 四根线遇到芯片一上电就跑飞的程序没有复位线会非常被动。注意买之前确认一下板子是 HID 固件还是 WinUSB 固件。两者外观可能一模一样但插上电脑后设备管理器里显示的名称不同HID 会显示为符合 HID 规范的供应商自定义设备WinUSB 则会枚举成一个具体的 USB 设备。这一点直接决定了你后面能用哪些工具。2.2 CDC、MSC、HID 三条通道各自干嘛用的一块标准的 DAPLink 插上电脑通常会枚举出三个逻辑设备理解它们的分工能省掉很多困惑。CDC 是虚拟串口对应调试器的 UART 通道一般引出 TX、RX 两根线接到目标板的串口引脚上用来看打印、发命令。注意电平是 3.3V别直接怼 5V 的目标板中间要么确认目标板兼容要么加电平转换。MSC 是大容量存储设备也就是那个拖拽盘。这是 DAPLink 最讨喜的设计把 hex 或 uf2 文件直接拖进去固件自动烧写并复位。它的实现原理是固件内部挂了一个小的文件系统解析拖进来的文件后调用烧录逻辑。限制也很明显早期固件只支持解析 hex不支持 bin而且盘符容量显示是假的写大文件会报磁盘已满。HID 是 CMSIS-DAP 调试通道走 HID 协议免驱、兼容性最好但吞吐受限。WinUSB 是同一通道走批量传输速度快得多代价是 Windows 下需要驱动通常安装 pyOCD 时会一并装上 WinUSB 驱动或者用 Zadig 手动替换。3. 环境搭建驱动装不对后面全是白干环境这一步是整个流程里最容易翻车的地方。工具链本身安装很简单pip 一条命令的事麻烦的是 Windows 上的 USB 驱动匹配以及调试接口被其他软件占用这两种情况。我见过太多人在“设备管理器里能看到设备但工具就是连不上”这个状态里卡半天其实问题往往出在驱动不匹配或者后台还挂着一个 IDE。3.1 驱动识别与常见报错在 Windows 上把 DAPLink 插上后先看设备管理器。如果 HID 版本通常直接就能用不需要额外操作如果是 WinUSB 版本可能显示成一个带黄色感叹号的未知设备这时候需要用 Zadig 把它的驱动替换成 WinUSB或者干脆安装 pyOCD 的驱动包它会自动处理大部分常见设备。Linux 下一般免驱但要确认当前用户对 /dev/hidraw* 或者 USB 设备节点有读写权限否则会报权限拒绝。macOS 也是免驱为主偶尔会遇到系统自带的某个后台进程抢占 HID 设备表现为第一次连接正常、第二次就失败重启一下就好。调试时最常见的报错是“no available debug probes”或者“unable to find a matching CMSIS-DAP device”。看到这类提示先做三件事确认 USB 线是数据线而不是只供电的充电线确认没有别的程序正在占用调试接口比如 Keil、IAR、另一个终端里的 OpenOCD 实例确认目标板的 SWD 线接对了很多调试器在没接目标板时也能被电脑识别但一连就报错这是正常的。3.2 pyOCD 与 OpenOCD 两条路线怎么选这两个工具都能驱动 DAPLink但风格差别很大。pyOCD 是 Python 写的安装简单命令直观对 CMSIS-Pack 支持好能自动识别芯片型号并加载对应的 Flash 算法。OpenOCD 是 C 写的配置文件更细能调的参数更多对特殊芯片或者特殊复位时序的支持通常更及时。我的建议是两套都装。日常烧录用 pyOCD一条命令搞定省心遇到 pyOCD 认不出芯片、或者需要精细控制复位行为时切到 OpenOCD用配置文件把参数一点点调出来。两者不会冲突因为它们只是前端工具底层驱动的都是同一个 CMSIS-DAP 接口只是不能同时运行。安装 pyOCD 很简单Python 环境下执行pip install pyocd即可。装完之后还需要给目标芯片装 Pack 支持包比如要给 STM32F103C8T6 烧录就执行pyocd pack install stm32f103c8。这个 Pack 包里包含了芯片的 Flash 算法和内存映射没有它pyOCD 就不知道该往哪个地址写、怎么擦除。# 安装 pyocd pip install pyocd # 查看当前连接的调试器 pyocd list # 安装 STM32F103C8 的器件支持包 pyocd pack install stm32f103c8 # 确认目标芯片是否被识别 pyocd list --targets | grep -i stm32f1033.3 调试器该接到板子哪几个脚SWD 模式最少需要四根线SWCLK、SWDIO、GND、以及可选的 nRESET。3.3V 这根线要不要接取决于目标板是否自己供电。如果目标板已经插了电源就不要再把 DAPLink 的 3.3V 接上去两个电源打架轻则烧调试器重则连目标芯片一起带走。只有当目标板没供电、要靠 DAPLink 的 3.3V 输出时才把电源线接上而且要先确认目标板的电流需求没有超过 DAPLink 板载稳压器的输出能力一般这类小板只能给到几百毫安。nRESET 这根线很有价值强烈建议接上。原因很简单如果目标芯片的程序里把 SWCLK 或 SWDIO 复用成了普通 GPIO或者程序一上电就进入低功耗模式把调试口关掉那在正常状态下调试器是连不上的。这时候只有靠 nRESET 把芯片拉住在复位释放的瞬间抢占调试口才能连上。pyOCD 里的--connect under-reset参数、OpenOCD 里的reset_config配置都依赖这根线。线材长度也别忽略。SWD 是高频信号杜邦线拉太长、走线乱很容易出现偶发连接失败或者校验错误。超过 15 厘米就能感觉到不稳定实在要长距离尽量让 SWCLK 和 GND 绞在一起或者换带屏蔽的排线。4. 固件格式互转bin、hex、elf、s19 的地址游戏拿到一份固件第一件事不是急着烧而是先搞清楚它是什么格式、隐含了什么地址信息。格式判断错了、地址填错了后面所有操作都是白做。这一节把几种常见格式的本质差异、互转命令和合并技巧讲清楚这部分内容在绝大多数教程里都被一笔带过但恰恰是最容易出问题的环节。4.1 几种格式的本质差异Intel HEX 是文本格式每行以冒号开头记录了数据长度、地址、类型和校验和。它的优势是带地址可以描述不连续的多个段比如一段在 0x08000000、另一段在 0x08008000解析工具会自动分别写入。bin 是最纯粹的二进制数据没有任何元信息文件大小就是数据长度地址完全靠外部指定。它的好处是体积小、通用性强任何工具都能处理坏处是没人告诉你它该放哪。elf 和 axf 是带完整元信息的可执行文件格式除了代码数据还有符号表、段表、入口地址等。烧录工具能从段表里读出哪个段属于 Flash、哪个属于 RAM自动落位。缺点是文件大而且里面包含调试信息不适合直接当发布包。s19 和 hex 类似也是文本格式带地址和校验在某些老工具链和汽车电子领域还挺常见。解析方式和 hex 差不多。4.2 objcopy 转换实操与基地址计算arm-none-eabi-objcopy 是转换格式的主力工具GCC 工具链里自带。下面几条命令是我最常用的。# hex 转 bin直接丢弃地址信息生成纯数据 arm-none-eabi-objcopy -I ihex -O binary firmware.hex firmware.bin # elf 转 bin arm-none-eabi-objcopy -O binary firmware.elf firmware.bin # 把一段没有地址的 bin 包装成带基地址的 elf arm-none-eabi-objcopy -I binary -O elf32-littlearm -B arm \ --change-addresses0x08000000 firmware.bin firmware_with_addr.elf # elf 转 hex arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex # 查看 elf 的段信息确认地址布局 arm-none-eabi-objdump -h firmware.elf关键在第二条包装命令。--change-addresses0x08000000这个参数告诉工具把这段裸数据放到 0x08000000 开头的地址空间里。转换出来的 elf 就带上了正确的地址信息可以直接被 pyOCD 识别烧录。如果是 Bootloader 升级包要放到 0x08008000就把这个值改成 0x08008000。基地址具体填多少要看目标芯片的 Flash 起始地址。STM32F103C8T6 是 0x08000000GD32F303 也是 0x08000000但有些国产芯片或者某些 ARM9 平台就不一样做之前一定要查芯片手册的存储器映射章节别照搬。4.3 多段固件合并与校验字段处理实际项目里经常遇到要把 Bootloader 和应用程序合并成一个包的情况比如 Bootloader 在 0x08000000 占 16KB应用在 0x08004000 开始。这时候需要把两个文件按地址拼在一起。方法有好几种最简单的用 srec_cat它支持各种格式的输入输出和地址偏移。# 把 bootloader.hex 和 app.hex 按各自地址合并成一个大 hex srec_cat bootloader.hex -intel app.hex -intel -o merged.hex -intel # 如果 app.bin 需要偏移到 0x08004000 srec_cat app.bin -binary -offset 0x08004000 -o app_off.hex -intel合并之后一定要做一次校验确认合并后的文件大小和预期一致。一个实用的小技巧是用arm-none-eabi-objdump -h或者 hex 解析工具看段分布确认每段的首地址和长度都对得上。有时候工具会自动填充空隙比如把 0x08001000 到 0x08004000 之间填 0xFF文件体积因此变大这是正常的。如果目标芯片有 CRC 校验字段且校验范围覆盖整个应用区那合并后还要重新计算 CRC。这个步骤没有通用命令得看你项目的具体实现。常见做法是在编译脚本里加入一个后处理步骤编译完自动算 CRC填到指定地址。手工算的话用 Python 读文件、按算法的参数算一遍再写回去。5. 完整实操把一份 bin 写进 STM32F103C8T6前面铺垫了这么多这一节把完整流程从头到尾走一遍。假设我手上有一份别人发来的 application.bin没有地址信息需要烧进一块 STM32F103C8T6 最小系统板芯片本身没有锁Flash 是空的。手头的调试器是一块 HID 模式的 DAPLink 小板已经装好驱动。5.1 硬件接线与上电顺序先断电操作。把 DAPLink 的 SWCLK 接到目标板的 PA14SWDIO 接 PA13GND 接 GNDnRESET 接目标板的复位脚。如果目标板有独立供电那 3.3V 这根线就不要接了。接好线之后再依次上电先给目标板上电再把 DAPLink 插到电脑上。这个顺序的原因是想让目标芯片在调试器建立连接之前已经处于稳定供电状态避免因为供电晃动导致的连接失败。如果是靠 DAPLink 给目标板供电的情况那就直接插上 DAPLink目标板跟着上电但这时候要特别小心电流最小系统板加一颗 LED 通常没问题带无线模块或者大电流外设的板子就别这么干了。5.2 pyOCD 命令行全流程第一步是确认调试器和芯片都能被识别。# 列出已连接的调试器 pyocd list # 输出大概是这样 # 0004:000b:0000 STMicroelectronics STM32F103C8 ... # Board: DAPLink # Target: stm32f103c8如果这里只列出了调试器但目标芯片显示 unknown说明 SWD 连接有问题检查线序和供电。第二步是烧录注意指定格式和基地址。# 烧录 bin 文件指定目标芯片、格式和基地址 pyocd flash -t stm32f103c8 --format bin --base-address 0x08000000 application.bin # 烧录 hex 文件格式自动识别 pyocd flash -t stm32f103c8 application.hex # 烧录后自动复位并运行 pyocd flash -t stm32f103c8 --format bin --base-address 0x08000000 application.bin --reset第三步是校验。pyOCD 在烧录时默认会做一次 CRC 比对如果校验失败会直接报错。如果对结果不放心可以手动读回内存比对。# 读回一段内存确认写入内容 pyocd cmd -t stm32f103c8 -c read32 0x08000000 16如果目标芯片处于锁定状态--connect under-reset这个参数能把芯片拉住再连。pyocd flash -t stm32f103c8 --connect under-reset \ --format bin --base-address 0x08000000 application.bin5.3 OpenOCD 配置文件与参数OpenOCD 的用法是把接口配置和目标配置拼起来再加一串命令。以 DAPLink 和 STM32F1 系列为例命令大概是这样。openocd -f interface/cmsis-dap.cfg \ -f target/stm32f1x.cfg \ -c adapter speed 1000 \ -c program application.hex verify reset exitadapter speed是 SWD 时钟单位 kHz默认值有时候偏低导致烧写慢调到 1000 或者 2000 通常没问题但如果线材质量差或者走线长调太快会连接不稳这时候往下调。program后面的参数依次是文件名、烧录后校验、烧录后复位、完成后退出。如果要烧 bin 文件需要加地址参数。openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg \ -c program application.bin 0x08000000 verify reset exit如果芯片的 SWD 引脚被程序复用或者芯片进了低功耗模式需要加上复位配置让 OpenOCD 在复位状态下连接。openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg \ -c reset_config srst_only srst_nogate \ -c init; reset halt \ -c program application.bin 0x08000000 verify reset exit5.4 拖拽模式与量产脚本如果固件是 hex 格式最简单的办法就是直接拖进 DAPLink 的盘里。操作时注意观察写入进度写完盘会自动消失再重新出现这时候固件已经烧好并复位运行。如果拖进去没反应多半是文件格式不对早期固件不认 bin也不认带密码的打包文件。量产场景下拖拽太慢要用脚本。下面这个 shell 脚本的思路是遍历目录下所有 bin 文件按文件名里的偏移量信息分别烧录每烧完一个记录日志。#!/bin/bash # 批量烧录脚本文件名格式app_0x08000000.bin for file in *.bin; do # 从文件名里提取基地址 addr$(echo $file | grep -oP 0x[0-9a-fA-F]) echo 烧录 $file 到 $addr pyocd flash -t stm32f103c8 --format bin \ --base-address $addr --reset $file if [ $? -eq 0 ]; then echo $file OK flash.log else echo $file FAILED flash.log fi done这个脚本套在夹具上、配合一个 USB Hub 和多个 DAPLink就能同时烧多块板子。我试过用四块调试器并行因为 pyOCD 每个实例独立占用一个调试器互不干扰整体吞吐能翻几倍。前提是电脑的 USB 供电要够别把 Hub 拖挂了。6. 问题排查速查表与踩坑记录调试器这类工具正常的时候一句话都不用说出问题的时候能折腾一整天。下面这些是我这几年踩坑记下来的按现象分类遇到问题可以对照着查。6.1 识别不到设备怎么一点点排排查顺序建议从物理层往上走。先换 USB 线这条最容易被忽略很多线是纯充电线数据线芯根本没有再换 USB 口前置面板的口供电和信号质量通常不如主板直出然后看设备管理器里有没有枚举没有就是硬件或驱动问题有但工具连不上就是软件占用或者目标板问题。现象可能原因处理方式电脑完全无反应USB 线只有供电、接口损坏换线换口插到另一台电脑验证设备管理器有黄色感叹号WinUSB 驱动缺失用 Zadig 替换驱动或装 pyOCD 驱动包调试器能列出目标 unknownSWD 线序错、目标板没供电核对 SWCLK/SWDIO/GND检查目标板电源第一次能连第二次失败调试口被占用关掉 IDE 和其他 OpenOCD 实例连接时断时续杜邦线过长、时钟过快缩短线长降低 adapter speed6.2 下载成功但不运行的几种原因这是新手最困惑的一类问题日志显示烧录成功、校验也过了芯片就是不工作。常见原因有四个。一是时钟配置问题。如果固件设计的是外部晶振而板子上的晶振没焊或者频率不对程序会卡在时钟初始化。这一点跟烧录本身无关但表现上很像“烧录失败”。二是 BOOT 引脚状态。有些板子把 BOOT0 拉高后忘了拉回芯片从系统存储器启动不跑你烧的固件。烧完检查一下跳线帽。三是基地址填错。bin 文件如果本来应该放在 0x08000000你却填了 0x08004000校验照样能过因为写进去的数据是完整的只是位置错了芯片复位后从正确地址取指取到的是空白。四是没复位。有些工具烧完不会自动复位芯片还在跑旧程序。加上--reset或者手动按复位键。6.3 读保护、选项字节与量产注意事项芯片被读保护之后调试器连上去会报错因为调试口被硬件层面禁用了。这时候需要先解锁用工具执行全片擦除擦除过程中保护位会被清除然后再正常烧录。pyOCD 的pyocd erase --mass和 OpenOCD 的stm32f1x unlock 0都能干这件事。量产环节要特别小心两点。第一是别把调试口永久禁用有些固件为了保护代码会把 SWD 引脚彻底关掉烧完之后再也连不上只能靠擦除或专门的解锁流程救回来。第二是记录每块板子烧录的固件版本和校验值一旦现场出问题能追溯到具体批次。注意涉及读保护、写保护这类选项字节操作先确认目标芯片的具体型号和手册定义的位含义不同系列差异很大。改错了可能把芯片锁死虽然一般都能通过擦除恢复但会浪费时间。7. 后续还能怎么玩工具链打通之后DAPLink 能做的事远不止手动烧一块板子。我把它往两个方向扩展过都挺实用。7.1 自动化测试与产线批量烧录把烧录脚本接到测试夹具上上电、烧录、读回校验、跑一段自检程序、读回串口输出整个流程自动化一个工人能同时看多台测试位。pyOCD 提供了 Python API可以写更复杂的流程比如先读芯片唯一 ID 记录到数据库再决定烧哪个版本的固件。from pyocd.core.helpers import ConnectHelper from pyocd.flash.file_programmer import FileProgrammer session ConnectHelper.session_with_chosen_probe(target_overridestm32f103c8) with session: # 读取芯片唯一 ID uid session.target.read_memory_block32(0x1FFFF7E8, 3) print(UID:, [hex(x) for x in uid]) # 烧录指定文件 FileProgrammer(session).program(application.bin, base_address0x08000000)这种写法比调命令行灵活得多能根据读到的信息动态决定烧什么、校验什么。7.2 用同一套工具做固件比对与版本管理发布固件之前我习惯把编译产物和上次的版本做一次比对看看哪些段变了。用 objcopy 转成 bin 之后直接做二进制 diff配合地址信息能快速定位改动区域。如果只是想确认某块板子上跑的固件和仓库里的版本是否一致就把板子上的 Flash 读回来和本地文件比对。# 从芯片读回 64KB pyocd cmd -t stm32f103c8 -c save 0x08000000 0x10000 readback.bin # 和本地文件比对 cmp readback.bin application.bin echo 一致 || echo 不一致这套流程用在返修和售后环节特别好使客户寄回的板子读一下就能知道上面跑的是哪个版本不用靠人回忆。我个人在实际操作中的体会是DAPLink 这类工具的价值不在于某个单项功能有多强而在于它把烧录、调试、串口三件事统一到了一个标准协议下让脚本化和自动化成为可能。真正花时间的从来不是点下烧录按钮那一下而是前面接线、驱动、格式转换这些琐碎环节。把这些环节理清楚、写成脚本固化下来后面每次新项目就能少踩一遍同样的坑。
返回列表