news 2026/9/28 7:44:27

Unity3D读取Modbus RTU:从RS485串口到数字孪生大屏的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity3D读取Modbus RTU:从RS485串口到数字孪生大屏的完整实现

前阵子帮客户做泵房可视化的项目,甲方提的需求很直接:把现场流量计、压力变送器和PLC的数据实时显示在Unity3d大屏里,延迟要低,界面不能卡。设备端清一色Modbus RTU,走的RS485总线。这种组合说实话太典型了,工业现场一堆老设备都是这个路子。用Unity3d实时读取Modbus RTU数据,听起来是引擎工程师和PLC工程师的交叉活,其实核心就三件事:搞懂RS485串口通讯的基本规则、吃透Modbus RTU的报文格式、再解决Unity主线程和串口线程之间的数据交换。这篇文章把我从驱动层到显示层的完整做法讲一遍,适合做数字孪生、设备监控大屏、虚拟仿真的朋友,尤其是不太了解串口那套机制、又需要对接PLC或者现场仪表的Unity开发者。

1. 项目核心思路与方案选型

1.1 这个需求到底难在哪

很多人第一次听到“Unity3d读Modbus RTU”,第一反应是去找现成插件。找了一圈发现,Unity商店里的Modbus插件要么老旧,要么只支持TCP,要么文档写得像天书。就算装上能用,遇到协议细节问题你改都没法改。所以我的建议很明确:自己写一个轻量级Modbus RTU客户端,控制在几百行C#代码内,完全够用。

先想清楚难点在哪。Unity3d本身是游戏引擎,它的主循环是渲染和逻辑更新,Update、FixedUpdate这些回调都在主线程跑。而Modbus RTU是基于串口的通讯协议,串口数据的到达是异步的、由操作系统底层线程触发。这两个模型的天然冲突就是整个项目的核心难点。如果你直接在串口收到数据的事件里修改场景里的物体位置,大概率会遇到两种情况:要么Unity直接报错,说不能在非主线程访问游戏对象;要么表现得很诡异,偶尔闪一下、卡一下,根本抓不到规律。

另一个难点是实时性的把握。Modbus RTU是主从模式,主站(也就是Unity程序)必须主动发请求帧,从站(PLC、仪表)才会回复数据。这就意味着“实时读取”本质上是“高频轮询”,轮询节奏、超时重试、返回数据解析,全都要有明确的工程方案。否则就变成一锤子买卖,读一次还行,持续运行一天就开始丢帧、假死。

1.2 Unity端通信方案怎么选

先盘一下可用的方案。第一是Unity引擎自带的System.IO.Ports命名空间,也就是.NET标准的串口类,Unity在Windows平台、.NET 4.x模式下直接可以用。这是最朴素的方案,靠谱、可控,没有第三方依赖,出了问题你能看到源代码。第二是GitHub上一些开源Modbus库,比如Himanshu的Modbus类库、NModbus等,功能完整,但往往偏向TCP或者通用性,RTU部分也有,作为参考非常好。第三是Unity商店付费插件,省事但不开源,协议细节不好调试。

我最终选了方案一:原生SerialPort类加自己写的报文解析。理由很简单:Modbus RTU协议本身不复杂,报文结构固定,就那几种功能码,CRC校验算法公开,自己写一遍等于把协议彻底吃透了。以后换设备、加寄存器、调整轮询,全部在代码里改,不用等插件作者更新。而且这个方案在PC端的数字孪生项目里特别合适,因为上位机往往就是一台工控机,串口或者USB转485直接插上就能用,没必要引入重量级中间件。

如果你是做产品级项目,需要在Android平板上跑,那方案会有变化,原生Android串口通讯需要走USB Host或者蓝牙转485,Unity的SerialPort在非Windows平台支持有限,这是后话。但PC端项目的核心流程,就是下面这套,可以完整复用。

2. Modbus RTU协议细节与报文构造

2.1 RTU帧格式与常用功能码

Modbus RTU报文就是靠“静止时间”来分帧的,所谓RTU模式,数据帧以字节流形式在串口上传输,帧与帧之间要求至少有3.5个字符时间的间隔。理解这一点对后边的粘包处理很重要。

