搞模型部署的朋友应该都有体会训练一个模型只是开始把一个模型塞进真实的产品里并且跑得又快又稳那才是真正磨人的阶段。尤其是当你的产品是Windows桌面应用业务代码用C写着模型又是PyTorch训练出来的如何在Visual Studio 2022里把ONNX模型推理环境配好让模型顺畅地跑起来就成了绕不过去的一关。ONNX作为开放、跨框架的模型中间格式加上ONNX Runtime这套高性能推理引擎已经成为本地部署场景里相当主流的选择。今天我想把整个过程完整梳理一遍从环境准备、模型导出的细节到VS2022里C推理代码的写法再到量化部署和常见问题排查全部走一遍。这篇文章适合准备把深度学习模型接入C桌面应用的工程师也适合刚接触模型部署、想搞明白ONNX和ONNX Runtime到底怎么配合的初学者。我会尽量说透“为什么这么做”而不是只给步骤。1. 项目概述与整体设计思路1.1 为什么要搭这套环境我拿自己手头的一个项目举例。之前做一个Windows上的视觉检测工具模型是PyTorch训练的检测逻辑在Python里跑得挺欢可产品要交付的是一套C桌面程序跟相机、UI、数据库全耦合在一起。如果直接把Python解释器打包进去不光体积夸张还面临版本冲突、进程管理和性能损耗一堆麻烦。这个场景不是个例凡是用Python训练模型、又要用C或C#交付产品的团队基本都会在部署环节卡一下。ONNX存在的价值就是把“训练框架”和“推理框架”彻底解耦。它不绑定PyTorch还是TensorFlow只是一个开放、可移植的模型中间表示。你把PyTorch模型导出成.onnx文件就相当于得到了一个跟框架无关的“可执行模型包”。而Visual Studio 2022作为Windows桌面开发的主力IDE配合ONNX Runtime的C API可以很自然地完成本地推理集成。简单总结一下这套环境解决的核心问题就是让PyTorch训练好的模型变成VS2022里的C代码能直接调用、稳定运行在Windows上的推理模块。训练代码可以永远留在Python生态里发布的产品只需要带模型文件加一个C推理库用户甚至不需要装Python。1.2 ONNX与ONNX Runtime格式和引擎要分清楚这里先澄清一个很容易混淆的概念很多新手一开始会把ONNX和ONNX Runtime当成同一个东西其实它俩是两回事。ONNX是模型格式标准描述的是模型的计算图结构定义了算子、张量类型、图结构这些内容。你可以把它理解成一段“乐谱”。ONNX Runtime则是微软开源的跨平台推理引擎负责把乐谱演奏出来。它读入.onnx模型对计算图做优化然后调度到不同的计算设备上执行。你可以用Python调用它也可以用C、C#、Java等语言调用。用看视频来打比方ONNX是MP4视频文件ONNX Runtime是播放器。MP4文件本身不负责播放播放器也不是文件本身。部署时VS2022里的C项目只需要引入ONNX Runtime库然后加载.onnx模型文件剩下的算子调度、内存管理、设备映射就都交给Runtime处理了。这里还有一个高频术语叫Execution Provider一般简写成EP。EP是Runtime的底层执行提供方负责把计算图调度到具体设备上。CPU部署用CPUExecutionProviderNVIDIA显卡用CUDAExecutionProviderWindows上做硬件加速还可以用DirectMLExecutionProvider。后面配置会话时会用到这个术语先记住它。2. 环境准备与组件选型2.1 Visual Studio 2022安装要点安装VS2022时很多人会默认全选所有工作负载结果装了一堆用不上的组件磁盘空间告急编译的时候还容易出各种奇怪问题。这里我建议只勾选“使用C的桌面开发”这一项它已经包含了MSVC编译器、Windows 10/11 SDK、CMake工具和调试工具足够支撑ONNX Runtime项目的编译和链接。如果你后面要用到CUDA加速还需要额外安装对应版本的CUDA Toolkit和cuDNN并且建议单独安装不要依赖VS2022的组件来管理。安装完成后在VS2022里确认一下C工具集版本。ONNX Runtime的NuGet包对MSVC版本兼容性做得很不错但我实测下来VS2022 17.4以上版本配最新的Runtime包最省心早期版本偶尔会碰到标准库头文件的兼容问题。如果项目里既有旧工程又有新工程优先保证新工程用较新的工具集。2.2 Python端准备别误会虽然最终是在VS2022里做推理但整套流程里Python端也绕不开至少在模型导出和验证两个环节你是完全离不开它的。推荐用Python 3.10或3.11装上PyTorch。如果只是做导出和验证CPU版PyTorch就够了不需要额外装CUDA版导出动作本身不要求跑GPU。然后装两个关键库onnx库负责模型的加载和结构校验onnxruntime库用于对导出的模型做一次“模拟推理”确保结果跟原始PyTorch模型一致。这里有个小技巧在Python虚拟环境里装这些依赖不要直接装到系统Python里。我见过很多环境冲突的案例都是因为系统Python里包的版本太杂。virtualenv或者conda都行自己用着顺手就好。虚拟环境一旦建好把所有依赖和版本号记录下来后续其他同事要复现环境时能省很多事。2.3 版本怎么配能少踩坑版本兼容性这个问题是配置ONNX环境时最容易劝退新手的地方。我总结了一个基本原则导出时指定的opset版本必须小于等于ONNX Runtime支持的opset上限。opset可以理解成ONNX算子规范的版本号每个版本会新增或修改若干算子。高版本的Runtime会向下兼容低版本的算子所以导出时用稍微保守一点的opset往往比激进地追最新版本更稳妥。如果你的模型用到的算子比较新那opset也得跟着提高这是少数需要动态权衡的情况。我平时常用的版本组合大致如下组件推荐版本说明Visual Studio 202217.6及以上C工作负载MSVC v143Python3.10或3.11用于模型导出和验证PyTorch2.xCPU版即可满足导出需求onnx1.14模型结构校验onnxruntime1.16导出后的Python端验证Microsoft.ML.OnnxRuntimeNuGet1.16VS2022里的C推理库如果项目已经用了较老版本的PyTorch导出导出时显式指定opset_version17通常是个不错的折中选择。ONNX Runtime从1.16开始已经支持更高的opset留出余量。反过来如果你的Runtime是很老的版本那导出opset就要跟着往下降否则模型加载阶段就会直接报版本不支持。3. PyTorch模型导出ONNX实操3.1 最小导出流程以视觉分类模型举例导出的代码非常精简。先把模型切到eval模式这步非常关键如果忘记写model.eval()带BatchNorm、Dropout这类层的模型导出结果会跟推理时的行为不一致做数值验证时差异会特别明显。接下来构建示例输入。示例输入的shape不需要跟训练数据完全一样但必须跟部署时实际喂给模型的shape一致。什么意思如果你的产品运行时输入是1张224x224的RGB图示例输入就用(1, 3, 224, 224)不要图省事用(2, 3, 224, 224)。因为静态shape导出时模型会把所有维度固定下来后续部署时一旦shape对不上就会报Invalid argument。核心导出代码如下import torch import torchvision.models as models model models.resnet18(pretrainedTrue) model model.eval() dummy_input torch.randn(1, 3, 224, 224) torch.onnx.export( model, dummy_input, resnet18.onnx, input_names[input], output_names[output], opset_version17, dynamic_axes{ input: {0: batch_size}, output: {0: batch_size} } )这段代码里有几个参数需要说明。input_names和output_names必须记好后面C代码里获取输入输出节点时要拿它们做索引。dynamic_axes用来声明哪些维度是动态的我习惯把batch维声明成动态这样部署时不管来1张图还是8张图都不用重新导出模型。如果你能确定batch永远固定为1也可以不设置dynamic_axes让模型保持完全静态部分情况下推理性能还会略好。3.2 导出后必须做的两项验证导出成功不代表模型就是对的很多新手导出完直接把文件丢给C结果推理结果全是垃圾值回头排查半天才发现是导出这一步就出了问题。第一步用onnx库做结构校验import onnx model_onnx onnx.load(resnet18.onnx) onnx.checker.check_model(model_onnx) print(ONNX model is valid)这一步能发现图结构损坏、缺失算子、维度不匹配这类问题。如果校验报错优先检查opset版本和模型中不支持的算子。第二步用onnxruntime做数值对比。把同一张输入分别喂给原始PyTorch模型和导出的ONNX模型对比输出结果。import numpy as np import onnxruntime as ort import torch dummy_input torch.randn(1, 3, 224, 224) with torch.no_grad(): torch_out model(dummy_input).numpy() ort_session ort.InferenceSession(resnet18.onnx, providers[CPUExecutionProvider]) ort_out ort_session.run([output], {input: dummy_input.numpy()})[0] print(max abs diff:, np.abs(torch_out - ort_out).max())如果最大绝对值差异在1e-4量级甚至更小说明导出完全没问题。如果差异很大先别急着排查C回到导出这一步重新审视。顺便说一句做数值对比时尽量固定随机种子保证两次输入完全一致不然差异会混入输入数据的随机性干扰判断。3.3 导出环节的坑与对策这里整理几个我实际遇到过的坑导出时如果碰上能省不少时间。第一个坑是模型里包含不支持导出的算子。PyTorch的某些高级操作比如动态控制流、稀疏张量操作ONNX里未必有对应的算子。导出时如果遇到Unknown operator要么换一种实现方式要么把相关逻辑挪到前处理或后处理代码里不让它出现在计算图中。对大多数部署场景来说把这类操作移出模型反而更干净。第二个坑是自定义层没有注册导出函数。如果你训练时写了一个非常规的ModulePyTorch不知道如何把它映射成ONNX算子导出会直接报错。可以给自定义Module添加symbolic函数来定义导出规则但成本偏高。更务实的方式是在导出前把自定义层展开成基础算子很多情况下这是最干净的方案。第三个坑是动态shape声明范围过宽。有些人会把所有维度都标成动态省事是省事了但Runtime对计算图做静态优化时会被限制。如果模型里有Reshape、Flatten这类对shape敏感的算子维度不确定还容易触发推理阶段的形状推导失败。正确做法是只把确实会变化的维度声明为动态其余全保持静态。4. 在VS2022里集成ONNX Runtime并完成推理4.1 创建项目与安装NuGet包新建一个C控制台项目作为演示工程后期要集成到桌面应用里把核心推理逻辑封装成一个类就行。在VS2022里调用ONNX Runtime最快的方式是NuGet包。右键项目选择管理NuGet程序包搜索Microsoft.ML.OnnxRuntime直接安装到项目里。这个包会把头文件、导入库、运行时DLL全部打包好编译时不需要手动添加include路径和库路径运行时DLL也会自动复制到输出目录省掉了最头疼的环境配置环节。如果你需要GPU支持则要安装Microsoft.ML.OnnxRuntime.Gpu它会额外包含CUDA EP相关的DLL但前提是机器上已经装好对应版本的CUDA Toolkit和cuDNN。有一点需要注意GPU版包比较大如果你只是做CPU推理装普通的CPU版就好没必要背这个体积。装完包之后在源文件里包含onnxruntime_cxx_api.h这个头文件就可以开始写推理代码了。这个头文件就是ONNX Runtime的C API入口几乎所有会话、张量、内存管理的类型都在里面。4.2 C推理代码骨架下面这段代码是我常用的最小推理骨架可以直接套到项目里。#include onnxruntime_cxx_api.h #include vector #include iostream int main() { // 创建推理环境日志级别可以按需调整 Ort::Env env(ORT_LOGGING_LEVEL_WARNING, onnx_inference); // 配置会话 Ort::SessionOptions session_options; session_options.SetIntraOpNumThreads(4); session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL); // 加载模型Windows下推荐用宽字符路径 Ort::Session session(env, Lresnet18.onnx, session_options); // 准备输入数据 const std::vectorint64_t input_shape{1, 3, 224, 224}; std::vectorfloat input_data(1 * 3 * 224 * 224, 0.0f); // 获取输入输出名实际项目中建议用Session方法遍历获取 const char* input_name input; const char* output_name output; Ort::MemoryInfo memory_info Ort::MemoryInfo::CreateCpu( OrtArenaAllocator, OrtMemTypeDefault); Ort::Value input_tensor Ort::Value::CreateTensorfloat( memory_info, input_data.data(), input_data.size(), input_shape.data(), input_shape.size()); // 推理 std::vectorconst char* input_names{input_name}; std::vectorconst char* output_names{output_name}; auto output_tensors session.Run( Ort::RunOptions{nullptr}, input_names.data(), input_tensor, 1, output_names.data(), 1); // 读取输出 float* output_data output_tensors[0].GetTensorMutableDatafloat(); size_t output_size output_tensors[0].GetTensorTypeAndShapeInfo().GetElementCount(); for (size_t i 0; i output_size; i) { std::cout output_data[i] std::endl; } return 0; }这段代码里有几个细节值得展开。第一Ort::Env是整个Runtime的全局上下文。日志级别用WARNING比较合适调试时可以临时改成ORT_LOGGING_LEVEL_VERBOSE会输出非常详细的算子调度日志对性能排查和错误定位很有帮助。第二SetGraphOptimizationLevel(ORT_ENABLE_ALL)这个配置很关键。Runtime默认会做常量折叠、算子融合等一系列图优化通常能带来可观的性能提升。别为了调试方便把它关掉实际部署时保持全开就好。第三Ort::Value::CreateTensor只是把已有的input_data内存包装成Tensor并不复制数据。所以input_data这个vector的生命周期必须覆盖session.Run的调用如果提前释放或者被重新分配推理时会读到已经失效的内存输出结果就会变成随机值。第四加载模型时Windows路径建议用宽字符串也就是L...这种形式。如果项目编码设置不对普通窄字符串路径一旦包含中文或者空格模型加载阶段就会莫名其妙失败。这个坑在中文Windows环境里非常常见我建议直接养成熟练工习惯。4.3 推理性能与执行设备配置上面的代码默认走CPU执行。对很多桌面工具来说CPU推理已经完全够用但如果你要跑检测模型或者分辨率比较大的图像CPU可能扛不住。这时把计算分流到GPU是常见的操作。ONNX Runtime的GPU支持有两种走法。第一种是CUDA EP需要在NuGet里换成Gpu版本并在SessionOptions里显式追加CUDA执行提供器。OrtCUDAProviderOptions cuda_options{}; session_options.AppendExecutionProvider_CUDA(cuda_options);第二种是Windows 10以上系统自带的DirectML不需要单独安装CUDA库绝大多数显卡都能用DirectML EP加速。实际项目中如果是给客户部署我会更倾向DirectML因为它省去了客户机器上装CUDA的麻烦对显卡品牌也没有限制。不管用哪种EP都要记住一个原则执行提供器的可用性不能靠猜要用代码判断。ONNX Runtime的C API提供了Ort::AvailableProviders接口可以遍历当前可用的provider列表再决定要不要走GPU分支。如果GPU初始化失败至少可以回退到CPU执行避免程序直接崩溃。线程数也是个容易被忽略的配置。SetIntraOpNumThreads控制单算子内部使用的线程数不是越大越好。我实测下来普通PC上设4到6个线程往往是最优的设成跟CPU核心数完全一样反而会因为线程切换开销导致性能下滑。如果你不确定先用默认值跑一遍再逐步调整对比。5. 常见问题排查与量化部署经验5.1 让日志告诉你问题在哪我排查ONNX Runtime问题的第一个动作永远是先开日志。把env的日志级别改成ORT_LOGGING_LEVEL_VERBOSE重新跑一遍日志里能看到模型加载时每个节点的解析情况、每个算子的执行时间、以及出错时具体卡在哪个节点。这比对着错误码瞎猜要高效得多。特别是模型加载成功但输出结果不对的时候日志能帮你区分是模型结构问题、输入数据排布问题还是算子融合导致的计算差异。我遇到过几次输出在数值上差了几个像素级别的情况最后查下来都是输入图像的通道排布跟模型训练时不一致。这类问题靠日志能很快对上前处理环节的线索。5.2 高频错误速查表把开发中最常遇到的报错整理一下。按我的经验绝大多数问题都集中在这几类。错误现象可能原因解决方案程序启动时找不到onnxruntime.dllNuGet未安装成功或手动复制了错误版本确认NuGet包版本检查输出目录的DLL加载模型报Model invalid模型文件损坏或不完整用Python端onnx.checker重新校验Run时报Invalid argument输入张量shape或类型与模型定义不匹配遍历模型输入信息核对name、shape、type输出全是0或NaN输入数据未归一化或推理结果读取方式错误检查前处理确认输出数据读取的指针和长度CPU推理速度远低于预期图优化被关闭或线程数设置不当打开ORT_ENABLE_ALL微调线程数GPU版本却无法使用GPUCUDA、cuDNN版本与Runtime不匹配查看日志中EP列表检查驱动和库版本关于输入输出名称不一致的问题我再多说一句。在生产代码里不要写死输入名最稳的做法是遍历Session的输入输出信息用GetInputNameAllocated和GetOutputNameAllocated动态获取。这样模型文件升级后只要C代码不依赖硬编码名字就能自动适配新的输入输出节点。5.3 INT8量化把模型体积和延迟一并降下来很多人会问ONNX模型怎么做INT8量化这个操作在桌面部署里确实很实用。ONNX Runtime提供了一套Python端的量化工具可以在导出后直接对模型做量化不需要重新训练。动态量化是最省事的方案几行代码就能完成from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( resnet18.onnx, resnet18_quant.onnx, weight_typeQuantType.QInt8 )量化之后模型体积通常会减到原来的四分之一左右CPU上的推理延迟也能明显下降。代价是精度会有轻微损失对分类任务通常可以接受对高精度检测任务建议用校准数据做静态量化或者干脆在训练阶段就做量化感知训练。这里有个容易被忽视的点量化后的模型在C端的调用方式完全不变。你只要把模型路径换成新的qint8模型文件就行不需要任何代码改动。所以量化完全可以作为部署流程里比较靠后的一步来做模型效果验证通过之后再量化发布这样排查问题时可以少一个变量。5.4 关于ONNX部署LLM模型的一点说明最近经常会看到ONNX部署LLM模型的话题这里简单说下我的看法。LLM模型确实可以导出成ONNX格式尤其是基于transformers生态的模型通过Optimum库可以比较顺畅地完成导出。但LLM部署跟普通视觉模型的侧重点很不一样。LLM是自回归生成推理时存在循环输入输出长度都是动态的所以导出时必须处理好动态维度和KV Cache的层叠关系。直接一次性导出整个LLM然后跑往往会遇到内存爆炸或者算力浪费的问题。我的建议是如果确实想用ONNX Runtime来做LLM部署优先考虑官方的模型优化工具。这类工具会针对Transformer结构做算子融合和KV Cache优化比手写导出和配置靠谱得多。也不要一开始就追求端到端先把单次生成步验证通过再逐步加长上下文和并发场景。这条路线目前发展很快但普通桌面应用的开发者还是先从视觉模型这类常规部署入手比较稳妥。6. 我的几点实操体会最后分享几条长期使用这套环境下积累的个人心得。第一务必遵循“先Python后C”的顺序。任何模型先确保ONNX导出和数值验证在Python端是通的再动C代码。我见过太多人直接跳到最后一步结果出了问题分不清是导出问题还是C调用问题排查成本翻倍。第二把模型文件和Runtime版本看成一套组合。模型导出时的opset、Runtime的版本、以及模型里用到的算子这三者必须锁定。项目沉淀下来后要把这些版本信息明确记录下来否则半年后再维护谁都不能保证模型还能正常加载。第三从小模型开始跑通整套流程。用resnet18这类轻量模型把环境、编译、加载、推理、输出整个链路打通确认每个环节没问题再切换到自己的大模型。这是最快的学习曲线也是排查问题的好办法。如果你准备把深度学习模型接入自己的Windows应用这套配置路径是走通的。我最初配环境也花了不少时间把关键节点梳理清楚之后整个流程其实非常直接。希望这篇内容对你有实际帮助。
企业数字化 ERP 产品动态
相关推荐
openrig:开放式硬件机架与开源监控打造个人设备管理方案 先声明一下,这个项目我前后折腾了大概三周,从第一版纯手搓铝型材架子,到后来加上整套开源监控系统,逐步演变成了一个叫“openrig”的个人设备管理方案。如果你手上也有多台GPU主机、存储服务器或者各种开发板混在一起,… · 2026/11/2 23:02:18
AI工程流水线:四语言协同构建生产级AI系统 1. 从零开始构建AI工程体系:这不是写几个模型脚本,而是搭一条生产级流水线“AI Engineering from Scratch”——这个标题乍看像极了某门网课的副标题,但真正做过AI落地的人一眼就明白:它根本不是教你怎么用PyTorch跑通MNIST&#… · 2026/11/2 23:02:18
Nginx时间显示异常?本质是全链路时区治理问题 1. 问题现象与真实场景还原Nginx本身并不处理“查询到的数据时间显示不正确”这类逻辑——它压根不查数据库、不解析业务时间字段、不渲染前端页面。但这个标题在运维和开发一线高频出现,背后往往指向一个典型链路断裂:Nginx作为反向代理或静态资源服务层… · 2026/11/2 23:01:18
OpenClaw与LINE集成提升养虾业沟通效率 1. OpenClaw与LINE集成的核心价值OpenClaw作为一款多平台即时通讯聚合工具,其与LINE的深度整合为养虾行业带来了前所未有的沟通效率提升。传统养虾场管理中,技术人员、养殖工人、销售团队之间的信息传递往往存在滞后性,而通过OpenClaw接入LIN… · 2026/11/2 23:59:44
Java record 规范构造器能否被反序列化或反射绕过 问题现象
record 类型通常靠规范构造器来把守不变式,比如禁止名字为空:
public record User(String name) {public User {if (name == null || name · 2026/11/2 23:59:44
Unity Package Manager刷新错误排查与解决方案 1. Unity Package Manager刷新资源报错问题解析最近在Unity开发过程中,Package Manager提示"Error Refreshing Asset"的问题困扰了不少开发者。作为Unity项目依赖管理的核心组件,Package Manager一旦出现问题,轻则影响资源加载&… · 2026/11/2 23:59:44
淘宝商品详情API开发实战与性能优化指南 1. 淘宝商品详情API核心功能解析淘宝开放平台的taobao.item.get接口是电商开发者最常调用的核心接口之一,它提供了获取单个商品完整信息的能力。这个接口的独特之处在于它能返回商品的基础信息、SKU数据、促销信息等全维度数据,且响应速度控制在200ms以内… · 2026/11/2 23:59:44
JJencode编码解密工具:前端JS代码混淆与源码还原实操 做前端的人大概都遇到过这种场面:交付给客户的是一个纯前端跑的小工具,或者一个 H5 小游戏,代码打包压缩完往服务器一放,懂行的人按 F12 就能把逻辑看个七七八八,改个变量名、换个皮肤就成他自己的了。这时候就会有人翻… · 2026/11/2 23:57:44
Apache HttpAsyncClient选型指南:Default与Minimal对比 1. 项目概述在构建高性能HTTP客户端时,Apache HttpAsyncClient提供了两个核心实现:DefaultHttpAsyncClient和MinimalHttpAsyncClient。这两个客户端的选择往往让开发者陷入纠结,特别是在生产环境部署时。作为在分布式系统领域深耕多年的架构师… · 2026/11/2 23:55:43
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 4:42:45
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 5:17:18