简介:《悬挂运动控制系统(完整版_原理图及代码)word》面向电子设计竞赛、单片机课程设计与自动控制实践者,围绕悬挂物精确位置控制这一典型问题,给出以SPCE061A单片机为核心的系统实现方案。资源包仅含1个doc文档,大小约517KB,将方案论证、模块选型、硬件设计与原理图、代码说明等内容集中整理,便于按控制核心、电机驱动、检测反馈和人机交互等模块查阅。文档重点讨论光电编码盘测位、直流电机配合L298驱动、反射式红外传感器阵列寻迹,以及以无线数传替代有线反馈的局部闭环修正思路,并涉及点阵液晶与触摸屏交互设计。已有993人学习,适合需要对照完整设计脉络、复盘竞赛方案或开展同类课程设计的中高级学习者参考。
1. 悬挂运动控制:两根吊线解出来的坐标,为什么比电机本身更决定精度
两根绳子吊着一支笔,笔尖悬在坐标纸上方,要求它在纸上走出误差不超过 5mm 的直线、圆和任意曲线——悬挂运动控制里最反直觉的一点是:限制精度的往往不是电机转得准不准,而是你用什么方式算出笔此刻到底在哪儿。电机转一圈,线走多少,账面上能算得很清楚;可线在轮子上是叠着绕的、打滑过、被拉长过,这些误差不会自己消失,只会一毫米一毫米地攒起来,最后在纸面上变成一条歪掉的轨迹。这套系统值得拆开看的地方,就在于它用「线长逆解 + 码盘计数 + 一处定长裸线校正」这套组合,把毫米级的账做平了,而且用的是最便宜的直流电机。
2. 线长逆解:悬挂物的坐标怎么从两根绳长算出来
悬挂机构的运动学关系朴素得让人意外:两个定滑轮固定在同一高度,悬挂物被两根线吊着,两根线的长度唯一确定了它在平面上的位置。整个控制流程从头到尾只做一件事——我希望笔到哪,就算出两根线各自该多长,再让电机把线收到那个长度。真正需要动脑子的是这个「算」的过程里的坐标系选择、精度分配和插值方式。
2.1 逆解与正解:两套公式各管一头
正解是已知两线长求坐标,本质是求两个圆的交点,要处理平方根和判别式;逆解是已知坐标求两线长,各自一次勾股定理就完事。控制系统的主链路走的是逆解:目标坐标是输入,线长是输出,电机只管执行。
按原资料给出的算式反推,坐标系大致是这样设定的:左滑轮 A 在 (-15, 115),右滑轮 B 在 (95, 115),两滑轮中心距 110,工作区是 80cm×100cm 的坐标纸,挂在滑轮连线下方。这个「滑轮比工作面高一截、并且往两边各外扩一点」的布局不是随手定的,后面讲误差敏感度时会看到它的作用。
逆解代码简单到几乎没什么可优化的余地:
/* 线长逆解:给定悬挂物目标坐标 (x, y),算出左右两绳的目标长度,单位 mm */ #define AX (-15.0f) #define AY (115.0f) #define BX ( 95.0f) #define BY (115.0f) typedef struct { float left; float right; } RopeLen; static RopeLen rope_from_xy(float x, float y) { RopeLen r; float dxa = x - AX, dya = y - AY; float dxb = x - BX, dyb = y - BY; r.left = sqrtf(dxa * dxa + dya * dya); /* AC:左滑轮中心到悬挂物 */ r.right = sqrtf(dxb * dxb + dyb * dyb); /* BC:右滑轮中心到悬挂物 */ return r; }参数说明:AX/AY/BX/BY是装配后实测的滑轮中心坐标,不是设计值,装配误差直接换算成定位误差,所以这几个数必须现场标定。返回值是两根线的绝对长度,不是增量——用绝对值的好处是每一步都重新算,误差不会沿着轨迹累积成漂移。
正解只在两个地方用得上:一是液晶屏要显示笔的实时坐标,二是做一致性自检。两圆求交在这个特定布局下可以化简成很短的式子:
/* 正解:由左右线长反推坐标,取工作区内(滑轮下方)的那一支交点 */ static int xy_from_rope(RopeLen r, float *px, float *py) { const float d = BX - AX; /* 两滑轮水平间距 110 */ float a = (r.left * r.left - r.right * r.right + d * d) / (2.0f * d); float h2 = r.left * r.left - a * a; if (h2 < 0.0f) return -1; /* 两圆不相交:计数已经不自洽 */ *px = AX + a; *py = AY - sqrtf(h2); /* 工作区在滑轮下方,取负支 */ return 0; }注意最后那个h2 < 0的判断,它不是防御性编程的摆设。两圆不相交在几何上只有一种解释:左线长和右线长的计数值已经互相矛盾了,通常是丢了脉冲或者校正吸附错了位置。把这个判断做成运行时的告警,等于给系统装了一个免费的自检探针,比事后拿着尺子量轨迹要省事得多。
2.2 从线长差到脉冲数:一圈到底算多少个数
线长算出来之后要变成电机要转多少。原资料的传动配置是带减速箱的直流电机,减速比 301,绕线轮半径取 10mm 做近似(忽略绕线层叠加带来的等效半径变化,这个近似后面会付代价)。那么电机输出轴转一圈,出线长度是 2πr ≈ 62.8mm。
码盘装在电机轴上、跟着电机一起转,所以每一圈码盘缝隙对应的出线长度要再除以减速比。假设码盘一圈有 20 条缝,那么一个脉冲对应的出线长度是:
62.8mm / (20 × 301) ≈ 0.0104mm也就是说分辨率是 10 微米量级。这个数字很关键,它说明一件事:这套系统的分辨率远远不是瓶颈,一个脉冲才 0.01mm,而最终误差是毫米级。差距全在累计误差上——丢脉冲、线的弹性、绕线层叠加、轮子打滑,每一样都比分辨率大两个数量级。搞清楚这一点,后面所有的设计取舍就都能看懂了。
驱动距离差值的计算直接做减法:
/* 点到点运动:只算线长差,正数收线、负数放线 */ static void move_to(float tx, float ty) { RopeLen now = rope_from_xy(g_cur_x, g_cur_y); /* 当前位置的线长 */ RopeLen want = rope_from_xy(tx, ty); /* 目标位置的线长 */ float d_left = want.left - now.left; /* >0 收左线 */ float d_right = want.right - now.right; rope_drive(ROPE_L, d_left); rope_drive(ROPE_R, d_right); }这里用的是「绝对目标线长减当前线长」,而「当前线长」必须来自码盘累计计数,不能用上一步的指令值。用指令值等于开环,一旦丢脉冲就永远补不回来;用计数值至少还有机会被后面的局部校正拉回来。
2.3 圆周运动:余弦定理加 α 角步进
画圆不能靠点到点逐步逼近,那样每步的弦高误差会累积成多边形。原资料用的是角度步进:设两滑轮中心 A、B,圆心 O,悬挂物当前位置 C。三角形 AOC 里用余弦定理:
/* 画圆一步:α 角步进,β 由张角关系推出,再用余弦定理算左右线长 */ static void circle_step(float cx, float cy, float R, float alpha, RopeLen *out) { float ao = hypotf(cx - AX, cy - AY); float bo = hypotf(cx - BX, cy - BY); float ab = BX - AX; /* 两滑轮对圆心的张角 ∠AOB,画圆过程中是常数,只需算一次 */ float cos_aob = (ao * ao + bo * bo - ab * ab) / (2.0f * ao * bo); float aob = acosf(cos_aob); /* α 是 ∠AOC,β 是 ∠BOC,两者之和恒等于 2π - ∠AOB */ float beta = 2.0f * 3.14159265f - aob - alpha; /* 余弦定理:AC² = AO² + CO² - 2·AO·CO·cosα */ out->left = sqrtf(ao * ao + R * R - 2.0f * ao * R * cosf(alpha)); out->right = sqrtf(bo * bo + R * R - 2.0f * bo * R * cosf(beta)); }参数说明:alpha从 0 步进到 2π,步进量决定圆的精度,不是越小越好——步进太小,每步的线长差值低于一个脉冲,电机根本分辨不出来,反而在同一个点上反复抖动。经验做法是让步进对应的弧长落在 1~2mm 这个区间,也就是略大于脉冲当量两三个数量级。
beta由2π - ∠AOB - alpha推出,这一步是整个算法的精髓:不需要对每个点单独解三角形,一个自变量就能驱动整个圆。∠AOB 只跟圆心位置和半径有关,画圆过程中不变,所以每画一个圆只算一次acosf,省下来的时间可以拿去提高插值频率。
2.4 误差敏感度:为什么工作面必须离滑轮远一点
线长对坐标的偏导是∂L/∂x = (x - Ax) / L。当悬挂物跑到左滑轮正下方附近时,L 很小,水平方向移动一点,左线长变化微乎其微——这时候左线的定位信息几乎退化,整个 x 方向的精度全靠右线撑着,误差被放大。
具体感受一下:笔在 (0, 100) 时,左线长是 √(15² + 15²) ≈ 21.2mm。笔往右挪 1mm 到 (1, 100),左线长变成 √(16² + 15²) ≈ 21.9mm,只变了 0.7mm。同一时刻右线在 (95, 115),长度接近 95mm,1mm 的位移能引起接近 1mm 的线长变化。左右两边对这个位移的敏感度差了将近 30%,如果两边的线长误差是同一个量级,折算到坐标上就会被这个权重差拉开。
这就是滑轮要外扩到 -15 和 95 的原因:让工作面整体落在两根线的「中段」,避开敏感度塌陷区。设计这种机构的时候,可以用一个小脚本把工作区四个角的敏感度矩阵打出来,比拍脑袋定坐标靠谱得多。
3. 局部闭环:一行刮掉漆的铜线,怎么吃掉累计误差
上一章反复提到「累计误差」这个词,现在该给它一个具体的数值感受了。假如每个脉冲的当量标定偏了 0.5%,走一趟 1000mm 就攒下 5mm,正好把误差预算吃干净。而这类偏差几乎无法靠标定消除,因为线的弹性、绕线层的叠加、轮子的磨损都在变。既然消不掉,就只能让误差不跨段累积——这就是局部闭环要做的事。
3.1 透射式对管与码盘的角位移量化
码盘的原理很直白:一个开了均匀缝隙的圆盘夹在红外发射管和光敏三极管中间,盘转一格,光路通断一次,接收端就出一个脉冲。原资料用的是透射式对管,相比反射式,透射式的信噪比高得多——光路是直射的,不依赖被测物表面的反射率,环境光的干扰也小。
信号链上有个容易被忽略的器件:74HC14 施密特反相器。码盘低速转动时,光敏管的输出是缓慢爬升的斜坡,直接送进 MCU 会在阈值附近来回抖动,一个缝隙被数成三五个脉冲。施密特触发器带滞回,把慢变沿整成干净的数字沿,同时提供反相。两个作用叠在一起,极性一定要在代码里写死并注释清楚,否则调试时会怀疑人生。
计数策略是单路脉冲配方向脚:
/* 码盘脉冲中断:只有一路脉冲,加减方向靠 L298 的方向脚电平判断 */ volatile int32_t g_rope_pulse[2]; /* 0: 左线 1: 右线,单位:脉冲 */ static void encoder_isr(int ch) { if (g_enc_gate[ch] == 0) return; /* 换向死区内不记账 */ if (GPIO_HIGH == gpio_get(DIR_PIN[ch])) g_rope_pulse[ch]++; /* 正转:收线,线长增加 */ else g_rope_pulse[ch]--; /* 反转:放线,线长减少 */ }g_enc_gate这个闸门变量是关键。方向脚是在 MCU 写 GPIO 的瞬间翻转的,而电机真正反转要等电枢电流过零、机械惯性消掉,这中间几十毫秒里电机还在按老方向转,产生的脉冲却按新方向记账——每一笔都是错账。换向之前把闸门关掉、延时几十毫秒再打开,能消掉大部分错账,剩下的交给 3.2 节的校正兜底。
3.2 图钉开关:最便宜的绝对位置基准
局部闭环的物理实现,是这套系统里最「土」也最巧的一笔。悬线用 0.29mm 的漆包线,每隔一段固定距离把漆刮掉一小截,露出一段裸铜。两个图钉做触点,线从中间穿过。当裸线段经过触点时,两个图钉被导通,MCU 就知道:此刻这根线的长度一定等于第 k 个整定值。
这个信号用来把计数值强拉回真值:
/* 局部闭环校正:检测到裸线段导通,把线长计数吸附到最近的整定点 */ #define MARK_PITCH_MM 100.0f /* 裸线段之间的间距,现场标定 */ static void rope_recalibrate(int ch) { float len_mm = pulse_to_mm(g_rope_pulse[ch]); long k = lroundf(len_mm / MARK_PITCH_MM); /* 最近的整定点序号 */ g_rope_pulse[ch] = mm_to_pulse((float)k * MARK_PITCH_MM); }参数说明:MARK_PITCH_MM是相邻两段裸线之间的距离,做多大取决于你允许误差攒多久。间距 100mm、单段漂移 0.3%,那么一个校正周期内最多攒 0.3mm,够用了。间距太小会带来另一个问题——裸线段本身有长度(比如 3mm),在这 3mm 里触点一直导通,如果每个采样周期都调一次校正,计数值会在整定点附近反复跳。
正确的做法是边沿触发:
/* 只在进入裸线段的那一拍校正一次,离开时解锁 */ static void mark_edge_check(int ch, int level) { if (level && !g_in_mark[ch]) { /* 上升沿:刚踩上裸铜 */ g_in_mark[ch] = 1; rope_recalibrate(ch); } else if (!level) { g_in_mark[ch] = 0; /* 离开裸段,等下一次上升沿 */ } }为什么一个整定点就够了,不需要做全程闭环?因为误差是单调累积的,每经过一个基准点清零一次,残留误差就被限制在「两个基准点之间的漂移量」这个上限内,不会随行程长度增长。这就是「局部」两个字的准确含义——不追求全程闭环,只追求误差不跨段累积。用一根刮了漆的线和两个图钉,换掉了整套绝对值编码器,这个性价比很难被超越。
3.3 无线数传替掉线缆:省的不只是一根线
原资料放弃了有线反馈,改用微功率无线数传模块。这个决定初看像为了「看起来先进」,实际是被物理规律逼的。
有线方案的问题在于,线缆不是理想的信号通道,它同时是一根弹簧。悬挂物在纸面上移动时,线缆的牵引力会随位置和弯折半径变化,从零点几牛到两牛都有可能。这部分力直接叠加在悬线上,等效于给电机加了一个随位置变化的未知负载。低速运行时更难受,线缆的弯曲刚度会让悬挂物出现「爬行」——走一小段停一下,画出来的曲线在显微镜下是一串台阶。
换成无线之后,悬挂端变成了一个自带电池和无线链路的独立节点,悬线上只有纯粹的张力。但代价也很明确:控制回路被拉长了。原来的反馈路径是「传感器 → MCU」,微秒级;现在变成「悬挂端采样 → 无线发送 → 主控处理 → 无线下发 → 悬挂端执行」,毫秒级,而且有丢包风险。
这个延迟直接决定了闭环该放在哪一层。任何需要高带宽的环都不能跨无线链路做,所以闭环必须在悬挂端本地完成,主控只负责发目标、收状态。这也解释了为什么这套系统的架构是「主控 + 悬挂端从机」两个节点,而不是一个大脑统管一切。
指令帧的设计要顺着这个思路来:
/* 主控到悬挂端的指令帧:seq 用于识别重发,避免同一条指令被执行两次 */ typedef struct { uint8_t head; /* 帧头,固定值,用于对齐 */ uint8_t seq; /* 序号,从机丢弃重复序号 */ uint8_t cmd; /* 指令码:点到点 / 画圆 / 寻迹 / 停止 */ int16_t arg0; /* 目标 x 或半径 */ int16_t arg1; /* 目标 y 或起始角 */ uint8_t crc; /* 校验 */ } LinkFrame;关键是arg0/arg1传的是绝对目标,不是增量。丢一帧的后果是悬挂端多停在原地一个周期,下一帧到达后照样走到正确位置;如果传增量,丢一帧就永久少走一段,误差直接进账。这个设计选择的背后是一条通用经验:在不可靠链路上,永远发绝对量、不发增量。
4. 八路红外寻迹:主探测器轮换与屏蔽三对的几何约束
任意曲线寻迹的难点不在识别黑线,而在识别「黑线往哪个方向拐」。单个传感器只能回答「我在线上还是不在线上」,答不出方向。用八个传感器围成一圈,才第一次有了方向信息——但八个全开也不行,背后的那几路会把已经走过的线误判成前方的线,于是就有了这套「主探测器 + 屏蔽三对」的调度算法。
4.1 反射式对管的黑白判据与阈值
反射式对管的工作点很朴素:红外发射管对着纸面照,黑线吸光、白底反光,光敏三极管在两种情况下导通程度差异很大。
原资料的电路处理值得仔细看一遍:探测到黑线时,反射光弱,光敏管截止,A 点为高电平;经过 74HC14 反相之后,MCU 读到低电平。探测到白底时反过来,MCU 读到高电平。所以最终的判据是「低电平 = 压线」,这个极性必须在代码里显式说明,否则第一版固件十有八九会把黑白判反。
/* 8 路红外采样,返回位图:bit i = 1 表示第 i 路压线 */ static uint8_t ir_sample(void) { uint8_t map = 0; for (int i = 0; i < 8; i++) { /* 对管输出经 74HC14 反相,因此低电平代表压在黑线上 */ if (GPIO_LOW == gpio_get(IR_PIN[i])) map |= (uint8_t)(1u << i); } return map; }单阈值比较的隐患在环境光。室内日光灯的光谱里含有大量红外成分,照到白纸上会让光敏管的静态工作点整体偏移,原本的阈值就不灵了。常见的处理办法有两类:一是把发射管做载波调制(几十 kHz 方波驱动),接收端只认这个频率的分量,直流环境光被自然滤掉;二是上电时分别对白底和黑线各采一次,动态算出阈值。前者硬件成本高一点但一劳永逸,后者改动最小,适合在已经画好的板子上打补丁。
4.2 主探测器轮换:用上一拍的命中分布预测下一拍朝向
八个传感器按 45° 均匀排布成一圈,编号 0 到 7。每一拍只启用其中 5 个:主探测器本身,加上它前方两侧的几路;主探测器背后三路被屏蔽。
为什么要屏蔽背后?因为曲线弯曲时,传感器环的后半圈很可能还压在刚刚走过的那段线上。如果不屏蔽,算法会认为「黑线既在前方也在后方」,方向判断直接失效,表现出来就是悬挂物在拐弯处反复横跳。
主探测器不是固定的,它每一拍都在转:
/* 由本拍命中的传感器相对当前主探测器的角度偏移,决定下一拍的主探测器 */ static uint8_t next_primary(uint8_t cur, uint8_t hit_map) { int sum = 0, cnt = 0; for (int i = 0; i < 8; i++) { if (!(hit_map & (1u << i))) continue; int diff = (int)i - (int)cur; if (diff > 4) diff -= 8; /* 环形最小角差,处理跨 0/7 的情况 */ if (diff < -4) diff += 8; sum += diff; cnt++; } if (0 == cnt) return cur; /* 全丢线,保持朝向,交给容错逻辑 */ /* 取平均偏移并四舍五入到整数档位 */ int step = (sum >= 0) ? (sum + cnt / 2) / cnt : -((-sum + cnt / 2) / cnt); return (uint8_t)((cur + step + 8) % 8); }逻辑说明:diff取的是相对当前主探测器的环形最小角差,范围被压到 -4 到 +4,这样跨过 0 号和 7 号的边界时不会算出 ±7 这种荒谬的值。sum / cnt取平均而不是取第一个命中,是因为曲线急弯时可能同时有三四路压线,取平均得到的是黑线的中心方向,取单个命中会偏向某一侧,长期跑下来轨迹会往一边飘。
cnt == 0的分支处理整圈丢线,此时保持朝向不变——这个选择比随机挑一个方向安全得多,因为下一拍照样可能丢线,随机挑会让悬挂物原地打转。
4.3 曲线微元化与断线容错
任意曲线没有解析式,只能微元化:把整段曲线切成足够短的直线段,每一段用点到点算法走一小步,走完重新采样,决定下一步。步长的选择有个硬约束——必须小于传感器阵列的有效探测半径。走得太长,一步跨过了黑线的拐弯处,传感器环整个悬空,直接丢线。
丢线之后不能立刻停,也不能立刻往回退,正确做法是分级降级:
typedef enum { TRACK_OK, TRACK_COAST, TRACK_LOST } TrackState; static TrackState track_update(uint8_t hit_map, float *step_mm) { if (hit_map) { /* 正常压线 */ *step_mm = 2.0f; return TRACK_OK; } if (g_coast_cnt < 3) { /* 短暂丢线:沿原方向惯性前进几拍 */ g_coast_cnt++; *step_mm = 1.0f; return TRACK_COAST; } *step_mm = -1.5f; /* 彻底丢失:原路退回一小段重新搜索 */ return TRACK_LOST; }断续黑线的测试数据里,最大偏差 5mm、时间 49s,靠的就是这套降级逻辑。COAST状态沿原方向小步前进,赌的是黑线只是断了一小截;连续三拍还找不到,就退回一小段重搜。退回而不是原地等,是因为悬挂物停在断口处永远不会自己好起来。
5. 避坑排查:这套系统最容易翻车的五处细节
纸面上的原理都通,落到实物上就开始出各种说不清的现象。下面这几条是这类悬挂系统最容易碰上的,按「现象 → 原因 → 解决」记下来,能省掉不少半夜调参的时间。
5.1 计数与信号链
现象一:往返运动之后,回到原点的坐标总偏 2~3mm,而且偏差方向跟运动方向有关。原因是换向瞬间的错账。方向脚在 MCU 写 GPIO 的瞬间就翻了,电机却要几十毫秒才真正停住并反向,这期间码盘还在按老方向产生脉冲,代码却按新方向加减。解决是加换向死区:换向前关掉计数闸门,延时到电机停稳(20~50ms,取决于负载惯量),改完方向脚再开闸。更稳妥的做法是在换向期间同时把 PWM 拉到零,让电枢电流先归零。
现象二:正解偶尔返回 -1,两圆不相交。这个报警的成因一般是累计丢脉冲,或者裸线段校正时吸附到了错误的整定点(比如刚好在两点中间,lroundf向两边任一边舍入都可能错)。解决分两步:一是把校正的触发条件收紧,只在裸线段上升沿触发一次;二是给正解的返回值加上处理逻辑——检测到 -1 就强制回到最近的基准点重新同步,而不是把这个错误值继续喂给显示和后续计算。
现象三:低速时轨迹是一串台阶,高速时反而更平滑。这不是算法问题,是静摩擦。直流电机加减速箱在极低占空比下转不动,占空比一过阈值又猛地一转,形成爬行。解决办法是给 PWM 加一个最小有效占空比的偏置,让输出在「零」和「能转起来」之间直接跳变,中间那段不连续的区间干脆不用。
5.2 机械与走线
现象四:走短距离很准,走长距离后线长换算出现系统性偏差,越长偏得越多。线在绕线轮上是叠着绕的,第一层和第二层的等效半径差了一个线径。0.29mm 的线,叠到第五层,等效半径就多了 1.45mm,换算成一圈出线长度差了 9%。解决要么在机械上加排线导轮让线一层层整齐排布,要么在软件里按当前层数做半径补偿——后者需要知道绕线轮宽度和层数,标定麻烦,工程上更推荐前者。
现象五:刮漆段附近的校正总是不准,误差忽正忽负。裸线段的长度(几个毫米)在整定值附近形成了一段「模糊区」,触点在这段距离内一直导通。如果代码在导通期间反复校正,计数值会在整定点上来回跳。解决就是 3.2 节说的边沿触发,再补一条:裸线段的两端要刮干净,残留的漆膜会让触点接触电阻不稳定,表现为校正偶发失效。
5.3 通讯与调试
现象六:偶尔卡住几百毫秒,然后突然冲一段。无线链路的典型丢包表现。如果指令帧里带的是增量,这一卡就永久少走一段;带绝对坐标则只是延迟。另一个隐患是从机侧的接收缓冲——如果主控发得太快,从机的串口中断来不及处理,新帧会覆盖旧帧,表现和丢包一样。加序号去重和超时保护能同时解决这两个问题。
现象七:在实验室调好的红外阈值,换到别的房间就不灵。环境光的红外成分差异比想象中大。白天靠窗和晚上开灯,光敏管的静态工作点能差出一大截。临时办法是加遮光罩,把传感器环罩起来只留探测窗口;正经办法是发射端加调制。判断是否需要正经办法,可以做个简单实验:把悬挂物举在空中不动,看八路输出会不会自己跳变,跳了就说明环境光已经在干扰了。
6. 设计取舍:为什么是直流电机加码盘,而不是步进电机
回到最初那个问题:位置怎么测。三条路摆在那里——位移传感器、步进电机、直流电机加码盘。位移传感器精度最高,但它测的是相对坐标,一旦掉帧就无法自校正,而且成品鼠标的接口协议对一颗资源有限的单片机来说是个不小的负担。步进电机看起来最省事,转一步 0.9°,账面精度够用,可一旦悬挂物有重量要求,就得选大转矩型号;转矩越大,静止时的锁死电流越大,功耗和发热都上去了。更要命的是高频失步——失步之后控制器还以为什么都没发生,这种「静默错误」比误差本身更难处理。
| 方案 | 分辨率 | 成本 | 主要风险 | 适合场景 |
|---|---|---|---|---|
| 位移传感器 | 高 | 中 | 相对坐标,掉帧不可恢复 | 短行程、有绝对基准 |
| 步进电机 | 中(0.9°/步) | 高(含驱动时序电路) | 高频失步且无感知 | 低速、负载恒定 |
| 直流电机 + 码盘 | 高(约 0.01mm/脉冲) | 低 | 需要额外的绝对基准做校正 | 变速、负载变化 |
选直流电机加码盘的真正理由,不是它精度更高,而是它的误差是「可见的」。码盘计数会漂,但漂了能被发现、能被校正;步进电机失步了,控制器一无所知。可控的误差胜过不可控的精确。
第二个取舍是局部闭环的位置。做成全闭环当然更好,但那需要沿全程布置高精度位置传感器,成本立刻翻几倍。把校正点稀疏地放在每 100mm 一处,用一根刮了漆的线和两个图钉实现,把误差限制在段内漂移的量级,性价比高得离谱。代价是你必须接受「误差在段内仍然存在」这个事实,并且保证机械结构不会在段内产生剧烈变化。
第三个取舍是无线。它换来了干净的悬线张力,代价是控制回路被拉长到毫秒级,闭环只能下沉到悬挂端。这个约束一旦接受,整个软件架构就定了——主控发绝对目标,从机本地闭环,通讯层做去重和超时保护。
我自己在这类系统上栽过最狠的一次,是把「方向脚翻转」和「脉冲计数」当成两个独立的事,没意识到它们中间隔着几十毫秒的机械惯性。那次的轨迹在往返段永远对不上,查了整整两天才定位到换向死区。从那以后我每次做带方向判据的编码器计数,都强制走一遍「正转—停—反转—停」的往返测试,看回到起点的计数值是否归零,不通过就不往下写算法。
这套资料里除了上面这些算法,还附了完整的原理图和代码,包括电机驱动、光电检测、无线链路的电路细节。对着图看一遍信号怎么从光敏管走到 MCU 的引脚,比只看公式要扎实得多。希望帮到你。
本文还有配套的精品资源,点击获取