首页/新闻资讯/正文详情

Linux下TCP Modbus服务端实现:从协议解析到并发处理

发布时间:2026/11/4 4:04:34 来源:云帆数科 栏目:资讯中心
Linux下TCP Modbus服务端实现:从协议解析到并发处理
简介面向Linux平台工业自动化通信开发者的TCPModbus服务端实现代码适合已具备C语言与网络编程基础、需要在嵌入式或工控项目中快速搭建Modbus从站的技术人员参考。压缩包共4个文件约10KB以C源码为主体辅以线程相关头文件、构建脚本及编译后的可执行程序覆盖从源码阅读到直接运行的完整链路。代码实现了0x02离散输入、0x04输入寄存器的只读查询以及0x03读保持寄存器、0x06单寄存器写、0x10多寄存器批量写的处理逻辑并借助多线程机制应对并发客户端连接可作为理解Modbus地址映射、功能码解析与响应组包的实操样本。目前已有972人学习适合用作协议学习、二次开发或工控通信调试的基础骨架。1. 一个跑在 Linux 上的 TCP Modbus 服务端到底能解决什么问题工业现场里最常见的对接场景是上位机、组态软件或者边缘网关要按 Modbus TCP 协议来读一批寄存器。这时候你手里往往只有一台 Linux 工控机或者一块 ARM 板子没有现成的 PLC也不方便为了调试去搭一整套商业组态环境。这个项目就是干这件事的在 Linux 上实现一个 TCP Modbus 服务端监听标准 502 端口按 Modbus TCP 的报文格式响应功能码请求把内部维护的寄存器区暴露给任意 Modbus 客户端读写。它适合两类人。一类是做设备侧开发的需要给自己的采集程序或控制逻辑套一层标准协议外壳让第三方客户端能直接连上来另一类是刚接触 Modbus 的开发者想找一个能编译、能抓包、能改功能码的最小实现把协议帧结构吃透。下面按「它怎么组织 → 怎么跑起来 → 核心代码怎么读 → 怎么改」的顺序拆。2. 架构与目录一个单进程 TCP 服务端是怎么搭起来的2.1 整体分层与线程模型这类服务端的骨架其实很固定常见做法是三层网络层负责 socket 监听、accept、收发包协议层负责解析 MBAP 头和 PDU校验功能码与数据长度数据层维护一块寄存器数组供协议层读写。项目正文没有给出更细的实现描述按这个场景下合格开发最可能采用的做法网络层用阻塞式 socket 加每连接一个线程或者用select/poll做单线程多路复用两种都能跑前者代码直观、后者资源占用低。线程模型上我一般会这样选连接数少、调试为主就用「主线程 accept 每连接一个工作线程」逻辑清晰出问题好定位如果预期几十上百个并发连接就换成epoll单线程事件循环避免线程切换和锁竞争。寄存器区是共享资源无论哪种模型读写都要加锁否则多客户端同时写同一个地址会出现读到半更新数据的情况这是后面避坑章节要重点说的。协议层的关键是 MBAP 头7 个字节事务标识 2 字节、协议标识 2 字节Modbus 固定为 0、长度 2 字节、单元标识 1 字节。后面跟 PDU第一个字节是功能码。服务端要做的事就是读满 7 字节 → 解析长度 → 再读长度减 1 字节的 PDU → 按功能码分发 → 组响应包原样带回事务标识。事务标识必须原样回填客户端靠它匹配请求和响应回错了客户端会一直等超时。2.2 目录结构与文件职责一个能编译运行的最小工程目录大致长这样tcpmodbus-server/ ├── Makefile ├── include/ │ ├── modbus.h # 协议常量、功能码、报文结构体 │ ├── regs.h # 寄存器区定义与读写接口 │ └── server.h # 服务端启动、连接处理声明 ├── src/ │ ├── main.c # 参数解析、信号处理、启动监听 │ ├── server.c # socket 监听、accept、连接循环 │ ├── protocol.c # MBAP 解析、功能码分发、响应组包 │ └── regs.c # 寄存器数组与互斥锁 └── test/ └── client.py # 用 pymodbus 或裸 socket 做冒烟测试modbus.h里放的是协议常量比如功能码0x03读保持寄存器、0x04读输入寄存器、0x06写单个寄存器、0x10写多个寄存器以及异常码0x01非法功能、0x02非法数据地址、0x03非法数据值。把这些常量集中在一个头文件里后面加功能码时只改一处。regs.c单独成文件是为了让数据区和协议解析解耦将来换成从共享内存或配置文件加载初值只动这一个文件。2.3 编译与启动参数Makefile 保持极简-Wall -Wextra -O2是标配-pthread在用到线程时加上。启动参数一般给三个监听地址、端口、寄存器数量。端口默认 502但 1024 以下端口需要 root 权限开发阶段我习惯先用 1502 这类高位端口避免每次sudo。make # 监听所有网卡的 1502 端口初始化 1000 个保持寄存器 ./tcpmodbus-server -a 0.0.0.0 -p 1502 -n 1000参数含义-a绑定地址0.0.0.0表示所有网卡只想本机调试就写127.0.0.1-p端口-n寄存器数量决定数据区大小客户端访问超出范围的地址应返回异常码0x02而不是越界读内存。启动后进程前台运行收到SIGINT或SIGTERM时关闭监听 socket、等待工作线程退出再释放寄存器区这套清理逻辑写在main.c的信号处理里不做的话CtrlC之后端口会短暂处于TIME_WAIT重启时报「Address already in use」。3. 核心代码拆解从 accept 到响应组包的完整链路3.1 监听与连接接入监听部分的套路是socket → setsockopt(SO_REUSEADDR) → bind → listen → accept。SO_REUSEADDR一定要开否则调试期间反复重启会被端口占用卡住这是血泪经验。listen的 backlog 给 16 到 128 都行工业现场并发连接通常个位数。int listen_fd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_port htons(port); inet_pton(AF_INET, bind_ip, addr.sin_addr); bind(listen_fd, (struct sockaddr *)addr, sizeof(addr)); listen(listen_fd, 16); while (running) { int conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { if (errno EINTR) continue; // 被信号打断继续等 break; } // 每连接起一个工作线程或交给 epoll 注册 handle_connection(conn_fd); }accept返回-1且errno EINTR是正常现象信号处理函数打断阻塞调用后要continue而不是退出循环这一点漏了会导致进程收到无关信号就挂掉。handle_connection里给每个连接设置接收超时SO_RCVTIMEO比如 30 秒防止半开连接把线程永久占住。3.2 MBAP 头解析与粘包处理TCP 是字节流一次recv可能只收到半个 MBAP 头也可能一次收到两个完整请求。正确做法是先读满 7 字节头从里面取出长度字段再循环读满剩余字节。下面这个读满函数是整条链路的地基// 读满 len 字节处理短读和 EINTR static int read_full(int fd, uint8_t *buf, size_t len) { size_t got 0; while (got len) { ssize_t n recv(fd, buf got, len - got, 0); if (n 0) return 0; // 对端关闭 if (n 0) { if (errno EINTR) continue; return -1; } got (size_t)n; } return 1; } // 解析一个完整请求 uint8_t hdr[7]; if (read_full(fd, hdr, 7) ! 1) return; uint16_t trans_id (hdr[0] 8) | hdr[1]; // 事务标识响应原样回填 uint16_t proto_id (hdr[2] 8) | hdr[3]; // 必须为 0 uint16_t length (hdr[4] 8) | hdr[5]; // 后续字节数含单元标识 uint8_t unit_id hdr[6]; if (proto_id ! 0 || length 2 || length 260) { close(fd); // 非法报文直接断开 return; } uint8_t pdu[260]; if (read_full(fd, pdu, length - 1) ! 1) return; // 长度含单元标识PDU 少 1这里有两个容易翻车的点。第一length字段包含单元标识那 1 个字节所以 PDU 实际长度是length - 1按length去读会多读一个字节把下一个请求的头吃掉表现为客户端偶发超时。第二length上限要卡住Modbus TCP 单帧最大 260 字节左右不校验的话恶意客户端发个超大长度就能让服务端分配巨量内存或者一直阻塞等数据。3.3 功能码分发与寄存器读写PDU 第一个字节是功能码后面跟起始地址 2 字节、数量或数据。以读保持寄存器0x03为例请求是「功能码 起始地址 寄存器数量」响应是「功能码 字节数 数据」。分发用switch最直观uint8_t fc pdu[0]; uint16_t start (pdu[1] 8) | pdu[2]; uint16_t count (pdu[3] 8) | pdu[4]; switch (fc) { case 0x03: { // 读保持寄存器 if (count 0 || count 125) { send_exception(fd, trans_id, unit_id, fc, 0x03); break; } if (start count reg_count) { send_exception(fd, trans_id, unit_id, fc, 0x02); break; } uint8_t resp[260]; int pos 0; resp[pos] fc; resp[pos] (uint8_t)(count * 2); // 字节数 pthread_mutex_lock(reg_lock); for (int i 0; i count; i) { uint16_t v holding_regs[start i]; resp[pos] (uint8_t)(v 8); // Modbus 是大端 resp[pos] (uint8_t)(v 0xFF); } pthread_mutex_unlock(reg_lock); send_response(fd, trans_id, unit_id, resp, pos); break; } case 0x06: { // 写单个寄存器 if (start reg_count) { send_exception(fd, trans_id, unit_id, fc, 0x02); break; } uint16_t value (pdu[3] 8) | pdu[4]; pthread_mutex_lock(reg_lock); holding_regs[start] value; pthread_mutex_unlock(reg_lock); send_response(fd, trans_id, unit_id, pdu, 5); // 原样回显 break; } default: send_exception(fd, trans_id, unit_id, fc, 0x01); // 非法功能码 }参数说明count上限 125 是协议规定读保持寄存器一次最多 125 个超了要回异常码0x03start count reg_count判断越界回0x02所有寄存器值按大端序写入响应小端机器上必须手动移位直接memcpy会得到字节序反的数据客户端读出来是乱值。send_response负责重新组 MBAP 头事务标识和单元标识用请求里的原值长度字段填PDU 长度 1。3.4 响应组包与异常码返回异常响应格式是「功能码 | 0x80 异常码」长度固定 2 字节 PDU。组包函数把 MBAP 头和 PDU 拼一起一次send发出去避免多次小包static void send_response(int fd, uint16_t tid, uint8_t uid, const uint8_t *pdu, int pdu_len) { uint8_t frame[270]; int len pdu_len 1; // 长度字段含单元标识 frame[0] tid 8; frame[1] tid 0xFF; frame[2] 0; frame[3] 0; // 协议标识固定 0 frame[4] len 8; frame[5] len 0xFF; frame[6] uid; memcpy(frame 7, pdu, pdu_len); send(fd, frame, 7 pdu_len, MSG_NOSIGNAL); } static void send_exception(int fd, uint16_t tid, uint8_t uid, uint8_t fc, uint8_t code) { uint8_t pdu[2] { (uint8_t)(fc | 0x80), code }; send_response(fd, tid, uid, pdu, 2); }send用MSG_NOSIGNAL防止对端已关闭时进程收到SIGPIPE直接退出这是 Linux 下写网络服务的必备习惯不加的话客户端一断连服务端就没了现场排查起来很玄学。4. 跑起来与联调编译、抓包、客户端验证4.1 编译与首次启动拿到源码先make看有没有缺头文件或者链接错误。编译通过后按 2.3 的参数启动另开一个终端用ss -lntp确认端口在监听ss -lntp | grep 1502 # 期望输出类似LISTEN 0 16 0.0.0.0:1502 0.0.0.0:* users:((tcpmodbus-server,pid1234,fd3))如果看不到先查进程是否已经退出前台运行的话终端里会有报错再查端口是否被占用。bind失败最常见的原因是上一个实例没退干净ps aux | grep tcpmodbus找到后kill掉。4.2 用 Python 客户端做冒烟测试不装任何 Modbus 库也能测裸 socket 拼一个读保持寄存器的请求就行这样能顺便验证字节序和事务标识import socket, struct def read_holding(host, port, start, count, tid1): # MBAP: 事务ID, 协议ID0, 长度6, 单元ID1 req struct.pack(HHHBBHH, tid, 0, 6, 1, 0x03, start, count) with socket.create_connection((host, port), timeout3) as s: s.sendall(req) hdr s.recv(7) tid_r, pid, length, uid struct.unpack(HHHB, hdr) body s.recv(length - 1) fc body[0] if fc 0x80: raise RuntimeError(f异常码 {body[1]}) n body[1] vals struct.unpack(f{n // 2}H, body[2:2 n]) return tid_r, vals print(read_holding(127.0.0.1, 1502, 0, 10))跑通的话会返回事务标识和 10 个寄存器值。如果卡在recv不动八成是服务端长度字段算错或者没回包如果返回异常码0x02检查start count是否超过启动时-n指定的数量如果值全是乱的回头查大端序那一步。4.3 抓包定位协议问题联调阶段tcpdump比打印日志管用直接看字节sudo tcpdump -i lo -nn -X tcp port 1502 -c 20-X把 payload 按十六进制和 ASCII 打出来对照 MBAP 头逐字段看前两字节事务标识、三四字节应为00 00、五六字节长度、第七字节单元标识。客户端说「连上了但读不到数」抓包一看请求根本没发出去或者发的是 RTU 帧没有 MBAP 头问题立刻定位。这一步能省掉大量「到底是网络问题还是协议问题」的扯皮。5. 避坑与排查五个现场踩过的坑5.1 客户端偶发超时重连就好现象客户端大部分请求正常偶尔一次等满超时重连后恢复。原因通常是粘包处理不对一次recv收到两个请求只处理了第一个剩下的字节被丢弃第二个请求永远等不到响应。解决按 3.2 的read_full严格读满头部和 PDU循环处理不要假设一次recv等于一个请求。5.2 读出来的寄存器值高低字节颠倒现象服务端写入0x1234客户端读出来是0x3412。原因是 x86 是小端直接把uint16_t内存拷进发送缓冲区就是小端序而 Modbus 规定大端。解决所有出站数据手动 8和 0xFF拆分入站数据手动移位拼接别用memcpy偷懒。5.3 多客户端同时写导致数据错乱现象两个客户端同时写相邻寄存器读出来一半新一半旧。原因是寄存器区没加锁或者锁的粒度只包住了单个寄存器。解决读多个寄存器和写多个寄存器都要在整段操作外加同一把互斥锁保证客户端看到的是某一时刻的一致快照。5.4 端口 502 绑定失败现象bind: Permission denied。原因1024 以下端口需要特权。解决开发阶段换 1502 等高位端口确实要用 502 就配setcap cap_net_bind_serviceep ./tcpmodbus-server比每次sudo干净。5.5 客户端断开后服务端进程消失现象客户端一close服务端进程直接退出日志里没有任何业务错误。原因是向已关闭的连接send触发SIGPIPE默认行为是终止进程。解决send加MSG_NOSIGNAL或者在main里signal(SIGPIPE, SIG_IGN)两者选其一我一般两个都做。6. 二次开发加功能码、换数据源、做压力验证改造这个服务端最常做的三件事是加功能码、换寄存器数据源、验证并发。加功能码的路径很固定在modbus.h补常量在protocol.c的switch里加case按协议算好请求和响应的字节布局异常分支别漏。比如加0x10写多个寄存器请求是「功能码 起始地址 数量 字节数 数据」响应回显「功能码 起始地址 数量」数量上限 123越界回0x02数量为 0 回0x03。换数据源是更有价值的改造。现在寄存器是进程内数组实际项目里往往要映射到真实设备状态或者共享内存。做法是把regs.c里的读写接口抽成函数指针表typedef struct { int (*read)(uint16_t addr, uint16_t *out, uint16_t count); int (*write)(uint16_t addr, const uint16_t *in, uint16_t count); } reg_backend_t; // 启动时按配置选择内存数组 / 共享内存 / 采集线程回填 extern const reg_backend_t reg_backend_mem; extern const reg_backend_t reg_backend_shm;协议层只调backend-read/write不关心底层是数组还是共享内存。这样将来接真实采集逻辑写一个后台线程周期性刷新寄存器区协议层一行不用改。注意后端实现里自己保证线程安全协议层不再重复加锁避免锁嵌套。验证方法上功能正确性用 4.2 的脚本覆盖正常、越界、非法功能码三类请求并发正确性用wrk不合适它不是 Modbus 协议我一般写个多线程 Python 脚本N 个线程循环写不同地址再读回比对跑十分钟看有没有不一致。性能上关注单连接 QPS 和连接数上升时的延迟epoll版本在几百连接下应该保持平稳如果延迟随连接数线性上涨检查是不是每连接一个线程且锁竞争过重。最后说个习惯。早些年我改这类协议服务端图快直接在主循环里加printf调试结果日志把recv的时序打乱问题越查越乱。从那以后我每次动协议层之前都先用tcpdump存一份正常交互的 pcap 当基线改完再抓一份对比字节差异确认只有预期字段变化才继续往下走。这套笨办法救过我好几次也希望帮到你。本文还有配套的精品资源点击获取

