资讯动态

没有USB转TTL?用Keil MDK虚拟串口调试STM32串口全攻略

发布时间:2026/9/29 7:42:23 来源:尧图企业网站定制
如果你跟我一样经常在深夜写完一段串口收发代码却发现手头既没有USB转TTL模块开发板上唯一的串口又被别的传感器占着只能把数据一条条打进调试器的Watch窗口里核对那这篇文章就是写给你的。我在Keil MDK里折腾虚拟串口调试串口这套流程前后试过VSPD、Com0Com、SSCOM、Commix这些工具也踩过驱动签名、printf卡死一类的坑最后的结论是在MDK里把串口调试搬进纯软件环境完全可行而且对验证协议、缓冲区逻辑和上位机交互特别有用。这篇文章不是教你背命令而是把为什么要用虚拟串口、虚拟串口到底是什么、没有硬件时怎么搭一条完整的串口调试链路讲透。适合三类人看刚接触STM32和Keil MDK的新手手头缺硬件的学生党以及需要在PC端联调串口协议但又不想频繁拔插USB转TTL的工程师。后面所有步骤我都在Windows 10/11 Keil MDK 5.36/5.37 STM32F103的环境下实际跑过你照着操作基本能复现。1. 没有USB转TTL时串口调试为什么卡在写代码容易验证难先说一个很现实的问题串口调试这个动作本质上是在验证两件事——数据格式对不对和时序逻辑对不对。但大多数时候我们手边没有完整的硬件链路尤其是写上位机、做通信协议、验证环形缓冲区时缺一个USB转TTL模块就卡住了。1.1 典型场景硬件不在手边但协议必须调我遇到最多的情况是这三类写了一个STM32程序要通过USART向上位机上报传感器数据但开发板还在路上手边只有一台装了Keil MDK的Windows电脑。项目里有两个MCU需要通过串口通信A板发帧、B板解析但手里只有一块板子另一块还没打样。正在写一个Qt/C#/Python的串口上位机程序需要连续接收数据帧并解析但下位机的固件还没写完没有真实设备可连。这些场景的共同点是**代码逻辑有一半在MCU里另一半在PC端而中间的物理连接不存在。**如果非要等硬件到位再调项目进度至少要往后拖一两天。虚拟串口的作用就是把中间那条物理串口线先从软件层面画出来让两侧的程序先跑起来。1.2 虚拟串口解决的是逻辑验证不是电气特性这里必须先泼一盆冷水虚拟串口模拟不了电平翻转、模拟不了线路噪声、模拟不了波特率失配时那种偶发乱码也模拟不了RS485的方向切换时序。它只能在数据链路层上给你一个足够真实的串口编程接口。打个比方真实串口调试是两个人隔着一条马路喊话你要验证的是声带、距离、环境噪音这些物理因素虚拟串口相当于两个人用对讲机在一个房间里通话你验证的只是我说的话你能不能听懂。协议格式、帧头帧尾、校验和、超时重发这些逻辑用虚拟串口完全能调明白但你要是担心实际线缆过长导致波形畸变那还得靠真机测试。所以我的建议是**虚拟串口负责把逻辑问题清零真实硬件负责验证电气边界。**二者是先后关系不是替代关系。2. 虚拟串口的模拟原理为什么它总是一对一对地出现很多人第一次打开VSPD这类软件时会有一个困惑为什么创建串口不能只建一个COM3非要同时创建COM3和COM4回答这个问题就理解了虚拟串口的全部原理。2.1 VSPD这类软件到底做了什么VSPDVirtual Serial Port Driver这类软件本质上是在Windows系统里安装了一个虚拟串口驱动。它向操作系统注册出若干标准的COM口设备这些COM口在设备管理器里看起来和真实串口一模一样任何调用CreateFile、ReadFile、WriteFile这些Windows串口API的程序都能正常打开它们。注意这个细节很重要驱动层面提供的是一套完全兼容标准串口的接口。所以串口调试助手、自己写的Python脚本、LabVIEW程序都不需要做任何修改当成普通串口打开就行。关键区别在于数据通路。真实串口的数据流是MCU的USART外设 → 电平转换芯片MAX3232等 → USB转TTL芯片 → Windows串口驱动 → 应用程序而虚拟串口对的数据流是应用程序A → Windows串口API → VSPD驱动 → 内存管道 → VSPD驱动 → Windows串口API → 应用程序B中间没有电平转换也没有USB包传输就是驱动在内存里做数据搬运。2.2 为什么必须成对创建串口是双向管道串口通信永远是双向的。一个应用程序往COM3写数据这些数据最终要能被另一个应用程序从某个地方读到。如果虚拟串口不成对存在数据写了就丢了收发链路是断的。VSPD的做法是创建一个串口对COM3和COM4被当作一条管道两端。往COM3里写的数据会从COM4里被读出来反过来往COM4里写COM3能读到。这个设计很像一根真实的串口线一头插在设备A上一头插在设备B上。你愿意把哪个口分配给串口助手把哪个口分配给自己的程序完全取决于你想让谁收、谁发。2.3 波特率在虚拟串口里的真实意义虚拟串口并不是完全无视波特率。VSPD在创建串口对时允许你设置初始波特率驱动也会记录每次程序打开的波特率参数。但由于数据本身走内存管道实际传输速率不取决于波特率而取决于驱动内部管道写得多快。这意味着什么你可以用9600波特率打开虚拟串口程序收发数据照样飞快不会像真实串口那样每秒只传9600个位。但有一个坑必须提醒如果你在程序里做了基于波特率的超时计算比如每字节传输时间10bit/波特率在虚拟串口上这个计算结果会严重偏离实际导致超时逻辑误判。我在调Modbus协议时就被这个坑过真实设备上1.5字符超时是几百微秒虚拟串口上却几乎瞬时完成一开始怎么调都对不上。结论是虚拟串口上的波特率只具有形式意义你最好把波特率设成和目标设备一致方便后续无缝切换到真机但不要依赖它做时序计算。3. 工具选型与安装VSPD、Com0Com与串口调试助手的搭配这个领域工具不少但真正稳定好用的其实就那么几个。我从实用角度做个对比然后说清楚安装时最容易出问题的地方。3.1 主流的虚拟串口软件横向对比我自己实际用过VSPD、Com0Com和Windows自带的串口映射功能列个表方便你选工具开源/免费创建串口对串口扩展/共享Windows 11兼容性适用人群VSPDEltima商业软件有试用期支持很方便支持好怕折腾、需要高级功能的人Com0Com完全开源免费支持需命令行或第三方GUI不支持需签名驱动略折腾喜欢折腾、追求免费的人Windows虚拟串口系统自带不支持直接创建部分场景支持好特定USB串口芯片扩展我的主力工具是VSPD原因有三个一是创建串口对时可以自定义串口号避免和已有的蓝牙串口冲突二是它支持把一个物理串口扩展成多个虚拟串口这样两个程序可以同时打开同一个物理串口一个负责收数据一个负责记录日志这在真机联调时非常有用三是卸载干净不会像某些驱动软件一样卸完系统还残留一堆无效COM口。如果你预算有限Com0Com完全够用。它不需要安装GUI在命令行里执行install.bat就能创建出默认的COM3/COM4对也能通过修改参数创建多对串口。但要注意Com0Com在Windows 10/11上需要驱动签名步骤比VSPD多一些。3.2 串口调试助手的选择SSCOM、Commix、XCOM有了虚拟串口还得有个对面的人来收发数据。串口调试助手我前后换过很多现在固定用的组合是接收MCU主动上报的数据用SSCOM。它收数据时不会抢CPU支持hex和ascii混合显示保存日志也方便。模拟上位机发送数据帧用Commix或者直接写Python脚本。Commix支持定时发送、支持按帧间隔发送适合模拟周期上报。临时看个数据XCOM也行界面干净但功能相对简单。这里有个容易被忽略的点虚拟串口调试时数据收发速度可能非常快如果调试助手显示性能不行几千条数据刷过去界面直接卡死。所以工具尽量选SSCOM或Commix这类老牌稳定的别用那种界面花哨的小众助手。3.3 安装与驱动签名的坑VSPD安装本身不复杂一路Next就行。但有两个坑我踩过第一某些版本在Windows 11上安装完创建串口时会提示驱动未签名。解决方法是进入Windows的高级启动选择禁用驱动程序强制签名再重新安装一次驱动。注意这个选项只对当前启动生效重启后会失效所以最好一次装完再创建串口对。第二创建虚拟串口对时如果提示串口号被占用别急着把那个口删掉。先在设备管理器里看清楚是哪个程序占用的有些蓝牙模块会预占COM3到COM6你把VSPD的串口对改成COM7/COM8就好不要去动蓝牙设备。4. STM32工程侧的准备工作USART初始化、printf重定向与收发方式取舍虚拟串口只是一个桥梁真正要调试的代码还是得在Keil MDK里跑。这一节我把STM32这边需要做好的准备工作讲清楚顺序是USART初始化、printf重定向、收发方式选择。这些是一切调试的基础。4.1 STM32F103的USART1初始化示例我用标准外设库和HAL库都可以不过考虑到网上资料最多、新手最容易上手的还是标准外设库加F103。下面是一段我常用的USART1初始化代码波特率115200、8位数据、无校验、1位停止位void USART1_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; // TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; // RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }这段代码没什么特殊的地方值得注意的就是中断优先级。如果你后面在FreeRTOS或者复杂中断系统里调试串口优先级要仔细规划否则数据收发时可能出现不可预期的丢字节。4.2 printf重定向不勾选MicroLIB就会卡死在Keil MDK里用printf输出串口日志是最常用的调试手段。标准做法是重定向fputcint fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }但这里有一个非常关键的设置需要在Keil MDK的Options for Target → Target标签页里勾选Use MicroLIB。如果不勾选程序编译没问题运行时会卡在printf的第一条语句上因为标准C库默认把printf输出到半主机模式的调试通道不死等在哪才怪。我当时第一次遇到这个问题排查了很久。程序跑在MDK的仿真器里点全速运行代码就停在printf内部Watch窗口怎么查都查不出问题。最后才想起来是半主机模式没有用MicroLIB关掉。这个坑几乎每个用MDK的人都会踩一次先写在这里后面章节还会讲排查链路。4.3 收发方式取舍调试阶段别一上来就DMA串口收发有轮询、中断、DMA空闲中断三种经典方式。很多新手一上来就照搬例程用DMA结果虚拟串口调试时数据时序不对反而不知道问题出在哪。我的建议是分场景选择调试阶段验证逻辑轮询发送中断接收足够。简单、容易定位问题printf也不受影响。验证协议帧解析中断接收配合一个简单的状态机能看出每一字节到达的时机是否正确。压测大数据吞吐DMA空闲中断。但前提是前面的逻辑已经用中断方式调通了不然DMA的缓冲区指针和中断回调叠加在一起排查难度会翻倍。下面是一段中断接收的示例我把收到的字节放进环形缓冲区避免在主循环里频繁关中断#define RX_BUF_SIZE 256 volatile uint8_t rx_buf[RX_BUF_SIZE]; volatile uint16_t rx_head 0; volatile uint16_t rx_tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { rx_buf[rx_head] data; rx_head next; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }有了这个环形缓冲区你就可以把收串口数据和解析数据帧解耦中断只管往缓冲区里塞字节主循环里做轮询解析。这个结构在真实硬件和虚拟串口调试时行为完全一致可以无缝切换。5. 实战链路MDK里最常见的三种虚拟串口调试连接方式铺垫了这么多终于到正题了。我在实际开发中总结出三种在Keil MDK环境下用虚拟串口调试串口的连接方式从零依赖到全仿真按需选择。5.1 链路A零外部依赖——MDK Simulator自带的串口窗口如果你的需求只是看看printf输出、验证某个函数的执行顺序那连虚拟串口软件都不用装。Keil MDK内置的Simulator模式可以模拟STM32的外设包括USART。操作步骤在Keil MDK中打开工程点击Options for Target → Debug标签页。选择Use Simulator点击OK。编译工程点击Start/Stop Debug Session进入仿真。在仿真界面中点击View → Serial Windows → UART #1打开串口窗口。全速运行程序printf输出的内容会直接显示在这个串口窗口里。这个方式的优点是完全免费、零配置、一个人就能玩。缺点是它只能在MDK的窗口里看外部程序读不到这些数据没法做上位机联调。适合刚移植完代码、想快速确认基本逻辑的新手。5.2 链路BVSPD虚拟串口对 串口助手 模拟MCU程序完整体验闭环这个链路是这篇文章的核心也是我建议所有要做串口协议开发的人掌握的。它的思路是在PC上创建一对虚拟串口COM3/COM4COM4接串口助手模拟接收端COM3接你自己的串口测试程序模拟MCU发送端从而在没有真实硬件的情况下做一次端到端联调。具体操作安装VSPD打开主界面点击Add pair创建COM3和COM4。打开SSCOM选择COM4波特率1152008N1打开串口。用Python、C#或者C写一个小程序打开COM3按协议定时发送数据帧。以Python为例import serial import time ser serial.Serial(COM3, 115200, timeout1) while True: # 模拟MCU周期上报帧头(0xAA) 长度(0x02) 数据(0x01 0x02) 校验(0x03) frame bytes([0xAA, 0x02, 0x01, 0x02, 0x03]) ser.write(frame) time.sleep(1)回到SSCOM窗口会看到每一秒收到一帧完整数据。如果SSCOM支持hex显示数据看起来会非常直观。有人会问这个流程和Keil MDK有什么关系关系在于你在MDK里写的上位机联调代码或者辅助测试代码可以复用这套链路。比如你在工程里写了一段模拟另一端设备的代码在Simulator里跑或者临时抽出来在PC上编译跑都能直接往虚拟串口上灌数据。更进一步如果你正在做的是两个MCU之间的串口协议联调可以在MDK里把设备A的程序编译好刷到真实板子上然后把板子的USART TX/RX接到USB转TTL模块模块插到电脑上形成物理COM口。接着用VSPD把这个物理COM口扩展成两个虚拟串口一个给串口助手监控数据一个给设备B的上位机解析程序。这就是链路B的进阶用法也是虚拟串口在真实开发中最有价值的使用场景。5.3 链路CMDK调试器 真机串口 VSPD扩展多个软件同时监视同一路串口第三种链路是工程上最常见的需求程序在STM32真机上跑串口输出的日志只有一根USB转TTL线但你想同时用SSCOM看数据、用自己写的Python脚本做关键字告警、用串口监视器抓协议帧。这三个程序都想打开同一个COM口Windows默认只允许一个程序独占。VSPD的Split功能可以解决这个问题。它能把物理COM5复制成COM6、COM7等多个虚拟串口每个虚拟串口都能独立打开数据流在所有串口之间广播。于是你可以在SSCOM里看着数据滚动Python脚本同时在后台做协议统计两边互不干扰。这个用法有个好处不会影响原程序的调试体验。因为真正的物理串口只有一个程序在读写VSPD只是做了一个分发操作数据给多个监听者各复制一份。对正在跑的MCU来说它完全感知不到后面有几个程序在监听。6. 进阶协议验证、模拟双MCU通信与DMA缓冲区调试基础链路跑通之后就可以开始做一些真正的调试工作了。这一节分享三个我在实际项目中反复用到的进阶玩法全是基于虚拟串口实现的。6.1 用虚拟串口验证自定义协议帧的边界条件做通信协议调试最怕的不是正常数据而是异常数据。真实硬件调试时想制造缺字节多字节校验错误这些异常比较麻烦因为你没法精确控制串口线路上出现的每个字节。但虚拟串口让这一切变得非常简单。你可以在COM4端用Commix的定时发送功能手动输入一段只有帧头没有帧尾的数据然后观察另一端设备程序是否能在超时后正确报错也可以故意把校验位算错看看协议栈的容错逻辑是否有效。我在调一个自定义的二进制协议时就是在虚拟串口上一次性构造了几十种异常帧批量跑完才上真机。如果没有虚拟串口这些测试全得靠按键和跳线去造效率完全不是一个量级。6.2 模拟双MCU通信一个程序扮演两个角色很多项目里是两个MCU通过串口协作比如主控板向传感器板发查询指令传感器板回传数据。在没有第二块开发板的情况下虚拟串口对可以让你在同一台PC上模拟出两端行为。操作方法是写两个Python/串口脚本一个模拟主控端的查询逻辑一个模拟传感器端的应答逻辑分别打开COM3和COM4。然后在MDK的Simulator里跑你的解析代码用串口窗口确认数据帧是否被正确解析。我还试过更极致的方式在Windows里用pySerial写一个虚拟传感器固件专门按协议回传传感器数据让MDK Simulator里的主控程序通过虚拟串口和外部的虚拟传感器完成一次完整的握手-查询-应答-超时重试流程。这样整条链路都不需要真实硬件但逻辑层面和周整机联调几乎一致。6.3 Debug模式下查看结构体变量与环形缓冲区串口调着调着难免要深入Debug模式看内存。很多人在MDK的Watch窗口里只能看到普通全局变量看到数组和结构体就不知道怎么看。这里分享两个实用技巧。第一个技巧是Watch窗口直接展开结构体。你在Watch窗口里添加结构体变量名点开前面的加号就能逐成员查看。如果结构体里有数组还可以输入结构体名.数组名, 数组长度的形式比如device_status.rx_buffer, 64这样能将数组的前64个字节以列表形式展示。第二个技巧是在仿真时观察环形缓冲区。你可以把示例代码里的rx_buf和rx_head、rx_tail都添加到Watch窗口。程序每收到一帧数据刷新一下Watch就能看到缓冲区的读数在变化。如果发现head和tail的距离越来越大说明数据处理速度跟不上接收速度这就是典型的丢数据前兆。7. 实测中的坑与排查链路虚拟串口不工作、HardFault、printf卡死最后这部分我把实际调试中踩过的坑和排查思路完整记录下来。每个坑都给出现象→排查过程→结论→解决办法的完整链路希望能帮大家省下一些无谓的排查时间。7.1 虚拟串口打不开提示串口被占用现象VSPD创建了COM3/COM4串口助手打开COM3报端口被占用。排查过程打开设备管理器查看端口COM和LPT确认COM3确实存在。打开任务管理器看看是不是有残留的Python进程或其它串口程序在后台占用了COM3。在命令行执行mode命令可以列出系统所有COM口状态。结论绝大多数情况是某一次运行的程序异常退出没有正常释放串口句柄。Windows不会自动回收那个句柄导致串口一直被占用。解决办法重启一次系统或者在任务管理器里把所有可疑进程结束掉再重新打开串口。如果经常遇到建议在程序里加入异常退出时的关闭逻辑比如Python的try...finally里调用ser.close()。7.2 MDK仿真时printf卡死现象工程在仿真模式下全速运行程序停在printf内部单步也出不来。排查过程点击暂停按钮程序停止的位置在fputc里的while循环中。查看USART的SR寄存器发现TXE标志一直为0说明USART外设状态不对。检查工程配置发现Options for Target里没有勾选Use MicroLIB。结论MDK的标准C库printf默认走半主机模式会触发软件中断等待调试器处理。工程没勾MicroLIBprintf自然就卡住了。解决办法勾选MicroLIB重新编译下载。这个坑我在第4章提到过但因为太典型值得放在排查链路里再强调一次。7.3 虚拟串口数据收不到但程序运行正常现象设备程序往虚拟串口发送数据串口助手那边收不到任何内容。排查过程先确认发送端程序打开的是哪个串口接收端打开的是另一个串口。很多人里把COM3的写操作对应到COM3的读操作那当然收不到——数据是从COM4出来的。检查串口助手的波特率虽然虚拟串口对波特率不敏感但不匹配时有些助手会拒绝显示。用VSPD自带的诊断工具看两个端口的连接状态确认串口对没有被断开。结论这个坑其实是我自己粗心把虚拟串口对都想当然地当作一个口收发忽略了它本质上是一条管道两端。解决办法在调试初期就用串口助手同时打开两个口先手动确保COM3发数据COM4能收到再做上位机联调。快速验证虚拟串口对是否正常这一步很重要。7.4 真机调试时进入HardFault现象代码在Simulator里跑得好好的刷到真机上运行一会儿就进入HardFault_Handler。排查过程在HardFault_Handler里打断点查看SCB-HFSR、SCB-CFSR寄存器的值。发现是总线错误BusFault访问了非法地址。检查指针发现是DMA接收缓冲区的地址没有正确对齐或者缓冲区内存在中断里被意外修改。结论这个坑不能全怪虚拟串口但在虚拟串口环境下写DMA代码时特别容易忽略地址对齐问题。因为Simulator对内存访问的严格度不如真实硬件很多非法访问在仿真时不报错。解决办法DMA缓冲区的定义使用__attribute__((aligned(4)))并确保缓冲区生命周期覆盖整个DMA传输过程避免在栈上定义大块DMA缓冲区。7.5 常用串口调试工具与应对场景速查工具作用应对场景VSPD创建虚拟串口对、扩展物理串口没有硬件时的串口联调、多程序监听Com0Com免费创建虚拟串口对预算有限、能用命令行的场景SSCOM串口数据查看、hex/ascii切换看MCU上报数据、保存日志Commix定时发送、协议帧构造模拟上位机发送指令帧MDK Serial Window仿真模式查看UART输出快速验证printf逻辑写在最后的个人体会折腾了这么久最大的体会是虚拟串口调试串口只是一个工具它不能代替你理解USART外设的中断标志位、DMA传输的优先级、协议帧的状态机设计但它能把这些逻辑层面的问题从看不见摸不着变成随时可以复现、随时可以制造异常。我现在的习惯是先在MDK里用Simulator快速过一遍代码逻辑再用VSPD和串口助手做一轮协议级联调最后才接真实硬件处理电气和时序问题。这样三明治式的调试流程让我省下了大量等待硬件、拆接线、抓波形的时间。尤其是做双机通信和上位机协议对接时虚拟串口几乎是不可替代的效率工具。最后分享一个小技巧用VSPD创建串口对时把两端的波特率都设成你项目最终的通信波特率比如115200或460800。这样以后切到真机时所有程序都不用改配置只需要把串口号从虚拟的COM3改成真实设备的COM口就能无缝运行。我在几个项目里都是这么干的省了不少来回改配置的功夫。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