一帧完整的RTU报文包括四个部分:从站地址、功能码、数据区、CRC16校验。从站地址占1字节,取值1到247;功能码1字节,决定这个请求是干什么的;数据区长度看功能码;最后是两个字节的CRC,低字节在前。一个常见的读保持寄存器请求帧长8字节,响应帧长取决于读多少个寄存器。

常用的功能码不多,我的建议是把下面这张表背下来。

功能码含义说明
01读线圈状态读DO离散输出,一个bit一个状态
02读离散输入读DI,输入开关量
03读保持寄存器最常用,读16位寄存器,PLC的D寄存器、仪表参数都在这里
04读输入寄存器读AI,模拟量输入,很多仪表把采集值放这里
05写单个线圈控制一路DO
06写单个寄存器写一个16位寄存器
16写多个寄存器批量写,做参数下发用得多

实时读取项目里,90%的场合只需要03和04功能码。比如压力变送器Modbus仪表,一般用03读保持寄存器或者04读输入寄存器,看你接的仪表说明书。PLC的话三菱、西门子、台达、汇川,走Modbus RTU从站模式,D区、V区、保持寄存器基本都支持03功能码。

拿读保持寄存器来说,请求帧格式是:从站地址 + 03 + 起始寄存器地址高字节 + 起始寄存器地址低字节 + 寄存器数量高字节 + 寄存器数量低字节 + CRC低字节 + CRC高字节。举个例子,读1号站,从40001开始读连续10个寄存器,寄存器地址在Modbus协议里是16位地址,40001对应地址0x0000。请求帧就是:

01 03 00 00 00 0A CRC_L CRC_H

响应帧格式是:从站地址 + 03 + 数据字节数 + 数据1高字节 + 数据1低字节 ... + CRC。对应上面请求,响应帧开头就是:

01 03 14 D0 02 0F A0 ... CRC_L CRC_H

注意地址那栏,Modbus协议里的地址是零基址的,PLC的40001对应地址0x0000,别搞混了。很多新手第一次对接就栽在这里:用手册上的寄存器编号去发请求,发现读出来的数据完全不对。

2.2 CRC16校验的手写实现

CRC是Modbus RTU的命根子。串口本身是物理层,有电气干扰可能导致某一位翻转,CRC就是报文完整性校验。Modbus RTU使用的是CRC16-Modbus算法,多项式是0xA001(反转后的0x8005),初始值0xFFFF。校验范围是从站地址到数据区结束的全部字节,CRC结果低字节先发,高字节后发。

这个算法不复杂,我直接把C#实现贴出来,实测过很多次,和Modbus调试助手算出来一致。

public static ushort CalculateCRC(byte[] data, int start, int length) { ushort crc = 0xFFFF; for (int i = start; i < start + length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if ((crc & 0x0001) != 0) crc = (ushort)((crc >> 1) ^ 0xA001); else crc >>= 1; } } return crc; }

发送请求帧的时候,把CRC算出来,低字节放前面,高字节放后面。接收响应帧的时候,对整个帧(不含CRC)也算一遍CRC,然后和帧尾的两个字节比较,相等就说明数据在传输过程中没有被破坏。我之前遇到过一次是现场有一台变频器干扰特别大,数据偶尔跳变,就是靠CRC把异常帧滤掉的。只要你做了这一步,数据可信度上一个台阶。

这里有个调试技巧:写代码之前,先用手头的Modbus调试工具构造一帧数据,比如01 03 00 00 00 0A,CRC应该是C4 0D(低字节C4,高字节0D)。拿这个当测试向量验证你的函数对不对,别一上来就对着真机调,容易各种因素搅在一起。

2.3 一主多从轮询节拍

Modbus RTU是半双工模式,主站发请求,从站回响应,同一时刻只能有一个设备在说话。而且整条485总线上可以挂多个从站,每个从站有独立地址,这就是“一主多从”。Unity程序作为主站,要一个一个去轮询从站地址,比如1号站、2号站、3号站依次来一圈,然后再循环。

轮询节拍的选型有一个核心矛盾:轮询太快,从站可能来不及响应,或者485总线在低波特率下带宽根本不够;轮询太慢,Unity画面看起来就是“迟滞”,数据半天不动。以9600波特率为例,一个字节传输时间是约1.1ms(1起始位+8数据位+1停止位,共10位,10/9600约等于1.04ms,加上校验位、系统调度,按1.1ms算比较保险)。一次读20个寄存器的完整交互,请求8字节、响应45字节左右,总共53字节,通讯时间大概60ms,再加上从站处理时间、帧间隔、系统调度,单站一轮至少给100ms才稳。如果你挂5个从站,完整轮询一圈就是500ms,那数据刷新率就是2Hz,动画就会感觉有点卡。

