ARTICLE DETAIL

资讯详情

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

CH32V303 SDI Printf实战:用RISC-V虚拟串口替代传统UART调试

CH32V303 SDI Printf实战:用RISC-V虚拟串口替代传统UART调试 在嵌入式开发里除了点灯printf 大概是最刚需的调试手段了。但如果你的板子上没有引出 UART或者串口被别的外设占了那就很尴尬。WCH 的 CH32V303RCT6 上有个不太好找但非常实用的功能——SDI Printf 虚拟串口本质是复用调试接口 PDIO 这根线把 RISC-V 内核的数据通过 WCH-Link 转成 USB 虚拟串口送到电脑上。这篇文章我会从协议原理、硬件连接、工程配置到常见坑位梳理一遍争取让第一次接触的人也能照着操作。1. 为什么是 SDI Printf三种调试方式的对比1.1 传统 UART 调试的问题很多人拿到新板子第一件事就是把 printf 重定向到串口 1接一个 USB 转 TTL然后打开串口助手看日志。这套流程确实最通用但也最容易踩坑。第一个问题是硬件占用。UART 至少需要 TX、RX 两根线加上 GND 就是三根。对于 LQFP64 封装的 CH32V303RCT6 来说引脚本来就不算富裕如果项目里已经用了多个 USART 做通信、控制再专门分一个 UART 出来做日志就得为它留引脚、走线、电平转换。很多量产板为了省成本根本不会把调试串口引出来。第二个问题是波特率。UART 是异步通信两边必须设置相同的波特率哪怕差个百分之几也可能出乱码。实际调试中经常遇到这种情况程序在 115200 下跑得好好的换了一块外部晶振频率略有差异的板子日志就变成乱码了。虽然可以用自动波特率检测但增加了复杂度。第三个问题是干扰。如果你的板子工作环境电磁干扰比较重或者调试线和电机驱动线绑在一起走串口传输很容易丢帧出现日志断断续续、内容错位的问题。排查到最后发现不是程序逻辑问题而是物理链路太差非常浪费时间。1.2 SDI Printf 虚拟串口解决什么问题SDI Printf 的思路和 UART 完全不同。它不单独占用串口外设而是复用调试器已经连接的 SWDIO 引脚在沁恒的体系里叫 PDIO通过芯片内部的调试接口硬件把 printf 的数据以 SDI 协议发送出去。WCH-Link 收到后再通过 USB 转成虚拟串口在电脑上显示出来。这样做的好处非常直接。首先你不再需要额外接 USB 转 TTL也不需要考虑 TX、RX 交叉连接的问题硬件上只需要一根 SWDIO 线和调试下载共用省了引脚也省了连线。其次因为数据走的是调试接口通道不占用 UART 外设你完全可以把所有串口都留给业务逻辑日志输出不用跟业务抢资源。第三虚拟串口本质上是一个 USB CDC 设备波特率只是一个“摆设”PC 端无论设置成多少都能收到数据不再有波特率不匹配导致乱码的问题。我把三种方式放在一起对比调试方式硬件资源连线复杂度波特率约束是否影响业务串口典型场景UART 调试占用一个 USART 两个引脚需要 USB 转 TTL接线较多有严格约束是串口资源充足、无干扰环境仿真器变量监视不占外设但需要暂停运行只需调试器无否查看实时变量、单步调试SDI Printf复用调试引脚不占外设和调试下载共用一根线无否日志输出、性能跟踪、长时间运行监控1.3 与其他 RISC-V 调试方案的差异相比 ARM 内核的芯片RISC-V 的调试接口本来就更“年轻”。CH32V303 系列用的是沁恒自研的青稞 V4C 内核调试接口没有采用标准的 JTAG而是用了一套单线调试协议也就是 SDI。这套协议只需要一根数据线而且还有一个特点它不仅能做断点、读写寄存器还能在调试的空闲时间隙里传输 printf 数据。这和 STM32 的 ITM/SWO 调试打印思路有点像但实现差异很大。STM32 的 SWO 是需要单独一根引脚的而沁恒的 SDI 直接复用了调试数据线本身硬件上更简单。另外一个差异是工具链ST 的 SWO 需要借助 J-Link、ST-Link 配合专用驱动而 CH32V303 用 WCH-Link 配合 MounRiver Studio 就能直接开放出虚拟串口整套流程对新手更友好。不过也正是因为这个功能藏得比较深很多人用了很久的 CH32V303 都不知道有这玩意儿。接下来我把原理层面拆开聊聊它到底是怎么工作的。2. SDI Printf 的工作原理与核心细节2.1 SDI 协议单线半双工的巧妙设计要理解 SDI Printf先得搞清楚 SDI 本身是什么。SDI 的全称是 Single Debug Interface看名字就知道这是一套“单线调试接口”协议。正常调试接口一般需要两条线一条时钟一条数据比如标准的 SWD 接口就是 SWDIO SWCLK。沁恒这套 SDI 把时钟信息也编码进了数据流里实现在单根线上完成半双工的通信。很多人第一次听到“单线调试”会觉得不靠谱没有独立时钟线信号怎么同步实际上它是靠异步帧格式来解决的每一帧数据里自带起始位和停止位接收方通过检测边沿变化来恢复时钟。这就好比两个人打电话不需要额外再拉一根“说话节奏控制线”只要按照约定的语速和语法说清楚对方就能听懂。半双工意味着同一时刻只能有一个方向在传输。在 SDI 调试模式下调试器向芯片发送命令、芯片返回响应都需要在这根线上分时完成。而 Printf 功能恰恰利用了调试器空闲的时间窗口当芯片没有在响应调试请求时它可以把 printf 产生的数据包主动塞出来调试器在收完命令响应之后顺带把这些日志数据一并取走。这也是为什么 SDI Printf 对调试器版本和固件有要求——老版本的 WCH-Link 固件可能根本没实现这个“捎带”逻辑。2.2 数据流路径从 MCU 到 PC 终端理解了协议整条数据链路就清晰了。CH32V303RCT6 的 printf 函数最终会调用底层字符输出函数这个输出目标被重定向到了 SDI 硬件模块。芯片内的 SDI 外设把要打印的字符串按 SDI 帧格式封装通过 PDIO 引脚发出去。PDIO 引脚在 F1 系列上通常复用 PA13在 CH32V303 的封装上默认也是这个位置和 SWDIO 是同一个物理引脚。WCH-Link 的 SWDIO 线接到这个引脚上一边负责调试数据传输一边接收芯片主动发来的打印数据。WCH-Link 收到这些数据后通过 USB 接口以 CDC 虚拟串口的形式枚举到操作系统里——在 Windows 设备管理器里会看到一个“WCH-Link Serial”或者类似名字的 COM 口。你打开任意串口助手选中这个 COM 口就能看到 printf 的内容了。关键点在于这个路径上并没有一个真正的 UART 物理层。MCU 侧的数据不是经过 TX 引脚发出的PC 侧也没有一个实体的串口芯片。两个“虚拟”的环节通过调试器桥接起来所以链路中间不存在波特率失配问题。PC 端串口助手里设置的波特率对这个链路来说没有任何实际意义随便设一个能打开串口就行。2.3 虚拟串口 VS 物理串口你关心的几个真相既然虚拟串口这么方便那它跟物理串口在行为上到底有什么区别我实测下来觉得有三点必须说清楚。第一虚拟串口的“波特率”是假的但数据时序是真的。USB CDC 协议本身没有波特率概念MCU 侧也不存在按波特率逐个发送 bit 的过程所以你在电脑上怎么设置都不影响。但是这不意味着数据不会丢。SDI 单线传输的带宽有限调试器还要分时处理调试请求如果 printf 输出得太猛缓冲区满了依然会丢数据。第二虚拟串口的时序响应比物理串口更“急”。物理串口是 MCU 的 UART 外设独立负责发送发送缓冲区满了 CPU 会被阻塞但至少数据是按顺序进缓冲区的。SDI Printf 是芯片内的调试模块在“抽空”转发如果你的代码在中断服务函数里疯狂 printf而这个中断优先级又很高那么调试模块可能来不及把数据取走表现为日志随机缺失或者卡顿。第三虚拟串口和调试会话是绑定的。物理串口只要接好线、供电正常就能一直输出。SDI Printf 必须保持 WCH-Link 连接、处于调试会话或者至少让调试器接管了 PDIO 线否则数据无法送到 PC。如果你的程序跑在独立环境里没有接调试器那 SDI Printf 就是无效的。这一点在从开发环境切换到产品演示时尤其要注意。3. 实战在 CH32V303RCT6 上开启 SDI Printf3.1 硬件准备与连接开始之前先把东西备齐。我这里用的是 CH32V303RCT6 的核心板作为示例但方法对所有支持 SDI 的沁恒 RISC-V 芯片都通用。需要准备的东西一块 CH32V303RCT6 核心板或自绘板一个 WCH-Link建议用 WCH-LinkE兼容性更好固件更新也方便杜邦线若干USB 线一根给 WCH-Link 供电和通讯用连线非常简单WCH-Link 的 SWDIO 接芯片的 PA13如果有 SWCLK 也一起接上PA14同时两根地线必须共地。如果板子是外部供电的3.3V 不接也能工作如果板子没有供电可以把 WCH-Link 的 3.3V 接出来供电。切记先确认板子的电压域是 3.3V再接电源线不要盲目去接 5V。注意WCH-Link 的 SWDIO 线必须接触可靠SDI Printf 本身对时序比较敏感如果杜邦线松动或者接触不良最典型的表现不是下载失败而是打印数据断断续续或者完全无输出。建议尽量用短线、直接焊接的方式连接。3.2 软件环境与工程配置软件方面需要安装 MounRiver StudioMRS版本建议用新版我试过的几个老版本对 SDI Printf 的支持不够完善经常出现能下载但打印不出来的问题。WCH-Link 的固件也要尽量更新到官方最新版本这一步容易被忽略但非常重要。工程配置的步骤打开 MRS新建一个 CH32V303 工程选择芯片型号时选 CH32V303RCT6。MRS 会自动生成一个包含debug.h、debug.c的官方模板工程。确认工程里已经包含了调试相关的初始化文件。官方模板里的main.c顶部会 includedebug.h如果你用的不是模板工程需要手动添加头文件路径。打开调试配置页面。在工程上右键选择“Debug As”然后进入“Debug Configurations”。找到你的调试配置在 Debugger 选项卡里找到与 SDI 或 Printf 相关的选项勾选启用 SDI Printf 功能。注意这个选项的具体名称和位置在不同版本的 MRS 里可能略有差异有的版本叫 “Enable SDI Printf”有的版本放在高级选项里。找不到的话直接搜索 “SDI” 关键词。配置完成之后先编译一次确保没有错误再往下写代码。3.3 代码实现与验证整个代码实现其实非常简单这可能是 SDI Printf 最让人惊喜的地方——不需要自己写重定向函数。核心代码就这几段#include debug.h int main(void) { u32 i 0; Delay_Init(); SDI_Printf_Init(); printf( WCH CH32V303RCT6 SDI Printf Demo \r\n); printf(SystemClk:%d\r\n, SystemCoreClock); while(1) { i; if((i % 1000) 0) { printf(Running time: %d ms\r\n, i); } Delay_Ms(1); } }这里最关键的调用是SDI_Printf_Init()。这个函数在官方 SDK 的debug.c里实现作用是初始化 PDIO 引脚和 SDI 外设并把fputc系列的底层重定向到 SDI 输出。不同版本的 SDK 函数名可能略有不同有的是SDI_Printf_Init()有的是直接在USART_Printf_Init()内部做了条件编译如果编译报错就查一下手头 SDK 的函数原型。写完代码后点击调试按钮。MRS 会进入调试界面实际上你如果已经在调试配置里启用了 SDI Printf程序跑到第一个断点或者直接全速运行PC 端通常会在 Console 窗口自动出现一个虚拟终端输出。如果你的 MRS 版本没有自动弹出终端可以在电脑上打开任意串口助手选择 WCH-Link 枚举出来的 COM 口设置波特率为任意值比如 115200打开串口就能看到源源不断的日志输出。我第一次跑通的时候看到 Console 里直接滚出 “Running time:” 的日志确实有“原来这么简单”的感觉。但后续在多个项目里用下来也踩了不少坑下面这部分是我觉得比“怎么跑通”更值得看的。4. 常见问题与排查技巧实录4.1 printf 中文乱码这个应该是大家问得最多、也最容易排查错的问题。相同一段代码用 UART 输出中文没问题换成 SDI Printf 就乱码了很多人第一反应是 SDI 编码有问题。实际上一半以上的情况问题出在 PC 端的接收工具和源文件字符编码不一致。打个比方程序源文件是 UTF-8 编码编译器把字符串常量按 UTF-8 的字节序列存到了 Flash 里而电脑端串口助手默认用 GBK 解码两种编码对同一个中文字符的字节对应关系完全不同自然就显示成乱码了。解决办法要么把源文件改成 GBK 编码要么把串口助手切换到 UTF-8 接收模式。我用过的串口助手里有一些是默认 GBK 的需要在设置里手动切换。另外还有一个容易被忽略的点有些编译器在字符串字面量里做了字符集转换比如 GCC 可能把窄字符串转换成执行字符集如果你的编译选项里指定了-fexec-charsetGBK之类那么 printf 出来的中文就是 GBK 字节流这时 PC 端必须用 GBK 解码。所以最简单的方法是先确认两边的字符集完全一致再做其他排查。如果乱码是“部分字符正常、部分乱码”那就要检查是不是编译器优化导致字符串没对齐或者你的打印内容里有%s但传入的参数类型不对。我在调试时遇到过%d传了 32 位整型却写成了%hd导致高位被截断日志里数字不是乱码但是明显不对这属于格式符使用问题跟 SDI 本身无关。4.2 输出丢失、重复和延迟第二个高频问题就是输出内容不完整或者同一行日志重复出现再或者延迟了好久才一次性刷出来。这三种现象的原因其实是同一个——缓冲区和时序。先看输出延迟。SDI Printf 的数据不是实时逐字节送出的芯片的 SDI 外设会把要打印的数据放入一个 FIFO然后由调试器的轮询周期把数据取走。如果你在 while 循环里每隔几毫秒打一条日志数据大概率是“攒着”一起送出的在 PC 端看起来就是延迟后突然冒出一大堆内容。这不是硬件坏了不要浪费时间去查代码逻辑。再看丢数据。FIFO 的深度是有限的如果你的 printf 输出频率超过了 SDI 通道的转发能力FIFO 满了之后新的数据会被丢弃。三种情况最容易触发一是在中断回调函数里直接 printf二是在两个高频任务里同时打印三是用了printf打印大量浮点数或长字符串。我建议的做法是不要在中断或者实时性要求高的任务里直接调用 printf。可以定义一个简单的日志缓冲队列中断里只做log_putc()把字符塞进一块内存环形缓冲区主循环或者低优先级任务里再把缓冲区的内容通过printf输出。这样既不会阻塞中断也不会因为 FIFO 满了丢数据缺点是实现起来稍微多一点代码量。如果输出内容出现重复通常是调试器还挂在目标板上并且你同时打开了 MRS 的 Console 和另一个串口助手两边都在读取同一个虚拟串口。Windows 的虚拟 COM 口默认不支持多客户端同时打开但如果你用了共享型软件可能两边都能打开于是出现日志两边各显示一半或者重复显示。遇到这种情况关掉一个接收端即可。4.3 下载时报错与引脚冲突再说一个非常影响体验的问题SDI Printf 开启之后下一次下载程序失败提示无法连接调试器。这个现象的原因是用完 SDI Printf 后没有关闭打印功能芯片一直通过 PDIO 往外发数据干扰了调试器的握手时序。解决办法有几个。最简单的程序里做一个条件编译开关发布版本里完全不调用SDI_Printf_Init()调试版本才启用。还有就是下载时按住芯片的复位键让芯片在复位状态下暂停运行connect under reset等调试器接管了 PDIO 线之后再松开复位键。在 MRS 的调试配置里可以找到 “Connect under Reset” 选项遇到下载失败时把它勾上能解决很大一部分问题。还有一种情况是代码里把 PA13 和 PA14 所在的 AFIO 重映射配置改了导致 PDIO 引脚被复用成了普通 GPIO 或者其他外设功能。这种情况下调试器也连不上因为调试引脚被占用了。排查思路是检查代码里有没有对 PA13/PA14 做引脚复用配置尤其是那些把 PA13 当按键输入用的工程。4.4 常见问题速查表现象可能原因排查方法中文乱码源文件编码和 PC 工具解码编码不一致统一为 UTF-8 或 GBK检查编译器 exec-charset中文字符显示正常但有“菱形问号”编译器字面量编码转换导致字节流不完整尝试在字符串前手动指定转义序列或改源文件编码日志延迟一次性刷出SDI FIFO 攒批输出属于正常现象减少单次打印长度降低打印频率日志缺失FIFO 满了被丢弃不在中断高频打印增加环形缓冲队列日志重复多个终端同时打开虚拟串口关闭多余接收软件只保留一个下载失败SDI 输出干扰调试握手或引脚被复用勾选 Connect under Reset检查 PA13/PA14 复用配置完全无输出未启用调试配置里的 SDI Printf 选项或固件过旧检查 MRS 配置升级 WCH-Link 固件编译报错找不到 SDI_Printf_InitSDK 版本不同函数名或头文件有差异查看当前 SDK 的 debug.h确认函数原型5. 进阶扩展把 SDI Printf 变成交互式命令行5.1 Letter Shell 接入思路在上面“日志输出断了”之后的下一步很多玩过 STM32 的朋友会想起一个叫 Letter Shell 的开源项目。这个库能把单片机的串口变成一个小型命令行支持自动补全、参数解析、历史记录比单纯 printf 查看日志高效得多。既然 SDI Printf 能输出数据那能不能也接一个命令行呢理论上可以但有一个天然的约束SDI 是半双工协议同一根线无法同时收发。如果只是把 Log 通道接到 Shell 的输出端那只能实现“只能看不能输”的伪命令行。要想真正可交互需要 PC 端向调试器发送数据再通过 PDIO 传回给芯片。WCH-Link 的 SDK 里其实有相关的 USB 转发机制可以实现双向透传但配置起来比纯 Printf 要复杂而且不同版本固件的支持程度也不一样。我的实际建议是如果你只是想快速试验一下 Letter Shell 的效果可以用一个真正的 UART 来跑 Shell 的收发SDI Printf 继续保留作为独立的日志输出通道。两个通道互不干扰Shell 的接收走 UART RXSDI Printf 只负责把日志数据吐出来。这样既避免了半双工的麻烦又能同时体验两个调试手段。5.2 实际效果与建议如果你决定走“UART Shell SDI Log”的双通道方案代码结构上建议把日志输出统一封装成一个接口比如void log_info(const char *fmt, ...) { va_list args; va_start(args, fmt); vprintf(fmt, args); va_end(args); }底层调用的是重定向到 SDI 的printf这样项目里所有日志都走同一个接口未来如果想切回 UART 日志只需要改动fputc的重定向目标业务代码完全不用动。实测下来这种组合方式在排查复杂状态机问题时非常高效UART Shell 可以输入指令切换状态、读取变量、执行自检SDI Printf 则把每一次关键状态跳转的时间戳和参数完整记录下来。两者配合定位 BUG 的速度比单独用任何一种都快不少。想深入折腾的话可以把 Shell 的输入通道也做成 USB CDC直接从 WCH-Link 的虚拟串口输入命令。不过这块涉及 USB 上层驱动和高速数据交互建议先把基础功能和坑位摸清楚再做这层扩展。从工程角度讲SDI Printf 是一个典型“知道的人觉得很简单、不知道的人绕远路”的功能。整个链路里最容易出的问题反而不在代码而在硬件连接、调试器固件和编码设置这三个外围环节。我踩过的坑里最典型的一次是用了旧版 WCH-LinkSDI 打印怎么调都没反应换了个新固件的 Link 就好了。如果你遇到“代码没错、就是没输出”的情况先检查 Link 固件版本再检查接线最后才去怀疑代码逻辑。这个顺序能帮你省下不少时间。
返回列表