简介这是一套基于Visual C开发的Windows平台网络流量监控系统源码面向C中级开发者、网络编程学习者及网络安全实践人员用于理解数据包捕获、协议解析、实时流量统计与MFC界面集成等核心能力。资源共24个文件含8个头文件如Globals.h、MFNetTraffic.h定义全局结构与网络模块接口5个CPP源文件如mftrafficbutton.cpp、NetTrafficButtonDlg.cpp实现主逻辑与对话框功能以及ICO图标、RC资源、DSP/DSP工程配置等配套文件完整支撑从编译到运行的全流程压缩包仅67KB轻量易上手。已有602人学习下载适合通过真实项目掌握WinPcap/Libpcap底层调用、多线程流量采集、连接级带宽计算及MFC图表化展示等关键技术点代码目录结构清晰模块职责分明含Debug调试目录与标准StdAfx预编译支持便于快速编译验证与二次扩展。1. 这不是Wireshark的简化版一个能嵌进工业网关、跑在WinXP SP3上、靠Raw Socket硬抓包的轻量级流量监控系统某产线边缘设备突然频繁掉线运维只看到交换机端口流量突增但查不到是哪个进程在发疯。他们试过用现成工具——Wireshark太重没GUI权限NetLimiter要装服务驱动客户环境禁用Windows自带的Resource Monitor又只能看本机出向。最后翻出这个VC老项目编译后仅286KB的单文件exe双击即启不依赖任何运行库连msvcr71.dll都不需要——它直接调用ws2_32.lib里的WSAIoctl和WSARecvFrom用AF_INET SOCK_RAW IPPROTO_IP模式在网卡混杂模式下收包。它不解析HTTP/FTP应用层只统计每IP每端口的字节数、连接数、SYN洪峰速率数据存CSV支持按阈值自动截断并触发回调函数。适合嵌入老旧工控机、做网络准入探针、或当自研IDS的底层流量采集模块。如果你正被“不能装第三方驱动”“必须兼容WinXP”“要C裸写不许用MFC”三条红线捆住手脚这个项目就是你翻盘的底牌。2. 编译与运行从VC6.0工程到VS2019兼容的完整链路2.1 工程结构还原为什么它能在VC6.0里编译成功解压后目录极简/NetworkMonitor/ ├── NetworkMonitor.dsp ← VC6.0工程文件关键 ├── NetworkMonitor.dsw ├── Main.cpp ← 主循环消息泵 ├── PacketCapture.cpp ← 核心抓包逻辑 ├── FlowStat.cpp ← 流量聚合与阈值判断 ├── ExportCSV.cpp ← CSV写入无第三方库纯FILE*操作 └── Resource.h这个结构暴露了它的设计哲学零框架依赖。没有.rc资源脚本界面用CreateWindow硬建没有CWinApp派生类WinMain里直接WSAStartup→socket→ioctlsocket(SIO_RCVALL)→while(1){recvfrom}。NetworkMonitor.dsp里明确写着# ADD BASE CPP /nologo /MT /W3 /GX /O2 /D WIN32 /D NDEBUG /D _WINDOWS /YX /c # ADD CPP /nologo /MT /W3 /GX /O2 /D WIN32 /D NDEBUG /D _WINDOWS /YX /c/MT表示静态链接CRT所以打包后无需分发msvcrXX.dll/GX启用异常处理但实际代码里几乎没用try-catch全靠if (SOCKET_ERROR ret)判断/O2优化体积而非速度——这解释了为何最终EXE才286KB。提示VC6.0默认生成__declspec(dllimport)符号而此项目所有API调用都显式声明为extern C例如int __stdcall WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);。这是为了绕过VC6.0对ws2_32.lib的隐式链接规则确保在无.lib导入库时仍能链接成功。2.2 VS2019迁移实操三步解决“无法打开输入文件‘wsock32.lib’”VS2019新建空Win32项目后需手动补全四件事替换头文件包含顺序原Main.cpp开头是#include windows.h #include winsock.h // ❌ VS2019已废弃改为#include winsock2.h // ✅ 必须在windows.h之后 #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) // 显式链接关闭SDL检查否则strcpy报错项目属性 → C/C → 常规 → SDL检查 → 设为“否”修正ioctlsocket参数类型原代码DWORD dwBuffer 1; ioctlsocket(sock, SIO_RCVALL, dwBuffer); // ❌ VC6.0接受DWORD*VS2019要求u_long ulBuffer 1; // ✅ u_long是WSA定义的标准类型 ioctlsocket(sock, SIO_RCVALL, ulBuffer);编译后生成NetworkMonitor.exe右键以管理员身份运行——这是硬性要求因为SIO_RCVALL需要SE_NETWORK_MANAGE权限。2.3 首次运行必做的三件事确认网卡索引程序启动后弹窗显示“Select Adapter: 0-2”这不是随便选的。执行以下命令查真实索引netsh interface ipv4 show interfaces输出中Idx列对应程序里的数字。若选错recvfrom会立即返回WSAENETDOWN。关闭防火墙临时规则Windows Defender防火墙可能拦截原始套接字。临时执行netsh advfirewall set allprofiles state off生产环境请改用netsh advfirewall firewall add rule...放行特定端口验证抓包有效性启动后立刻开cmd执行ping -n 5 192.168.1.1回到监控窗口应看到ICMP协议行出现Bytes: 3205×64字节且Packets列5。若为0说明混杂模式未生效——检查是否以管理员运行或网卡是否为虚拟网卡VMware/VirtualBox虚拟网卡不支持SIO_RCVALL。3. 核心模块拆解Raw Socket抓包、流状态机、阈值触发的三位一体实现3.1 PacketCapture.cpp如何用127行代码完成稳定抓包核心函数CaptureLoop()结构如下void CaptureLoop(SOCKET sock, HWND hList) { char buffer[65536]; // MTU上限避免分片 sockaddr_in addr; int addrLen sizeof(addr); while (g_bRunning) { int nRet recvfrom(sock, buffer, sizeof(buffer), 0, (sockaddr*)addr, addrLen); if (nRet SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAEMSGSIZE || err WSAECONNRESET) continue; // 忽略分片/重置 Sleep(10); // 防止CPU占满 continue; } // 解析IP头跳过以太网帧因SIO_RCVALL返回的是IP层起始 IPHeader* ip (IPHeader*)buffer; if (ip-ver_ihl ! 0x45) continue; // 只处理IPv4无选项 // 提取五元组源IP、目的IP、协议、源端口、目的端口 FlowKey key; key.srcIP ntohl(ip-srcIP); key.dstIP ntohl(ip-dstIP); key.protocol ip-protocol; key.srcPort 0; key.dstPort 0; // 若是TCP/UDP继续解析传输层头 if (ip-protocol IPPROTO_TCP || ip-protocol IPPROTO_UDP) { TCPHeader* tcp (TCPHeader*)(buffer (ip-ver_ihl 0x0F) * 4); key.srcPort ntohs(tcp-srcPort); key.dstPort ntohs(tcp-dstPort); } UpdateFlowStat(key, nRet); // 交给统计模块 } }关键点说明buffer[65536]大小是血泪经验——太小丢包尤其UDP大包太大导致recvfrom阻塞时间不可控ip-ver_ihl 0x0F提取IP头长度单位4字节因为IP选项字段存在时头长20字节UpdateFlowStat()不在此文件实现体现模块解耦——抓包只负责“喂数据”不碰业务逻辑。3.2 FlowStat.cpp基于哈希表的实时流聚合算法FlowStat类用std::mapFlowKey, FlowRecord存储FlowKey重载operatorstruct FlowKey { DWORD srcIP, dstIP; BYTE protocol; WORD srcPort, dstPort; bool operator(const FlowKey rhs) const { if (srcIP ! rhs.srcIP) return srcIP rhs.srcIP; if (dstIP ! rhs.dstIP) return dstIP rhs.dstIP; if (protocol ! rhs.protocol) return protocol rhs.protocol; if (srcPort ! rhs.srcPort) return srcPort rhs.srcPort; return dstPort rhs.dstPort; } };FlowRecord结构体精简到极致struct FlowRecord { DWORD totalBytes; // 累计字节数 DWORD packetCount; // 累计包数 DWORD lastActive; // GetTickCount()时间戳用于超时清理 DWORD synCount; // 仅TCPSYN包计数检测SYN Flood DWORD lastSynTime; // 上次SYN时间戳 };UpdateFlowStat()逻辑void UpdateFlowStat(FlowKey* key, int bytes) { auto it g_flowMap.find(*key); if (it g_flowMap.end()) { g_flowMap[*key] {bytes, 1, GetTickCount(), 0, 0}; } else { it-second.totalBytes bytes; it-second.packetCount; it-second.lastActive GetTickCount(); // TCP SYN检测仅当flags 0x02为真时计数 if (key-protocol IPPROTO_TCP IsSynPacket(buffer)) { it-second.synCount; it-second.lastSynTime GetTickCount(); } } }注意IsSynPacket()需从buffer中定位TCP flags字段位置IP头长20TCP固定头长取第13字节flags所在偏移。此处省略具体计算但务必注意buffer传入时是IP层起始非以太网帧。3.3 阈值触发机制不是简单告警而是可编程的回调链FlowStat.cpp末尾定义typedef void (*AlertCallback)(const FlowKey*, const FlowRecord*); AlertCallback g_pAlertCB nullptr; void SetAlertCallback(AlertCallback cb) { g_pAlertCB cb; } // 在UpdateFlowStat内部当满足条件时触发 if (it-second.synCount 100 GetTickCount() - it-second.lastSynTime 1000) { // 1秒内超100个SYN if (g_pAlertCB) g_pAlertCB(it-first, it-second); }这意味着你可以写自己的DLL在DllMain里调用SetAlertCallback(MyHandler)实现写入Syslog服务器调用netsh advfirewall firewall add rule...动态封IP发送SNMP trap甚至调用TerminateProcess杀掉本机可疑进程这才是工业场景真正需要的“可扩展告警”而非弹窗提示。4. 避坑指南那些让开发者凌晨三点还在抓头发的典型问题4.1 现象程序启动后列表始终为空recvfrom返回0原因SIO_RCVALL在部分网卡驱动尤其是Realtek RTL8168系列上需先调用WSAIoctl设置SO_BINDTODEVICE绑定到具体网卡否则静默失败。原代码假设所有网卡行为一致。解决在socket创建后、ioctlsocket前插入// 绑定到指定网卡索引idx从netsh命令获取 char szAdapter[32]; sprintf_s(szAdapter, 0x%08X, idx); setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, szAdapter, strlen(szAdapter)1);4.2 现象CSV导出中文路径乱码文件名显示为?????.csv原因ExportCSV.cpp用fopen(D:\\监控\\流量.csv, w)但VC6.0默认ANSI编码而Windows记事本读CSV默认UTF-8无BOM。解决改用_wfopen并写入UTF-8 BOMFILE* fp _wfopen(LD:\\监控\\流量.csv, Lw, ccsUTF-8); if (fp) { fwrite(\xEF\xBB\xBF, 1, 3, fp); // UTF-8 BOM fprintf(fp, 源IP,目的IP,协议,字节数\n); }4.3 现象高流量下CPU占用率100%界面卡死原因CaptureLoop是单线程死循环UpdateFlowStat中std::map::find在万级流时耗时飙升且GUI消息泵被阻塞。解决分离线程无锁队列。在WinMain中// 创建抓包线程 HANDLE hCapThread CreateThread(nullptr, 0, CaptureThreadProc, nullptr, 0, nullptr); // 主线程只处理GUI和定时刷新 SetTimer(hWnd, 1, 1000, nullptr); // 每秒刷新一次列表CaptureThreadProc中用std::queueFlowEvent暂存事件主线程WM_TIMER中批量消费。4.4 现象虚拟机中运行报错WSAENOPROTOOPT原因VMware Workstation 16默认禁用混杂模式且SIO_RCVALL在NAT模式下无效。解决VMware设置 → 网络适配器 → 高级 → 勾选“混杂模式接受”网络连接方式改为“桥接模式”代码中增加fallback逻辑若SIO_RCVALL失败则尝试AF_PACKET需Linux或降级为GetIfTable轮询接口统计精度低但可用。4.5 现象导出CSV时程序崩溃调试显示fwrite访问违规原因ExportCSV.cpp中fopen后未检查返回值且fprintf格式化字符串含未转义的%如IP地址192.168.1.100%被误认为格式符。解决// 安全写入IP地址避免%被解析 fprintf(fp, %s,%s,%d,%lu\n, inet_ntoa(*(in_addr*)key-srcIP), inet_ntoa(*(in_addr*)key-dstIP), key-protocol, record-totalBytes); // 或更稳妥用sprintf先格式化到缓冲区 char line[1024]; snprintf(line, sizeof(line), %s,%s,%d,%lu\n, ...); fputs(line, fp);5. 二次开发实战从“能跑”到“能定制”的五个关键改造点5.1 协议识别增强在IP层之上加一层轻量解析原项目只识别TCP/UDP/ICMP但工业协议如Modbus TCP端口502、S7comm102、DNP320000需要深度识别。不必引入libpcap只需在UpdateFlowStat()前插入协议探测// 在PacketCapture.cpp中recvfrom后添加 if (ip-protocol IPPROTO_TCP) { TCPHeader* tcp (TCPHeader*)(buffer (ip-ver_ihl 0x0F) * 4); if (ntohs(tcp-dstPort) 502) { // Modbus TCP ModbusHeader* mb (ModbusHeader*)((char*)tcp 20); // TCP头长20 if (mb-length 6) { // 最小Modbus ADU key-appProtocol APP_MODBUS; key-functionCode mb-functionCode; } } }FlowKey新增BYTE appProtocol和BYTE functionCode字段FlowRecord中增加WORD modbusErrorCount。这样统计时就能区分“正常读寄存器”和“非法功能码攻击”。5.2 存储后端替换用SQLite替代CSV支持SQL查询ExportCSV.cpp可整体替换为ExportDB.cpp#include sqlite3.h sqlite3* g_db nullptr; void InitDB() { sqlite3_open(flow.db, g_db); sqlite3_exec(g_db, CREATE TABLE IF NOT EXISTS flows( id INTEGER PRIMARY KEY, src_ip TEXT, dst_ip TEXT, protocol INT, src_port INT, dst_port INT, bytes INT, packets INT, timestamp DATETIME);, nullptr, nullptr, nullptr); } void SaveToDB(const FlowKey* key, const FlowRecord* rec) { char sql[1024]; sprintf(sql, INSERT INTO flows VALUES(NULL,%s,%s,%d,%d,%d,%d,%d,datetime(now)), inet_ntoa(*(in_addr*)key-srcIP), inet_ntoa(*(in_addr*)key-dstIP), key-protocol, key-srcPort, key-dstPort, rec-totalBytes, rec-packetCount); sqlite3_exec(g_db, sql, nullptr, nullptr, nullptr); }优势支持SELECT * FROM flows WHERE bytes 1000000 AND timestamp 2023-01-01可用DB Browser for SQLite直接查看无需Excel后续可加索引加速WHERE src_ip 192.168.1.100查询。5.3 实时可视化用GDI画流量热力图在主窗口WM_PAINT中绘制case WM_PAINT: PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); // 绘制Y轴IP地址取后两段 for (int i 0; i min(100, (int)g_flowMap.size()); i) { auto it g_flowMap.begin(); advance(it, i); char ipStr[16]; sprintf(ipStr, %d.%d, (it-first.srcIP 16) 0xFF, (it-first.srcIP 24) 0xFF); TextOut(hdc, 10, 20 i*16, ipStr, strlen(ipStr)); // 按字节数画条形图0-100%映射到0-200像素 int width min(200, (int)(it-second.totalBytes / 10000.0)); HBRUSH hBrush CreateSolidBrush(RGB(0, 128, 255)); FillRect(hdc, RECT{30, 20i*16, 30width, 20i*1612}, hBrush); DeleteObject(hBrush); } EndPaint(hWnd, ps); break;效果左侧IP列表右侧蓝色条形图直观显示各IP流量占比比数字更易发现异常峰值。5.4 网络策略联动调用netsh动态封禁IP在AlertCallback中实现void MyAlertHandler(const FlowKey* key, const FlowRecord* rec) { if (rec-synCount 200) { // SYN Flood阈值 char cmd[256]; sprintf(cmd, netsh advfirewall firewall add rule name\Block %s\ dirin actionblock remoteip%s enableyes, inet_ntoa(*(in_addr*)key-srcIP), inet_ntoa(*(in_addr*)key-srcIP)); system(cmd); // 生产环境建议用CreateProcess并捕获输出 } }提示netsh规则名需唯一建议拼接时间戳避免重复封禁后需记录到日志否则重启防火墙规则丢失。5.5 跨平台移植预备抽象Socket层为Linux编译铺路当前代码Windows强耦合改造第一步是定义跨平台Socket封装// SocketWrapper.h #ifdef _WIN32 #include winsock2.h typedef SOCKET SocketHandle; #define INVALID_SOCKET_HANDLE INVALID_SOCKET #define CLOSE_SOCKET(s) closesocket(s) #else #include sys/socket.h #include netinet/in.h typedef int SocketHandle; #define INVALID_SOCKET_HANDLE -1 #define CLOSE_SOCKET(s) close(s) #endif class SocketWrapper { public: static SocketHandle CreateRawSocket(); static int RecvFrom(SocketHandle sock, char* buf, int len, sockaddr_in* addr); static void SetPromiscuousMode(SocketHandle sock, bool enable); };然后将PacketCapture.cpp中所有socket/recvfrom/ioctlsocket调用替换为SocketWrapper::静态方法。这样后续只需实现Linux版SocketWrapper.cpp用PF_PACKETAF_PACKET即可编译出Linux版本。从那以后我每次接手老项目第一件事就是检查#include链和#pragma comment——它们比注释更诚实。这个VC流量监控系统教会我真正的轻量不是删功能而是把每个字节的用途刻进代码逻辑里真正的兼容不是堆条件编译而是用接口抽象把平台差异锁进一个cpp文件。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
零基础3个月速成网络安全工程师实战指南 1. 项目概述作为一名在网络安全领域摸爬滚打多年的从业者,我经常被问到同一个问题:"零基础如何快速入门网络安全?"今天我就把自己当年用3个月时间从完全不懂到能够独立完成基础渗透测试的学习路径完整分享出来。这份清单之所以被称… · 2026/11/5 4:40:54
AI驱动的数据采集:Bright Data MCP与Trae实践指南 1. 项目概述:AI时代的数据采集新范式最近半年我一直在探索如何将AI能力融入传统数据采集工作流。作为一名长期从事网络数据采集的开发者,我深刻感受到大模型技术对爬虫领域的颠覆性影响。过去我们需要花费大量时间处理反爬机制、解析网页结构、维护代理I… · 2026/11/5 4:40:54
Java C++算法题解leetcode801使序列递增的最小交换次数 前言
LeetCode 801 的题意是:给定两个长度相同的整数数组 nums1 和 nums2,每次操作可以交换某个下标 i 上的两个元素,问最少交换多少次,能使两个数组都严格递增。题目保证答案存在。
这道题最容易掉进的坑是贪心:看到某… · 2026/11/5 4:40:54
360度评估工具计费方式解析:四种口径、四个必问项与成本折算方法 更新时间:2026 年 10 月摘要
360度评估工具的计费方式可归纳为四种口径。选择哪种取决于企业用量的形状——被评人数、年度轮次与评价人构成三者共同决定成本结构。本文给出四种口径的适用边界、采购阶段必须确认的四项条款,以及用人力成本折算投入产出的… · 2026/11/5 5:31:08
稀土功能纤维的制备原理与功能特性 稀土在很多人印象中,是用于永磁电机、催化剂、激光晶体的工业原料。但近两年,稀土正在以一种更贴近生活的方式进入日常——功能纤维。稀土如何“织”进衣服里稀土功能纤维的制备,并不是简单地把稀土粉末混入纺丝液。以包头稀土研究院发布的“… · 2026/11/5 5:31:08
10-03-互操作-JobSystem与GC隔离 JobSystem 与 GC 隔离篇章:10-核心技术-托管互操作篇 | 阅读时间:约 35 分钟一、引言Unity Job System 要求 Job 中不能访问托管对象。这不是一个"推荐"规则,而是一条硬性约束——违反会导致运行时错误(InvalidOperatio… · 2026/11/5 5:31:08
从一家奶茶店讲透扫码登录:手机怎么让电脑登录 目录 一、扫码登录要解决的真问题 二、完整流程:从生成二维码到换取登录态 三、动手写一个能跑的扫码登录后端 四、为什么是长轮询,而不是 WebSocket 五、一次领取:三层保护缺一不可 六、状态机:pending 到 confirmed 到 con… · 2026/11/5 5:31:08
LInux | sudo 与 su 操作 sudo 与 su 操作手册
目录
sudoers 配置sudo 常用操作sudo 安全机制sudo 高级用法安全规范故障排查同类工具对比su、sudo 环境与 PATH 行为 1 sudoers 配置
sudo 的权限控制依托 /etc/sudoers,文件默认权限 440。
不直接使用 vi、vim 编辑,语法错误会… · 2026/11/5 5:31:08
焊接还是压接?USB、DC 线焊点虚焊的判定与工艺边界 焊点是冶金连接,压接是冷变形机械连接,失效模式不同:压接坏了是电阻慢慢爬、通电发热;焊点坏了常常一次震动就断,而静态通电还是通的。判焊点好坏,第一判据是润湿角,不是「亮不亮」。大电流、强… · 2026/11/5 5:30:08
双极步进电机控制实战: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/4 3:16:49
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/4 6:59:29
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/4 7:02:38
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战: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/4 3:16:49
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/4 6:59:29
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/4 7:02:38