所以想提高实时性,优先做两件事:一是读寄存器的时候尽量连续读,一次把当前站需要用到的寄存器都读回来,不要一个两个地发请求,那是最浪费带宽的做法;二是适当提高波特率,从9600换到19200甚至38400,效果立竿见影。我自己在大多数项目里用的轮询周期是每站50ms超时、下一站自动轮询,整体表现比较均衡。

3. Unity3d串口读取与主线程调度

3.1 SerialPort初始化配置

Unity里使用SerialPort,要先在脚本里引入System.IO.Ports命名空间。打开串口的配置有几项必须和从站设备保持一致,否则数据读出来就是乱码或者根本不通:波特率、数据位、停止位、校验位。工业设备绝大多数是8数据位、1停止位、无校验,波特率看设备拨码或者参数,常见9600和19200。少数老设备用偶校验,这个要看设备手册,调不对时最常见的表现就是“数据有,但一帧都对不上”。

打开串口的代码如下:

using System.IO.Ports; SerialPort port = new SerialPort(); port.PortName = "COM3"; port.BaudRate = 9600; port.DataBits = 8; port.StopBits = StopBits.One; port.Parity = Parity.None; port.ReadTimeout = 100; port.WriteTimeout = 100; port.Open();

打开之前先枚举一下可用串口,避免把设备插在COM5上来回报COM3的错误。SerialPort.GetPortNames()能拿到当前系统的串口列表。端口被占用是非常常见的坑,串口是独占设备,如果你的电脑上还开着串口调试助手、PLC编程软件,Unity这边就open不成功。调试的时候确认一下:首发地先排除端口占用问题。

3.2 后台线程与ConcurrentQueue

SerialPort收到数据后触发的事件是DataReceived,这个事件在后台线程执行,不是Unity主线程。很多人在这里直接写transform.position = ...,结果就是各种报错或者灵异现象。正确的处理方式是把收到的原始字节先丢进一个线程安全的队列里,然后在Unity主线程的Update里取出来解析、应用数据。

这里我推荐用ConcurrentQueue,它是.NET自带的无锁并发队列,不需要额外加锁,在Unity里使用完全没问题。基本流程是:DataReceived事件里读串口缓冲区的字节,拼到一个接收缓冲区里,看看有没有完整帧(后面我会细讲怎么拼帧),拼好一帧就入队;Unity主线程Update里每帧出队,解析、分发。

