news 2026/9/24 12:49:56

TwinCAT 3 + EtherCAT FOE:从站固件远程升级实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TwinCAT 3 + EtherCAT FOE:从站固件远程升级实战指南

几年前我第一次被产线上的固件升级逼到想骂人的时候,是半夜十一点。一台伺服的固件版本太老,和新换的运动控制库对不上,厂家说刷个新固件就行,然后发来一个串口升级工具,要求我拿网线一台台连过去刷。现场几十台设备分散在三层车间,那一刻我就在想:EtherCAT总线明明是通着的,为什么非得一台台手动刷?后来我才知道,EtherCAT的FOE协议本身就是专门干这事的——通过总线直接传文件,TwinCAT 3里也提供了现成的ADS接口,只是很多人没往这个方向想,或者被"远程升级"这四个字吓住了。

这篇文章就把我实际用TwinCAT 3通过EtherCAT FOE给从站远程升级固件的完整流程写出来,包括协议原理、环境准备、C#代码逐行解析,以及我在现场踩过的坑。适合做设备调试、产线维护、OEM设备开发的工程师参考。你不需要对FOE有太多基础,只要熟悉TwinCAT的基本操作,就能照着文章把升级工具搭起来。

1. 先搞明白FOE在EtherCAT里到底是什么位置

1.1 从站固件升级的传统路线和痛点

先说说我之前用过的几种"土办法",理解了痛点,你就知道FOE到底解决了什么问题。

第一种是拆机接调试器。不管是JTAG还是SWD,都得把设备外壳打开,找到板子上的调试接口,用杜邦线或者探针连上去。遇到安装位置刁钻的从站,可能还得先把旁边的线缆拆开,整个过程费时费力。一台设备光是拆装可能就要十几分钟,更别说调试器驱动的兼容性问题。

第二种是用厂家的专用串口工具。很多从站厂商提供基于RS232或RS485的升级软件,速度慢不说,还要找一条合适的串口线,电脑上装对应的驱动。最烦的是,有的设备串口在机箱内部,还是要拆盖。

第三种更原始——直接换件。备件成本高,而且换下来的旧板子还得返厂处理。

这些方式的共同问题在于:整个EtherCAT总线本来就是一根完整的通信链路,主站和从站之间数据交互完全正常,可固件升级却要绕过这条链路,另外搭一套物理连接。这在单台设备调试时还能忍,一旦面对几十台设备的批量维护、或者设备已经交付到客户现场,就非常被动。

1.2 FOE协议的基本盘:邮箱服务、密码和文件方向

FOE的全称是File over EtherCAT,从名字就能看出来,它是把文件传输建立在EtherCAT邮箱通信机制之上的一种应用层协议。EtherCAT的邮箱通信(Mailbox)本身就支持多种上层协议,比如CoE(CANopen over EtherCAT)、SoE(Sercos over EtherCAT)、EoE(Ethernet over EtherCAT),以及我们这里要用的FoE。

从协议栈的角度理解,FOE和CoE是平级的关系,都跑在邮箱数据报上。它在EtherCAT帧中对应的服务号是0x0C,也就是我们在代码里看到的indexGroup固定为0x0000000C

FOE定义了五个基本命令码:

  • 读请求(Read):主站想从从站读文件
  • 写请求(Write):主站要把文件写给从站
  • 数据传输(Data):文件内容分包传输
  • 确认(Ack):接收方确认收到
  • 错误(Error):传输过程中出错

整个传输过程有点像快递发货:主站先发一个写请求,告诉从站"我要发一个叫servo_v2.1.bin的文件,密码是xxx",从站如果允许接收,就回确认;然后主站把文件内容拆成一个个数据块连续发送,每个数据块从站都回确认;全部发完后,主站可以再发一个复位命令,让从站重启并运行新固件。

这里有两个关键点需要注意。一个是密码字段。FOE的报文头里有password字段,从站bootloader可以校验这个密码,防止误刷。很多从站出厂默认密码是0,但也有厂商设了固定值的,升级前务必查手册。另一个是文件名。从站bootloader会根据文件名和自身预设的规则决定是否接受这个文件,文件名不是随便起的,必须和从站固件约定的名称一致。

1.3 为什么"远程升级"在EtherCAT体系里特别顺

我之前一直觉得"远程"两个字很玄,实际用下来发现,EtherCAT天生就是干这个的。

