1. 项目缘起从一块“古董”开发板开始的智能硬件之旅最近在整理工作室的物料箱翻出来一块几年前入手的 Intel Curie Nano 开发板。这板子当年也算是 Intel 进军物联网和可穿戴领域的一个标志性产品内置了 Intel Curie 模组集成了低功耗的 Quark SE 微控制器和一个 32 位的 ARC 架构传感器协处理器还有 6 轴 IMU 和蓝牙。不过后来 Intel 在物联网硬件上战略收缩这板子也就渐渐淡出了主流视野成了我们这些硬件爱好者手里的“收藏品”。看着它静静地躺在防静电袋里总觉得有点可惜毕竟硬件底子还是不错的。正好手头有个一直想做的项目——一个能自动分类、压缩甚至提醒你倒垃圾的“智能垃圾桶”不如就用它来当主控也算是让这块板子发挥一下余热。这个“智能垃圾桶”项目我打算分成几期来做从易到难从基础到复杂。第一期我们就从最根本的“底盘”开始。为什么是底盘因为一个好的移动平台是整个项目稳定运行的基础。想象一下你的垃圾桶需要自主移动到指定位置或者根据指令调整朝向如果底盘不稳、控制不灵后面加上再多的传感器和智能算法都是白搭。这就像盖房子地基没打好上面装修得再漂亮也住不踏实。本期“底盘篇”的核心目标很明确利用 Intel Curie Nano 作为主控搭配常见的电机驱动模块和轮子搭建一个稳定、可控的双轮差分底盘。我们会深入探讨如何通过 Arduino 环境为这块相对小众的板子编程如何通过 SPI 总线与外围设备高效通信比如读取更高精度的编码器数据并解决在驱动和控制中可能遇到的实际问题。无论你是对 Intel Curie 这块板子感兴趣还是想学习如何从零搭建一个机器人底盘亦或是想了解 SPI 通信在电机控制中的实际应用这篇内容都会给你带来一次扎实的实战体验。2. 硬件选型与核心组件解析搭建一个差分底盘硬件选型是第一步它直接决定了后续开发的难度和最终性能的上限。我的选型原则是在满足基本功能的前提下尽量利用手头已有的物料同时兼顾学习的价值。2.1 主控Intel Curie Nano 的再审视首先是我们今天的主角Intel Curie Nano。它本质上是一个基于 Intel Quark SE 微控制器的 Arduino 兼容板。其核心优势在于极低的功耗和内置的传感器融合引擎非常适合可穿戴设备。但对于机器人底盘来说我们更关注以下几点GPIO 与 PWM它提供了足够数量的数字 IO 和 PWM 输出足以驱动两个电机并连接必要的传感器。通信接口具备 I2C 和 SPI 接口这对于连接现代传感器如 SPI 接口的磁编码器至关重要。开发环境兼容 Arduino IDE极大地降低了开发门槛。虽然 Intel 官方对其的支持已减弱但社区的基础库和示例仍然可用。它的局限性也很明显主频较低32MHz内存有限SRAM 仅 24KB处理复杂的控制算法如 PID 调参过程或大量数据时会有些吃力。但这对于本期实现底盘的基础移动和速度控制来说是完全足够的。选择它正是为了在资源受限的环境下实践高效编程。2.2 动力与驱动电机、驱动模块与电源电机我选择了两个常见的 N20 微型减速电机。它们体积小、扭矩足自带减速箱非常适合小型底盘。关键是我手头有配套的增量式光电编码器AB 相输出可以用于测量电机转速实现闭环速度控制。电机驱动模块这是连接弱电主控和强电电机的关键。我选用的是 TB6612FNG 双路电机驱动芯片模块。相比于经典的 L298NTB6612FNG 效率更高、发热更少支持 PWM 调速和正反转控制逻辑电压与 Curie Nano 的 3.3V 完美兼容无需电平转换。电源整个系统需要两组电源一是为 Curie Nano 和逻辑电路供电的 3.3V/5V二是为电机供电的功率电源。我使用一块 7.4V 的 2S 锂聚合物电池。通过一个降压模块如 MP1584EN将电池电压降至 5V 给 Curie Nano 的 VIN 引脚供电同时电池电压直接接入 TB6612FNG 的 VMOT 引脚驱动电机。这里有个重要细节必须确保逻辑部分Curie Nano, TB6612和电机部分的地GND是共用的即整个系统只有一个共同的参考地否则控制信号会紊乱。2.3 感知与反馈编码器的选择与 SPI 的引入要让底盘走直线、转特定的角度开环控制只给 PWM不管实际转速是绝对不行的漂移会非常严重。因此我们需要电机编码器来提供速度反馈。我手头的光电编码器是增量式的通过 AB 相输出脉冲。Curie Nano 可以利用外部中断引脚来捕获这些脉冲计算转速。但这里我想引入本期的一个技术重点SPI 通信。为什么考虑 SPI增量式编码器在高速或需要绝对位置时对主控的中断响应和计数能力要求高容易丢失脉冲。而一些更先进的磁编码器如 AS5047P、MA730则通过 SPI 接口直接输出一个绝对角度值比如 14 位0-16383。主控只需定期通过 SPI 读取这个值通过计算角度差就能得到精确的速度和位置信息无需担心中断丢失抗干扰能力也更强。这为后续实现更精准的位置控制打下了基础。因此在硬件连接上我预留了 SPI 接口Curie Nano 的引脚 10-SS, 11-MOSI, 12-MISO, 13-SCK为未来升级为 SPI 编码器做好准备。本期我们先使用中断方式读取增量编码器但会详细讲解 SPI 的配置为“第二期”做好铺垫。2.4 结构件与杂项底盘结构我用的是激光切割的亚克力板轻便且易于加工。轮子、万向轮或从动球、电机固定支架、电池仓等都是模型用的标准件。连接线建议使用杜邦线进行原型搭建但为了可靠性后期最好焊接或使用更可靠的连接器。下表总结了本期核心硬件清单及连接要点组件型号/规格关键接口/参数与 Curie Nano 连接要点主控Intel Curie Nano3.3V 逻辑 Arduino 兼容核心控制中心电机驱动TB6612FNG 模块双路逻辑电压 3.3V-5VPWMA/B接 PWM 引脚AIN1/AIN2, BIN1/BIN2接数字 IO电机N20 减速电机 (带编码器)工作电压 6V减速比 1:30电机线接驱动模块输出编码器线接中断引脚编码器增量式光电编码器 (AB相)每转脉冲数 (PPR)A相接外部中断引脚B相接普通数字输入引脚电源7.4V 2S 锂聚合物电池-经降压模块至 5V 接 Curie VIN电池直连驱动 VMOT结构亚克力底盘、轮子、万向轮-确保电机安装牢固底盘重心平衡3. 软件环境搭建与 Curie Nano 的 Arduino 配置虽然 Curie Nano 相对小众但在 Arduino IDE 中配置它并不复杂。关键在于找到正确的板卡支持包。3.1 安装 Arduino IDE 与板卡支持首先确保你安装了最新版的 Arduino IDE1.8.x 或 2.0均可。打开 IDE进入“文件”-“首选项”。在“附加开发板管理器网址”中添加以下 URLhttps://github.com/01org/corelibs-arduino101/raw/master/package_index.json这个链接包含了 Intel Curie 系列开发板的支持包。添加后点击“确定”。接着打开“工具”-“开发板”-“开发板管理器”。在搜索框中输入“Curie”。你应该能看到一个名为“Intel Curie Boards by Intel”的包。选择它并点击“安装”。安装过程可能需要几分钟请保持网络通畅。安装完成后你就可以在“工具”-“开发板”列表中看到“Intel Curie Nano”的选项了选择它。3.2 关键设置与端口选择选择好开发板后还需要注意“工具”菜单下的几个选项端口将 Curie Nano 通过 Micro-USB 线连接到电脑这里会出现对应的串口如 COM3 或 /dev/ttyACM0选择它。编程器保持默认的“AVRISP mkII”即可。其他选项如 CPU 频率等本期项目保持默认设置。3.3 验证与基础程序上传为了测试环境是否搭建成功我们可以上传一个最简单的 Blink 程序。但 Curie Nano 的板载 LED 连接在引脚 13而这个引脚同时也是 SPI 的 SCK 引脚。为了避免冲突我们可以用另一个数字引脚比如引脚 4接一个外置 LED 来测试。void setup() { pinMode(4, OUTPUT); // 设置引脚4为输出 } void loop() { digitalWrite(4, HIGH); // 点亮LED delay(1000); // 等待1秒 digitalWrite(4, LOW); // 熄灭LED delay(1000); // 等待1秒 }点击上传按钮如果一切顺利程序将被编译并上传到 Curie Nano外接在引脚 4 的 LED 会开始闪烁。这证明你的开发环境已经就绪。注意有时在上传过程中可能会遇到错误比如“下载超时”或“找不到设备”。可以尝试以下步骤1) 按一下板子上的“MASTER RESET”按钮再重新上传2) 更换一条质量好的 USB 数据线3) 检查电脑的端口是否被其他软件占用。4. 底盘基础驱动PWM 与电机控制逻辑底盘要动起来最核心的就是控制两个电机的转速和方向。我们将通过 PWM 信号来控制电机速度通过两个 GPIO 的电平组合来控制方向。4.1 TB6612FNG 驱动逻辑详解TB6612FNG 每一路电机控制需要三个信号AIN1/AIN2或BIN1/BIN2用于方向PWMA/PWMB用于速度。其真值表如下AIN1AIN2PWMA电机A状态00X停止刹车10PWM正转速度由 PWM 占空比决定01PWM反转速度由 PWM 占空比决定11X停止刹车“刹车”状态相当于将电机两端短路能产生一定的制动效果比单纯断开电源停止得更快。4.2 Curie Nano 的 PWM 配置与代码实现在 Arduino 中我们使用analogWrite(pin, value)函数来输出 PWM。其中value范围是 0-255对应占空比 0%-100%。Curie Nano 的特定引脚支持 PWM我们需要查看其引脚定义图。通常数字引脚 3, 5, 6, 9, 10, 11 是支持 PWM 的。假设我们将电机 A 的PWMA接 Curie Nano 的引脚 5AIN1接引脚 7AIN2接引脚 8。电机 B 同理接在引脚 6, 9, 10 上。下面是一个基础的电机控制函数封装// 电机引脚定义 #define MOTOR_A_PWM 5 #define MOTOR_A_IN1 7 #define MOTOR_A_IN2 8 #define MOTOR_B_PWM 6 #define MOTOR_B_IN1 9 #define MOTOR_B_IN2 10 void setup() { // 初始化所有电机控制引脚为输出模式 pinMode(MOTOR_A_PWM, OUTPUT); pinMode(MOTOR_A_IN1, OUTPUT); pinMode(MOTOR_A_IN2, OUTPUT); pinMode(MOTOR_B_PWM, OUTPUT); pinMode(MOTOR_B_IN1, OUTPUT); pinMode(MOTOR_B_IN2, OUTPUT); } // 控制单个电机的函数 // motor: 0 代表电机A, 1 代表电机B // speed: -255 到 255负数为反转 void setMotor(int motor, int speed) { speed constrain(speed, -255, 255); // 限制速度范围 int pwmPin, in1Pin, in2Pin; if (motor 0) { pwmPin MOTOR_A_PWM; in1Pin MOTOR_A_IN1; in2Pin MOTOR_A_IN2; } else { pwmPin MOTOR_B_PWM; in1Pin MOTOR_B_IN1; in2Pin MOTOR_B_IN2; } if (speed 0) { // 正转 digitalWrite(in1Pin, HIGH); digitalWrite(in2Pin, LOW); analogWrite(pwmPin, speed); } else if (speed 0) { // 反转 digitalWrite(in1Pin, LOW); digitalWrite(in2Pin, HIGH); analogWrite(pwmPin, -speed); // 取绝对值 } else { // 停止刹车 digitalWrite(in1Pin, LOW); digitalWrite(in2Pin, LOW); analogWrite(pwmPin, 0); } } void loop() { // 测试让底盘前进2秒原地右转2秒停止1秒 setMotor(0, 200); // 电机A正转速度200 setMotor(1, 200); // 电机B正转速度200 delay(2000); setMotor(0, 150); // 电机A正转速度150 setMotor(1, -150); // 电机B反转速度150 - 差分转向 delay(2000); setMotor(0, 0); // 停止 setMotor(1, 0); delay(1000); }上传这段代码你的底盘应该能执行一个简单的前进-右转-停止的动作序列。如果电机转向与预期相反只需交换对应电机的IN1和IN2接线即可。5. 速度闭环控制增量式编码器与中断计数开环控制下即使给两个电机相同的 PWM 值由于电机特性、电池电压波动、轮子摩擦等因素底盘也几乎不可能走直线。我们必须引入编码器反馈构成闭环。5.1 编码器计数原理与接口增量式编码器输出两路相位差 90 度的方波A 相和 B 相。通过检测 A 相的上升沿和下降沿并结合 B 相的电平状态可以在一个脉冲周期内进行 4 次计数即 4 倍频大大提高分辨率。例如我的编码器电机标称是每转 11 个脉冲PPR经过 4 倍频后每转的计数就是 44。我们将编码器 A 相接 Curie Nano 的外部中断引脚如引脚 2 或 3B 相接普通数字输入引脚如引脚 4。5.2 使用外部中断实现高速计数Curie Nano 的 Arduino 核心支持attachInterrupt()函数。我们需要在中断服务函数中根据 A、B 相的状态判断是加计数还是减计数。// 编码器引脚定义 #define ENCODER_A_A 2 // 中断引脚接编码器A相 #define ENCODER_A_B 4 // 普通输入接编码器B相 #define ENCODER_B_A 3 // 中断引脚接另一个编码器A相 #define ENCODER_B_B 12 // 普通输入接另一个编码器B相 // 编码器计数变量使用 volatile 修饰确保在中断内修改能被主循环正确读取 volatile long encoderCountA 0; volatile long encoderCountB 0; // 电机A的编码器中断服务函数 void handleEncoderA() { int a digitalRead(ENCODER_A_A); int b digitalRead(ENCODER_A_B); // 根据A、B相状态判断方向。此逻辑取决于你的编码器相位和接线可能需要调整 if (a b) { encoderCountA--; } else { encoderCountA; } } // 电机B的编码器中断服务函数 void handleEncoderB() { int a digitalRead(ENCODER_B_A); int b digitalRead(ENCODER_B_B); if (a b) { encoderCountB; } else { encoderCountB--; } } void setup() { Serial.begin(115200); // 初始化串口用于调试输出 // 初始化编码器引脚 pinMode(ENCODER_A_A, INPUT_PULLUP); pinMode(ENCODER_A_B, INPUT_PULLUP); pinMode(ENCODER_B_A, INPUT_PULLUP); pinMode(ENCODER_B_B, INPUT_PULLUP); // 附着中断在ENCODER_A_A引脚变化上升沿或下降沿时触发中断函数 attachInterrupt(digitalPinToInterrupt(ENCODER_A_A), handleEncoderA, CHANGE); attachInterrupt(digitalPinToInterrupt(ENCODER_B_A), handleEncoderB, CHANGE); // ... 电机引脚初始化代码同上 } long lastPrintTime 0; long lastCountA 0, lastCountB 0; void loop() { // 每100毫秒计算一次速度脉冲数/时间 if (millis() - lastPrintTime 100) { long currentCountA encoderCountA; long currentCountB encoderCountB; long deltaA currentCountA - lastCountA; long deltaB currentCountB - lastCountB; // 计算转速脉冲数/100ms你可以将其转换为 RPM转/分钟 float speedA deltaA; // 这里只是脉冲数需要根据PPR和倍频换算成实际转速 float speedB deltaB; Serial.print(Speed A: ); Serial.print(speedA); Serial.print( | Speed B: ); Serial.println(speedB); lastCountA currentCountA; lastCountB currentCountB; lastPrintTime millis(); } // 这里可以加入PID控制逻辑根据目标速度和当前速度speedA/B调整PWM输出 // setMotor(0, pidA.update(targetSpeed, speedA)); // setMotor(1, pidB.update(targetSpeed, speedB)); }这段代码实现了编码器脉冲的计数和速度的粗略计算。打开 Arduino IDE 的串口监视器设置波特率为 115200手动转动电机轮子你应该能看到速度值的变化。重要经验中断服务函数handleEncoder必须尽可能短小只做最简单的计数操作。避免在中断中使用Serial.print()、delay()等耗时函数否则会导致系统响应异常甚至崩溃。所有复杂计算如 PID和输出都应放在主循环loop()中。5.3 差分底盘的运动学模型要控制底盘走直线或旋转需要理解差分底盘的运动学。两个轮子的速度差决定了底盘的角速度。假设两个轮子间距为L轮子半径为r左轮转速为v_left右轮转速为v_right单位脉冲数/时间。线速度V (v_right v_left) * r / 2角速度ω (v_right - v_left) * r / L我们的速度闭环控制目标就是通过 PID 等算法让两个轮子的实际转速v_left和v_right精确地跟踪我们根据上述模型计算出的目标转速。由于篇幅所限完整的 PID 实现和调参将是一个独立的话题但有了精确的编码器反馈数据我们已经迈出了最关键的一步。6. SPI 通信基础与为未来升级做准备尽管本期我们使用的是中断读取增量编码器但为了项目的可扩展性以及应对更高精度的需求预先配置好 SPI 接口是很有必要的。未来我们可以无缝地将编码器更换为 SPI 接口的绝对式磁编码器。6.1 SPI 在 Arduino 中的使用Arduino 提供了SPI库来简化 SPI 操作。Curie Nano 的 SPI 引脚是固定的SS (Slave Select):引脚 10 (但可以作为普通数字 IO 使用)MOSI (Master Out Slave In):引脚 11MISO (Master In Slave Out):引脚 12SCK (Serial Clock):引脚 13我们需要在代码中引入SPI.h头文件并进行初始化。#include SPI.h // 假设我们未来连接一个 SPI 编码器其片选引脚接 Curie Nano 的引脚 1 #define ENCODER_SPI_CS 1 void setup() { Serial.begin(115200); SPI.begin(); // 初始化 SPI 总线 pinMode(ENCODER_SPI_CS, OUTPUT); digitalWrite(ENCODER_SPI_CS, HIGH); // 默认不选中从设备 // ... 其他初始化代码 } // 一个模拟读取 SPI 编码器的函数以 AS5047P 为例 unsigned int readSPIEncoder() { unsigned int data 0; digitalWrite(ENCODER_SPI_CS, LOW); // 选中编码器 delayMicroseconds(1); // 等待片选稳定 // SPI 传输先发送一个读取命令例如 0xFFFF同时接收高字节 data SPI.transfer16(0xFFFF); // transfer16 用于16位数据交换 digitalWrite(ENCODER_SPI_CS, HIGH); // 取消选中 // AS5047P 返回的14位数据在最高位需要处理 data (data 0x3FFF); // 取低14位为角度值 return data; } void loop() { // 定期读取 SPI 编码器 unsigned int angle readSPIEncoder(); Serial.print(SPI Encoder Angle: ); Serial.println(angle); delay(100); // 每100ms读取一次 }这段代码展示了 SPI 通信的基本流程初始化总线、控制片选引脚、使用SPI.transfer()或SPI.transfer16()进行数据交换。不同的 SPI 设备编码器其通信协议命令字、数据格式、时钟极性 CPHA 和相位 CPOL可能不同需要查阅具体的数据手册。在SPI.begin()后有时还需要用SPI.setBitOrder(MSBFIRST);、SPI.setDataMode(SPI_MODE1);等函数来配置通信模式这必须与从设备严格匹配。6.2 中断与 SPI 的权衡使用中断方式读取增量编码器优点是响应实时性极高几乎无延迟适合高速计数。缺点是需要占用两个中断引脚并且在高转速或代码复杂时频繁中断可能影响主程序运行。使用 SPI 读取绝对编码器优点是不占用中断资源通过轮询方式获取绝对位置数据更可靠抗噪声能力强且能获得绝对角度信息。缺点是读取速度受 SPI 时钟频率和轮询间隔限制存在一定的延迟。对于智能垃圾桶底盘这种对实时性要求不是极端高的应用SPI 轮询方式通常足够可靠且为未来增加更多 SPI 设备如 SPI 接口的显示屏、Flash 存储等提供了便利。这也是我倾向于为未来升级预留 SPI 接口的原因。7. 系统集成、调试与常见问题排查将电机驱动、编码器计数、电源管理集成到一起并让底盘稳定受控地运行是本期最后也是最考验耐心的一步。7.1 完整系统接线与供电检查在将所有模块焊接或牢固连接之前强烈建议先在面包板上搭建整个系统并逐一测试每个功能。完整的接线示意图如下文字描述电源部分7.4V 电池正极接降压模块输入正输出 5V 接 Curie Nano 的 VIN 和 TB6612 的 VCC逻辑电源。电池正极同时接 TB6612 的 VMOT电机电源。所有模块的 GND 连接到一起最后接到电池负极。控制部分Curie Nano 的 PWM 引脚 (5,6) 接 TB6612 的 PWMA/B数字 IO (7,8,9,10) 接 TB6612 的 AIN1, AIN2, BIN1, BIN2。反馈部分编码器 A 相接中断引脚 (2,3)B 相接普通数字输入引脚 (4,12)。编码器电源接 3.3V 或 5V需确认编码器工作电压地接共同 GND。上电前务必用万用表检查电源线路确保无短路。先单独给 Curie Nano 上电测试程序能否下载和运行。然后再连接电机驱动和电机。7.2 典型问题与排查思路问题一电机不转或只振动。排查首先检查电机驱动模块的 VMOT 电压是否正常接近电池电压。然后检查 STBY待机引脚是否已接高电平如果模块有的话。用analogWrite给一个较小的 PWM 值如 100同时用digitalWrite设置正确的方向引脚用万用表测量驱动模块输出端是否有变化的电压。如果驱动模块输出正常则检查电机本身和接线。问题二编码器计数不准或为0。排查首先确认编码器供电正常。在中断服务函数中除了计数可以尝试简单翻转一个 LED 的状态观察在转动电机时 LED 是否闪烁以确认中断是否被触发。如果触发但计数方向错误调整中断服务函数中的方向判断逻辑交换ab的判断结果。如果完全不触发检查接线是否正确特别是中断引脚是否连接到了编码器的 A 相。另外机械编码器有时需要上拉电阻如果模块没有内置需要在信号线和电源之间接一个 10kΩ 的上拉电阻。Curie Nano 的INPUT_PULLUP模式内部提供了上拉通常可以省去外接电阻。问题三底盘严重跑偏或无法走直线。排查这是开环控制的必然结果。首先即使不给 PWM用手推动底盘检查两个轮子转动是否一样顺畅机械结构是否对称。然后在开环下给两个电机相同的 PWM 值用编码器计数功能查看两个轮子的实际转速是否一致。通常会发现差异。这就是我们需要闭环控制的原因。在引入 PID 之前可以尝试手动微调两个电机的 PWM 补偿值形成一个简单的“校准表”。问题四系统运行时 Curie Nano 意外复位。排查最可能的原因是电源问题。电机启动瞬间电流很大可能导致电压骤降使得微控制器复位。确保电池电量充足连接线足够粗。可以在 Curie Nano 的 VIN 和 GND 之间并联一个较大容量的电解电容如 100uF-470uF作为储能缓冲。另外检查代码中是否有数组越界、死循环或堆栈溢出等问题。7.3 初步调试与功能验证在确保各部分硬件工作正常后可以编写一个综合测试程序让底盘以固定 PWM 值前进 5 秒通过编码器数据计算实际走过的脉冲数估算距离。让底盘原地旋转 360 度通过给两个电机相同大小、相反方向的 PWM检查编码器计数是否对称旋转是否准确。尝试实现一个简单的 P 控制器比例控制设定一个目标速度脉冲数/100ms根据当前速度与目标的误差调整 PWM 输出。newPWM oldPWM Kp * (targetSpeed - currentSpeed)。从小Kp值开始试观察速度是否能稳定在目标值附近。这个过程可能会反复多次需要耐心记录数据、调整参数。当你能通过串口指令让底盘以相对稳定的速度前进、后退、转弯时本期“底盘篇”的核心目标就圆满达成了。我们得到了一个“听话”的、具备基本感知速度反馈能力的移动平台这为下一期集成超声波/红外避障传感器、摄像头进行目标识别乃至最终实现垃圾桶的智能功能奠定了无比坚实的物理基础。