ARTICLE DETAIL

资讯详情

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

CH32V303 SDI虚拟串口:不占UART的printf调试方案

CH32V303 SDI虚拟串口:不占UART的printf调试方案 很多用RISC-V内核MCU尤其是沁恒CH32V系列的朋友最初都是从STM32转过来的。习惯了STM32上“一个UART接USB转TTL然后printf满天飞”的开发节奏换到CH32V303RCT6之后第一个不习惯的地方就是串口只有一个还够不够用或者说默认的调试口到底能不能省下来干别的沁恒其实给了一个很巧妙的答案——SDI虚拟串口英文全称是Serial Debug Interface也就是调试接口上的单线数据输出功能。配合WCH-Link调试器不用额外占用一个UART不用接USB转串口模块直接在电脑上弹出一个虚拟COM口printf的内容就源源不断地打出来。这个功能我在实际项目里用了很久值得好好拆一拆原理也把踩过的坑整理出来。这个方案本质上解决的痛点是MCU的串口资源本来就不宽裕跟传感器通信、跟无线模块收发、跟主机做协议交互每个功能都要吃掉一个UART留给调试打印的那个口往往就是压死骆驼的最后一根稻草。SDI虚拟串口直接把调试信息输出挂到了SWD调试接口的同一根线上硬件上零额外成本软件上一个函数切换堪称“白嫖”串口的典范。本文围绕CH32V303RCT6这颗芯片从SDI的底层原理、开发环境配置、代码重定向到实际调试中的多种用法一次性讲清楚。不管你是刚拿到开发板准备点灯的新手还是正在做产品调优的老手这篇都可以当一份参考手册来用。1. 项目概述SDI虚拟串口解决的到底是什么问题1.1 为什么嵌入式调试离不开Printf嵌入式开发里printf的地位一直很微妙。正经工程师都知道“printf调试法”听起来不够高级但真到了现场排查问题时手里有一个能随时打印变量、打印状态机跳转、打印错误码的家伙比什么都管用。示波器能看到波形逻辑分析仪能看到协议时序但你看不到一个全局变量在某个时刻为什么变成了奇怪的数值也看不到一个函数在返回值异常之前到底执行到了哪一步。printf就是那个能把“程序内部状态”变成人可读文本的最短路径。早期MCU资源紧张一个串口都要省着用那时候大家普遍用LED灯闪烁次数来代表错误码或者用J-Link的SWO引脚输出调试信息。到了CH32V303RCT6这种主频144MHz、Flash有256KB的芯片上打印输出的成本已经变得很低主要矛盾反而变成了“串口接口数量够不够用”。CH32V303RCT6虽然有多个UART外设但封装引脚有限很多引脚还要复用给ADC、SPI、I2C、定时器PWM真正能空闲出来专门用作调试打印的UART经常根本没有。1.2 SDI虚拟串口相比传统UART打印的优势SDI虚拟串口最大的优势可以从三个角度看。第一是省引脚。传统UART打印至少需要TX一根线如果还要接USB转串口模块那目标板上还得留3.3V和GND。SDI虚拟串口直接复用PA13这根SWDIO引脚烧录和调试时它是调试数据线运行阶段切换成串口数据输出线引脚数量零增。对做小尺寸PCB或者引脚分配非常紧张的项目来说这一个引脚的节省可能就直接决定了方案的可行性。第二是省硬件成本。传统方案里如果要把日志上报到电脑必须配一个USB转UART的桥接芯片比如CH340、CP2102这些。SDI方案只需要一个WCH-Link调试器而WCH-Link本来就是你烧录调试CH32V系列必备的工具相当于把已有的调试器功能复用了一下电脑端多了一个虚拟串口设备不用再买任何额外硬件。第三是接入门槛低。WCH-Link在PC端会枚举出一个VCOM虚拟串口打开任意串口调试助手选对COM口号和波特率就能看到MCU发来的数据。整个过程跟用普通串口没有任何区别不需要协议转换不需要特殊驱动流程甚至连波特率都不需要跟MCU端精确到分毫不差因为SDI的输出时钟是独立的不用依赖外部晶振的精确分频。2. 原理拆解一根线上跑出来的“虚拟串口”2.1 认识SDI单线调试接口SDI的全称是Serial Debug Interface是沁恒在自家RISC-V内核MCU上实现的一种单线调试接口。和ARM内核芯片上常见的SWD两线调试接口SWDIO SWCLK不同SDI只使用一根数据线时序协议里同时承载了调试命令和调试数据。CH32V303RCT6的SDI引脚默认就是PA13也就是我们常说的SWDIO芯片出厂时这个引脚承担着烧录和在线调试的功能同时也具备被复用为数据输出的能力。这里有一个容易混淆的点SDI的“调试数据输出”模式和“普通UART TX”模式并不是同一回事。SDI虚拟串口更像是把一根引脚从调试协议里“借”出来充当一个单线的UART发送端。但由于底层硬件设计的原因这个输出并不需要占用UART外设而是由芯片内部的调试模块直接产生符合串口电平时序的波形。这也就意味着即使你把所有UART外设都用完了SDI依然能作为独立的日志输出通道存在。2.2 数据通路分析MCU内部到电脑串口的完整链路从MCU内部到电脑的串口助手这条数据通路其实分成了四段。第一段是MCU内部的printf重定向。标准C库的printf函数最终会调用一个底层字符输出函数比如_write或者fputc。我们需要做的就是把数据传递给调试模块的寄存器由调试模块将并行的字节数据转换成串行的单线时序信号从PA13引脚输出。第二段是PA13到WCH-Link的物理连接。正常调试时WCH-Link的SWDIO引脚和MCU的PA13之间就是一根杜邦线或者PCB走线。SDI输出模式下PA13直接产生UART格式的电平信号通过这根线进入WCH-Link。第三段是WCH-Link内部的桥接处理。WCH-Link识别到目标芯片的SDI输出数据后会把它重新封装成USB串口数据包通过USB接口上报给PC主机。这个过程对用户完全透明。第四段是电脑端的虚拟串口呈现。WCH-Link在系统里枚举出一个VCOM设备对应一个COM口号串口助手软件打开这个COM口就能读到MCU发来的文本。整条链路里最容易出问题的是第一段的配置和第四段的驱动识别中间两段只要硬件接线没问题一般都很稳。2.3 SDI Printf与普通串口打印的对比为了更直观地看明白这个方案的价值可以直接对比一下SDI Printf和普通UART打印的差异。对比项SDI虚拟串口普通UART打印占用引脚PA13复用SWDIO一个TX引脚通常还要一个RX可选占用UART外设不占用占用一个UART额外硬件无用WCH-LinkUSB转串口模块或板载转接芯片电脑端接口WCH-Link虚拟串口USB转串口设备波特率匹配无需严格匹配需与串口助手一致在线调试同时打印需要根据固件模式区分可以完全并行适合场景UART资源紧张、板子空间有限UART资源充裕、需要双向通信从这张表能看出来SDI虚拟串口并不是一个“全能替代品”它也有自己的局限之后在踩坑部分会详细说。但单论调试信息输出这个场景它确实是最省资源、最省事的一条路。3. 工程搭建让工程从“哑巴”变“话痨”3.1 开发环境与硬件准备要玩转SDI Printf需要准备的软硬件并不复杂。硬件方面一块CH32V303RCT6核心板或自制目标板一个WCH-Link调试器建议用最新固件版本老固件对SDI虚拟串口的支持有差异两根杜邦线用于连接SWDIO和GND。注意传统SWD调试需要SWDIO、SWCLK、GND三根线但在SDI打印场景下PA13复用时不再需要SWCLK所以最少只需要两根线。如果后续要恢复在线调试再把SWCLK接上即可。软件方面推荐使用MounRiver Studio简称MRS这是沁恒官方基于Eclipse二次开发的IDE对CH32V系列的支持最完整工程模板里已经集成好了SDI相关库函数。另外还需要准备一个串口调试助手任意你用得顺手的都行推荐支持UTF-8和GBK编码切换的中文日志会用到。WCH-Link的驱动需要正常安装装好之后在设备管理器里能看到一个“WCH-Link”设备以及一个虚拟串口通常命名为“WCH-Link VCOM”或者类似名称。3.2 使能SDI输出功能的关键配置在MounRiver Studio里新建CH32V303RCT6工程后默认的代码其实已经包含了调试打印的轮子只是默认情况下没启用。核心函数是SDI_Printf_Enable()这个函数在官方EVT的debug.c中定义作用是配置PA13的复用模式把它从SWD调试数据线切换为SDI数据输出线同时调整相关时钟和引脚参数。从寄存器层面看这个函数做的事情大致有三步一是使能GPIOA时钟和AFIO时钟二是把PA13配置为复用推挽输出模式并且复用功能选择到SDI输出三是使能SDI模块的输出使能寄存器。不同批次的芯片可能在寄存器名称上有细微差异但功能逻辑一致。如果你用的是官方标准库或HAL库直接调用即可如果自己写寄存器操作需要查阅对应芯片参考手册里SDI章节的寄存器描述。实操时把这个函数放在main函数最前面紧接着时钟初始化之后就好。比如int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); SystemCoreClockUpdate(); Delay_Init(); USART_Printf_Init(115200); // 这里其实是普通UART打印的初始化 SDI_Printf_Enable(); // 关键切换到SDI输出 printf(SDI printf test\r\n); while(1) { // 主循环逻辑 } }这里有一个很容易误导初学者的点官方EVT模板里通常还会调用USART_Printf_Init(115200)来初始化一个普通UART并重定向printf。如果你要使用SDI打印就必须在调用USART_Printf_Init之后再调用SDI_Printf_Enable否则printf的输出仍然会走普通UART。正确做法是如果确定只用SDI打印就不要调用USART_Printf_Init或者让SDI的初始化覆盖掉UART的重定向配置。3.3 搞定printf函数重定向printf重定向的本质是把标准C库中负责字符输出的底层函数替换成我们自己的实现。在MounRiver Studio的GCC工具链环境下需要重定向的函数是_write原型为int _write(int file, char *data, int len) { for(int i 0; i len; i) { // 将单个字符交给SDI输出函数 } return len; }官方EVT里通常使用fputc但在新版本的newlib库中_write才是真正的输出入口。更稳妥的做法是同时实现fputc或者直接实现_write具体看你的工具链版本。我自己的经验是在MRS里用_write最靠谱可以避免printf缓冲区不刷新导致的数据丢失问题。如果不想修改底层重定向函数也可以直接调用SDI模块提供的单字符输出函数来打印比如SDI_Printf_Enable之后底层会用一个名为SDI_SendData或类似的函数发送单个字节。但这种用法每次都要自己转换格式比较麻烦不推荐。4. 代码实现一个函数搞定日志输出4.1 核心初始化代码详解完整的SDI打印代码其实非常短核心就是三步使能时钟、配置PA13复用、打开输出。参考官方EVT的写法贴一份可以直接用的代码#include debug.h void SDI_Printf_Enable(void) { GPIO_InitTypeDef GPIO_InitStructure {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); /* 启用SDI功能具体寄存器操作参考实际库版本 */ SDI_Output_Enable(); }这几行代码里GPIO_Mode_AF_PP是为了把PA13配置成复用推挽输出GPIO_Speed_50MHz保证信号边沿足够陡峭满足较高的波形质量要求。SDI输出的波特率并不像普通串口那样需要两端约定它内部有独立的时序处理但在开启之前必须确保PA13没有被其他外设占用尤其要注意某些板载LED或按键会不会跟PA13打架。4.2 重定向代码的写法细节重定向函数的细节直接决定printf能不能正常工作。一个常见的坑是重定向函数里只调用了一次输出函数就返回了导致printf内部缓冲区没有正确清空。正确写法是逐字节发送并且发送完一个字节后等待一点时间确保数据完整从引脚送出去。例如int _write(int file, char *data, int len) { for (int i 0; i len; i) { while (SDI_IsBusy()); // 等待上一个字节发送完成 SDI_SendData((uint8_t)data[i]); } return len; }SDI_IsBusy是判断当前发送是否完成的函数具体名字可能因为库版本不同而略有出入但原理一样。如果不加这个等待连续高频调用printf时可能出现丢字节、乱码或卡死的问题。另一个细节是_write的返回值必须等于len表示所有数据都被成功发送。如果返回值不等于传入的长度printf内部会认为写入失败可能会重试或者丢弃后面的数据。这个细节在标准库的实现中非常关键很容易被忽略。4.3 中文乱码问题的两个根源与对策关于网上讨论最多、踩坑最密集的“printf中文乱码”问题我在CH32V303RCT6的SDI打印上遇到过很多次总结下来源头基本就两个。第一个根源是源码文件的编码格式与IDE内部编译器默认的字符集不一致。MounRiver Studio基于Eclipse默认工作空间编码可能是GBK而Windows下的串口助手通常默认GBK解码。如果你的源码文件是UTF-8编码中文字符串在编译时以UTF-8字节序列被编入固件串口助手用GBK解码就会变成乱码。对策是把源码文件统一转换为UTF-8编码或者把工程编译字符集设置为GBK然后在串口助手里也选择GBK解码。最省心的办法是所有日志尽量用英文确需中文时保证“源码编码”和“串口助手解码”两边一致。第二个根源是串口助手的编码选项设置错误。常用的串口助手比如SSCOM、XCOM通常在界面右下角或者设置里有编码选择有的默认ASCII/GBK有的默认UTF-8。用SDI打印时因为数据是PC端WCH-Link虚拟串口接收的编码处理方式跟普通USB转串口没有区别。如果你在工程里统一用了UTF-8却忘记把串口助手切换到UTF-8乱码是必然的。我个人实际调试时的做法是先用纯英文打印跑通链路确认SDI功能正常再逐条加入中文日志。这样一旦出现乱码至少能确定问题出在编码环节而不是SDI链路本身。5. 实战记录把调试利器用起来5.1 完整接线与烧录流程实测接线非常简单。WCH-Link的SWDIO接到CH32V303RCT6的PA13WCH-Link的GND接到板子的GND如果目标板是独立供电还需要共地。连接完成后在MounRiver Studio里编译工程用WCH-Link烧录。烧录过程不需要额外处理正常下载即可。烧录完成后打开设备管理器确认虚拟串口已经出现。如果没有出现先检查WCH-Link的驱动是否正常再把WCH-Link重新插拔一次。如果设备管理器里只看到“WCH-Link”设备但没有VCOM说明当前WCH-Link固件版本过旧需要用WCH-LinkUtility工具升级固件到支持VCOM的版本。5.2 实测printf输出的过程记录我测试时用的串口助手是SSCOM设置如下端口选择WCH-Link VCOM对应的COM号波特率随便填比如115200数据位8、停止位1、无校验。打开串口后按下目标板复位键立刻就能看到模板初始化和主循环里的printf输出。实际打印效果举例[SYSTEM] CH32V303RCT6 SDI Printf Demo [INFO] SystemCoreClock 144000000 [INFO] FreeRTOS Start, Heap: 4096 [MAIN] Loop counter: 1 [MAIN] Loop counter: 2这里有个值得注意的现象SDI打印的波特率设置其实不影响显示效果因为虚拟串口在PC端只是“接收”UART数据数据内容本身不依赖波特率解析。这也是SDI和普通串口在体验上的一个巨大差异——你再也不用担心串口助手波特率选错导致乱码了。5.3 进阶玩法日志分级、断言与命令行SDI Printf跑通之后就不应该只局限于“能看到printf就行”的水平。我在项目里把SDI打印玩出了三种更实用的形态。第一种是日志分级输出。封装一个简单的宏比如LOG_INFO、LOG_WARN、LOG_ERROR内部根据全局日志等级决定是否调用printf。这样产品发布时可以把日志等级调到ERROR只输出关键错误开发时调到DEBUG所有细节都打出来。SDI打印不占用UART外设所以即使等级全开也不用担心影响其他通信功能。第二种是断言辅助。在参数校验、状态机异常等关键位置加入断言宏比如#define ASSERT(x) \ do { \ if (!(x)) { \ printf(ASSERT FAILED: %s line %d\r\n, __FILE__, __LINE__); \ while(1); \ } \ } while(0)程序跑飞或者逻辑异常时SDI串口会立刻输出出错位置能极大缩短定位问题的时间。第三种是将简易交互式命令行引入SDI串口。既然SDI虚拟串口在PC端是一个标准COM口那它天然就可以支持“MCU收——PC发”的双向交互。虽然SDI的引脚是单线输出不能直接接收PC发来的数据但你可以把WCH-Link的另一个通道或者利用USB虚拟串口的反向通路做点文章。我在一个项目中用letter shell命令行框架让PC端通过WCH-Link的虚拟串口发送命令给MCUMCU通过SDI打印响应结果实现了“动动键盘就能实时查看和修改参数”的调试体验。这种方式比单纯printf更有交互感适合做设备内部状态观测和参数整定。6. 踩坑笔记常见问题与排查实录6.1 常见问题速查表问题现象可能原因排查方法与解决设备管理器没有VCOMWCH-Link固件太旧升级WCH-Link固件打开串口后无任何输出PA13与其他外设冲突检查板卡原理图释放PA13复用输出乱码源码编码与串口助手解码不一致统一编码或用英文打印打印一段后卡死SDI发送未等待完成_write里增加忙等待逻辑复位后只能打印一次SDI打印模式受调试模式影响检查是否处于在线调试模式SDI打印时尽量使用运行模式和在线仿真冲突SDI功能与SWD调试功能互斥先烧录后切换运行模式再打印这张表里最值得展开说的是“和在线仿真冲突”这一条。SDI功能把PA13从调试口切换成数据输出口之后理论上在线调试功能会受到影响。实际情况是如果你用MounRiver Studio点击调试Debug会把芯片置于调试模式此时SDI的打印输出也会被调试协议干扰经常出现“能进调试但看不到打印”或者“能看到打印但程序暂停不了”的尴尬局面。我的建议是调试阶段用在线仿真和断点需要长期观察运行数据时切换到直接运行Run模式这时候SDI打印最稳定。6.2 三个让人头疼的坑和解决方法第一个坑是SDI打印与看门狗冲突。如果工程里打开了IWDG独立看门狗而喂狗操作在中断里printf输出比较慢时可能会拖长主循环时间导致看门狗复位。解决办法是不要在持续高频的日志输出中做太多格式拼接或者把看门狗喂狗放在定时器中断里不要让主循环的日志打印阻塞喂狗时机。第二个坑是中断内使用printf导致系统卡死。中断里调用printf如果_write里带了忙等待逻辑而SDI发送的优先级比当前中断低就可能产生死锁。我的经验是中断里只做标志位设置把日志输出放到主循环或者专用的日志任务里。如果实在要在中断里打印就用直接发送单字符的非阻塞方式尽量减少连续输出。第三个坑是WCH-Link连接不稳定带来的“幽灵数据”。有时候SDI打印会偶发出现一个奇怪的字符或者整行数据偶尔丢失。排查之后发现是杜邦线接触不良特别是PA13引脚的插线松了。因为SDI本身是高速单线协议对信号质量有一定要求建议接线尽量短或者直接用排针焊接而不是插杜邦线。这个问题在STM32的SWO调试输出中也很常见本质都是差不多的信号完整性问题。这三个坑看起来都不复杂但在项目交付阶段遇到时往往很折腾人。我的建议很简单日志输出尽量放在主循环和任务级代码里接线焊死不要用杜邦线编码统一不要混用这样80%的坑都可以提前规避。从CH32V303RCT6的SDI Printf虚拟串口这个功能上我能明显感受到沁恒在“降低嵌入式调试门槛”这件事上的用心。对开发者来说真正省下来的不只是那一个UART更是调试过程中不断插拔线、来回设置波特率、纠结引脚分配的烦心时间。我现在做CH32V系列项目基本默认把SDI打印当成主力日志通道只有在需要在线断点调试时才临时切回SWD模式。如果你手头正好有一块CH32V303RCT6开发板建议花十分钟把SDI Printf跑通这个功能用一次就很难回去了。
返回列表