首先,物理链路是现成的。EtherCAT是一条总线,主站和所有从站都挂在这条链路上。FOE的邮箱数据可以在正常的周期通信间隙传输,不需要额外布线,也不需要中断产线通信。这对于已经部署好的设备来说,等于零成本获得了一条固件升级通道。

其次,FOE有确认机制。每个数据块从站都会应答,主站能实时知道传输进度和是否出错。相比UDP那种"发了就不管"的传输方式,FOE更适合固件这种"要么全对、要么全不"的场景。固件文件只要有一个bit错了,设备启动可能就直接罢工,所以可靠传输是刚需。

还有一点很关键:FOE和周期过程数据通信并不冲突。在TwinCAT主站里,FOE传输走的是邮箱通道,优先级低于实时过程数据,所以就算在运行时做升级,轴控等实时任务也不会被完全打断。当然,实际生产环境我建议还是在安全状态下升级,这个后面细说。

2. 软硬件准备:哪些条件不满足,后面全白做

2.1 主站侧:TwinCAT 3版本与ADS路由

先列一下我这边的环境。主站IPC装的是TwinCAT 3.1.4024,网卡是Intel I210,从站是几台支持FOE的伺服驱动器。TwinCAT版本建议至少3.1.4024以上,低版本虽然也能跑,但部分ADS API和界面显示有差异,后面代码不一定完全对得上。

升级程序我写的是C#,目标框架.NET 6以上,通过NuGet安装TwinCAT.Ads包。这里要特别提醒:不要直接用.NET Framework自带的那个旧版ADS DLL,TwinCAT.Ads 4.x以上版本的API更规范,而且持续在维护。

ADS路由是第一个容易卡住的地方。如果你的升级程序直接跑在主站IPC上,问题不大;如果像我一样,升级工具跑在开发电脑上、通过以太网连接主站IPC,那必须在开发电脑上正确配置AMS Router。最简单的做法是在开发电脑上也安装TwinCAT XAE(哪怕不激活),或者安装TcAdsRouterService,然后在TwinCAT路由设置里把主站IPC的AMS NetId加进去。

网络这块另外两个经验:网卡驱动务必确认打开了巨型帧(Jumbo Frame),大小建议9000字节,EtherCAT对帧长度有要求,默认1500也能跑,但性能会差一些;第二,Windows电源管理里的网卡节能选项一定要关掉,否则跑批量升级时网卡偶尔睡过去,会让你排查到怀疑人生。

2.2 从站侧:Bootloader是FOE升级的前提

很多工程师把固件升级想象成"直接把文件写进Flash",实际上从站侧必须有一个配套的Bootloader,FOE升级才能成立。

从站的Flash通常分为两个区域:Bootloader区和Application区(也就是App区)。Bootloader是出厂烧录的一段小程序,功能很单一:检测App区是否有效、通过FOE接收新固件、把新固件写入App区。正常运行的时候,Bootloader把控制权交给App;进入升级模式后,Bootloader接管并等待FOE数据。

这就引出了一个关键结论:如果从站的Bootloader本身损坏或不支持FOE,你在TwinCAT这边怎么调代码都没用。所以做方案前,一定要确认从站厂商提供的Bootloader是否支持FOE,以及密码和文件名规则。自己开发的从站,则要在设计固件架构时就把Bootloader规划好。

关于从站在升级模式下的行为,各厂商实现不同,常见三种:

  • 上电检测到App区无效,自动停留在Bootloader,等待FOE
  • 通过CoE对象或控制字,软件命令进入Bootloader
  • 通过硬件的拨码开关或跳线强制进入Bootloader

你要做的第一件事,就是把从站手册翻开,搞清楚它属于哪种。后面写代码的时候,"进入Bootloader"这个动作没有统一标准,只能按厂商文档来。

2.3 我建议的升级流程套路

先不急着写代码,把完整的升级流程设计出来,后面写程序就是按图索骥。我最终采用的是下面这个流程:

  1. 枚举EtherCAT总线上的从站,确定目标设备的AMS地址和端口
  2. 读取当前固件版本号,和待升级固件版本比较,相同就跳过
  3. 如果有需要,发送命令让从站进入Bootloader模式
  4. 等待从站进入可接收FOE的状态
  5. 调用FoeWriteFile传输固件文件
  6. 调用FoeReset复位从站
  7. 等待从站重新上线,读版本号确认升级成功
  8. 记录升级日志

