不用J-Link也能用RTT:DAPLink+SEGGER RTT嵌入式调试日志实战指南

📅 发布时间:2026/9/21 15:06:32
不用J-Link也能用RTT:DAPLink+SEGGER RTT嵌入式调试日志实战指南
去年做一个主频200MHz的Cortex-M4项目时我一度被串口日志搞得非常狼狈一路UART被通信协议占用第二路UART的引脚又被某个外设复用只能把USB转TTL模块用杜邦线硬搭在板子上日志一多还直接把系统的实时性拖垮。后来无意中把开发板上那块吃灰的板载DAPLink利用起来配合SEGGER RTT才算找到了一套既不断线、不占外设又几乎不影响CPU运行的调试日志方案。这套组合的关键词就三个CMSIS DAP、DAPLink、RTT。如果你手头也有带板载DAPLink的板子又想体验SEGGER RTT这种事后诸葛亮式的日志工具这篇文章应该能帮你少走不少弯路。1. 不花一分钱让DAPLink跑RTT先说清楚它的使用价值与边界1.1 串口日志为什么越来越不够用很多嵌入式工程师的第一调试手段就是串口我原来也是。串口的优点是简单板子只要有UART和USB转TTL模块就能打印但用久了你会发现四个很头痛的问题占外设和引脚UART的TX/RX两个引脚被调试日志占死很多板子引脚复用紧张最后只能牺牲功能。带宽天花板低115200波特率下理论吞吐只有约11.5KB/s稍微多打几行日志就会丢帧提到1.5M波特率又要求双方硬件都支持上位机软件也未必认。影响实时性printf逐字符往UART写要占用CPU大量时间特别是中断里加打印可能直接改变程序时序查bug变成制造bug。接线麻烦外接USB转TTL模块要接TX、RX、GND三根线杜邦线接触不良、环境干扰都会让调试体验大打折扣。相比之下RTTReal Time Transfer由SEGGER提出目标MCU侧只维护RAM中的一段环形缓冲区调试器通过SWD/JTAG接口在后台读取既不额外占用引脚也不需要UART外设参与日志开销只是一次内存写入的量级。而CMSIS-DAP/DAPLink正是ARM生态里最常见的开源调试探针方案很多开发板板载了它不需要再花几百上千元买J-Link。1.2 认识三个词CMSIS-DAP、DAPLink、RTT三者经常被混着说但它们不是同一个层次的概念CMSIS-DAP是ARM定义的调试器主机端协议标准说白了就是调试器固件和调试软件之间的通信接口规范。市面上大量免驱DAP下载器使用的就是CMSIS-DAP协议。DAPLink是ARM官方开源的调试探针固件项目硬件通常基于LPC11U35、K20等低成本USB MCU。它实现了CMSIS-DAP协议同时自带拖拽烧录、虚拟串口等功能。很多开发板上的板载调试器实际跑的就是DAPLink固件。SEGGER RTT是SEGGER提供的日志/双向数据传输组件包含目标侧源码SEGGER_RTT.c等和主机侧工具RTT Viewer、Ozone等。它并不绑定J-Link只要能通过调试接口读RAM的探针原则上都能配合使用。理解这个关系很重要因为很多教程默认RTT必须用J-Link导致有人手里的DAPLink一直吃灰。实际上新版SEGGER工具已经支持CMSIS-DAP探针OpenOCD和pyOCD也都能通过通用调试协议做RTT转发方案可以很灵活。1.3 这套方案适合谁、不适合谁我个人的判断是如果你的项目已经有板载DAPLink或者愿意花几十块钱买个通用CMSIS-DAP下载器又只需要日志输出、命令行交互这类功能DAPLinkRTT完全够用成本几乎为零。需要升级到J-Link的场景通常是这几类要用SystemView做系统级可视化分析、要稳定跑10MHz以上SWD且日志吞吐要求极高、需要SEGGER完善设备数据库里的冷门芯片支持等。说白了大部分裸机或RTOS项目的日志调试DAPLinkRTT都具备实用价值别被必须J-Link的刻板印象限制住。2. RTT读内存却不打断CPU控制块与环形缓冲的工作原理2.1 调试器偷看内存的通道RTT能工作的底层基础是Cortex-M内核允许调试器在CPU正常运行的情况下通过调试接口访问系统内存。这个机制不是RTT独有的调试器在断点之外做memory watch本身就依赖它。RTT的做法很朴素目标MCU往RAM里写日志调试器像看门卫一样隔一段时间通过SWD把那段RAM读出来然后在PC端显示。整个过程CPU不需要暂停也不需要打断当前正在执行的中断或任务。所以RTT对目标系统的影响几乎可以看作程序里多了一段往内存写数据的代码仅此而已。这也是RTT能保持实时性的关键。2.2 SEGGER RTT的控制块与环形缓冲区SEGGER RTT在目标侧的核心是一个全局结构体官方叫RTT Control Block控制块。它里面保存了ID标识、上行缓冲和下行缓冲的描述信息。每个缓冲区本质上是RAM中的一段线性数组配合读偏移和写偏移组成环形缓冲。目标侧一旦调用SEGGER_RTT_Write或SEGGER_RTT_printf就往环形缓冲里写数据并更新写偏移调试器侧的轮询逻辑读取缓冲时从读偏移到写偏移之间的数据就是新增日志。两个偏移各自独立更新读写双方通过这个巧妙的设计实现无锁解耦。2.3 调试器如何找到控制块调试器要读RTT首先得在目标RAM里定位到那个控制块。SEGGER的做法是在控制块开头放一个固定字符串SEGGER RTT调试器在指定的RAM地址范围内搜索这个特征串找到地址后就能解析出缓冲区详情。所以工具里一般需要你填写两个关键参数RAM起始地址和RAM大小用于限定搜索范围。例如HC32F460这类Cortex-M4芯片RAM基址是0x20000000如果你知道SRAM实际大小是192KB就把搜索范围设为0x20000000到0x20030000。如果工具默认搜索范围不覆盖你的控制块所在区域就会一直报RTT not found。2.4 RTT时延的正确理解很多初学者会问用RTT打日志日志从MCU到PC要多长时间其实要分三段看目标侧写入延迟RTT默认是非阻塞模式缓冲区有空位就写没有空位就跳过或截断基本不回阻塞CPU。单次写入只有几条内存操作指令耗时在纳秒级。调试器轮询与传输延迟调试器是周期性通过SWD读取缓冲区内容的。轮询间隔和SWD时钟频率决定了这一段的延迟通常从几十微秒到几毫秒不等。端到端的可见延迟还包括工具软件刷新界面、缓冲累积的时间实际显示上会有轻微延迟。也就是说RTT适合观察系统行为趋势和调试日志不适合做严格的时间测量。如果你要精确到微秒级的时序应在目标侧自己打时间戳再用日志带出来而不是依赖RTT收到数据的PC时间。还有一个容易误解的点RTT吞吐量再高也有限缓冲区写满后非阻塞模式会丢弃新日志不能指望它像无底洞一样吞数据。3. 动手前确认物料DAPLink版本、固件与接线3.1 CMSIS-DAP v1与v2的差异CMSIS-DAP协议分两个大版本对RTT体验影响很大CMSIS-DAP v1基于USB HID传输单次传输只有64字节轮询周期又受限实际带宽约几十KB/s。很多老式DAP下载器和早期板载调试器是v1跑RTT会明显感觉日志刷新慢。CMSIS-DAP v2改用USB Bulk传输配合高速SWD通信带宽大幅提升。跑RTT时日志刷新更流畅焊接、调参的体验也好很多。怎么判断手里的DAPLink是v1还是v2Windows设备管理器里查看调试器枚举出的USB接口v1通常只有一个HID设备v2会出现一个带CMSIS-DAP v2字样的WinUSB接口。如果你不确定最保守的办法是直接下载官方DAPLink固件包看固件描述和Release Note里是否写明支持v2。3.2 固件与驱动检查DAPLink固件一般不需要你重新编译官方发布的是编译好的hex或bin。刷固件方法也不复杂按住板载调试器上的BOOT按键将USB口连接到电脑会弹出一个U盘盘符把新固件拖进去就完成更新。我建议在跑RTT之前先把固件升级到官方最新版因为老固件对CMSIS-DAP v2和高SWD速率的支持不如新版。驱动方面Win10/11对标准HID和WinUSB设备基本免驱插入后设备管理器就能识别。如果出现未知设备或感叹号推荐用Zadig把设备驱动换成WinUSB很多异常都是驱动冲突导致的。3.3 接线规范调试器与目标板之间一般只要接四根线SWDIO、SWCLK、GND以及VCC信号参考线。很多人不接VCC也能下载但RTT需要稳定的调试会话电压参考如果不一致SWD信号电平可能漂移容易出现偶发通信失败。线的长度和布局也不能忽视。SWD对高速信号比较敏感杜邦线超过10cm就可能出问题。我自己踩过的坑是在面包板上飞线跑5MHz SWD结果OpenOCD隔三差五报错降到1MHz就稳定了。如果你板载DAPLink与目标芯片走线很短基本不存在这个问题如果是外接调试器建议用短的排线并把SWD速度控制在4MHz以内。4. MCU侧移植把RTT源码加入工程并重定向printf4.1 获取RTT源码SEGGER RTT的目标侧源码可以直接从SEGGER官网下载也可以在安装J-Link软件后在安装目录下找到Samples\RTT文件夹里面有SEGGER_RTT.c、SEGGER_RTT.h、SEGGER_RTT_Conf.h、SEGGER_RTT_printf.c、SEGGER_RTT_printf.h等文件。有的版本还有一个汇编文件SEGGER_RTT_ASM_ARMv7M.S用于在Cortex-M7等内核上优化缓冲区复制属于可选优化。把这些文件原封不动加入工程再把RTT目录加入头文件搜索路径即可。不推荐自己修改RTT核心代码因为后续升级维护会很痛苦。4.2 最小工程集成最小集成代码非常简单初始化甚至都不是必须的因为RTT控制块由C编译器自动清零并注册#include SEGGER_RTT.h int main(void) { // ...芯片时钟和外设初始化... SEGGER_RTT_printf(0, RTT ready, CPU %lu Hz\r\n, SystemCoreClock); while(1) { // 业务代码 } }SEGGER_RTT_printf的第一个参数是上行通道号默认0通道就是终端输出通道。执行到这里后调试器侧只要保持连接就能在主机工具里看到日志。4.3 重定向printf不同编译器的做法直接调SEGGER_RTT_printf虽然够用但项目里老代码往往到处是printf把它们一个个改成RTT接口显然不现实。更优雅的做法是把标准库的底层输出函数重定向到RTT。GCC工具链arm-none-eabi-gcc下实现_write函数即可#include sys/stat.h #include SEGGER_RTT.h int _write(int file, char *ptr, int len) { (void)file; SEGGER_RTT_Write(0, (const char *)ptr, (unsigned)len); return len; }ARMCC或ARMCLANGKeil MDK下重定向fputc#include stdio.h #include SEGGER_RTT.h int fputc(int ch, FILE *f) { (void)f; SEGGER_RTT_PutChar(0, (char)ch); return ch; }IAR下重定向__write#include stddef.h #include SEGGER_RTT.h size_t __write(int handle, const unsigned char *buffer, size_t size) { if (handle 1) { SEGGER_RTT_Write(0, (const char *)buffer, (unsigned)size); } return size; }重定向完成后项目里所有printf、puts都会走RTT输出原有串口打印代码甚至不需要改。4.4 缓冲区与内存的取舍SEGGER_RTT_Conf.h里有几个关键配置BUFFER_SIZE_UP上行缓冲区大小默认1024字节。BUFFER_SIZE_DOWN下行缓冲区大小默认16字节。SEGGER_RTT_MAX_NUM_UP_BUFFERS/SEGGER_RTT_MAX_NUM_DOWN_BUFFERS上下行通道数量。上行缓冲区是日志的主管道Log密集时1024很容易写满非阻塞模式会直接丢数据。我的实践是调试阶段调到4096甚至8192RAM占用也就几KB对MCU来说完全可接受。下行缓冲区用于接收主机下发的命令16字节做回显命令行会偏小建议至少64字节。但也要注意缓冲区是全局变量会一直占用RAM产品发布前如果不需要RTT记得关掉或改小。5. 主机侧四套工具横评RTT Viewer、Ozone、pyOCD与OpenOCD5.1 方案ARTT Viewer CMSIS-DAPSEGGER官方的RTT Viewer是最直观的选择。新版RTT Viewer在连接设置里已经可以识别CMSIS-DAP探针老版本只支持J-Link如果你打开软件发现Emulator下拉框里没有CMSIS-DAP需要升级到较新版本。操作步骤是打开RTT Viewer弹出Connection Settings对话框后Emulator选择CMSIS-DAPInterface选SWDTarget Device选你的芯片型号。如果列表里没有你的芯片先试试同系列或同内核型号但要注意外设寄存器访问可能受影响。之后点击确认目标芯片只要处于运行或调试状态RTT Terminal窗口就会实时滚动输出日志。RTT Viewer的界面左侧是各个上行通道右键可以配置通道名称和缓冲大小。下方输入框可以往下行通道发数据配合目标侧回显代码能实现类似串口命令行的效果。5.2 方案BOzone可视化调试Ozone是SEGGER推出的独立调试器同样支持CMSIS-DAP探针。相比RTT Viewer只做RTT显示Ozone把下载、调试、断点、变量观察和RTT终端集成在一个界面里适合需要同时看RTT和调试状态的时候。使用方法是启动Ozone新建Project在Debug Probe里选择CMSIS-DAPDevice选对应芯片型号设置SWD速度后点击连接。连接成功后通过View菜单打开RTT Terminal窗口。如果Ozone的设备数据库里找不到你的芯片处理办法和RTT Viewer类似优先到官网更新版本仍然没有就换成OpenOCD/pyOCD方案它们不依赖厂商设备数据库也能连上做RTT转发。5.3 方案CpyOCD命令行pyOCD是一个基于Python的开源调试工具天然支持CMSIS-DAP。它内置了RTT子命令可以直接在终端里打印目标日志适合CI或自动化测试场景。pip install pyocd pyocd list --probes pyocd rtt -t hc32f460如果pyOCD不认识你的目标芯片就需要用--pack参数加载芯片厂商的CMSIS-Pack文件pyocd rtt -t hc32f460 --pack /path/to/HC32F460_DFP.packpyOCD的好处是完全可控、跨平台、能嵌入Python脚本缺点是需要处理Python环境和打包文件对纯Windows用户来说门槛比RTT Viewer高一些。5.4 方案DOpenOCD RTT ServerOpenOCD从0.11.0版本开始内置RTT支持配合CMSIS-DAP接口使用非常灵活。它的思路是把RTT数据转成TCP流你可以在本机用telnet或者其他工具接收。先用OpenOCD启动调试会话配置接口和目标核心source [find interface/cmsis-dap.cfg] transport select swd adapter speed 4000 target create hc32f460.cpu cortex_m -endian little然后在OpenOCD的telnet控制台默认端口4444里输入rtt setup 0x20000000 0x30000 SEGGER RTT rtt start rtt server start 19021 0最后另开一个终端连接telnet 127.0.0.1 19021这样就能看到RTT输出OpenOCD侧还能同时处理GDB调试适合喜欢命令行工作流的工程师。5.5 如何选方案上手难度界面对冷门芯片适配典型场景RTT Viewer低图形化依赖设备数据库日常快速看日志Ozone中图形化调试器依赖设备数据库调试日志窗口同时用pyOCD中命令行需加载CMSIS-Pack自动化、脚本化日志采集OpenOCD中高命令行几乎都能连配合GDB、自定义流程我的建议是平时调板子优先用RTT Viewer或Ozone一旦遇到设备型号搜不到、工具连接不上或者要集成到自动化脚本里立刻切换到OpenOCD方案它几乎不存在找不到设备的问题。6. 实测HC32F460 DAPLink跑通RTT的完整流程6.1 硬件连接与IDCODE确认这里以HC32F460开发板为例板上自带DAPLink同时引出了SWD接口。先把开发板通过USB连接到PC打开设备管理器确认DAPLink被正确识别。为了稳妥我先用OpenOCD读取IDCODEopenocd -f interface/cmsis-dap.cfg -f target/hc32f460.cfg -c dap info; shutdown如果没有现成的HC32F460 target配置可以直接用通用Cortex-M核心启动再执行dap info看IDCODE是否和Cortex-M4一致。能读到IDCODE就说明SWD链路完全正常可以继续RTT配置。6.2 工程配置与关键编译参数HC32F460的工程我使用ARMCC编译器内存起始地址0x20000000SRAM 192KB。在工程里加入RTT源码后把BUFFER_SIZE_UP设为4096BUFFER_SIZE_DOWN设为64。编译优化级别我建议调试阶段用-Og或-O0方便源码级调试发布版再用-O2。RTT代码本身是声明了volatile的优化级别不会导致它失效但会影响某些变量在调试器中的可观测性所以调试阶段低优化会更舒适。主函数里初始化时钟后添加一行SEGGER_RTT_printf(0, [APP] HC32F460 RTT demo, sysclk%luHz\r\n, SystemCoreClock);然后编译下载。这里单独说明一下HC32F460的启动文件需要正确初始化堆栈和系统时钟RTT源码不关心这些只要你的工程原本能正常点灯加入RTT就不会有问题。6.3 从下载到看到第一行日志我实际用的链路是OpenOCD RTT Server在Windows终端里执行openocd -f interface/cmsis-dap.cfg -f target/hc32f460.cfg -c init; halt; program app.hex verify reset exit烧录后重新启动OpenOCD连接目标并进入控制台依次输入rtt setup 0x20000000 0x30000 SEGGER RTT rtt start rtt server start 19021 0另开一个终端执行telnet 127.0.0.1 19021确认目标板复位运行后telnet窗口里立即出现了我打印的那行带系统时钟频率的日志。同样如果用RTT Viewer只要连接时设备选对进入界面后也能直接看到输出不需要额外命令。实测下来4MHz SWD下RTT日志刷新非常跟手我故意在循环里每秒打印十几行也没有出现明显丢帧。这说明DAPLink跑常规RTT日志完全可行。7. 踩坑实录连接失败、乱码、卡死与控制块找不到7.1 控制块搜不到RTT工具最常见的报错就是RTT Control Block not found。问题的本质是工具在指定RAM范围内没找到SEGGER RTT特征串。排查顺序我建议这样确认程序有没有执行到RTT初始化SEGGER_RTT_printf之前如果程序就崩了控制块虽然存在但没有输出有些工具也可能报找不到。确认搜索范围HC32F460的SRAM从0x20000000开始OpenOCD里rtt setup的第二个参数就表示搜索范围太小可能覆盖不到控制块所在位置。确认控制块没有被优化掉RTT结构体声明为全局volatile理论上不会但如果你用了一些链接脚本或板级支持包把.bss段放到了0x20000000之外的保留RAM区域搜索范围就要跟着调整。确认工具连接的目标确实是你烧录程序的芯片有时候调试器连到了板载另一个核或者J-Link与DAPLink共存导致连错接口也会搜不到。7.2 SWD速率过高外接DAPLink到HC32F460开发板线长超过10cm时如果把SWD速率设到10MHzOpenOCD会报SWD ACK错误或者超时界面卡死。这不是RTT的问题而是SWD物理层不稳定。处理办法非常简单降低adapter speed。4MHz是我在各种板子上验证后最稳定的值1MHz虽慢但最保险。板载DAPLink走板内短走线时5~8MHz通常也稳定但没必要为RTT强上高速因为日志吞吐已经不是主要瓶颈。7.3 乱码与丢帧RTT出现乱码首查缓冲区大小和写入方。如果多个线程或中断都在调用SEGGER_RTT_Write又没有锁保护两条日志可能交错写入看起来就会乱。这种情况下需要在写入时做临界区保护最简单的做法是调用SEGGER_RTT_LOCK/SEGGER_RTT_UNLOCK或者干脆自己在RTOS里加一个互斥锁。还有一类乱码来自printf内部缓冲和RTT缓冲的交互。标准库printf通常有内部缓冲比如nano newlib的1024字节会在缓冲满或换行时刷新到_write如果_write里直接SEGGER_RTT_Write大字符串可能被拆成多段中间被高优先级任务插入其他日志从而出现交错。此时优先保证写入原子性。丢帧则多半是上行缓冲太小非阻塞模式下写满即丢。把BUFFER_SIZE_UP从1024改成4096观察是否好转。如果日志极其密集比如中断里每次事件都打印那任何缓冲都救不了应该把日志级别放大只在关键状态切换时打印。7.4 打印卡死RTT默认非阻塞模式不会把CPU卡死但如果你修改了通道的Flags为MODE_BLOCK_IF_FIFO_FULL缓冲满时SEGGER_RTT_Write就会一直等调试器读走数据。调试器一旦断开或轮询慢目标CPU会被日志拖死。这个模式只适合你非常确定日志量远小于缓冲容量的场合或者专门用来做有意的流量控制。在生产代码里我的建议是保持MODE_NO_BLOCK_SKIP宁可丢日志也不能影响业务。另外在中断里做RTT打印也要注意中断失能时长的差别、多层中断嵌套写同一缓冲区都可能让日志出现奇怪的时序。中断里尽量只写入预先格式化好的短字符串绝对不要在中断里做printf格式化那样CPU开销会高到改变实时性。8. 进阶玩法回显、多通道、锁与性能优化8.1 回显法在RTT上做简易命令行RTT不只是单向日志它有下行通道主机可以往目标发送数据。所谓回显法就是目标侧读取下行通道数据再原样写回上行通道。这样你在RTT Viewer的下方输入框敲命令目标收到后回显就能实现一个不占串口的简易命令行。目标侧的回显任务大概是这样void RTT_EchoTask(void) { char buf[64]; int n SEGGER_RTT_Read(0, buf, sizeof(buf)); if (n 0) { SEGGER_RTT_Write(0, buf, n); // 这里可以解析buf并执行命令 } }把这个函数放在主循环或RTOS任务里配合命令解析就能通过RTT控制LED、读取传感器、查变量值很多需要现场指挥的调试场景都变得很方便。8.2 多通道分离日志级别RTT默认支持多个上行通道。你可以把普通日志、错误日志、调试时序分别放在不同通道主机工具按通道过滤查看效果比在字符串里加[ERR]标记再手动搜索好得多。目标侧配置通道SEGGER_RTT_ConfigUpBuffer(1, App, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); SEGGER_RTT_ConfigUpBuffer(2, DBG, NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP); SEGGER_RTT_WriteString(1, app status ok\r\n); SEGGER_RTT_WriteString(2, debug: counter...\r\n);注意通道0是默认终端通道不用额外配置。RTT Viewer左下角的通道列表可以单独开关每个通道这样大量调试信息不再淹没关键日志。8.3 中断与多线程环境下的安全打印RTT自身对缓冲区指针的更新是中断安全的但安全只限于不破坏缓冲区结构不代表多线程日志不会互相穿插。要在多任务环境下保证一条完整的日志不被撕裂最好在应用层做原子化写入void App_Log(const char *msg) { SEGGER_RTT_LOCK(); SEGGER_RTT_WriteString(0, msg); SEGGER_RTT_UNLOCK(); }如果你用的是FreeRTOS也可以改用taskENTER_CRITICAL()包起来效果类似。但注意临界区不能长时间占用否则中断响应会被拖慢。大段日志尽量拆成短行输出每次写入控制在几十字节以内比较合理。8.4 提升RTT吞吐量的实战技巧经过一段时间使用我总结了几条提升RTT效率的经验合并小包为大包频繁调用SEGGER_RTT_PutChar逐字符输出效率最低尽量用SEGGER_RTT_Write一次写完整行。避免在热点路径做格式化SEGGER_RTT_printf内部也有字符串解析开销实时性敏感的地方可以先把你需要的数据拼到一个静态缓冲里出临界区后再统一打印。合理分配上下行缓冲日志密集型任务上行缓冲给大命令交互频繁才需要大的下行缓冲不要无脑把两方向都配到8KB。用字符串常量代替格式化定长日志用SEGGER_RTT_WriteString省掉格式解析性能最好。最后分享一个我在多个项目里验证过的小技巧调试阶段把BUFFER_SIZE_UP设置为8192等发布前再改回1024或直接关闭RTT。因为调试时你最怕漏日志宁可多占几KB RAM也要保证现场信息完整。发布版如果还想保留RTT做远程维护建议单独做一个编译开关把RTT代码和缓冲区统一管理避免上线后才发现日志缓冲把RAM吃得太多。这套DAPLink RTT的方案我后来从M0到M4的多个项目里都在用实测稳定性和可读性都远远好过当初的杜邦线串口。它不完美遇到极高吞吐或芯片级事件追踪时我依然会拿起J-Link和SystemView但对绝大部分日常调试这只吃灰的板载DAPLink已经能完成所有工作了。