相关推荐

向量数据库技术选型与实战优化指南
向量数据库技术选型与实战优化指南

1. 向量数据库的核心价值与应用场景第一次接触向量数据库是在处理一个商品推荐系统项目时。当时我们团队发现传统关系型数据库在处理相似性搜索时性能捉襟见肘——每次用户浏览商品后,系统需要从百万级商品库中找出最相似的20个推荐项,MySQL查询耗时经常… · 2026/11/4 4:03:34

SpringBoot+HDFS构建企业级分布式云盘实践
SpringBoot+HDFS构建企业级分布式云盘实践

1. 项目概述这个分布式云盘系统是我去年为一个企业客户开发的实际项目,经过半年多的线上运行验证,目前已经稳定服务了3000用户。系统采用SpringBootVue3前后端分离架构,后端使用HDFS作为分布式存储引擎,前端采用Element Plus组件库… · 2026/11/4 4:03:34

生成式推荐、大推荐模型与推荐基础模型:从判别式到生成式的技术演进与落地实践
生成式推荐、大推荐模型与推荐基础模型:从判别式到生成式的技术演进与落地实践

1. 推荐系统正在经历一次底层范式的切换如果你这两年一直在做推荐系统,应该能明显感觉到一个变化:过去我们讨论的是“特征怎么交叉”“序列怎么建模”“多目标怎么融合”,而现在越来越多的一线团队开始聊“生成式推荐”“大推荐模型”“推荐基… · 2026/11/4 4:03:34

