
玩硬件这几年我越来越发现一个规律很多人最早接触的交互器件是按键和电位器但真正让设备有“质感”的往往是旋钮。不管是音响音量、示波器档位还是3D打印机菜单翻页一颗EC11旋转编码器配合合理的逻辑就能让整个产品从“能用”变成“好用”。这篇文章就围绕“EC11旋转编码器 ESP32 PWM控制”这条主线把从机械输入到数字输出的完整链路拆开讲透。如果你正好在做一个需要旋钮调节输出的项目——调速风扇、调光台灯、恒温加热台、迷你CNC的轴速度控制这篇文章会把EC11的工作机制、硬件接法、软件解码、ESP32的LEDC模块配置以及实际调试经验全部过一遍附上可以直接抄的代码。为了让不同基础的读者都跟得上我会把每个关键点背后的“为什么”也讲清楚而不是只给结论。1. 先理解EC11一颗旋钮的自我修养1.1 从手感说起为什么选编码器而不是电位器很多新手做旋钮调节第一反应是用电位器加ADC采样这方案在简单场景下确实能跑但实际用下来问题不少。电位器是模拟器件旋到哪个位置电压就对应多少听起来很直观。可它有三个硬伤第一是碳膜磨损转个几千次阻值就容易不稳定旋钮会有“沙沙”声第二是温漂环境温度一变阻值就动半夜风扇会突然自己加速第三是开机位置不固定你关机时旋钮停在什么角度上电就是什么值没法做记忆。EC11旋转编码器则是数字输出内部是金属触点配合定位片每旋转一个刻度产生一组脉冲信号。它没有绝对位置的概念输出的是“相对变化量”——往左转一下、往右转一下控制器自己数步数。这种机制带来几个突出的好处结构简单耐磨损、输出天然适合数字系统解析、支持掉电后重新设定绝对位置。在需要手感明确、精度可靠、可记忆的场景下EC11几乎是几十元内旋钮方案的最优解。1.2 A/B两相信号方向是怎么被识别的EC11背面看过去一般是五个引脚两个用于接地和电源的公共端剩下A、B两相就是核心信号输出。有些型号还有一个按压开关引脚SW旋钮本身可以按下去当作一个按钮这点后面会用到。A相和B相在旋转时会交替通断波形上表现为相位差90度的方波这个90度就是所谓的“正交信号”。当你顺时针转时A相上升沿出现时B相是低电平逆时针转时A相上升沿出现时B相变成高电平。控制器只要判断A相边沿时刻B相的电平状态就能知道旋转方向。理解这一步是整个项目的地基。方向判断逻辑还可以进一步抽象成状态机A、B两相各有高低电平组合起来有4种状态00、01、11、10旋转过程中状态按顺序循环。顺时针是00→01→11→10逆时针反过来。每次状态变化都对应一个“步”程序里用一张查表就能同时得出“走了几步”和“方向是正是负”比简单的上升沿判断更稳后面第4节会给出具体实现。1.3 规格细节别买回来才发现和预期不符EC11看似是标准件但不同型号差异很大选型时主要看三点。一是每转脉冲数常见的有20、24、30。这个数字代表旋钮转一整圈会产生多少步脉冲数值越大旋钮转一圈能调节的档位越多但转完一整圈需要的手感阻力也越明显。做音量控制我推荐20脉冲/转手感轻快做精密参数调节可以用30脉冲/转一个刻度对应一个数值心里踏实。二是定位感类型有带定位detent和不带定位之分。带定位的旋钮每转一格会有一个明显的“咔哒”卡位感适合菜单翻页与档位调节不带定位的是平滑旋转适合音量这种需要微调的场景。市面上常见的EC11大多是带定位的买的时候看清楚参数页写的“with detent”还是“without detent”。三是电气参数。多数EC11额定5V/10mA直接接3.3V的ESP32也没有问题因为编码器本质是开关量输出不涉及模拟精度反而是电压越低触点拉弧越轻微寿命更有保障。部分英制规格的编码器轴径是6.35mm公制的EC11轴径一般是6mm要配的旋钮帽规格别买错。2. 硬件连接与信号调理别让抖动毁了信号2.1 最小系统接线上拉电阻和RC滤波怎么选EC11输出引脚内部是开路的机械开关结构高电平必须靠外部上拉电阻“拉”出来。最简单的方式是直接把A、B两相接到ESP32的GPIO上开启内部上拉模式。内部上拉的阻值一般在几十千欧配合机械触点工作足够但如果你希望波形边沿更陡、抗干扰更强建议外部加4.7kΩ到10kΩ的上拉电阻再接电源。机械触点闭合瞬间必然会产生抖动bounce拨动时会看到一连串几毫秒的毛刺波形。这些毛刺如果不处理一次旋转会被解码成好几步导致数值乱跳。硬件上常用的办法是在每个信号脚对地并联一个100nF的陶瓷电容配合上拉电阻形成一个低通滤波器把高频毛刺压平。RC时间常数按τ R × C计算比如10kΩ配100nF就是1毫秒这个量级对EC11的旋转频率来说绰绰有余也不会把正常脉冲滤掉。软件上仍然要做消抖双保险最稳妥。2.2 引脚选择ESP32上哪些GPIO不能乱用ESP32的GPIO不是都能随意接外设这点新手很容易踩坑。编码器的两个信号脚需要频繁触发中断尽量选支持外部中断的常规IO同时避开以下几类特殊引脚。GPIO12MTDI引脚在部分开发板模块上用来控制Flash电压上电时如果被拉高会导致下载失败不建议接编码器。GPIO0和GPIO2是启动模式选择引脚接按键容易影响烧录最好也避开。另外还要注意GPIO34到GPIO39是纯输入引脚没有内部上拉接编码器时如果忘了外部上拉就会读到浮动电平。我习惯把编码器接在GPIO25、GPIO26干净省心也方便画原型。还有一点接编码器的GPIO不要离ESP32的天线区域太近虽然数字信号抗干扰能力还行但高频辐射可能让ADC等其他外设读数漂移。布局上让编码器信号线尽量短如果实在要延长线用双绞线并在末端加上拉电阻和电容。2.3 示波器实测波形抖动到底长什么样我在调试时用示波器挂过A相输出第一次看到波形还是有点吃惊的。正常旋转时A相会输出一串约2毫秒高电平、2毫秒低电平的方波边沿附近却跟着一堆亚毫秒级毛刺有的毛刺甚至把电平拖在半空中。这就是机械触点的真实面貌。毛刺时间一般在几十微秒到几百微秒如果直接用中断读取一次旋转可能触发好几次。用RC滤波后毛刺明显被压平波形变成干净的方波但代价是边沿会变缓如果电容选得太大比如1μF信号上升沿可能变成一条缓慢的斜坡导致电平穿越阈值的时间点不确定编码器反而更难判断。所以100nF是一个比较均衡的值线长或环境恶劣时可以加到220nF再大就不建议了余下的交给软件消抖处理。3. ESP32的PWM输出LEDC控制器的完整玩法3.1 LEDC模块的结构定时器和通道的关系ESP32实现PWM靠的是名为LEDC的硬件模块设计初衷是驱动LED调光但完全可以用它输出任意频率和占空比的PWM信号驱动舵机、直流电机、蜂鸣器都没问题。LEDC模块内部有16个通道分成两组8个高速通道和8个低速通道。高速通道挂在更精细的时钟源上抖动小适合对频率稳定性要求高的场合低速通道适合常规调速。每组通道又共享若干定时器定时器决定PWM的基础频率和分辨率通道则负责映射到具体GPIO上。简单理解定时器负责“定节奏”通道负责“选出口”。这种设计最大的好处是节省硬件资源。一个定时器配置好后多个通道可以复用同一个频率各自独立调整占空比。比如做一个三路调光灯只需要一个定时器三个通道分别控制红绿蓝三颗灯代码量非常小。3.2 参数怎么定频率、分辨率和占空比的关系LEDC核心配置只有两个参数频率和分辨率用比特数表示。分辨率8位代表占空比取值范围0到25510位代表0到1023。这里有一个重要的约束关系ESP32的LEDC定时器时钟最高约80MHz实际可输出的PWM频率和分辨率的组合受限于计数器的位数。公式可以简化成频率 ≈ 时钟源频率 / 2^分辨率。时钟源取80MHz分辨率8位时可设的最高频率约312kHz分辨率16位时最高只剩约1.2kHz。所以想在高频下获得高精度占空比是不可能的两者是跷跷板。做电机调速我一般选5kHz到20kHz的PWM频率分辨率10位。10位分辨率意味着1024级调速肉眼和手感都很难察觉到步进足够细腻。做LED调光则相反人眼对高频闪烁不敏感但频率太低会有可见频闪所以至少要选1kHz以上分辨率可以降到8位甚至更低。这里提醒一句PWM频率不要选在音频范围内太靠近人耳敏感区间的整数值。比如20kHz听起来超过人耳上限但实际因为扬声器或电源的谐波可能会听到“嗞嗞”声。我遇到过把频率设在15kHz时电机啸叫特别明显改到25kHz后声音基本消失了。3.3 负载驱动电路GPIO不能直接带电机ESP32的GPIO输出能力有限单个IO最大驱动电流在40mA左右直接驱动LED勉强可行驱动直流电机或加热丝远远不够。所以PWM信号一般只作为控制信号后面必须接驱动级。最简单的驱动方案是用N沟道MOS管做低边开关。电机一端接电源正极另一端接MOS管的漏极源极接地栅极通过一个10Ω到100Ω电阻接ESP32的GPIO。PWM高电平时MOS管导通电机通电低电平时关断电机断电。这个方案电路简单、成本低做直流电机调速完全够用。选用MOS管时重点看栅极阈值电压Vgs_th和导通电阻Rds_on。ESP32输出高电平是3.3V所以要选逻辑电平型MOS管也就是Vgs_th在1V到2V左右的型号比如常见的AO3400、IRLZ44N。普通工业MOS管Vgs_th三五伏3.3V驱动根本没法完全导通管子处于半导通状态内阻大、发热严重这就是很多人做完“MOS管烫手”的直接原因。电机两端还要反向并联一个续流二极管否则PWM关断瞬间电机的感性负载会产生很高的反向尖峰电压轻则干扰电源重则击穿MOS管。用1N5819这类肖特基二极管就行方向别接反。如果你控制的是大电流加热棒或更大功率设备可以考虑用MOS管驱动模块或固态继电器信号侧逻辑一样只是负载侧隔离和散热更完善。PWM到MOS管的栅极之间可以加一个下拉电阻10kΩ作用是防止ESP32复位期间引脚悬空导致MOS管误开通这个小细节在断电再上电时特别重要。4. 核心软件实现一次完整的旋钮控制4.1 编码器读取状态机与中断设计读完信号接下来就是写程序了。这里我给出一个实际项目中验证过的方案使用Arduino框架逻辑非常紧凑。编码器的A、B两相接在支持中断的GPIO上GPIO配置为输入上拉模式。中断触发方式用CHANGE也就是任意一个引脚电平变化都进入中断服务函数。服务函数里把A、B两相当前电平拼成一个2位状态值再结合上一次的状态做查表解码。#include Arduino.h #define ENC_A 25 #define ENC_B 26 // 旋转编码器状态表 // 下标 (上一次状态 2) | 当前状态 // 输出 -1 表示逆时针一步1 表示顺时针一步0 表示无效变化 const int8_t encStateTable[16] { 0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0 }; volatile int8_t lastEncState 0; volatile int32_t encoderCount 0; void IRAM_ATTR encoderISR() { uint8_t s (digitalRead(ENC_A) 1) | digitalRead(ENC_B); int8_t index (lastEncState 2) | s; encoderCount encStateTable[index]; lastEncState s; } void setup() { pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_A), encoderISR, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_B), encoderISR, CHANGE); Serial.begin(115200); } void loop() { Serial.printf(count %d\n, encoderCount); delay(50); }这段代码里有几个关键点。IRAM_ATTR宏必须加。ESP32的中断服务函数如果放在Flash里可能出现“中断触发时Flash正被读取其他代码导致异常”的情况加上这个宏后函数会被放入内部RAM保证中断响应不出幺蛾子。很多人编码器数值偶尔跳一下查了很久发现是这里没加。状态表的原理值得多说一句。A、B两相任意一相变化时状态都会从旧状态迁移到新状态。合法的旋转迁移顺序只有两条路径其他迁移比如从00直接跳到11都是抖动或干扰造成的非法变化。查表法把非法变化返回0天然带一点纠错能力比简单的边沿加电平判断要稳得多。4.2 计数累加、边界判断与映射encoderCount是全局计数理论上可以通过加减来控制任何变量。直接拿它当PWM占空比显然不合适因为旋转的方向正负会影响数值走向而且要加边界限制不能让占空比跑到0以下或100%以上。我习惯的做法是在主循环里定时读取encoderCount先做边界裁剪再映射到目标值。用50毫秒作为处理周期这个间隔足够平滑又不会让界面响应觉得卡顿。#define PWM_PIN 27 #define PWM_CHANNEL 0 #define PWM_FREQ 5000 #define PWM_RES 10 const int32_t maxDuty (1 PWM_RES) - 1; // 1023 void setupPWM() { ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RES); ledcAttachPin(PWM_PIN, PWM_CHANNEL); } void loop() { static int32_t lastCount 0; static uint32_t lastProcessMs 0; uint32_t nowMs millis(); if (nowMs - lastProcessMs 50) { int32_t diff encoderCount - lastCount; lastCount encoderCount; int32_t targetDuty currentDuty diff; if (targetDuty 0) targetDuty 0; if (targetDuty maxDuty) targetDuty maxDuty; currentDuty targetDuty; ledcWrite(PWM_CHANNEL, currentDuty); lastProcessMs nowMs; } }这里用diff而不是直接用encoderCount是刻意为之。这样即使外部逻辑清零计数、或者编码器中途被其他代码修改过主循环仍然只关心“相对变化量”逻辑更干净。边界裁剪放在映射前避免中间变量溢出程序可读性也好很多。4.3 完整示例代码一个旋钮控制一个风扇把上面两部分拼起来就是一个完整的“旋钮调风扇转速”程序。为了让输出更直观我还加了一个OLED显示当前占空比的步骤这里是省略版只保留核心控制链路。#include Arduino.h #define ENC_A 25 #define ENC_B 26 #define PWM_PIN 27 #define PWM_CHANNEL 0 #define PWM_FREQ 5000 #define PWM_RES 10 const int8_t encStateTable[16] {0,-1,1,0,1,0,0,-1,-1,0,0,1,0,1,-1,0}; volatile int8_t lastEncState 0; volatile int32_t encoderCount 0; int32_t currentDuty 0; void IRAM_ATTR encoderISR() { uint8_t s (digitalRead(ENC_A) 1) | digitalRead(ENC_B); encoderCount encStateTable[(lastEncState 2) | s]; lastEncState s; } void setup() { pinMode(ENC_A, INPUT_PULLUP); pinMode(ENC_B, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(ENC_A), encoderISR, CHANGE); attachInterrupt(digitalPinToInterrupt(ENC_B), encoderISR, CHANGE); ledcSetup(PWM_CHANNEL, PWM_FREQ, PWM_RES); ledcAttachPin(PWM_PIN, PWM_CHANNEL); ledcWrite(PWM_CHANNEL, 0); } void loop() { static int32_t lastCount 0; static uint32_t lastProcessMs 0; uint32_t nowMs millis(); if (nowMs - lastProcessMs 50) { int32_t diff encoderCount - lastCount; lastCount encoderCount; currentDuty diff; if (currentDuty 0) currentDuty 0; if (currentDuty 1023) currentDuty 1023; ledcWrite(PWM_CHANNEL, currentDuty); lastProcessMs nowMs; } }这个程序跑起来之后顺时针转旋钮风扇加速逆时针减速反应灵敏基本没有丢步。如果你要调节的步进更细还可以把diff乘一个系数比如diff * 2这样转一格相当于调两格占空比。系数太大手感会发飘1到3之间比较合适。5. 手感进阶加速度、曲线与记忆功能5.1 转得快就调得快旋转速度检测与变速调节基础版本有个体验问题从0%转到100%需要转很多圈调起来手累。很多消费电子产品会加入“加速度”逻辑旋钮转得快每个脉冲对应的数值变化就大。实现方式也不复杂在主循环里记录两次采样之间encoderCount的变化量和时间间隔用变化量除以时间就得到旋转速度。速度高时把映射系数放大速度低时缩小。关键参数是阈值和倍率我实测下来这样一组参数手感不错。uint32_t dtMs nowMs - lastProcessMs; float speed abs(diff) * 1000.0 / dtMs; // 每秒步数 int32_t step diff; if (speed 500) step diff * 4; else if (speed 200) step diff * 2;转得慢只有每秒几十步时每格对应1个占空比单位可以精确微调暴力快速旋转每秒超过500步时每格映射到4个单位快速横跨整个范围。这个设计本质上是在“精细度”和“调节效率”之间做权衡实测应用在调速风扇界面上非常自然用户很快就能体会到。5.2 PWM变化曲线软启动不冲头直接按旋钮的步进量改变占空比电机转速会肉眼可见地“一格一格”跳尤其是低速区间顿挫感明显。更平滑的做法是让实际PWM值缓慢逼近目标值而不是瞬间跳变。软件里可以用一个简单的斜坡函数每次控制循环把currentDuty向targetDuty靠近一个固定步长。步长选得小转速变化丝滑但反应慢选得大响应快但能感觉到阶梯。我一般把步长设在2到5之间控制周期设在10毫秒也就是每秒变化200到500个占空比单位整体手感介于敏捷和柔顺之间。int32_t targetDuty currentDuty diff; // 限幅 targetDuty constrain(targetDuty, 0, 1023); // 斜坡逼近 int32_t step 3; if (currentDuty targetDuty) { currentDuty min(currentDuty step, targetDuty); } else if (currentDuty targetDuty) { currentDuty max(currentDuty - step, targetDuty); }如果你做的是加热台或恒温控制这种软启动还有额外好处避免上电瞬间满功率冲击保护加热元件和电源。5.3 掉电记忆把档位存进非易失存储机械编码器的相对输出模式有个天然问题断电重启后系统不知道自己应该恢复哪个档位。解决办法是把当前占空比存入ESP32的NVS非易失存储区域每次上电自动读取。Arduino框架下用Preferences库就行整个读写只有几行代码。#include Preferences.h Preferences prefs; void setup() { prefs.begin(fan, false); currentDuty prefs.getInt(duty, 0); } void saveDuty() { static uint32_t lastSaveMs 0; if (millis() - lastSaveMs 2000) { prefs.putInt(duty, currentDuty); lastSaveMs millis(); } }注意保存时机。不要每次改变PWM值都写FlashFlash有擦写寿命限制。用2秒甚至更长的时间间隔去重写一次用户正常操作一天也写不了多少次寿命完全不用担心。这个设计在实机测试中很实用风扇开机自动回到上次的档位体验和家用电器一致了。6. 实测调试我踩过的坑和排查心得6.1 编码器数值乱跳、丢步的排查顺序如果你发现旋钮转一下数值跳了好几个或者转完一圈数值少了一截按下面的顺序排查基本能覆盖九成问题。第一步检查上拉。EC11接ESP32最常见的问题是忘记开内部上拉或外部上拉导致悬空引脚电平浮动中断被随机触发。用万用表量一下引脚空闲时的电压应该稳定在高电平3.3V附近。第二步看硬件滤波。如果电路在工作环境中有电机、继电器这些干扰源A、B信号线上会耦合噪声。先尝试在信号引脚对地并联100nF电容还不行就再软硬结合在软件里加入短延时消抖比如在中断服务函数中记录micros()小于1毫秒内再次触发就跳过。不过这招会牺牲一些快速旋转时的精度能硬件解决就不要软件硬扛。第三步确认状态表逻辑。如果方向反了顺时针时count减小检查接线是否把A、B接反或者换一张镜像的状态表。如果只是某个方向漏步多半是状态迁移表写错了比如用了非法的四态解码而非状态机方式这种问题最容易出现在抄代码改引脚时。还有一种情况中断里用了digitalRead。digitalRead内部经过层层封装执行速度相对较慢在高频旋转时中断函数还没跑完下一次中断就来了造成丢步。解决方法是改用GPIO寄存器直接读取速度可以提升一个量级。uint8_t s (GPIO.in ENC_A 0x1) | ((GPIO.in ENC_B 0x1) 1);6.2 PWM输出不正常频率、占空比和通道冲突PWM不输出或者波形不对先别怀疑芯片用示波器或逻辑分析仪看GPIO引脚上的原始波形。如果引脚完全没有波形检查ledcSetup的通道号和ledcAttachPin的通道号是否一致。很多新人把两个通道写错PWM配置在0通道信号却挂在2通道上自然没有输出。还要确认ESP32型号老版本ESP32和ESP32-S3的LEDC通道数量不完全一样S3的LEDC通道组织方式不同代码要稍微调整。如果波形有但频率不对基本是分辨率和频率的计算关系出了问题。比如想输出50Hz舵机信号分辨率就不能设太高我一般设16位分辨率加50Hz1000步对应约1.5度舵机角度变化够用且精度足够。还有一个容易忽略的坑如果你同时用了Arduino的analogWrite要注意ESP32的analogWrite底层其实也走LEDC会和手动配置的通道抢资源。项目里混用analogWrite和ledcWrite时一定要确认它们用的是不同LEDC通道否则后执行的配置会把前面的覆盖掉。6.3 MOS管发热啸叫一个被忽视的频率因素PWM控制电机出现“嗞嗞”啸叫和管子发烫往往不是管子选错而是PWM频率落在人耳可听范围内。我前文提过15kHz左右的频段特别容易碰到电机线圈发出电磁啸叫而且25kHz以上会明显改善因为超出了听觉上限。发热问题另有一个常见原因PWM频率太高MOS管的开关损耗变大。MOS管每一次开关过程都有短暂的线性区损耗频率越高每秒损耗次数越多发热自然越大。电机驱动的PWM频率一般推荐5kHz到20kHz如果电机本身不大、电流在1A以内10kHz左右是个兼顾噪声和效率的折中值。栅极驱动电阻也值得留意。栅极电阻太小开关速度太快电压尖峰高太大开关变慢线性区时间变长损耗增大。10Ω到100Ω是常见取值我用过的多数电路在22Ω到47Ω之间工作最稳定。用示波器看MOS管栅极波形如果上升沿太陡出现振铃就稍微加大电阻如果沿太平缓就减小找到一个折中点。6.4 干扰导致的“幽灵旋转”实际项目里电机启动瞬间电流很大电源电压会有一个跌落如果编码器用同一个电源供电这个跌落可能让编码器信号产生毛刺表现为电机一转旋钮数值自己跳。解决办法是电源隔离或至少用一个大容量的电解电容稳住母线电压。我用过一个470μF电解电容并联在电机电源两端明显改善了电机启动时的干扰。编码器和单片机的电源尽量从稳压模块独立取出不要和电机共用同一根线这是硬件调试里最容易忽略却非常有效的措施。7. 这套方案的更多玩法7.1 从调速风扇到更多应用场景EC11加ESP32加PWM这套组合本质是把“相对位移输入”转换为“连续可调输出”的通用框架今天做的是风扇调速换个负载就是完全不同的产品。做LED调光灯只需要把LED驱动换成MOS管或专用LED驱动芯片代码里把PWM频率调到1kHz以上以避免频闪再加一个RGB三通道循环配色立马变成氛围灯控制器。做温控系统把PWM输出接到固态继电器控制加热棒变量从“占空比”换成“加热功率百分比”配合DS18B20温度传感器就能搭一个PID温控的雏形。做小型CNC或3D打印机的轴手动调节把PWM占空比映射到步进电机的速度值旋钮变成手动JOG拨盘手感比按键调节好太多。EC11还有一个按压开关引脚很多场合可以当作“确定键”配合菜单系统使用。比如按一下切换调节对象速度、亮度、温度旋转改变数值再按下确认单旋钮就能完成整个产品设置流程。这种交互模式在桌面仪器上大量出现效果确实是按键无法替代的。7.2 最后一点个人体会这套方案做完之后我最大的感受是硬件调试的很多问题根源都在机械和电子的交界处。EC11的抖动看似是软件要解决的问题实际上硬件滤波做好了软件压力会小很多PWM驱动看似是代码的问题实际上频率和驱动电路选对了运行就安静稳定。如果你打算从零复刻这个项目我的建议是先把编码器读取部分单独跑通用串口打印计数确认旋转方向正确、数值稳定再做PWM输出。分步验证比一口气写完再调试省太多时间。最后再分享一个调试小技巧编码器计数和PWM输出之间加一条50毫秒的延迟处理看起来微不足道实际对手感和稳定性影响巨大这也是我反复调参后最终保留的习惯。