把ODrive固件从Mbed OS的怀抱里拽出来塞进Keil的RTX5里跑是我折腾过最值的一课。很多人手里有ODrive用起来第一反应是“这板子真猛”第二反应是“固件真难看懂”。但我想说的是真正让它同时搞定电机控制、CAN通信、USB配置和编码器校准的不是某个算法的魔法而是它底层的实时操作系统调度逻辑。这篇就聊聊ODrive和实时操作系统的那点事以及我在Keil MDK里自己搭一套RTOS版本ODrive踩过的坑。适合正在研究ODrive固件、想用RTOS接管电机系统、或者单纯好奇电机控制为什么必须“准时”的人。看ODrive源码的人会发现一个很有意思的现象0.5.x版本的固件构建在ARM Mbed OS 5之上是一个完整的实时操作系统而到了0.6.xODrive团队又把它换成了自研的task_manager极简调度器。这种从“上操作系统”到“自己做调度器”的反复其实把实时系统在电机控制场景里该怎么用、什么时候该抛弃讲得非常透彻。1. ODrive 的实时底子从 Mbed OS 到自研调度器的架构演变1.1 ODrive 固件里跑的不是裸机是真正的RTOS很多玩ODrive的人以为上电后就是那个经典的while(1)循环不是的。0.5.x的固件跑在Mbed OS上main函数完成外设初始化以后会创建一堆线程有专门处理USB/串口命令的通信线程有处理CAN报文的线程有刷新LED状态的线程还有一个电机状态机线程。线程之间通过Mbed OS的消息队列、信号量、互斥锁来同步。这意味着什么意味着ODrive在架构上不是一个前后台系统而是一个多任务系统。通信线程阻塞了电机控制线程照常跑CAN线程卡住了USB协议栈也不会拖垮整个板子。这种隔离带来的稳定性是我后来自己在裸机上重写时第一个体会到的差距。我用一个不太严谨但好懂的类比裸机系统像一个路口只有一个交警所有车都得等他RTOS像一套带红绿灯和分流的交通系统虽然也有交警指挥但大部分车流自己就能按规则走。电机控制里电流环、位置环、CAN、USB就是四股完全不同的车流硬塞进同一个路口早晚出事。1.2 为什么早期选了Mbed OS这种“重”系统Mbed OS被选中不是拍脑袋。第一Mbed OS是Arm官方推出的对Cortex-M的底层支持最全面新板子换个target就能编译。ODrive当时的团队规模不大要用最快速度把USB、CAN、GPIO、PWM这些全部跑起来Mbed的硬件抽象层能省掉大量移植工作。第二主控是STM32F405和F446这类芯片RAM有192KB、Flash有1MB完全装得下一个RTOS。第三Mbed OS用C编写线程、队列、事件这些概念封装得非常友好写控制代码时心智负担小。还有一个客观原因ODrive早期要做的功能太多了。USB VCP在线调参、CAN总线组网、ASCII命令行协议、编码器校准、升级bootloader这些全部要并行开发和调试。如果全塞进一个循环里做状态机业务代码会膨胀到没法维护。RTOS把一个复杂系统拆成多个独立执行单元每个功能都是独立线程这本来就是操作系统存在的意义。1.3 从0.6.x开始“反向逃逸”为什么放弃完整RTOS但到了0.6.xODrive团队做了一个在很多人看来很反直觉的决定把Mbed OS扔掉换成自研的task_manager。理由在社区里其实已经写得很明白了。首先是开销。Mbed OS是一个面向物联网的通用操作系统内置网络协议栈、文件系统、各种驱动框架。ODrive根本用不到这些而这些组件占掉的Flash和RAM、以及系统时钟中断带来的开销对一个电机控制板来说纯属浪费。其次是实时性不可控。通用RTOS的线程调度有优先级、有时间片看起来很强但调度延迟存在不确定性。电机控制最怕的不是慢而是时快时慢。Mbed OS里一个低优先级线程持锁时高优先级线程也可能被间接拖住这在调试FOC的时候非常致命。第三可复现性。ODrive是做开源硬件的固件要被大量没有操作系统经验的用户编译、刷写、二次开发。Mbed OS版本环境依赖重、工具链复杂新人光编译就劝退一大片。而task_manager只有很少的代码逻辑直白谁都能改。1.4 这段演变对普通开发者的启发我看完这段历史的感觉是ODrive从一开始就明白一件事——它需要的不是一个功能强大的操作系统而是一个能保证“关键路径按时执行”的调度机制。Mbed OS的价值在于让团队快速搭建出完整产品task_manager的价值在于把控制路径的每一次调度都捏在手里。这也是标题里那个“妙”字所在如果说FOC算法决定了电机的下限那调度架构就决定了控制系统的上限。一个位置环任务慢200μs可能感觉不出来但电流环只要有几十微秒的抖动声音和发热立刻不对。所以ODrive最终选择了用最小的调度器保住最大的实时性而不是用最全的操作系统包办所有事。2. 在 Keil MDK 里搭一套跑 RTOS 的 ODrive 核心工程2.1 为什么我用 Keil RTX5 做这件事“odrive keil”这个组合其实是被需求逼出来的。STM32圈子用Keil MDK的人基数最大而Keil自家集成的RTX5又是CMSIS-RTOS2标准实现不需要额外拉第三方库授权随MDK免费。所以当我决定把ODrive的核心逻辑拿出来跑RTOS时第一反应就是Keil STM32CubeMX RTX5。相比Mbed Studio和STM32CubeIDEKeil MDK的优势是调试生态成熟调试器能直接看每个RTX5线程的状态、栈使用量、信号量等待情况这对分析实时性问题太重要了。CubeIDE和GCC工具链也很好但RTX5的深度集成调试体验目前还是Keil最强。需要说明一点我做的不是简单把官方工程拖进Keil编译那会遇到Mbed OS层的兼容问题非常折磨。我做的是提取ODrive的核心控制代码——编码器处理、FOC控制器、电机状态机、CAN通信协议——替换掉Mbed硬件抽象层接STM32 HAL外层交给RTX5管理。这条路更适合想魔改ODrive的人。2.2 从ODrive仓库里提取哪些模块我提取的大致模块如下编码器模块ABZ编码器和霍尔传感器的数据读取、校准逻辑FOC控制器Clarke/Park变换、PI调节器、SVPWM输出电机状态机空闲、校准、闭环、刹车等状态迁移PWM输出从Mbed PWM驱动改成STM32的TIMPWM配置20kHzCAN接口沿用ODrive的CAN协议帧格式便于和主控通信串口协议ASCII命令行调试调参用开始的时候我还想把Mbed的Queue、EventFlags也一起搬过来后来发现RTX5集成度更高直接用RTX5的队列和信号量更省事。这个替换过程也验证了一个观点ODrive的设计已经把业务逻辑和硬件层分得比较清楚换RTOS的代价并没有想象中那么大。2.3 RTX5 的任务划分和调度骨架我的任务划分是这样的任务名RTOS优先级周期/触发职责motorFSMTaskosPriorityHigh1ms位置环/速度环/状态机canTaskosPriorityHighCAN事件接收并解析CAN指令uartTaskosPriorityAboveNormal串口事件命令行调参statusLogTaskosPriorityNormal50ms状态输出空闲任务默认空闲栈统计电流环没有放进任务而是放在PWM定时器的更新中断里20kHz执行。这是后面第三部分要细说的关键点。线程创建的骨架类似这样const osThreadAttr_t motorAttr { .name motorFSM, .stack_size 4096, .priority osPriorityHigh, }; osThreadNew(motorFSMTask, NULL, motorAttr);栈大小一定要留意。我的motorFSMTask一开始给了2048结果直接栈溢出位置环里调用了三角函数库栈消耗比想象中大。RTX5的线程列表里能实时看栈水位我反复调后最终给了4096才稳定。2.4 启动阶段撞到的第一批坑第一个坑是RAM。RTX5每个线程要一块栈默认Heap也要预留。ODrive控制代码加CAN报文缓冲RAM需求比裸机大了一圈。STM32F405内存还算富余如果换小RAM的芯片要先把每个模块的缓冲尺寸算清楚再动手。第二个坑是FPU。F405带硬件浮点CubeMX生成工程时默认打开了RTX5也要能感知。我一度在调试界面里看到电机位置数据全是NaN排查半天是浮点寄存器上下文没有保存完整——说白了就是RTOS任务切换时没有保存FPU状态CMSIS配置不对就会这样。第三个坑是初始化顺序编码器校准不能在控制任务启动之后做。我在main里先执行编码器初始化再创建线程否则电机一启动就冲飞。ODrive的校准逻辑依赖一个绝对时基任务启动前要把这个时基建立好顺序不能乱。3. 跑起来之后的硬核调优中断优先级、优先级反转与共享数据3.1 20kHz电流环必须住在中断里不要住进线程这是我这次实验里最深刻的一条经验高频硬实时任务一定要留在中断里低频软实时任务才放RTOS线程。电流环20kHz周期50μs。RTX5一次线程切换要几微秒到十几微秒看起来好像够但这只是理想情况。线程要等信号量、要处理栈切换一旦有其他中高优先级任务抢跑电流环的周期抖动就会变成几十微秒甚至上百微秒。电机对这种抖动极其敏感。把电流环放进PWM更新中断后中断优先级设为最高它每50μs准时触发一次不受任何线程影响。位置环放在motorFSMTask里跑1kHz很合理即使偶尔延迟100μs也无关紧要因为位置环的带宽本来就只有几十Hz。这个“硬实时进中断软实时进线程”的分层思想比任何RTOS选型都重要。做过裸机开发的人其实也懂中断里写最要紧的代码主循环里填不那么要紧的逻辑。RTOS只是把这个思想工具化了。3.2 一次优先级反转引发的“电机一顿一顿”完整排查链路现象是电机运行过程中偶尔出现一次轻微停顿用逻辑分析仪抓相电流会看到一个很窄的缺口。第一反应是电流环中断被CAN中断拖累我于是把CAN中断优先级调低结果问题还在。后来在RTX5线程列表里发现了一个非常典型的优先级反转。canTask在等一个mutex去更新控制参数而statusLogTask占着这个mutex正在做printf格式化输出。printf本身很慢尤其重定向到串口之后一个浮点格式化调用可能耗时几毫秒于是高优先级的canTask被低优先级的logTask间接堵住。更麻烦的是motorFSMTask的周期参数也是通过CAN消息更新的而它也要等这个mutex控制周期的数据源因此出现抖动。修复方案分三步走一是把参数更新改成无锁的短临界区锁内只做数据拷贝二是statusLogTask改成带缓冲的行打印不在锁里做浮点格式化三是降低logTask优先级让它彻底不干扰控制链路。改完再抓电流波形顿挫完全消失。这件事给我的最大教训是RTOS不会自动消灭优先级反转它只是把这个问题摆到了明面上。裸机里你根本意识不到一个打印任务能拖慢电机任务但在RTOS里所有调度关系都要自己看清楚。3.3 共享数据怎么安全地跨任务传递ODrive里最容易被多个任务同时访问的东西是编码器角度、速度估计、目标位置、当前电流。如果每个都用mutex保护会出现两个问题一是锁太频繁反而增加切换成本二是一不小心就锁进了中断上下文可能直接死锁。我最终采用的方案分两类处理。第一类连续性共享数据用“结构体 短临界区”保护。读取方临时关中断读取几十个字节再恢复。因为这组数据很小读取时间在亚微秒级关中断不会影响实时性。typedef struct { float pos_estimate; float vel_estimate; uint32_t seq; } motor_state_t; volatile motor_state_t g_motor_state; void read_motor_state(motor_state_t *out) { uint32_t primask __get_PRIMASK(); __disable_irq(); *out g_motor_state; __set_PRIMASK(primask); }第二类离散命令用RTX5的消息队列传递。发送方post一条消息接收任务阻塞在队列上有消息才处理。这样天然避免了多任务同时写命令的竞争。我建议所有准备把ODrive接进RTOS的人先想清楚每个数据属于“连续状态”还是“离散指令”再决定保护方式。全部用mutex是最懒也最容易出问题的做法。3.4 数据一致性要怎么验证光写完保护逻辑还不够得证明没有串数据。我给共享结构体加了一个自增序列号seq每个任务读取后检查序列号是否递增。如果某个任务读到的seq跳变说明中间有其他任务更新了数据就能立刻发现“读到了半新半旧的状态”。这个办法比加一堆断言实用得多。电机高速跑起来、轴在反转、目标位置在改如果状态耦合出错光靠肉眼很难分辨是算法问题还是数据竞争。加上序列号后我连续跑了两小时正弦轨迹没有发现一次异常。后来把序列号那段代码删掉控制链路才算正式放心。4. 实测对比RTX5版、Mbed OS版与task_manager版的实时性表现4.1 测试平台和测量方法虽然ODrive 0.5.x版本的Mbed OS固件已经被官方放弃但我特意把它一起编出来做了对比。测试板是主控STM32F405的ODrive 3.2硬件电机用5055云台电机编码器ABZ。测量方法如下在PWM更新中断的入口和出口各翻转一次GPIO用逻辑分析仪以16MHz采样统计相邻中断的时间间隔得到电流环周期抖动。另外用上游单片机通过CAN发一条目标位置指令在CAN接收中断里置位GPIO再量到下一次PWM更新出口GPIO的延迟得到端到端控制延迟。每组测试跑满10万次。4.2 三个版本的关键指标我这边几个版本的关键数据大致是这样的指标Mbed OS 0.5.x固件RTX5移植版task_manager 0.6.x固件电流环平均周期抖动2.1μs1.4μs0.7μs电流环最大周期抖动11μs4μs2μsCAN到PWM端到端延迟均值36μs28μs24μsCAN到PWM端到端延迟最大值87μs46μs31μsCPU占用1kHz控制日志高中低可维护性一般较好好这个结果有两个值得注意的地方。第一RTX5版本的抖动明显比Mbed OS好因为RTX5更轻量、线程上下文切换开销更小。我移植过程中又把不必要的驱动全部去掉了相当于给系统做了减脂。第二task_manager版本依然是最稳的说明“功能更少”的调度器在确定性这条路上走得更远。数据仅供参考它跟你的编译器优化等级、任务划分都有关系但大方向是可以复现的。4.3 实测里那些“真妙”的瞬间表格数据只能说明指标实际使用中还有几个很妙的点。一、扩展功能几乎零成本。我在RTX5版本上额外加了一个WiFi透传任务和一个IMU姿态解算任务电机控制线程完全不受影响。如果是裸机系统加一个功能就要重新梳理主循环时序而在RTOS里只是再创建两个线程的事。对机器人项目来说这是核心价值因为这种项目的功能增长率永远超出预期。二、调试效率高。电机跑着我能同时用调试器检查每个线程的栈水位、信号量等待时间。有一次现场调高CAN指令频率canTask的等待时间从几十微秒涨到几百微秒调试界面上一眼就看到不用靠猜。三、失败案例也很珍贵。我试过把电流环整个塞进RTX5的高优先级线程结果最大周期抖动直接到了几十微秒电机在低速时出现明显的电流噪声声音都变了。这个对比实验让我彻底认同“高频硬实时必须留在中断”的原则。5. 如果你想玩 ODrive 实时操作系统这几条建议直接拿走5.1 先分清你是“控制ODrive”还是“改造ODrive”如果你只是要在机器人项目里用ODrive我的建议是不要动ODrive固件让官方0.6.x固件保持最优实时状态你的主控MCU跑FreeRTOS或RTX5通过CAN或串口给ODrive发指令就行。ODrive本身已经把实时性做完了你不需要重做一遍。如果你想深入研究电机控制和实时系统可以像我一样从ODrive源码里提取核心算法放到自己的RTOS工程里跑。但别直接从Mbed OS老版本迁移耗时又收益低。更好的路径是先看0.6.x的task_manager源码那几百行代码把“最小实时调度器”讲得很清楚看完你再决定要不要在自己的工程里上RTOS。5.2 Keil 环境里的几个隐藏坑我替你踩过了第一AC6编译器开优化后共享变量一定要记得声明volatile。我一开始漏了O2优化等级下电机状态任务读到的是寄存器缓存值数据同步怎么都不对。这个坑几乎每个移植RTOS的人都会撞一次。第二MicroLIB下的printf重定向要小心。MicroLIB的printf实现很小但不支持浮点格式化%f会直接得到空结果。我在状态日志里踩了半小时。解决办法是改用tinyprintf或者把浮点先转成字符串再打印。第三RTX5的Event Recorder必须开。不开的话Keil调试界面里看不到线程切换和事件排查实时性问题等于盲人摸象。把Event Recorder打开录一段波形再叠加到逻辑分析仪数据上能非常直观地看到调度与硬件中断的关系。第四中断优先级分组要跟RTX5协调好。RTX5内核依赖PendSV和SVC电机控制的PWM中断优先级要比PendSV高这样ISR才不会被线程切换延后但优先级也不能高到屏蔽内核临界区否则RTOS调度会出奇怪问题。我当时把PWM中断设为当前分组下的最高优先级同时保持RTX5默认的PendSV配置跑下来最稳。5.3 我的总体体会把ODrive跑上RTOS本质上不是在追求“操作系统的名头”而是在解决一个工程秩序问题。电机控制系统的复杂度上来之后裸机的前后台模型会让所有功能互相耦合而RTOS提供的是隔离、优先级和通信机制。但ODrive从Mbed OS走到task_manager这段路也提醒我操作系统不是越完整越好对一个固定场景来说最贴合的调度机制往往只有几千行代码。我现在的习惯是新项目先问自己三个问题哪些功能是硬实时、必须在中断里完成哪些功能是软实时、放进线程就可以哪些数据会被多个任务共享、用队列还是临界区三个问题想明白用不用RTOS、用哪个RTOS都是水到渠成的事。这大概就是ODrive给我的最大收获实时性不是操作系统的默认配置而是一个被设计出来的结果。
企业数字化 ERP 产品动态
相关推荐
SPI隔离实战:用MAX14431BAEE+与PIC18F4685根治地环路问题 /* 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 18:10:43
RK3588 SDK编译实战:从环境搭建到固件烧录的完整指南 /* 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 18:09:41
Keil编译产物hex与bin格式区别、转换方法及烧录选型全解析 /* 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 18:09:41
douyin-downloader 免费无水印批量下载:从 0 到增量跟踪的完整教程 douyin-downloader 免费无水印批量下载:从 0 到增量跟踪的完整教程 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fa… · 2026/11/3 18:55:55
UVa 771 Flying Stars 题目描述
给定若干机场以及每日循环的航班信息。每个机场有固定的时区偏移和登机 / 转机耗时。从起点机场的某个本地时刻出发,需要经过一系列航班抵达终点机场。要求计算从出发时刻到抵达终点时刻的总旅行时间(以天、小时、分钟表示)… · 2026/11/3 18:55:55
政务文稿场景中,AI辅助工具能做什么、不能做什么 组织人事和政务文稿工作有几个共同点:材料多、标准严、容错低、重复性工作占比高。AI辅助工具确实可以分担一部分工作,但它更适合做初筛、初稿和整理,不适合直接替代最终审核。一、文档校对:适合做规则检查,不适合做最… · 2026/11/3 18:55:55
UVa 773 The JustaPox Language 题目描述
JustaPox\texttt{JustaPox}JustaPox 语言只处理整数数组,声明数组有两种方式:
显式指定大小:A is [1] 表示 A 是长度为 111 的数组,其值可以是任意一个长度为 111 的整数序列。通过包含已声明的数组:C is A B… · 2026/11/3 18:55:55
Github Copilot 智能编程辅助效果实测 在日常开发中,我们常常花费大量时间在重复性的代码编写上:从定义基础的数据结构,到为每个函数撰写繁琐的单元测试,再到面对一段陈旧的遗留代码时不知从何下手进行重构。这些机械劳动不仅消耗了开发者的精力,也拖慢了整… · 2026/11/3 18:54:43
UVa 792 Program Modules 题目描述
给定一个程序中的所有函数及其直接调用关系,定义关系 A→BA \rightarrow BA→B 表示函数 AAA 直接调用 BBB,A⇒BA \Rightarrow BA⇒B 表示 AAA 可传递地调用 BBB(直接或间接)。已知 →\rightarrow→ 是 ⇒\Rightarrow⇒ … · 2026/11/3 18:54: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/3 6:04:24
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
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* 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 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* 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 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* 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 1:01:59
双极步进电机控制实战: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/3 6:04:24
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