简介:一套基于STM32的智能搬运码垛小车完整工程资源,面向单片机学习者、嵌入式开发者和自动化相关专业学生,适用于研究小车运动控制、机械臂抓取与传感器融合定位等场景。资料涵盖STM32核心控制代码、伺服电机与舵机驱动、颜色识别、超声波避障及QTI力感知等模块,并附带Keil工程文件、编译中间文件、hex固件及调试文档,便于直接打开工程查看软硬件交互逻辑。压缩包共772个文件,以C源码(.c)、头文件(.h)为主,辅以uvproj工程配置、hex固件、map/lst调试文件及txt说明文档,整体大小约15.83MB。已有1340人下载学习。通过本项目可掌握多传感器协同控制方法、电机闭环调节思路以及码垛任务的状态机设计,适合需要完整嵌入式项目范例以提升综合开发能力的读者。
1. 先分清搬运码垛小车到底难在哪:闭环、时序和定位
STM32智能搬运码垛小车,这个标题的含金量不在STM32的选型,也不在底盘跑得多快,而在“码垛”两个字。一辆会跑的循迹小车,入门教程遍地都是;但要从取货点抓起一个货块、沿路走到码垛区、按两层三列摆好、再回到起点等下一趟,这套完整动作链才算闭环。搬运码垛小车要处理的不只是电机转不转,还有去哪里、抬多高、什么时候松手,本质上是运动控制加动作时序的组合。这套项目适合准备课设、竞赛作品,或者刚入嵌入式控制方向想做点有完成度的东西的人;不需要上视觉,把轮子、编码器和舵机配合好,就已经能做出一个能演示、能答辩、能复现的完整项目。
2. 硬件选型与系统骨架:围绕STM32怎么搭一条能走能抓的主链路
搬运码垛小车的硬件可以切成三条链:底盘驱动链负责走,码垛机构链负责抓和放,控制与感知链负责告诉车“现在在哪、下一步干什么”。三条链之间没有太深的耦合,先把每条链上的关键器件选对,后面调PID和写时序才能少翻车。
我常看到有人一上来就纠结主控要不要上F407、要不要加FreeRTOS,实际上一辆码垛小车最重的计算也就是速度环和舵机角度映射,根本喂不饱和。控制逻辑的主体是一张状态机表和三五个PID,芯片资源和OS都不是瓶颈,选型的第一要素是外设够用、开发资料多、坏了能马上换。
2.1 主控选型:STM32F103C8T6是多数项目的最优解
就搬运码垛这个负载来说,STM32F103C8T6几乎是为它量身定做的:72MHz主频,两路高级定时器加两路通用定时器,正好能覆盖两路电机PWM、两路编码器计数和一路舵机PWM;再加上串口、I2C、ADC,OLED显示和电池电压检测也都接得下。
F103C8T6这一类芯片虽然flash不算大,但搬运码垛小车的代码量通常在几千行以内,64KB完全够。如果后续想加蓝牙遥控或者串级PID,也只是多占几十行代码,不需要因此升级主控。
F407的优势是主频高、定时器多,但对这个场景属于性能过剩。反而要注意的是:F103的编码器模式在同一时刻不能既当编码器输入又当PWM输出,所以两路电机用TIM2出PWM,编码器用TIM3和TIM4采集,舵机单独挂到TIM1的通道上,这样外设分配才不会打架。完整的外设规划如下表。
| 外设 | 作用 | 说明 |
|---|---|---|
| TIM2 | 左右电机PWM输出 | 输出比较模式,频率调到16kHz左右 |
| TIM3 / TIM4 | 左右电机编码器输入 | 编码器模式,采集霍尔编码器AB相 |
| TIM1 | 舵机PWM输出 | 50Hz周期,控制升降舵机和夹爪 |
| TIM6 | 10ms时基中断 | 读编码器、跑PID、刷新状态机 |
| USART1 | 串口调试 | 打印实际转速、PID输出、当前状态 |
| I2C1 | OLED显示屏 | 显示状态、电池电压、故障码 |
| ADC1 | 电池电压采样 | 分压后监测主电源是否掉压 |
2.2 电机驱动选型:TB6612比L298N更适合这个场景
电机驱动板在搬运小车上直接决定两件事:发热和续航。L298N是很多教程里的默认选项,但它用的是三极管饱和导通,压降经常超过两伏,电池电压稍微低一点,电机端的实际电压就掉得很厉害。码垛小车要爬坡、要反复启停,电流本来就是脉冲式的,L298N在这种工况下发热很明显,是常见的“电机没劲”根源。
TB6612或同级别的MOSFET驱动芯片导通压降只有零点几伏,体积也小得多,可以直接焊在主控板底下或者立着贴在小车底盘上。单路持续电流虽然标称比L298N小,但对于30cm级别的小车、130到370电机、堵转电流不超过2A的情况完全够用。如果做的是载重更高的搬运平台,电机已经用到大扭矩减速电机,那就该考虑带散热片的更大电流驱动,而不是继续在L298N和TB6612之间纠结。
| 对比项 | TB6612 | L298N |
|---|---|---|
| 导通压降 | 0.5V左右 | 2V左右 |
| 发热 | 低 | 高 |
| 尺寸 | 拇指大小 | 巴掌大小 |
| 持续电流 | 1.2A每路 | 2A每路 |
| 逻辑电压 | 3.3V/5V兼容 | 5V逻辑电平 |
| 适合场景 | 中小型搬运小车 | 大功率底盘 |
选驱动板时还要确认逻辑电平:F103的IO是3.3V,部分L298N模块的逻辑输入需要5V,直接接STM32可能出现电平识别不稳定。TB6612的逻辑输入兼容3.3V,省掉一个电平转换的麻烦。
2.3 码垛机构两种方案:舵机夹爪和丝杆升降台
码垛机构的方案选择,取决于你要搬运的货物长什么样、有多重。最常见也最容易复现的是舵机夹爪方案:一个标准舵机控制夹爪开合,另一个舵机或小型推杆负责升降。夹爪用两片亚克力或PLA打印件,内侧贴一层泡棉或橡胶,就能夹住纸盒、瓶子、3D打印的测试块。这方案的好处是结构轻、控制简单、响应快,坏处是夹持力有限,超过500克的货物基本抓不稳。
另一个更接近工业形态的方案是小行程丝杆升降台加上固定托板:小车到位后,升降台把货物抬到目标层高,托板低端一撤,货物滑进垛位。这种方案载重能力大得多,码垛高度也更规整,但需要额外一路电机驱动,动作慢,换向时还有回程间隙,控制时序要专门处理。
我一般建议做课设或竞赛演示用舵机夹爪,理由很实际:舵机的角度控制是一个定时器通道就能解决的事,而丝杆升降涉及限位、堵转保护、停止位置标定,调起来至少多花三天。先把夹爪方案跑通,如果答辩时被问到载重能力,再说明升级到丝杆方案的技术路径,反而更能体现你清楚两种方案的边界。
2.4 电源分配:12V电池不能直接喂给舵机
这是新手最容易忽略的地方。底盘电机直接用电池电压供电没问题,但STM32、编码器、舵机和传感器不能全挂在同一个未经稳压的电源上。电机启动瞬间会拉低母线电压,如果单片机和舵机直接从同一根线上取电,轻则复位,重则flash里的配置被改写,这是典型的“跑一下就死机”的玄学问题。
常见做法是:3S锂电池(标称11.1V)给电机驱动供电;电池出来接一个DC-DC降压模块,输出5V/3A左右,一路给STM32板和编码器,另一路给舵机供电。要注意舵机堵转电流很大,标准舵机堵转可能到1A以上,所以舵机电源和单片机电源要从降压模块出来之后分两路走,各走各的接线端子,避免舵机堵转的尖峰串进单片机电源域。
给一份我做过的低成本配置清单参考,不用照抄,但按这个思路选不会出大问题:
| 部件 | 常见规格 | 数量 | 用途 |
|---|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1 | 控制核心 |
| 电机 | 带编码器的直流减速电机 | 2 | 左右轮驱动,减速比1:30左右 |
| 电机驱动 | TB6612模块 | 1 | 双路H桥 |
| 编码器 | 霍尔AB相编码器 | 2 | 测速与里程 |
| 舵机 | 9g或20kg级别共2个 | 2 | 夹爪开合 + 升降 |
| 电源 | 3S锂电池 + 5V/3A降压 | 1 | 动力与逻辑分开供电 |
| 传感器 | 光电反射开关 | 1到2 | 到位校准和边界检测 |
3. 底盘运动控制落地:编码器测速、PID调速和差速转向
底盘是所有动作的载体,底盘运动控制没做好,后面码垛位置歪了、夹爪对不齐,都是从这里累积出来的。搬运码垛小车对底盘的要求不是快,而是“每次停在同一位置”。要给出一致的停靠结果,必须满足两个前提:轮子转速闭环可控,左右轮速度差可控。这两点分别对应编码器测速加PID,以及差速转向公式。
3.1 编码器测速与PWM输出:先把轮子转速变成能读的数
搬运小车用的直流减速电机通常是带霍尔编码器的型号,减速比1:30左右时,输出轴转一圈对应电机轴转30圈,编码器输出轴端的脉冲数会乘上这个减速比。假设编码器线数是13线,4倍频后电机轴每圈52个计数,再乘减速比30,输出轴转一圈就是1560个计数。这个数后续要经常用,写代码时最好定义成宏,不要散落在计算公式里。
PWM输出这边有一个关键参数:频率。很多默认工程把PWM频率设在1kHz,电机在1kHz下会产生明显的啸叫,转速也会跟着PWM纹波抖动。常见做法是让PWM频率落在16kHz以上,超过人耳可听范围,电机的电流也更连续。
/* TIM2 CH1/CH2:输出16kHz的PWM驱动左右电机 */ void MotorPWM_Init(void) { __HAL_RCC_TIM2_CLK_ENABLE(); htim2.Instance = TIM2; htim2.Init.Prescaler = 0; /* 72MHz / (0+1) = 72MHz 计数频率 */ htim2.Init.Period = 4499; /* 72MHz / 4500 = 16kHz */ htim2.Init.CounterMode = TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(&htim2); TIM_OC_InitTypeDef oc = {0}; oc.OCMode = TIM_OCMODE_PWM1; oc.Pulse = 0; /* 初始占空比为0 */ oc.OCPolarity = TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(&htim2, &oc, TIM_CHANNEL_1); HAL_TIM_PWM_ConfigChannel(&htim2, &oc, TIM_CHANNEL_2); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2); } /* TIM3:编码器模式,统计左电机AB相脉冲 */ void Encoder_Init(void) { __HAL_RCC_TIM3_CLK_ENABLE(); TIM_Encoder_InitTypeDef enc = {0}; enc.EncoderMode = TIM_ENCODERMODE_TI12; /* 双沿采样,4倍频 */ enc.IC1Polarity = TIM_ICPOLARITY_RISING; enc.IC2Polarity = TIM_ICPOLARITY_RISING; /* 若方向反,把这里改成下降沿 */ enc.IC1Prescaler = TIM_ICPSC_DIV1; enc.IC2Prescaler = TIM_ICPSC_DIV1; enc.IC1Filter = 0x0F; /* 滤波,去掉毛刺 */ enc.IC2Filter = 0x0F; htim3.Instance = TIM3; htim3.Init.Period = 0xFFFF; /* 16位自动重装 */ HAL_TIM_Encoder_Init(&htim3, &enc); HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL); __HAL_TIM_SET_COUNTER(&htim3, 0); }编码器初始化里最值得说的是IC1Filter这个字段。霍尔编码器的信号在电机启动瞬间容易带毛刺,不滤波的话,后续PID读到的速度会一跳一跳的,调到后面全是噪音。把滤波器开到一个合理档位,能省很多排查时间。方向反了不必改接线,把IC1Polarity改成下降沿,或者读回负数后直接取反,效果一样。
3.2 PID速度环:三个参数怎么调才不飘
有了编码器读数,下一步是让轮子精确转到目标转速。直流减速电机的转速随负载变化很明显,搬运小车载货前后负载差异大,开环PWM根本稳不住速度,所以必须做反馈闭环。搬运速度环最常用的是增量式PID,配合一个10ms定时中断读取当前速度,计算输出增量。
参数整定上,我习惯遵循一个顺序:先把Kp从0慢慢往上加,让速度能接近目标但不震荡;再加Ki消除稳态误差;最后加一点Kd抑制超调。Kd在速度环里很容易引入噪声,因为微分对高频干扰敏感,编码器读数的波动会被放大,所以Kd一般是三个参数里最小的,甚至有些速度环只做PI就够了。
/* 10ms定时中断:读编码器、算实际速度、跑PID、更新占空比 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance != TIM6) return; int16_t cnt = (int16_t)__HAL_TIM_GET_COUNTER(&htim3); __HAL_TIM_SET_COUNTER(&htim3, 0); /* cnt是10ms内的编码器脉冲数,乘以100折算成“每秒计数” */ int16_t speed_actual = cnt * 100; int16_t pid_out = SpeedPID(target_speed_L, speed_actual); /* 上一次占空比数值加上增量,然后限幅到±1800 */ static int16_t last_ccr_L = 0; last_ccr_L += pid_out; if (last_ccr_L > 1800) last_ccr_L = 1800; if (last_ccr_L < -1800) last_ccr_L = -1800; /* 负值代表反转,TB6612需要切换IN1/IN2方向 */ if (last_ccr_L >= 0) { HAL_GPIO_WritePin(MOTORL_IN1_GPIO_Port, MOTORL_IN1_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(MOTORL_IN2_GPIO_Port, MOTORL_IN2_Pin, GPIO_PIN_RESET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, last_ccr_L); } else { HAL_GPIO_WritePin(MOTORL_IN1_GPIO_Port, MOTORL_IN1_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(MOTORL_IN2_GPIO_Port, MOTORL_IN2_Pin, GPIO_PIN_SET); __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, -last_ccr_L); } }速度环PID实现:
#define Kp 0.6f #define Ki 0.05f #define Kd 0.05f int16_t SpeedPID(int16_t target, int16_t actual) { static int16_t err_last = 0; static int32_t err_sum = 0; int16_t err = target - actual; err_sum += err; if (err_sum > 500) err_sum = 500; /* 积分限幅,防饱和 */ if (err_sum < -500) err_sum = -500; float d_err = err - err_last; err_last = err; int32_t out = (int32_t)(Kp * err + Ki * err_sum + Kd * d_err); return (out > 400) ? 400 : ((out < -400) ? -400 : out); }这里有两个容易被忽略的细节。第一是积分限幅,没有它,小车长时间堵转时err_sum会积累到很大,一旦障碍物移开,轮子会猛冲一下,这就是“堵转后发疯”的原因。第二是输出限幅,PID输出的是增量,累加到占空比上,但累加结果必须限制在PWM比较值的合法范围里,否则超过ARR值后,STM32的比较值会从高位溢出,表现为占空比突然跳变。增量限幅我给的是±400,配合Kp=0.6,单次输出修正量不超过当前占空比的百分之十几,调试时从这个量级起步比较稳。
3.3 差速转向:搬运车行进时的两种姿态
码垛小车在场地里只有两种姿态:直行到目标垛位前,以及原地旋转对准角度。两种姿态都可以用差速模型表达:左右轮速度分别等于线速度加减一个角速度项。搬运小车的转向不需要像竞速车那样平滑过弯,通常走到码垛区入口后原地转90度,然后再直行对位,这样每个动作都是可预测的。
/* 设置差速:linear为平均速度,angular为角速度,映射到左右轮目标转速 */ void SetDifferentialSpeed(int16_t linear, int16_t angular) { /* 角速度换算系数0.15由车体轮距决定,实际按调试效果微调 */ int16_t diff = (int16_t)(angular * 0.15f); target_speed_L = linear + diff; target_speed_R = linear - diff; if (target_speed_L > 1500) target_speed_L = 1500; if (target_speed_L < -1500) target_speed_L = -1500; if (target_speed_R > 1500) target_speed_R = 1500; if (target_speed_R < -1500) target_speed_R = -1500; }原地旋转时linear给0,angular给一个固定值,左右轮速度相反,车就会绕几何中心转。码垛对位最怕的是转完角度后车轮打滑导致航向偏掉,所以我会在旋转前把速度降到很低,旋转结束后延时200ms让车彻底停稳,再进入直行对位。别小看这个停顿,舵机抓取和底盘停稳如果有重叠,货物会晃动,放下去的位置就偏了。
4. 码垛动作怎么规划:舵机时序、垛位映射和到位检测
底盘把货物运到码垛区之后,真正考验控制逻辑的是码垛动作本身。码垛听起来只是“把东西放上去”,但拆开看有三个问题:夹爪什么时候张开、升降舵机转到哪一层、每个垛位在哪里。这三个问题的答案如果分散写在中断里各管各的,程序很快就会变成一团乱麻。我一般会把码垛动作提前固定成一条“动作序列”,码垛时只查表,不现场算角度。
4.1 舵机控制与抓取时序:开合角度和抱紧力之间的取舍
舵机控制的核心是50Hz周期下的PWM脉宽映射,0.5ms到2.5ms对应0到180度。这个映射是所有舵机控制的基础,写成一个通用函数后,夹爪和升降舵机都复用同一个入口。
/* 舵机角度设置:50Hz周期,脉宽500us~2500us */ void Servo_SetAngle(TIM_HandleTypeDef *htim, uint32_t channel, uint8_t angle) { if (angle > 175) angle = 175; /* 避免打到机械限位 */ if (angle < 5) angle = 5; uint32_t ccr = 500 + ((uint32_t)angle * 2000) / 180; __HAL_TIM_SET_COMPARE(htim, channel, ccr); }注意这个函数只负责把角度写到比较值寄存器,舵机真正转到目标角度需要时间。标准舵机的响应时间通常在一两百毫秒级,所以每次改角度后必须延时,不能紧接着执行下一个机械动作。抓取时序上,我常用的顺序是:先下降、再张开、再闭合、最后上升;放货时反过来,先到位、再张开、小幅度后退、闭爪回收。夹爪的张开角度不要总给满,给到刚好能包住货物外轮廓就行,角度过大反而会让夹持行程变短,小尺寸货物根本夹不住。
升降舵机的角度选择也要留余量。看起来0到180度都能用,但很多舵机在极限角度附近扭矩会下降,而且机械结构的死点风险高,我会把实际使用的角度限制在20度到160度之间。这样的话,升降行程略小于理论值,但重复精度更稳。
4.2 垛位映射表:两层三列的坐标怎么定义
码垛规划的本质是把物理空间的垛位映射成一组底盘停靠距离和升降舵机角度。我做两层三列的码垛,地图表按“先低后高、逐列摆放”的规则设计:先把第一层三个位置放满,再放第二层。这张表写死在代码里,通过行列下标访问,码垛顺序就是遍历下标。
typedef struct { uint16_t run_mm; /* 从取货点前进到该垛位的距离,单位mm */ uint8_t lift_deg; /* 升降舵机角度,决定货物放置高度 */ } PalletCell; /* 两层三列码垛表:第一层低角度,第二层高角度 */ const PalletCell PAL_PLAN[2][3] = { { { 100, 25 }, { 180, 25 }, { 260, 25 } }, /* 第一层,高度角25度 */ { { 100, 80 }, { 180, 80 }, { 260, 80 } } /* 第二层,高度角80度 */ }; #define GRIP_OPEN 150 /* 夹爪张开角度 */ #define GRIP_CLOSE 30 /* 夹爪闭合角度 */ void PlaceOnce(uint8_t layer, uint8_t col) { PalletCell cell = PAL_PLAN[layer][col]; MoveByMillimeter(cell.run_mm); /* 底盘走指定距离 */ Servo_SetAngle(&htim1, TIM_CHANNEL_2, cell.lift_deg); /* 升到目标层高 */ delay_ms(250); /* 等升降舵机到位 */ Servo_SetAngle(&htim1, TIM_CHANNEL_1, GRIP_OPEN); /* 张开夹爪 */ delay_ms(300); /* 等货物脱离 */ MoveByMillimeter(-15); /* 后退15mm,避免碰撞垛位 */ Servo_SetAngle(&htim1, TIM_CHANNEL_1, GRIP_CLOSE); /* 闭爪收回 */ }这里有两个设计细节值得展开。第一,第二层的高度是用升降舵机角度表达的,没有用位移传感器,角度和层高之间是近似线性关系,调一次标定后,重复精度一般够用。如果货物比较重导致舵机下垂,角度和实际高度就不匹配了,那时候才需要换丝杆方案或者加限位开关。第二,放货时的“后退15mm”不是可有可无,很多翻车都发生在夹爪松开后还停在原处,货物其实还靠夹爪虚托着,一闭爪又把货物带歪了。
4.3 到位检测:用限位开关还是编码器归零
码垛小车的到位检测有三种常见做法:限位开关、编码器里程、光电开关校准。限位开关是最可靠的绝对位置判据,但每个垛位都装开关成本太高;编码器里程适合长距离直线运动,但轮子打滑会造成累计误差;光电开关通常装在码垛区入口,车身经过时触发,用来把编码器计数归零,消除之前的累计误差。
我会结合着用:运动过程中以编码器里程为主,在进入码垛区入口处放一个光电反射开关,车身挡光触发中断,把里程计数清零。此后从入口到每个垛位的距离都是短距离相对运动,误差很小,这个方案比单纯依赖编码器靠谱得多,也比每个垛位装限位开关省事。
/* 光电开关触发中断:入口清零里程 */ void EXTI_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(SENSOR_PIN) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(SENSOR_PIN); encoder_L = 0; encoder_R = 0; flag_entry_triggered = 1; } }里程闭环函数用左右轮编码器的平均值作为判断依据,因为单轮计数在转弯和打滑时都会漂移,平均值能抵消一部分左右不均的偏差。长距离段之间加一个清零点,是搬运小车定位最实用的低成本手段。
5. 搬运码垛小车最常见的5个坑与排查方法
这一章列的是我自己做搬运码垛小车时踩过、也帮别人排查过的问题。每条都按现象、原因、解决的顺序写,适合在调试时对照定位。
5.1 电机只抖不走,还伴随着明显的电流声
现象:上电后电机嗡嗡响,轮子小幅振动但转不起来,用手摸电机壳发烫,电池电压掉得很快。
原因:大概率是PWM频率太低。很多开发板模板默认把定时器分频做得很大,PWM频率只有1kHz左右,这个频率下直流电机的电感会限制电流上升,导致转子只在每个周期的前段获得少量能量,表现出来就是抖动。另一个常见原因是电源线太细,电机启动瞬间压降太大,芯片都被拉复位了,表现是“一启动就重启”。
解决:先把PWM频率改到16kHz,也就是把定时器的Prescaler设为0、Period设为4499,然后用直径0.75平方毫米以上的硅胶线重新给驱动板供电,最好从电池端直接焊接,不走杜邦线。这两个改动做完,抖动问题基本消失。
5.2 PID一上电就冲过头,转速来回震荡
现象:给定目标速度后,轮子先猛加速,然后明显减速到停,再加速,如此往复;串口打印实际转速能看到一条围绕目标值大幅摆动的曲线。
原因:最常见的是Kp过大,系统进入振荡;其次是编码器AB相方向接反,导致反馈符号错误,PID把一个正误差当成负误差来纠正,形成正反馈。还有一种隐蔽原因是没有做输出限幅,PID输出累加到占空比后超出定时器ARR,发生比较值回绕。
解决:先把Kp降到原来的一半以下,Ki和Kd都清零,确认转速能缓慢逼近目标;然后检查编码器方向,把车架起来用手缓慢转轮子,观察串口打印的计数值是否和实际方向一致,不对就把IC1Polarity改成下降沿。最后检查PID输出累加值是否永远落在0到1800之间,超出则强制限幅。
5.3 夹爪明明闭合了,圆柱体货物还是滑落
现象:夹爪夹一个方形测试块没问题,换成矿泉水瓶或圆管,夹紧后稍微一晃就掉,有时瓶身还被夹出了凹痕。
原因:夹爪接触面是平的,圆柱体只有两条线接触,压强很大但摩擦力小;另外,圆柱体受力后会产生轴向滑移,平夹爪没有限制轴向移动的结构。
解决:在夹爪内侧贴一层2mm厚的泡棉双面胶或橡胶垫,接触面会从线接触变成面接触,摩擦力立刻上来;如果你有条件打印夹爪,把夹爪内壁改成V型槽,V型槽能同时限制圆柱体的上下和左右移动。还有一个调试习惯:夹持力靠舵机角度表达,太紧会挤压货物,太松会滑落,实测时以夹住后轻轻抖动不脱落为准。
5.4 码垛位置每次偏移几厘米,越码越歪
现象:第一次放货位置正,第二箱开始逐渐偏右或偏前,到第三层时货块边缘已经超出了前一层,甚至碰倒旁边的箱子。
原因:这是典型的累计误差问题。编码器里程依赖轮子滚动,但小车在加速、负重和微转弯时都会打滑,每次打滑损失几毫米,经过几次动作放大成厘米级偏移。另外,底盘每次停稳的瞬间惯性滑动也会贡献偏差,车速越快,偏差越大。
解决:在码垛区入口安装一个光电反射开关,每次进码垛区时触发清零,把累计误差重置掉。另外,把最后一段直行的目标速度降到巡航速度的一半,停稳后延时200ms再执行夹爪动作,惯性滑移会大幅减少。我见过不少人把精力花在修改PID追求零误差,实际上这个场景下做归零校准比调PID更治本。
5.5 舵机一动作,单片机就复位
现象:夹爪或升降舵机转动的瞬间,OLED闪一下或者直接重启,串口输出停在初始化日志处。
原因:舵机的堵转电流瞬间可以冲到1A以上,如果舵机供电线和单片机供电线在同一个降压模块的同一路输出上,电流突变引起的电压跌落会直接打到单片机的复位门槛。更严重的情况是电源线上寄生电感导致的负压尖峰。
解决:把舵机电源和单片机电源从降压模块出来后分两条支路供电,各自独立走线到负载端;在舵机电源端和单片机电源端各加一个470uF电解电容和一个0.1uF陶瓷电容,陶瓷电容负责吸收高频尖峰,电解电容负责稳住跌落。这个组合是我测过最有效的低成本抗复位方案。电源地线不要吝啬,用同一根粗线在主电容处汇合,避免形成地环路。
6. 把控制逻辑收进状态机,外加一个开机自检习惯
前面几章的代码解决的是“单个动作怎么做”,但一辆码垛小车需要连续完成取货、行走、摆放、归位一整套动作。如果这些动作用一堆延时调用硬串起来,后期加一个传感器或者改一个时序都很痛苦。把逻辑收进状态机是这一步的关键,也是搬运小车从“能跑”变成“像样”的分水岭。
typedef enum { ST_IDLE, ST_PICK, /* 到取货点,闭爪抓取 */ ST_LIFT, /* 升到运输高度 */ ST_DRIVE, /* 前往码垛区 */ ST_PLACE, /* 按垛位表放货 */ ST_RETURN /* 回取货点,进入下一轮 */ } SysState; SysState state_cur = ST_IDLE; void Task_StateMachine(void) { static uint8_t layer = 0, col = 0; switch (state_cur) { case ST_IDLE: if (start_button_pressed) state_cur = ST_PICK; break; case ST_PICK: MoveByMillimeter(PICK_POSITION_MM); Servo_SetAngle(&htim1, TIM_CHANNEL_1, GRIP_CLOSE); delay_ms(200); state_cur = ST_LIFT; break; case ST_LIFT: Servo_SetAngle(&htim1, TIM_CHANNEL_2, TRANSPORT_HEIGHT); delay_ms(250); state_cur = ST_DRIVE; break; case ST_DRIVE: /* 到达码垛区入口,光电开关清零里程 */ if (flag_entry_triggered) state_cur = ST_PLACE; else SetDifferentialSpeed(SPEED_CRUISE, 0); break; case ST_PLACE: PlaceOnce(layer, col); col++; if (col >= 3) { col = 0; layer++; } if (layer >= 2) state_cur = ST_RETURN; else state_cur = ST_DRIVE; break; case ST_RETURN: MoveByMillimeter(-RETURN_DISTANCE); layer = 0; col = 0; state_cur = ST_IDLE; break; } }状态机的每个状态只负责一件事:进入条件、执行动作、退出条件。我最开始调试时总希望把延时写短一点让动作快一些,后来发现码垛动作的正确率依赖每个状态的稳定完成,所以我在每个状态入口都加一个100ms超时,如果状态没在预期时间内完成,就进入故障状态并停止所有输出。这个习惯后来救过我很多次,至少每次翻车都能从串口日志里看到卡在哪个状态,而不是对着一个静止的小车猜原因。
我建议所有状态都预留一个手动单步测试入口:按键按一下,状态机只前进一个状态。这样调试夹爪时序时不需要让小车完整跑一圈,大大缩短了迭代周期。如果你能做到开机自检里包含“舵机回中、编码器清零、光电开关检测、电压低报警”这几项,再配合状态机日志,这台搬运码垛小车就已经具备了一个小型自动化设备该有的排错能力。
希望这些从选型到排查的经验能帮你少走弯路,也欢迎你按这个框架自己改出一套更顺手的码垛动作链。
本文还有配套的精品资源,点击获取