带头结点单链表:统一边界、简化指针操作的核心设计
带头结点单链表:统一边界、简化指针操作的核心设计

简介:本资源是北京邮电大学数据结构课程首次实验的完整线性表实践报告,面向计算机类专业本科生及算法初学者,聚焦带头结点单链表的核心实现与深度剖析。内容涵盖存储结构原理(逻辑/物理次序差异、指针域设计)、全部基础… · 2026/11/4 4:39:45

Codex:前端组件秒级HMR的AST驱动编译加速器
Codex:前端组件秒级HMR的AST驱动编译加速器

1. 项目概述:Codex不是AI工具,而是前端组件开发的“编译器级加速器”“Codex破局:前端组件秒级 1”——这个标题里藏着一个被严重误读的概念。过去半年,我在三个中大型前端团队做技术巡检时,反复听到开发者把Codex当成… · 2026/11/4 4:39:45

TimesFM 3.0与VLX-Seek:零样本时序预测与空间语义理解的工程实践
TimesFM 3.0与VLX-Seek:零样本时序预测与空间语义理解的工程实践

1. 这不是又一个“时间序列预测模型”,而是重新定义零样本能力的分水岭最近在几个工业AI项目组做技术复盘时,反复被问到一个问题:“有没有那种不调参、不训练、不微调,扔进去就能用的时间序列预测工具?”——不是不想训… · 2026/11/4 4:39:45