这个流程看似简单,但每一步都有讲究。比如第2步,读版本号不仅仅是"给用户看一眼",它是整个升级程序的"冗余校验"。我曾经遇到过一个场景,现场工程师把固件文件搞混了,以为刷的是2.1版,实际上拿的是2.0版。有了版本比较逻辑,程序会直接拦截掉这种低级错误。

3. 代码解析:连接、传文件、复位这三个动作怎么写

3.1 连接从站AMS设备的正确姿势

前面说了FOE跑在ADS之上,所以第一步是建立ADS连接。这里有个概念必须理清:你要连接的不是TwinCAT系统(端口851),而是具体的从站设备。

using System; using TwinCAT.Ads; namespace FoEUpdater { class Program { static void Main(string[] args) { // 从站的AMS地址和端口,在TwinCAT XAE设备树中,选中从站后 // 在Online/Information页签里能看到对应的AMS NetId和Port string slaveNetId = "192.168.0.1.1.1"; int slavePort = 0x1000; // 以实际设备显示为准 string firmwarePath = @"C:\firmware\servo_v2_1.bin"; uint password = 0; // 从站手册里查默认密码 using (TcAdsClient client = new TcAdsClient()) { // 大固件传输必须调大超时,默认值扛不住 client.Timeout = 180000; Console.WriteLine($"正在连接从站 {slaveNetId}:{slavePort}"); client.Connect(slaveNetId, slavePort); Console.WriteLine("连接成功"); // 后续FOE操作都在这里 } } } }

关于从站AMS地址的获取,我再多说一句。TwinCAT 3中每个从站在ADS路由表里都有一个独立的AMS设备,地址规则通常是主站的AMS NetId加上一个从站端口号。如果你懒得在界面上翻,可以用TcAdsClient.EnumDevices()把系统路由表里的设备列出来,从站设备会以类似设备名称的方式出现在列表里。

另外一个经验:有些从站在Bootloader模式和正常运行模式下,AMS端口号可能不同。比如正常模式端口是0x1000,进了Bootloader变成0x1008之类的。遇到这种情况,连接逻辑要分别处理。

3.2 FoeWriteFile逐行拆解与进度回调

连接建立后,核心操作就是一行FoeWriteFile。但为了把它用对,我把参数和背后的逻辑拆开讲。

Console.WriteLine("开始通过FOE写入固件..."); // 参数1:indexGroup固定为0x0000000C,这是FOE协议的标识 // 参数2:indexOffset固定为0 // 参数3:固件文件路径,注意是主站电脑上的路径 // 参数4:密码 // 参数5:进度回调 client.FoeWriteFile( 0x0000000C, 0, firmwarePath, password, FoeProgressHandler); Console.WriteLine("固件写入完成");

FoeWriteFile内部的动作,可以理解为:把文件读入内存,按照从站支持的邮箱大小分包(通常每包1KB左右),逐包通过ADS写入从站。每一包发出后等待从站确认,收到确认再发下一包。

进度回调的写法:

private static void FoeProgressHandler(uint bytesSent, uint totalBytes) { double percent = totalBytes == 0 ? 0 : (double)bytesSent * 100 / totalBytes; Console.Write($"\r传输进度:{percent:F1}% ({bytesSent}/{totalBytes} bytes)"); }

关于进度回调,有两点实际经验:

第一,回调里不要做耗时操作。比如别在回调里写数据库、发邮件、刷新UI控件,否则会阻塞FOE传输线程,轻则进度卡顿,重则直接超时。正确做法是把进度值放到一个变量里,由另一个UI定时器去读取显示。

第二,bytesSenttotalBytes是TwinCAT.Ads帮你统计的,不是从站那边反馈的。当bytesSent等于totalBytes时,只代表数据已经从主站发出去了,不代表从站已经写完Flash。有些从站在写完Flash之后还要做校验,所以紧接着的复位操作时机很重要,不要一收到完成事件就立刻复位,可以在FoeWriteFile返回之后稍微等一下(比如几百毫秒),让从站把收尾工作做完。

还有个细节,固件文件格式一定要确认。如果厂商提供的是Intel HEX文件(.hex)或Motorola S19文件(.s19),不能直接传给从站。FOE传输的是原始二进制数据,你必须先用工具转换,比如objcopy或者srec_cat把HEX转成纯BIN。我之前就见过有人直接把HEX发给从站,结果从站启动后跑飞了。

3.3 FoeReset和版本回读:如何确认升级成功

写完固件后,下一步是让从站跑起来。这里用FoeReset

Console.WriteLine("正在复位从站..."); client.FoeReset(0x0000000C, 0, password); Console.WriteLine("复位指令已发送,等待从站重新上线...");

FoeReset的作用是告诉从站Bootloader:"固件写入完成,你退出升级模式,重启微控制器吧。"从站收到复位命令后,会跳转到App区,尝试启动新固件。如果新固件有问题,从站上电后发现App校验失败,又会回到Bootloader——这一点对排查非常有帮助。

升级完成后,不能光看"没报错"就放心,一定要有一个验证步骤。我的做法是轮询读取固件版本号,确认从站已经带着新固件回来了:

private static bool WaitForSlaveBackOnline(TcAdsClient client, uint expectedVersion) { for (int i = 0; i < 30; i++) { try { uint version = client.ReadAny<uint>(0x0000F101, 0, 4); Console.WriteLine($"从站已上线,当前版本号:0x{version:X8}"); if (expectedVersion != 0 && version != expectedVersion) { Console.WriteLine($"警告:版本不匹配,期望0x{expectedVersion:X8},实际0x{version:X8}"); return false; } return true; } catch (Exception ex) { Console.Write($"等待从站重新上线... ({i + 1}/30),错误:{ex.Message}\r"); Thread.Sleep(1000); } } Console.WriteLine("从站重新上线超时"); return false; }

这里的0x0000F101是举例,实际从站的版本号对象地址要看手册。有的从站放在CoE对象0x1018的子索引里,有的放在0xF101,有的是厂商自定义的对象。核心逻辑是:轮询读取一个代表版本号的对象,读到就说明从站能正常响应ADS请求了,再结合版本号判断是不是我们刷的那个版本。

这里我又要提一个坑:不要只验证"从站能通信"就完事。有的从站升级后能正常响应CoE通信,但App内部参数初始化失败,处于一种"半死"状态。所以稳妥的做法是,如果有办法读取从站的状态字或错误字,也一并读一下,确保从站处于正常使能状态。

4. 踩坑记录:超时、密码、传输中断,一个个说清楚

4.1 超时和包大小:大固件为什么会半路失败

我第一次跑FOE升级时,固件文件只有几十KB,一切顺利。后来给一个从站刷300多KB的固件,FoeWriteFile直接抛了AdsErrorException,错误码是0x702,典型的ADS超时。

排查过程是这样的:先确认从站网络是通的,因为正常周期通信没断。然后看日志,发现前几十KB传输正常,到某个点之后突然没响应。反复试了几次,问题定位到client.Timeout上。

TwinCAT.Ads的默认超时时间是5秒(有些版本是10秒),对于小固件足够,但大固件传输过程中,如果某个数据块的重传、Flash写入耗时超过了这个时间,ADS通信就被判定为超时。解决办法很简单:

client.Timeout = 180000; // 180秒

调大超时之后,300多KB的固件传完大概花了40多秒,再没出过问题。这里我给个经验参考:建议按固件大小估算,每100KB至少预留30秒以上的超时余量,如果从站Flash写入速度慢,余量还要更大。

另一个相关问题是EtherCAT邮箱的传输效率。FOE传输过程中,从站要对每个数据包回ACK,网络延迟越大、丢包越多,整体耗时越长。如果你发现同样的固件在A设备上40秒传完、在B设备上要2分钟,先检查B设备连接的网线质量、接头是否松动,再看从站本身是否有其他高优先级任务在抢占CPU。

4.2 密码不匹配与错误码定位

FOE的密码机制说简单也简单,说坑也确实坑。有一次我给一台从站升级,刚开始用默认密码0测试,从站立刻返回错误。查了手册才知道,这款从站的出厂密码是0x12345678,而且还不支持密码为0的特殊值。

密码错误时,FoeWriteFile抛出的异常里会带上错误码。我整理了几个常见的,供你排查时对照:

错误码含义常见原因
0x9811服务不支持从站FOE未开启,或Bootloader不支持FOE
0x9813设备忙从站正在处理其他邮箱通信,或刚退出升级模式
0x702ADS超时传输超时,通常因为Timeout设置太短
0x1000常规设备错误参数错误、状态不对,需要看从站日志
0x1001未知索引组indexGroup不是0x0000000C

密码这东西,没有破解的办法,只有三件事可做:第一,查从站手册,找默认密码;第二,问厂商技术支持;第三,如果从站支持CoE读取密码对象,进设备树里读一下。注意,千万不要把密码写在代码的明文里还提交到Git仓库,这个细节我在交付项目时被客户的安全审计提过两次,后来改成从配置文件读取了。

4.3 传输中断后的恢复逻辑:别让从站变砖

这是现场最慌的场景:固件传了一半,突然断电,或者有人手贱把网线拔了。从站那边Flash里App区可能已经写了一半,完整性校验过不了。设备重启后,App起不来,只能待在Bootloader里。

好消息是,只要Bootloader本身没被破坏,这种情况完全可以恢复。恢复方法就是重新执行一遍FoeWriteFile和FoeReset,把完整的固件写进去。

坏消息是,你连接从站的方式可能需要变。有些从站正常运行模式和Bootloader模式使用不同的AMS端口号或者不同的网段IP,传输中断后,程序原本的连接参数可能连不上从站了,需要在程序中加入"扫描设备当前模式"的逻辑。

我给出的设计建议是这样的:

  • 升级前,先记录从站当前固件版本和参数配置,能备份尽量备份
  • 升级中,捕获所有AdsErrorException异常,记录完整上下文
  • 升级失败后,不退出程序,而是进入"重试模式",自动重连从站并重新发送固件
  • 重试超过3次仍失败,才需要人工介入

用这套逻辑,我在现场遇到过两次中断事故,都是程序自动重试就恢复了,没有一次需要拆机救援。

另外一个和"变砖"相关的设计建议:如果从站硬件支持双App区(A/B分区),优先开启。这样每次升级都是先写入备用分区,确认无误后再切换启动分区。即使新固件有问题,还能回退到旧版本。这个能力不是所有从站都有,但从系统设计的角度,值得在选型时作为重要指标。

5. 批量升级和与PLC联动的高级玩法

5.1 批量升级:串行比并行可靠得多

单个从站的升级代码跑通之后,很自然地会想:能不能同时给多个从站升级?我告诉你,能,但我强烈不建议。

原因在于,FOE传输虽然走的是邮箱通道,但EtherCAT总线上所有的数据帧都是主站统一调度的。如果同时发起多个FOE传输,主站要把这些数据包和实时过程数据一起调度,总线负载会明显上升,实时任务可能受影响。而且从站Bootloader的Flash写入操作往往不重入,多个从站同时擦写Flash,从站本身也可能出现异常。

我最终采用的批量策略是:串行升级,一个接一个来。

var slaveList = new List<SlaveInfo> { new SlaveInfo { NetId = "192.168.0.1.1.1", Port = 0x1000, Name = "从站1" }, new SlaveInfo { NetId = "192.168.0.1.1.2", Port = 0x1000, Name = "从站2" }, // 从配置文件读取 }; foreach (var slave in slaveList) { Console.WriteLine($"开始升级:{slave.Name}"); bool result = UpgradeSlave(slave, firmwarePath); LogResult(slave, result); }

这个UpgradeSlave函数就是前面第三部分的完整流程,包括连接、读版本、跳过相同版本、写固件、复位、验证。每一台结束后写日志,记录升级时间、升级前版本、升级后版本、结果状态。

批量升级前有个动作很重要:检查所有从站处于安全状态。如果升级的是伺服驱动器,务必确认轴已经停止、抱闸已经抱死、急停回路有效。我的标准做法是,在升级工具的界面上一键触发"全轴释放使能",然后等待PLC那边的"允许升级"信号,两路确认都通过,才开始跑循环。

5.2 升级工具的安全设计:权限、备份和审计

做生产环境用的升级工具,功能不是第一位的,安全才是。我总结了一下,至少要覆盖下面几点:

权限控制。不是什么人都能刷固件。工具要支持登录,区分管理员和操作员权限。操作员可以执行升级,但不能修改固件文件、不能删除升级日志。管理员才有权限管理配置文件和密码。

固件文件校验。每次升级前,程序应该计算固件文件的SHA256或MD5值,和配置文件里记录的期望值比对。这一步能拦截掉被人篡改过的固件文件,也能防止文件在拷贝过程中损坏。

升级前备份。上面提到过用FoeReadFile回读旧固件,实际操作是这样的:

// 如果从站支持回读,升级前先把当前固件读出来归档 // 不同版本TwinCAT.Ads的FoeReadFile签名略有差异,以实际API为准 byte[] oldFirmware = client.FoeReadFile( 0x0000000C, 0, "current_firmware.bin", password, out uint bytesRead); File.WriteAllBytes(backupPath, oldFirmware);

并不是所有从站都开放固件回读功能,如果从站不支持,这个步骤就跳过,但至少要在日志里标注"备份跳过"。

操作审计。升级日志至少要包含:操作员账号、操作时间、目标从站标识、升级前版本、升级后版本、固定文件哈希、升级结果。这些数据建议以结构化格式(比如JSON或CSV)保存,方便后续追溯。

5.3 和PLC程序的联动思路

最后再聊一个我实际项目中用过的模式——升级工具和PLC程序配合。

因为固件升级过程中,从站可能会短暂离线(进入Bootloader、复位、重新启动),如果PLC那边还在正常运行,可能会触发各种报警、急停,甚至导致设备处于不安全的状态。比较好的做法是:

在PLC程序里定义一个"维护请求"字,上位机升级工具通过ADS写这个字,PLC读到后执行安全停机流程,然后置位"维护允许"字。升级工具只有在读到"维护允许"之后,才开始对从站做升级。升级结束后,升级工具清除"维护请求"字,PLC确认从站都恢复正常,再置位"维护完成"。

这套握手逻辑,本质上是在硬件急停之外再加了一层软件安全护栏。对于有实时控制要求的设备,这一步少不得。

6. 最后再分享几个实操细节

工程上有个很容易忽略的小事:升级前先备份XML配置文件或者从站参数,不要只刷固件。很多从站的固件升级会重置部分参数,尤其是伺服驱动的电子齿轮比、增益参数,如果不备份,刷完固件后参数全部回到出厂值,那才是真正的灾难。

我现在的标准流程是:升级前先从从站导出参数到本地(TwinCAT的CoE在线存取、或者从站厂商工具都行),固件升级完成后再把参数批量写回去。整个过程也全部自动化,不依赖人工操作。

另一个实用经验:如果是在开发调试阶段试FOE升级,建议准备一台单独的备用从站专门用来测试。别直接拿产线上的设备折腾。我第一次调试的时候,因为密码写错、超时设置太短、文件格式搞反,前前后后把一台从站刷了二十几次,还好Bootloader足够顽强,没有变砖。但那次经历让我养成了一个习惯:任何固件升级工具的发布版本,都必须先在备用设备上完整跑通流程,再允许在生产环境使用。

最后说一下TwinCAT 3从站设备树里那个"Firmware Update"菜单。TwinCAT XAE其实自带了图形化的固件更新功能(在从站右键菜单里),如果你的环境允许手动操作,用它也行。但我个人还是推荐用代码实现,理由很简单:GUI操作不可追溯、不可自动化、不可批量,而今天讲的这套ADS+FOE的方案,能嵌入到你自己的产线管理工具里,升级记录自动留存,这才是远程升级该有的样子。

固件升级这件事,听起来风险很大,一旦把协议原理、流程设计、异常恢复都理顺了,它就是一个普普通通的文件传输加一个复位操作。希望这篇文章能帮你把这条远程升级的路走通,以后不用再半夜蹲在机柜前面,一台台手动刷固件了。

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

昆仑通态触摸屏U盘CSV导出全链路实战指南

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

作者头像 李华
网站建设 2026/9/24 12:48:15

STM32 Debug Viewer:不用串口的printf实时可视化调试

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

作者头像 李华
网站建设 2026/9/24 12:48:13

FOC电流环带宽不能只靠1:10法则,必须实测扫频

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

作者头像 李华
网站建设 2026/9/24 12:47:22

STM32G0B1 FDCAN实战:从CubeMX配置到CAN FD收发调试

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

作者头像 李华
网站建设 2026/9/24 12:46:48

【单片机毕业设计】基于 STM32 的水体参数阈值配置与自动换水系统设计 基于 STM32 的水质在线监测与继电器联动控制装置设计(011009)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/24 12:45:51

华为eNSP实战:安装配置、VLAN实验与故障排查

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

作者头像 李华