private ConcurrentQueue<byte[]> receivedFrames = new ConcurrentQueue<byte[]>(); private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { int available = port.BytesToRead; byte[] temp = new byte[available]; port.Read(temp, 0, available); // 拼帧逻辑,拼接完整Modbus帧后入队 // receivedFrames.Enqueue(completeFrame); }

有几个细节要特别注意。DataReceived事件里读数据,优先把串口缓冲区里现有的字节全部读出来,因为事件触发不代表只来一个字节,可能攒了一批,你读漏了就会把一帧拆成两个残帧。另外读串口用port.Read,设置好ReadTimeout,防止某些异常情况下线程卡在Read上出不来。

3.3 主线程消费与请求发送

主线程里做的事情有两件:一是把收到、解析好的寄存器数据保存成一个数据快照,供UI或场景逻辑读取;二是按轮询节拍发送下一站的Modbus请求帧。请求发送的时机很关键:必须等当前站的响应帧回来了才能发下一站。如果上一个请求还在等响应就发出下一个,TCP那边可能没事,但RTU的485总线上会直接撞车,两边同时发送数据,整个通讯立刻崩掉。

我的做法是维护一个简单的状态机:空闲时到时间就发请求,然后进入等待状态;收到对应站号、对应功能码的响应或者超时,就回到空闲状态。伪代码如下:

void Update() { // 处理接收队列 while (receivedFrames.TryDequeue(out byte[] frame)) { if (IsValidFrame(frame)) // CRC校验、地址匹配 { SaveToSnapshot(frame); requestPending = false; } } // 发送逻辑 if (!requestPending && Time.time - lastRequestTime >= interval) { SendNextRequest(); requestPending = true; lastRequestTime = Time.time; } // 超时保护 if (requestPending && Time.time - lastRequestTime > timeoutDuration) { requestPending = false; // 跳过当前站,下一帧发下一个站 } }

注意Time.time在Unity暂停(Time.timeScale = 0)的时候不会推进。如果你的应用有暂停菜单或者程序复位逻辑,记得用Time.realtimeSinceStartup做轮询节拍,否则暂停之后数据就不会刷新了。这个细节我是在一个“当机大屏”项目里踩到的,画面暂停了,数据层也冻结了,重启才恢复,后来才改成realtimeSinceStartup。

4. 数据解析与工程化细节

4.1 粘包断包与帧完整性判断

串口通讯是流式的,没有“包”的概念,操作系统把收到的字节一股脑丢给你。这可能出现两种情况:一包数据才来了一半,DataReceived就触发了;或者两台设备连续上报,几帧的数据粘在一起到了。所以必须自己做“分帧”处理,这是Modbus RTU读取最考验代码功底的地方。

分帧的思路是维护一个接收缓冲区,把每次事件读到的数据追加进去,然后尝试从中取出完整帧。完整帧的判断靠两点:第一个是帧内字节数,03功能码响应的字节数由第2个字节(功能码)后面那个字节数决定,地址(1)+功能码(1)+字节数(1)+数据N+CRC(2),总长是3+N+2。知道N,就知道整帧应该多长,如果缓冲区长度够,先算CRC,通过了就拿走这一整帧;不通过就说明帧头部有问题,丢掉第一个字节重新找同步。第二个是帧间间隔,相邻两帧之间至少有3.5个字符时间的空闲,这个在纯软件层不好精确判断,我倾向于靠字节数+CRC锁定帧,实测足够稳定。

// 在DataReceived事件调用,传入本次读取的字节 void AppendToBuffer(byte[] data) { foreach (byte b in data) { recvBuffer[recvLength++] = b; } while (true) { int frameLen = TryExtractFrame(); if (frameLen > 0) { byte[] frame = new byte[frameLen]; Array.Copy(recvBuffer, frame, frameLen); // 把剩余数据往前挪 Buffer.BlockCopy(recvBuffer, frameLen, recvBuffer, 0, recvLength - frameLen); recvLength -= frameLen; receivedFrames.Enqueue(frame); } else break; } }

TryExtractFrame里就是刚才说的逻辑:缓冲区长度小于最小帧长(3功能码响应最小是7字节:地址+功能码+字节数+2个数据+CRC2字节)就返回0等待更多数据;能凑够长度就先算CRC,通过就返回帧长,不通过就整体丢弃第一个字节,再试。

这种“字节流+CRC校验”的思路,是串口通讯很底层的常规操作。一旦写好了,后面不管是从站主动上报还是应答模式,你都不会慌。

4.2 寄存器值映射:整数、浮点与位

Modbus寄存器是16位的,读取出来是一对字节(高字节在前)。最常见的原始数据是整数,比如压力传感器的量程编码值0到4095,直接按量程换算成工程值就行。换算公式不复杂:工程值 = 原始值 × (量程上限 - 量程下限) / 65535 + 量程下限。很多仪表出厂时把量程配置写死在内部,你读到的原始值要自己乘系数,这个系数不叫“精度”,叫分辨率或者LSB权重,仪表手册里会给。

浮点数是个大坑。很多流量计、智能仪表用32位浮点(IEEE 754)表示测量值,32位需要两个16位寄存器存储。这里存在字节顺序问题:有的设备是两个寄存器按 高低高、低高低 排列,也就是“AB CD”和“CD AB”的差异,解析错了小数点位置完全不对,读出来全是离谱的几十万。我在解析层做了一个可配置项:寄存器0和寄存器1组成float,支持“寄存器内高字节在前,寄存器间第二个在前”等几种组合。实测下来,国内仪表最常见的组合是:寄存器内大端(高八位在前),寄存器间CDAB,也就是低地址寄存器是浮点数的高16位。

如果你只有开关量、状态量要读,用03功能码读回16位寄存器后,按位取就行。一个寄存器的16个bit对应16个开关状态,某个bit是1代表某个阀门开到位或者某个报警触发。解析的时候(value & (1 << bitIndex)) != 0就是true。这个在读取设备状态字、自检字时非常常用。

4.3 数据去抖与平滑滤波

工业现场的数据没有你想象得那么干净。传感器本来就有底噪,加上信号线上的干扰,解析出来的数值可能在小幅度范围内来回跳。直接拿这个值驱动场景里的仪表盘指针、水位动画,画面会抖得很难看。这时候需要加两个简单的处理:死区阈值和轻量低通滤波。

死区阈值的思路是:只有当新值相比上次有效值变化量超过某个阈值时,才更新数据。比如水位值设置0.05米死区,0.02米的抖动就被滤掉了。低通滤波用一阶滞后滤波,公式是smoothValue = smoothValue * alpha + newValue * (1 - alpha),alpha取0.3到0.7之间,视觉效果比较自然。alpha越大,曲线越平滑但滞后明显;alpha越小,响应越快但抖动残留多一些。对于实时监控大屏,我一般取0.5左右,兼顾了平滑和响应。

这两个处理看起来不起眼,但对最终观看体验影响很大。尤其是大屏上数值如果每秒跳好几次,客户一眼就会觉得“系统不稳定”。当然了,如果你做的是精确的控制逻辑,比如根据传感器数据触发某个动作,滤波可能导致响应变慢,这时候建议只在显示层做平滑,控制逻辑用原始值,避免安全风险。

5. 实测记录与问题排查

5.1 测试环境搭建

没有条件接真实PLC的时候,可以用软件模拟器把整个链路调通。我常用的组合是Modbus Slave模拟器加虚拟串口工具,再在Unity里跑自己写的客户端。虚拟串口工具(比如VSPD)可以创建一对互相连通的虚拟串口COM3和COM4,模拟器监听COM4,Unity程序打开COM3,从软件层面模拟完整485链路。这个环境搭起来只要十分钟,但能让你在开发阶段就把协议层、线程层全部调试到位,不用反复跑现场。

Modbus Slave模拟器里配置好从站地址、功能码(比如03)、寄存器初始值,设置成连续循环响应。然后在Unity里跑起来,观察数值是否刷新、刷新频率是否正常、断线重连是否工作。我强烈建议在模拟器阶段就故意制造异常:改错地址、关掉从站、改波特率,看看程序会不会崩、会不会卡、恢复后能不能自动回来。这些问题在现场遇到可就麻烦多了。

如果手头有真实的USB转485模块,建议买一个带隔离的,大概几十块钱。连接真实仪表时,接线一定不要凭颜色猜测,用万用表确认A、B端:通常485的A接D+,B接D-,接反了的现象是设备完全没响应,偶尔需要交换。长距离传输时总线两端加120欧姆终端电阻,这个很多人会漏掉,短距离测试不明显,上百米就能看到通讯质量明显变差。

5.2 常见问题速查表

这段时间接触的人问的最多的几个问题,我整理成一个查表,照着排查就行。

现象可能原因排查与解决
SerialPort.Open()抛异常端口占用、端口不存在、权限不足关闭串口调试软件;用GetPortNames核对端口;以管理员身份运行
数据能收到但CRC老不对波特率或校验位配置不一致、485线序接反、干扰严重先核对设备手册的串口参数;交换A/B试一下;检查屏蔽层接地
CRC校验正确但数值完全不对寄存器地址错位、字节序不对、数据解析类型错误打印原始字节流逐字节对照;寄存器地址减1试试;切换浮点字节序
刷新频率太低,画面卡顿轮询周期太大、波特率太低、读取寄存器数量太多增大波特率;合并读取寄存器范围;减少单次读取数量
运行一段时间后不再更新从站掉线或主程序卡在等待状态实现超时跳站机制;记录最后心跳时间;掉线后自动重连
Time.timeScale=0后数据停更用了Time.time做调度改用Time.realtimeSinceStartup做轮询计时

有个问题很多人会忽略,就是Unity编辑器和打包后的表现不一样。编辑器里打开串口有时候因为Unity进程权限或者系统限制报错,打包成exe反而正常。反过来也有类似情况。所以调试时如果编辑器环境搞不定,先打包跑一次看看,别在一个方向上死磕。

5.3 几条实操心得

这几次项目做下来,有几点感触特别深。第一,协议栈这种东西,自己实现一遍比用任何插件都踏实。Modbus RTU报文换算、CRC计算这些代码加起来没多少,你写一遍之后,所有透明度和可控性都握在自己手里。现场出了问题,你能打开日志看原始帧,而不是对着第三方库的源码纠结。

第二,务必给自己留一个调试模式。上线运行时打开一个调试面板,实时显示原始收发帧、当前轮询站号、超时次数、数据快照。我用的是Unity的GUI系统简单画了几个框,客户现场出问题的时候,远程看一眼调试面板就能定位七七八八,比过去靠猜快得多。

第三,通讯线程和主线程的数据交互一定要按前面说的队列方式来,不要图省事用公共字段直接共享。物理上,Modbus RTU通讯是可以有几十到上百毫秒波动的,你用一个队列把通讯线程和Unity渲染线程解耦,主线程永远从快照拿数据,怎么都不会卡渲染。曾经的教训是:某次提交代码图省事,在DataReceived里直接调用一个Unity API,结果打包后在高负载机器上偶尔崩溃,查了很久才发现是线程问题。从那以后,凡是串口、Socket、硬件状态相关回调,一律先进队列再给主线程处理。

这套方案目前在我的几个项目里跑得都很稳定,一台普通工控机,带七八个从站,大屏上数据和现场仪表基本同步,肉眼也分辨不出多少延迟。你按这个思路做,第一步先拿模拟器跑通,再把真实设备和接线那一套加上去,这个方向绝对错不了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 7:43:39

从零搭建UM982厘米级RTK定位系统:硬件接线、NTRIP配置与飞控接入实战

前阵子调试一架DIY远航机&#xff0c;GPS定点模式悬停时飞机自己在天上画圈&#xff0c;返航落点每次都偏出好几米。后来换上了UM982模组做RTK定位&#xff0c;同一个飞场、同一套飞控&#xff0c;定点和返航的误差直接压到了厘米级。这篇就把我从零搭建UM982厘米级RTK定位系统…

作者头像 李华
网站建设 2026/9/28 7:42:56

矩阵转置深度解析:从数学基础到NumPy/PyTorch性能陷阱

1. 矩阵转置到底是什么先说结论&#xff1a;矩阵转置就是把一个矩阵的“行”和“列”互换。一个 m 行 n 列的矩阵 A&#xff0c;转置之后会变成一个 n 行 m 列的矩阵 Aᵀ&#xff0c;原来在第 i 行第 j 列的元素 aᵢⱼ&#xff0c;转置后会跑到第 j 行第 i 列的位置。这个操作听…

作者头像 李华
网站建设 2026/9/28 7:42:40

东方财富净利润数据抓取:Python接口调用与量化实战

最近有做个股研究的朋友问我&#xff0c;能不能写一份直接从东方财富抓上市公司纯利润&#xff08;也就是净利润&#xff09;的 Python 代码。他不是程序员&#xff0c;只是想要一份能自动跑的数据底稿&#xff0c;每天别手工复制粘贴。这个需求其实特别典型&#xff0c;量化交…

作者头像 李华
网站建设 2026/9/28 7:42:40

Python爬虫实战:抓取东方财富净利润数据并生成表格

做个股财务分析的时候&#xff0c;我一直有个挺头疼的需求&#xff1a;每家公司发布季报年报&#xff0c;我第一眼想看的数字就是“纯利润”&#xff0c;也就是净利润。东方财富网站上这份数据很全&#xff0c;但靠手工去翻页面、复制表格&#xff0c;再粘贴到Excel里&#xff…

作者头像 李华
网站建设 2026/9/28 7:41:52

没Manus邀请码?用Flowith配TaoToken打通GPT-4工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 7:41:20

AT32F4xx调试引脚复用:JTAG/SWD被占用如何排查与恢复

做嵌入式开发这些年&#xff0c;AT32F4xx系列用得越来越多&#xff0c;但这个系列有个特别容易让新手栽跟头的点——GPIO复用&#xff0c;尤其是JTAG/SWD引脚。很多人辛辛苦苦把板子画好、程序写出来&#xff0c;结果上电后调试器死活连不上&#xff0c;弹出的报错要么是“Coul…

作者头像 李华