1. microduck 到底是什么:项目全貌与设计思路
最近两周我一直在折腾 microduck 这个小车机器人项目,从硬件选型到跑通第一行代码,前前后后花了两个完整的周末。说实话,这个 GitHub 上公开的开源项目名字挺讨喜——microduck,就是一只小鸭子造型的双轮小车。它的核心玩法很朴素:用一块主控板控制两个电机,配上几个传感器,让它在桌面上自己避开障碍、沿着黑线跑,特殊情况还能用手机蓝牙遥控。
听起来不复杂,但实际上手之后你会发现,从芯片选型到电机死活不转的排查,每一步都藏着坑,也每一步都有收获。我本职是写软件的,硬件这边不算老手,所以这篇文章里的路线图可能特别适合两类人:一类是"会写代码但没怎么焊过板子"的软件工程师,另一类是刚接触嵌入式的学生朋友。如果你想找一个低成本、高信息密度的练手项目,microduck 是一个非常好的起点。
1.1 一句话讲清 microduck 的定位
microduck 本质上是一个轻量级的双轮差速机器人平台。所谓"轻量级",指的是它对材料、工具、金钱的投入都压到了极低:
- 主控板用 ESP32 系列开发板,市价大概 30 到 50 块钱
- 电机是常见的 TT 减速电机,或者 N20 微型减速电机,一对一二十块
- 驱动芯片用 TB6612FNG 或 DRV8833,几块到十几块
- 传感器按需购买:寻迹、避障、测距,每样都是几块钱的模块
全部物料加起来,不算工具、外壳和电池,总成本能控制在 150 元以内。这个成本比买成品套件还便宜——当然,重点是成品套件不会让你理解每一个零件为什么存在。
它解决的问题也很明确:让一个只写过软件的人,用最短路径把"硬件选型—接线—烧录—跑起来"这一整条链路走通。它刻意保持简单,简单到每个模块都能看懂、能替换、能修。这种"可修复性"恰恰是商业成品给不了你的学习价值。
1.2 从原理图到成品:整体系统怎么拆
我一开始就给自己定了个原则:不追求一步到位,而是把整个系统拆成五个可以独立验证的模块。
- 供电模块:电池、电源开关、稳压电路,负责给主控和电机分别供能
- 控制模块:ESP32 主控,负责读传感器、做决策、输出 PWM 信号
- 运动模块:电机驱动板加两个减速电机,加两个轮子
- 感知模块:ED330 红外寻迹传感器、HC-SR04 超声波测距模块
- 交互模块:0.96 寸 OLED 显示当前状态,蓝牙负责手机遥控
拆完之后,每个模块都能单独测试。比如先把电机驱动这块焊好,单独写一小段代码让电机转起来,再往下做别的。这个思路救了我很多次——如果一次性把所有东西接好再写代码,任何一环出了问题,你都不知道该从哪里查起。模块化拆解不只是代码架构的概念,硬件调试同样适用。
1.3 为什么选这条路:方案对比与取舍
做这种小车机器人,市面上其实有几条经典路线。最省事的是直接买成品套件,说明书一翻,半个小时装完。但那样你学不到东西,因为你没有做过任何一个选型决策,出了问题也只能问客服。
第二条路是照着网上的 Arduino 入门教程,用 UNO 板子加 L298N 驱动模块。这个方案的资料堆积如山,但有两个硬伤:一是 Arduino UNO 用的是 AVR 单片机,性能太弱,跑不了复杂逻辑,以后想加摄像头基本没戏;二是 L298N 模块内部是双极型晶体管,导通压降能到 2V 以上,电池电压 6V 时真正到电机上的不到 4V,续航和扭力都受影响。
第三条路,也就是 microduck 采用的路线:ESP32 加 TB6612 驱动,配合模块化传感器。ESP32 自带 Wi-Fi 和蓝牙,性能最高能到 240MHz 双核,以后想加摄像头做视觉、做联网控制都留了余地。TB6612 是 MOS 管驱动,压降低、效率高,两路电机只需要一路 PWM 加一路方向信号,接线也简洁。这个方案在学习成本、性能、扩展空间三者之间是最平衡的。
2. 硬件选型全解析:从主控到每一个螺丝
这一节详细说说选型过程,包括当时为什么这么选、有什么替代方案,以及我实际踩过的坑。硬件选型是这个项目里最容易让人纠结的地方,因为每个零件都有几十种选择,但不代表你需要把所有选择都搞明白才能下手。
2.1 主控芯片:ESP32 系列为什么是首选
主控是整台 microduck 的脑子。为什么不是 STM32?因为 STM32 的开发门槛对新手极不友好,光环境搭建和调试器配置就够劝退一大批人。为什么不是树莓派?因为它太大、太贵、太耗电,对一台桌面小车来说属于杀鸡用牛刀。ESP32 则是折中得最好的选项:性能足够跑实时控制,开发环境可以用 Arduino 框架快速上手,后期想深入还能切换到 ESP-IDF。
具体选哪一颗 ESP32,我给三个参考建议:
- ESP32-WROOM-32(经典款):双核 240MHz,520KB RAM,开发板价格 30 到 50 块,社区资料最全,适合绝大多数人。
- ESP32-C3:单核 RISC-V,性能弱一些,但价格能压到十几块,引脚也够用,适合低成本改装。
- ESP32-S3:性能和功能最强,带向量指令加速,如果你已经想好后续要接摄像头做视觉识别,直接上它。
我自己用的是经典款 ESP32 DevKitC,原因很简单:microduck 仓库里的示例代码几乎都按这块板子的引脚布局来写,抄作业的时候不用自己改引脚映射,能省下不少排查时间。
2.2 电机与驱动:TT 电机 + TB6612 的经典组合
电机这块,microduck 默认配置是 TT 减速电机。网上搜"TT 电机 3-6V 减速马达"能出来一大片,通常配橡胶轮子一起卖。TT 电机的优势是便宜、结实,扭矩对桌面小车来说绰绰有余;缺点是转速一致性一般,同一批次的两个电机实际转速可能差 5% 到 10%,这个问题后面我会讲怎么在代码里做软件校准。
驱动芯片我强烈建议用 TB6612FNG,而不是新手教程里常见的 L298N。我把两者的关键差异整理成一张表:
| 对比项 | TB6612FNG | L298N |
|---|---|---|
| 输出器件 | 场效应管(MOSFET) | 双极型晶体管 |
| 导通压降 | 约 0.5V | 2V 以上 |
| 最大电流 | 单路 1.2A | 单路 2A |
| 逻辑电压 | 2.7V-5.5V | 5V |
| 模块体积 | 很小 | 很大带散热片 |
| 价格 | 十几块 | 十几块 |
压降这个参数最要命。L298N 在 6V 供电下,电机实际只能拿到不到 4V,扭矩直接掉一截,电池稍微没电就趴窝。TB6612 的压降几乎可以忽略,这也是 microduck 项目选择它的核心原因。
TB6612 的接线逻辑非常清晰:每个电机需要三个信号,一个 PWM 脚负责调速,两个方向脚决定正反转。我把引脚定义留到第三节的表格里一起讲,避免重复。
2.3 传感器与显示:ED330 模块和它的兄弟们
microduck 的寻迹功能靠的是灰度传感器。我在淘宝买的是很常见的 ED330 红外寻迹模块——就是板子上印着 ED330 字样、中间一个红外发射管加一个接收管的小板子。它的原理是反射式红外探测:红外光打到地面后反射回来,接收管根据反射光的强弱输出不同的电平信号。黑色胶带吸光,反射弱;白色桌面反射强,模块就能分辨当前压没压到线。
ED330 模块通常只有三根线:VCC、GND、OUT,其中 OUT 是数字输出。板上有一个可调电位器,用来设置触发阈值。校准的时候有个小技巧:把模块放在白色桌面上,看 OUT 输出是什么电平;再放到黑色胶带上,看输出是否翻转。电位器要调到黑白两种状态都能稳定区分,不能只调到一边灵敏。如果你发现模块在两个场景下输出始终不变,先怀疑电位器没调好,再怀疑模块坏了。
除了 ED330,我还加了两个辅助模块:HC-SR04 超声波测距模块,用来检测前方障碍物;0.96 寸 OLED 显示屏,用 I2C 接口接主控,实时显示当前状态和传感器数值。OLED 表面看起来是装饰品,实际是调试利器——不接屏幕的话,你只能靠串口打印,而小车跑起来之后根本没法连着电脑看串口,屏幕能让你直观看到程序内部的状态。
2.4 机械结构与供电方案:两个最容易翻车的部分
很多教程只讲电路,不讲结构,但机械部分恰恰是让 microduck "像一只鸭子"的关键。车身我用的是一块 3mm 亚克力板,两层结构:上层放 ESP32 和屏幕,下层放电池盒和驱动板。如果你没有切割条件,直接用塑料盒、甚至硬纸板加扎带也能凑合,但要保证两个电机轴平行,否则小车会跑偏。
供电是自制小车里翻车率最高的部分,没有之一。TT 电机瞬间启动电流能到 1A 以上,如果主控和电机共用一个电源,电机一启动,电压瞬间跌落,ESP32 会直接掉电重启。我前几次调试反复遇到这个问题,一度以为是代码死循环。
我的解决方案是:两节 18650 锂电池串联成 7.4V 给电机驱动板供电,同时从电池端接一个 5V 降压模块(我用的 MP1584,输出电流 1A 以上)单独给 ESP32 供电。这样在物理上做到电机和主控电源分离,电机的电流冲击不会干扰主控。
线材方面单独提醒一句:杜邦线便宜,但接触不可靠,尤其是电机这种大电流线路。建议至少用 20AWG 的硅胶线直接焊接到驱动板焊盘上。我踩过的坑就是电机线用杜邦线插在驱动板上,小车一跑就抖动断电,排查半天最后发现是杜邦线虚接。
3. 开发环境与工程骨架:从 GitHub 到第一行代码
硬件都齐了,接下来就是很多人最头疼的环境搭建环节。这一节把工具链安装、引脚规划和工程拉取的完整过程写清楚,照做就行。
3.1 工具链安装:Arduino IDE 还是 ESP-IDF
microduck 的示例代码基于 Arduino 框架,所以最省事的路径就是用 Arduino IDE 开发。建议直接装 2.x 版本,界面比 1.x 舒服,自带串口监视器增强,还能自动补全。
装完 IDE 之后,需要在"文件 → 首选项 → 附加开发板管理器网址"里填上 ESP32 官方的 JSON 地址,然后在"工具 → 开发板 → 开发板管理器"里搜索 ESP32 并安装支持包。这一步要下载几十 MB 的数据,国内网络偶尔会卡,多试几次就行,或者手动下载安装包解压到 Arduino 的 hardware 目录下。
装好支持包后,在"工具 → 开发板"里选择 "ESP32 Dev Module" 或对应板型。还有一个很多人忽略的点:串口驱动。经典款 DevKitC 用的 USB 转串口芯片是 CP2102,Windows 系统第一次插入需要装 CP210x 驱动,否则电脑设备管理器里根本看不到串口。如果你买的板子是 CH340 芯片,对应驱动又不一样,买板子的时候最好问清楚。
3.2 引脚分配表:一次规划,省掉三次飞线
接线之前,强烈建议先把引脚分配画成一张表。microduck 的参考接线可以这样规划,我直接把有效的引脚分配表放出来:
| 功能模块 | 信号 | ESP32 引脚 |
|---|---|---|
| 电机 A PWM | PWMA | GPIO 25 |
| 电机 A 方向 | AIN1 / AIN2 | GPIO 26 / 27 |
| 电机 B PWM | PWMB | GPIO 14 |
| 电机 B 方向 | BIN1 / BIN2 | GPIO 12 / 13 |
| ED330 寻迹模块 | OUT | GPIO 34 |
| HC-SR04 超声波 | Trig / Echo | GPIO 5 / 18 |
| OLED 显示屏 | SDA / SCL | GPIO 21 / 22 |
我特意避开了 GPIO 0、GPIO 2、GPIO 12、GPIO 15 这些有特殊功能的引脚。比如 GPIO 0 是 BOOT 引脚,如果接了强下拉设备,可能导致开发板无法进入下载模式;GPIO 12 是 MTDI 引脚,在某些板子上会影响启动电压。新手最容易犯的错误就是随手选一个引脚,结果烧录之后板子没法正常工作还不明所以。
3.3 拉取工程模板与仓库结构
GitHub 上搜 microduck 能找到官方仓库,通常会有 examples 目录,放着几个不同难度层级的示例代码。如果你从 GitHub 下载 ZIP 包比较慢,可以去国内的代码托管平台找找镜像仓库,搜 microduck 一般能搜到别人同步过来的副本,下载速度快不少。
但我要给一个操作上的建议:不要直接把仓库里的代码整个复制过来用,而是建立一个自己的工程目录。虽然 Arduino 的示例通常是一个.ino文件,但当代码超过一百行之后,我强烈建议按模块拆文件。我的目录结构供参考:
microduck/ ├── src/ │ ├── main.cpp │ ├── motor.cpp │ ├── motor.h │ ├── sensor.cpp │ ├── sensor.h │ └── config.h ├── lib/ ├── docs/ └── README.md这样做的核心价值在于:后面每加一个功能就新建一个模块文件,出了问题定位收缩得极快。很多人一上来就在一个 main 文件里堆几百行代码,出 bug 的时候头都要大,运行日志和功能逻辑搅在一起,改一处崩三处。
4. 核心代码实现:点灯、转电机、站起来
这一节是大家最关心的部分——怎么把硬件变成真正能动起来的 microduck。我从最基础的第一行代码讲起,再说电机控制、传感器读取,最后给出一个完整的运动逻辑用例。
4.1 第一行代码前的烧录测试
很多人拿到板子第一件事就是写业务代码,我的建议是先做一个最小验证:插上电脑,打开 Arduino IDE,先烧一个 Blink 例程。这个例程让板载 LED 一闪一闪,通过它你能确认三件事:电脑能识别串口、IDE 的编译链路正常、烧录过程顺畅。
Blink 的代码核心就三行:pinMode(LED_BUILTIN, OUTPUT)设脚为输出,digitalWrite(LED_BUILTIN, HIGH)点亮,digitalWrite(LED_BUILTIN, LOW)熄灭,中间夹着delay(500)。这个例程如果跑通,说明开发环境没问题,接下来写任何代码都有底。如果这个都跑不通,不要急着往下走,先检查串口是不是选对了、驱动装了没有。
4.2 电机控制:PWM 是什么,怎么调
电机驱动最关键的概念是 PWM,即脉冲宽度调制。说人话就是:用高电平在一个固定周期内占的比例(也就是占空比)来模拟不同的输出电压。占空比 100% 就是全速转,50% 就是半速,0% 就是停。电机本质上是惯性器件,它在每个脉冲周期内来不及反应到完全停止,所以平均电压决定了它的平均转速。
Arduino 框架下有一个大坑:ESP32 的 PWM 和 Arduino UNO 不一样,UNO 上的analogWrite()在 ESP32 上不可用。ESP32 必须用ledcSetup()和ledcAttachPin()来配置。下面是一个简化版的电机控制代码,可以直接抄:
#define PWM_FREQ 1000 // 1kHz 频率 #define PWM_RES 8 // 8 位分辨率,占空比 0-255 void motorInit() { ledcSetup(0, PWM_FREQ, PWM_RES); // 配置通道 0 ledcAttachPin(25, 0); // GPIO25 绑定到通道 0 } void setMotorA(int speed) { // speed 范围 -255~255 if (speed >= 0) { digitalWrite(26, HIGH); digitalWrite(27, LOW); ledcWrite(0, speed); } else { digitalWrite(26, LOW); digitalWrite(27, HIGH); ledcWrite(0, -speed); } }思路很简单:速度值的正负决定方向,绝对值决定大小。正数让 AIN1 高、AIN2 低,电机正转;负数就反过来。这样上层逻辑只需要传一个带符号的数,不用操心方向引脚的状态。
关于 PWM 频率的选择,有个知识点。频率太低,电机会发出明显的高频"嗡嗡"声,而且转动不均匀;频率太高,驱动芯片的开关损耗增大,芯片发热。对 TT 电机这种惯性较大的负载,1kHz 到 10kHz 都能工作,我最终选了 1kHz,噪音和发热平衡得好。如果你换用 N20 微型电机,可以把频率提到 20kHz 以上,避开人耳敏感区——这也是为什么有些 N20 小车跑起来几乎静音。
4.3 传感器读取:让 microduck 有知觉
读 ED330 寻迹模块是数字 IO 的入门操作,直接digitalRead()就能拿到结果。但实际用的时候要注意:单次读值偶尔会有抖动,尤其是在黑白边缘附近,电平会在高低之间快速跳动。所以我在代码里加了一个简单的软件滤波——连续读三次,取出现次数最多的值作为最终结果。这个"三取二"的算法虽然简单,但对付传感器抖动非常有效。
超声波测距模块 HC-SR04 稍微复杂一点,它的工作流程是:主控给 Trig 脚发送一个 10 微秒的高电平脉冲,模块内部发出声波,然后 Echo 脚输出一个高电平,高电平持续的时间就是声波往返的时间。换算公式如下:
float readDistance() { digitalWrite(TRIG, LOW); delayMicroseconds(2); digitalWrite(TRIG, HIGH); delayMicroseconds(10); digitalWrite(TRIG, LOW); long duration = pulseIn(ECHO, HIGH); return duration * 0.034 / 2; // 声速约 340m/s,取一半是单程 }有人会问为什么除以 2——因为声波发出去再从障碍物反射回来,走的是两倍距离,所以要折半。返回值单位是厘米。这里有个使用限制:HC-SR04 的最小测量距离约 2 厘米,最大约 4 米,对桌面小车来说绰绰有余。
4.4 主逻辑:一个简单的运动状态机
各个模块都测通之后,终于可以写主逻辑了。microduck 最简单也最好玩的玩法是"避障巡逻",我给它的整体逻辑设计了一个三态状态机:前进、左转、右转。
enum State { FORWARD, TURN_LEFT, TURN_RIGHT }; State state = FORWARD; void loop() { float dist = readDistance(); bool onLine = digitalRead(ED330_PIN); switch (state) { case FORWARD: if (dist < 15.0) { state = TURN_RIGHT; // 前方有障碍,向右转 } else if (onLine == LOW) { state = TURN_LEFT; // 压到黑线,向左躲 } break; case TURN_LEFT: setMotorA(-150); setMotorB(150); delay(200); // 原地左转 200ms state = FORWARD; break; case TURN_RIGHT: setMotorA(150); setMotorB(-150); delay(200); // 原地右转 200ms state = FORWARD; break; } if (state == FORWARD) { setMotorA(180); setMotorB(180); } delay(20); }这个逻辑非常朴素,但它是理解"传感器输入 → 决策 → 执行器输出"闭环的最小样例。你可以看到,整个程序就是在不断重复:读数据、判断状态、控制电机。后面无论是换成 PID 直线校准、还是加上蓝牙遥控指令解析,核心框架都不会变。
4.5 锦上添花:OLED 状态显示与避障提示
在调试过程中,我发现 OLED 屏幕还能帮助做"可视化"排错。我写了一个简单的显示函数,把当前状态、前方距离、寻迹传感器电平三个值循环刷新在屏幕上:
void updateDisplay(float dist, int sensorVal, int state) { display.clearDisplay(); display.setTextSize(1); display.setCursor(0, 0); display.println("MicroDuck v1.0"); display.print("Dist: "); display.println(dist); display.print("Line: "); display.println(sensorVal); display.print("State: "); display.println(state); display.display(); }这段代码有个实际价值:小车在跑的时候,你不需要连电脑串口,只要低头看屏幕就能知道它的"内心活动"。比如电机不动,屏幕显示 State 一直在切换,那问题多半在电机驱动;如果屏幕显示距离一直是 0,那先查超声波模块的接线。用显示模块辅助定位问题,效率翻倍。
5. 常见问题与排查技巧实录
最后这部分,我把做 microduck 过程中遇到的、以及星友群里其他朋友反馈过的高频问题整理出来,每个都附上排查思路和解决方案。这些内容大多不会写在官方 README 里,属于实战经验。
5.1 上电反复重启的元凶:供电不足
现象:插上电池之后,ESP32 板载 LED 亮一下灭一下,OLED 显示一下又黑屏,串口监视器里反复输出 "Brownout detector was triggered"。
原因分析:ESP32 内部有一个欠压检测器,当供电电压低于阈值(通常是 3.0V 左右)时,它会强制系统复位,防止程序在电压不稳的状态下跑飞。这不是代码 bug,而是电源问题。
解决办法:首先确认电池电压,两节 18650 串联是 7.4V,经降压模块到 5V 没问题;其次确认降压模块输出电流能力,ESP32 加 OLED 加传感器,峰值电流可能到 300mA,建议降压模块至少支持 1A;最后,在 ESP32 的 5V 和 GND 之间并联一个 470uF 电解电容,滤掉电机启动瞬间的电压毛刺。这个电容几乎成了我所有项目的标配。
5.2 电机不转:三种最常见的故障
电机不转是新手遇到概率最高的问题,我总结下来基本逃不出三个原因。
第一种:驱动板电源接错。TB6612 的 VM 和 VCC 是两个不同的电源脚,VM 接电池正极,VCC 接主控的 3.3V 或 5V 逻辑电源。两者接反或者漏接,芯片都不会正常工作。注意 VM 和 VCC 千万不能混在一起。
第二种:使能脚没接。TB6612 上有一个 STBY 待机控制脚,这个脚必须拉高,驱动器才会实际输出。很多接线图为了简化没有画出这个脚,导致电机死活不动。处理方法是把 STBY 直接接到 3.3V 或者主控的一个 GPIO 并拉高。
第三种:PWM 通道冲突。ESP32 有多个独立的 PWM 通道,如果你在代码里把两个电机配置到同一个通道,就会出现"一个转一个不转"或"两个一起转但调速失效"的怪现象。所以电机 A 用通道 0,电机 B 一定要换一个通道编号。
5.3 排查方法论:从现象定位到根因
我用了这么多年嵌入式项目,总结出一个固定排查顺序:先看电源,再看接线,最后看代码。这个顺序不要乱。很多时候大家第一反应就是代码写错了,查了半天,最后发现是杜邦线松了——这种经历一次就够了。
我的排查动作拆开来讲:
- 看电源:用万用表量各节点电压,VM 有没有 7.4V,VCC 有没有 3.3V 或 5V,共地点是否连通
- 看接线:对照引脚表逐根检查,特别是 GND 必须所有模块共地
- 看代码:用
Serial.println()把传感器原始值、状态变量打印出来,确认程序运行到了哪一步
还有一个我反复强调的小技巧:把每个部分拆开单独测。电机不转,就先注释掉传感器逻辑,只跑电机的代码,看电机本身有没有问题;传感器读不到数据,就单独打印传感器值,看有没有跳动。能拆就拆,拆到最后剩下的问题才是真正的问题。
我自己用得最多的调试工具是两样:串口打印和万用表。这两样加起来能解决掉 90% 的问题。示波器当然更好,但新手没必要一开始就投入那么多,等真遇到难啃的定时问题再上仪器也不迟。
做 microduck 这个项目给我最大的感受是:它让你亲手把"想法"变成"实物",这个过程中学到的每一个小坑,都是单纯写代码永远碰不到的。最后再分享一个实际建议——从开工第一天就用文档记录每个模块的测试结果,哪怕只是几行字。等你做到第三版、第四版,回头看这些记录,它们会比任何教程都珍贵。