LLM应用缓存实战:三档方案对比与LangChain落地经验
LLM应用缓存实战:三档方案对比与LangChain落地经验

给LLM应用做缓存这件事,我一开始是有点抗拒的。项目刚上线那个月,脑子里全是怎么把回答质量保住、把知识库召回率调上去,根本顾不上想“缓存”这种听起来就很朴素的东西。直到财务把客户一个月的token账单拍在桌上,再看到支持群里… · 2026/11/4 4:39:45

2026年网络安全五大趋势与技能转型指南
2026年网络安全五大趋势与技能转型指南

1. 行业背景与趋势概览网络安全领域正在经历前所未有的变革期。随着数字化转型的深入,攻击面呈指数级扩张,传统安全防护模式已难以应对新型威胁。根据我过去三年参与企业安全评估的经验,超过70%的组织正在重构其安全架构,这直接催… · 2026/11/4 4:39:45

美妆电商智能推荐系统:协同过滤算法实战
美妆电商智能推荐系统:协同过滤算法实战

1. 项目背景与核心价值美妆电商平台每天面临海量商品与用户行为的匹配难题。传统人工推荐效率低下,而基于协同过滤的智能推荐系统能有效解决这个问题。我在实际项目中验证过,这套算法能将用户点击率提升35%以上,转化率提升28%左右。这个系统最… · 2026/11/4 4:38:45

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战: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 步进电机驱动实战解析
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/3 6:04:24

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
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/3 5:52:46

AI辅助技术栈评估:提升研发决策效率60%
AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17

从生成Token到直接决策:Jev如何重塑AI Agent工程实践
从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17

32G统一内存跑大模型:内存账、TPS实测与端云分流
32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17

双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试
双极步进电机控制实战: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 步进电机驱动实战解析
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/3 6:04:24

DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案
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/3 5:52:46

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码