
1. 项目概述从一块免费开发板到一台iOS遥控小车前段时间我在一个开发者社区看到了Bluno Beetle开发板的免费试用活动。Bluno Beetle是DFRobot推出的一款集成了蓝牙4.0BLE功能的超小型Arduino兼容板尺寸比一枚硬币大不了多少但“麻雀虽小五脏俱全”。看到它的第一眼我就觉得这玩意儿太适合做点小巧的嵌入式项目了尤其是那种需要无线控制、又对体积有要求的场景。正好手头有几个闲置的微型减速电机和轮子一个念头就冒了出来何不用它来做一台能用iPhone控制的蓝牙遥控小车顺便把这次试用当作一次“交作业”式的项目实践这个想法背后的核心需求很明确摆脱线缆的束缚利用我们最随身携带的设备——手机来实现对一个物理实体的精准控制。对于创客、电子爱好者或者想入门物联网的学生来说这是一个非常经典的练手项目。它串联起了硬件电路搭建、嵌入式编程、无线通信协议蓝牙BLE以及移动端应用开发iOS等多个环节。整个过程就像搭积木你需要理解每一块“积木”硬件模块、代码库、通信协议的功能然后把它们严丝合缝地拼接在一起最终让小车听命于你指尖在屏幕上的滑动。我选择iOS平台一方面是因为我个人主力机是iPhone开发环境比较熟悉另一方面iOS系统对蓝牙BLE的支持相对规范和统一减少了因设备碎片化带来的兼容性调试工作。当然这个项目的思路完全可以平移到Android平台核心的硬件和Arduino代码部分几乎可以复用。下面我就把这台“iOS蓝牙遥控小车”从零到一的实现过程、踩过的坑以及收获的经验毫无保留地分享出来。无论你是刚接触Arduino的新手还是想了解如何将手机与硬件结合起来的开发者相信都能从中找到有用的参考。2. 核心硬件选型与电路设计思路做硬件项目第一步永远是“手里有什么”和“需要什么”。这次项目的核心是Bluno Beetle所以整个硬件架构都围绕它来展开。2.1 主控与通信核心Bluno Beetle深度解析Bluno Beetle可以看作是一个极致精简版的Arduino Uno加上一个蓝牙4.0模块。它的核心是一颗ATmega328P微控制器这和经典的Arduino Uno是同款意味着海量的Arduino库和代码资源都可以直接使用学习成本极低。其最大亮点在于板载的TI CC2540蓝牙芯片这颗芯片实现了蓝牙4.0包含低功耗蓝牙BLE和经典蓝牙的双模支持。对于我们这个遥控小车项目我们主要利用其BLE模式因为BLE的功耗更低连接更快速非常适合这种间歇性发送控制指令的场景。板子的尺寸极小约22mm x 22mm引出了数字I/O口D0-D13、模拟输入口A0-A5、电源VCC, GND以及串口RX, TX。需要注意的是由于体积限制它没有像Uno那样标准的USB接口而是用一个Micro USB口同时负责供电和程序烧录。这带来一个好处你可以直接用手机充电宝或者一个USB小电源给它供电非常方便。在项目初期我强烈建议你先不要焊接任何东西用面包板和杜邦线来搭建测试电路等所有功能调试稳定后再考虑焊接或使用洞洞板制作一个更稳固的版本。2.2 动力与执行机构电机、驱动与电源方案小车要动起来电机是核心。我手头有的是两个N20微型减速电机工作电压3-6V体积小扭力尚可直接驱动小车轮子没问题。Arduino开发板的I/O口输出电流很小通常每个引脚最大20mA根本无法直接驱动电机。因此一个电机驱动模块是必不可少的。电机驱动选型L9110S vs L298N 对于这种微型电机常见的选择有L9110S和L298N驱动模块。L298N功能强大可以驱动更高电压和电流的电机但体积相对较大电路也稍复杂。而L9110S模块小巧玲珑一片就能驱动两个电机正好符合我们双电机差速转向即通过两个轮子转速差来实现转弯的需求且价格低廉。因此我毫不犹豫地选择了L9110S。它的接线非常简单每个电机对应两个控制引脚IA, IB分别接到Beetle的两个数字引脚上电机电源VM接外部电源正极GND共地。电源方案设计 电源是另一个需要仔细考虑的地方。Bluno Beetle通过Micro USB供电5V而电机最好使用独立的电源供电以避免电机启动和堵转时产生的大电流冲击影响主控板的稳定甚至导致复位。我采用了两套供电方案测试阶段使用一个旧的移动电源其USB口输出5V/2A通过一个USB分线器一路给Beetle供电另一路接出一个5V输出给L9110S的VM端。所有设备的GND必须连接在一起这是电路工作的基础切记最终成品阶段我计划使用一块小型锂电池如3.7V的14500电池配合一个升压模块将电压稳定在5V同时给整个系统供电。这样小车就完全无线化了。在选择电池和升压模块时务必关注其持续输出电流能力要能满足两个电机同时工作的峰值电流。2.3 车体结构与辅助材料车体我直接使用了现成的亚克力板智能小车底盘套件它包含了底盘、两个轮子、一个万向轮、电机支架和螺丝包。如果你喜欢动手完全可以用雪糕棍、乐高积木甚至3D打印来制作车体。原则就是坚固、轻便、能让电机稳固安装。此外你还需要准备面包板及若干杜邦线公对公、公对母用于快速搭建和测试电路。螺丝刀、钳子等基础工具。一个用于给Arduino烧录程序的电脑并安装好Arduino IDE。注意在连接电路时务必先断开电源。确认所有接线无误特别是电源正负极没有接反后再上电。接反电源是烧毁模块最常见的原因没有之一。3. 硬件连接与电路搭建实操理论清晰后动手搭建就是按图索骥。下面是我最终的接线图你可以像“抄作业”一样跟着接。3.1 接线图与引脚定义我将Bluno Beetle、L9110S电机驱动模块、两个N20电机和电源整合在一起。假设我们使用以下引脚定义左电机连接L9110S的其中一个电机通道例如A-1A, A-1B。A-1A 接 Beetle 数字引脚 D5A-1B 接 Beetle 数字引脚 D6右电机连接L9110S的另一个电机通道例如B-1A, B-1B。B-1A 接 Beetle 数字引脚 D9B-1B 接 Beetle 数字引脚 D10电源L9110S的VCC引脚接 Beetle 的5V引脚此为逻辑电源为驱动芯片本身供电。L9110S的VM引脚接外部电源的正极如移动电源的5V输出。这是电机的动力电源。L9110S的GND、外部电源的负极、Beetle的GND三者必须连接在一起。电机将两个电机的线分别接入L9110S对应的电机接口A和B。如果电机转动方向与预期相反只需将这两根线对调即可。为什么选择D5, D6, D9, D10这里有个小心思。在Arduino Uno/ATmega328P上D5, D6, D9, D10是支持PWM脉冲宽度调制输出的引脚。PWM可以让我们通过输出不同占空比的方波来模拟“模拟电压”从而控制电机的转速。虽然L9110S是电平控制高/低电平决定正/反转但通过PWM我们可以实现调速让小车不仅能前进后退还能快慢自如。D9和D10更是Timer1的PWM输出引脚精度更高。3.2 分步搭建与上电测试固定硬件先将Bluno Beetle、L9110S模块用双面胶或螺丝固定在车底盘上。将两个电机安装到支架上并固定到底盘装好轮子。连接电机与驱动将两个电机的引线焊接到L9110S的电机输出端子上如果电机没有导线需要先焊接。暂时不区分正反。连接控制线使用杜邦线公对母按照上述引脚定义将Beetle与L9110S的控制引脚连接起来。连接电源线关键先用一根杜邦线连接L9110S的VCC和 Beetle的5V。准备外部电源如移动电源。用一根USB线剪断露出红正极黑负极线。将红线接到L9110S的VM黑线接到L9110S的GND。最后用一根杜邦线将L9110S的GND与 Beetle的GND连接起来。此时所有GND已经共地。上电前最后检查肉眼检查所有接线确保无短路特别是正负极碰在一起、无虚接。将小车车轮悬空用手拿着或架起来防止上电后小车突然窜出去。初次上电给外部电源和Beetle通过Micro USB上电。你应该能看到Bluno Beetle上的电源指示灯PWR和蓝牙状态指示灯亮起。L9110S模块可能也有一个电源指示灯会亮。如果一切正常硬件部分就搭建完毕了。接下来我们要通过编写Arduino程序让Beetle“大脑”开始工作。4. Arduino端固件开发解析指令与控制电机Arduino程序固件是小车的“大脑”它需要做两件事1. 通过蓝牙接收来自手机App的指令2. 根据指令解析并控制电机驱动模块。4.1 建立蓝牙通信与指令协议Bluno Beetle的蓝牙模块已经预先烧录了固件使得串口通信Serial变得异常简单。在Arduino代码中我们使用Serial对象进行读写这些数据会自动通过蓝牙透传。也就是说手机App发送给蓝牙的数据会原封不动地从Serial读到反之Serial.write()发送的数据也会通过蓝牙发送给手机。首先我们在setup()函数中初始化串口通信void setup() { Serial.begin(115200); // 初始化串口波特率设置为115200。这个速率需要与手机App端匹配。 // 初始化电机控制引脚为输出模式 pinMode(5, OUTPUT); pinMode(6, OUTPUT); pinMode(9, OUTPUT); pinMode(10, OUTPUT); // 初始状态让电机停止 stopMotors(); }接下来我们需要设计一个简单高效的指令协议。为了便于在手机触控屏上实现我设计了一个基于二维坐标的协议。想象手机屏幕上有一个虚拟摇杆区域手指触摸的位置会生成一个坐标 (x, y)。我将这个坐标映射到一个以屏幕中心为原点的坐标系中然后将其编码成一个字符串指令。指令格式“X123Y-045\n”X和Y是标识符。123和-045是坐标值范围我设定在 -100 到 100 之间。用三位数字表示负数带符号。\n换行符作为指令的结束符。这是关键它让Serial.readStringUntil(\n)可以完整地读取一条指令避免粘包问题。在loop()函数中我们不断检查串口是否有数据并读取完整的指令行void loop() { if (Serial.available() 0) { String command Serial.readStringUntil(\n); // 读取直到换行符 command.trim(); // 去除可能的回车或空格 if (command.length() 0) { parseCommand(command); // 解析并执行指令 } } // 可以在这里添加一些其他任务如读取传感器等 }4.2 电机控制逻辑与PWM调速实现收到像“X050Y080”这样的指令后parseCommand函数需要从中提取出x和y的值。然后我们将这个二维坐标转换为对左右两个电机的控制量。这里采用经典的“差速转向”模型计算速度和转向将坐标y值映射为前进/后退的基础速度speed将坐标x值映射为转向比例turnRatio。例如y80表示80%的最大前进速度x50表示向右转的比例。计算左右轮速度左轮速度leftSpeed speed turnRatio右轮速度rightSpeed speed - turnRatio这样当turnRatio为正右转时左轮更快右轮更慢甚至反转小车就向右转。限幅处理计算出的leftSpeed和rightSpeed可能超出 [-100, 100] 的范围需要将其限制在这个区间内。映射到PWM值并输出Arduino的analogWrite(pin, value)函数value范围是0-255。我们需要将 -100到100的速度值映射到0-255的PWM值。但注意L9110S需要两个引脚来控制方向和速度。一种常见的接线和控制方式是让一个引脚如D5作为方向控制高电平正转低电平反转。另一个引脚如D6作为PWM速度控制。但这样控制逻辑稍复杂。更简单的方法是将两个控制引脚都用于PWM输出通过两个引脚输出PWM值的相对大小来决定转向和速度。不过经过实测和查阅资料L9110S更标准的用法是电平控制方向PWM控制速度。因此我调整了逻辑void setMotor(int pin1, int pin2, int speed) { // speed范围-100 (全速后退) 到 100 (全速前进) speed constrain(speed, -100, 100); // 确保速度在范围内 if (speed 0) { // 前进 digitalWrite(pin1, HIGH); // 方向引脚1高电平 digitalWrite(pin2, LOW); // 方向引脚2低电平 analogWrite(pin1, map(speed, 0, 100, 0, 255)); // 在方向引脚上输出PWM调速 } else if (speed 0) { // 后退 digitalWrite(pin1, LOW); digitalWrite(pin2, HIGH); analogWrite(pin2, map(-speed, 0, 100, 0, 255)); // 注意取反 } else { // 停止 digitalWrite(pin1, LOW); digitalWrite(pin2, LOW); analogWrite(pin1, 0); analogWrite(pin2, 0); } }然后在解析指令后调用setMotor(5, 6, leftSpeed); // 控制左电机 setMotor(9, 10, rightSpeed); // 控制右电机实操心得电机控制逻辑是调试中最容易出问题的地方。如果发现小车运动方向怪异比如该前进时后退该右转时左转首先检查电机接线是否接反对调电机两根线然后检查setMotor函数中pin1和pin2的高低电平逻辑是否符合你的L9110S模块。不同厂家生产的L9110S模块其控制逻辑可能略有差异最好能找到对应的数据手册或通过简单实验写个测试程序让电机正反转来确定。4.3 固件烧录与蓝牙配对将完整的Arduino代码编译上传到Bluno Beetle。上传时确保在Arduino IDE的“工具”菜单中正确选择了板卡类型Arduino Uno和端口识别出的USB串口。上传成功后小车就具备了“思考”能力。接下来是蓝牙配对。打开iPhone的“设置”-“蓝牙”在设备列表中应该能发现一个名为“Bluno”或类似名称的设备具体名称可能在Beetle的固件中设定。点击配对连接。连接成功后Bluno Beetle板上的蓝牙状态指示灯应由闪烁变为常亮。此时从手机发送到该蓝牙设备的所有数据都会被Beetle通过串口接收到。5. iOS端App开发构建简易遥控器要让iPhone成为遥控器我们需要一个能连接蓝牙BLE并发送数据的App。对于iOS开发者可以使用SwiftUI或UIKit配合CoreBluetooth框架从头开发。但对于大多数爱好者和快速原型来说使用一些现成的、可定制的App或者图形化开发工具会更高效。这里我分享两种思路5.1 方案一使用现成App最快上手在App Store里有一些通用的蓝牙串口调试工具比如“LightBlue Explorer”、“BLE Scanner”或“Serial Bluetooth Terminal”。这些App可以搜索、连接BLE设备并提供一个简单的界面让你发送字符串。操作流程在App中扫描并连接你的“Bluno”设备。找到发送数据的界面通常是一个输入框和一个发送按钮。根据我们定义的协议X123Y-045手动输入指令并发送。例如输入X000Y100然后发送小车应该全速前进。这种方法零代码适合快速验证蓝牙通信和基础控制逻辑是否畅通。缺点是操控体验很差无法实现流畅的实时控制。5.2 方案二使用SwiftUI CoreBluetooth自主开发推荐为了获得更好的操控体验如虚拟摇杆我选择用SwiftUI自己写一个简单的App。即使你没有深厚的iOS开发经验跟着以下步骤也能实现。1. 创建新项目与引入CoreBluetooth 在Xcode中创建一个新的“App”项目语言选Swift界面选SwiftUI。在项目设置中需要在Info.plist文件中添加蓝牙使用描述Privacy - Bluetooth Always Usage Description并填写说明文字例如“用于连接并控制蓝牙小车”。2. 构建蓝牙管理器 创建一个名为BluetoothManager的类它继承自NSObject并遵循CBCentralManagerDelegate和CBPeripheralDelegate协议。这个类将负责所有蓝牙相关的操作扫描、连接、发现服务与特征、读写数据。import CoreBluetooth import SwiftUI class BluetoothManager: NSObject, ObservableObject { private var centralManager: CBCentralManager! private var targetPeripheral: CBPeripheral? // 目标设备我们的Bluno Beetle private var writeCharacteristic: CBCharacteristic? // 用于写入数据的特征 Published var isConnected false Published var discoveredDevices: [CBPeripheral] [] override init() { super.init() centralManager CBCentralManager(delegate: self, queue: .main) } // 开始扫描 func startScanning() { // 可以指定服务UUID来过滤设备Bluno Beetle通常会有特定的服务UUID // 这里我们先扫描所有设备 centralManager.scanForPeripherals(withServices: nil, options: nil) } // 连接设备 func connect(to peripheral: CBPeripheral) { centralManager.stopScan() targetPeripheral peripheral targetPeripheral?.delegate self centralManager.connect(peripheral, options: nil) } // 发送指令 func sendCommand(_ command: String) { guard let characteristic writeCharacteristic, let data command.data(using: .utf8) else { return } targetPeripheral?.writeValue(data, for: characteristic, type: .withoutResponse) } } // 实现CBCentralManagerDelegate和CBPeripheralDelegate协议方法... // 这里需要实现 centralManagerDidUpdateState, didDiscover, didConnect, didDiscoverServices, didDiscoverCharacteristicsFor 等回调方法。 // 在 didDiscoverCharacteristicsFor 中找到可以写入Write的特征并保存到 writeCharacteristic 变量中。由于篇幅限制完整的协议实现代码较长但其核心逻辑是在didDiscoverCharacteristicsFor回调中遍历服务的特征找到属性包含.write或.writeWithoutResponse的那个特征并将其保存。之后就可以通过sendCommand函数向这个特征写入数据了。3. 设计UI与虚拟摇杆 在SwiftUI的ContentView中我们可以设计一个简单的界面一个设备列表一个连接状态显示以及一个作为虚拟摇杆的区域。struct ContentView: View { StateObject private var bluetoothManager BluetoothManager() State private var joystickPosition CGSize.zero // 摇杆偏移量 var body: some View { VStack { // 1. 设备列表与连接状态 List(bluetoothManager.discoveredDevices, id: \.identifier) { device in Button(device.name ?? Unknown Device) { bluetoothManager.connect(to: device) } } Text(bluetoothManager.isConnected ? 已连接 : 未连接) // 2. 虚拟摇杆区域 GeometryReader { geometry in ZStack { Circle() // 摇杆背景 .fill(Color.gray.opacity(0.3)) .frame(width: 200, height: 200) Circle() // 摇杆手柄 .fill(Color.blue) .frame(width: 60, height: 60) .offset(joystickPosition) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in // 限制手柄在背景圆内移动 let radius: CGFloat 70 // 背景圆半径 - 手柄半径 let distance sqrt(pow(value.translation.width, 2) pow(value.translation.height, 2)) let limitedTranslation distance radius ? CGSize( width: value.translation.width / distance * radius, height: value.translation.height / distance * radius ) : value.translation joystickPosition limitedTranslation sendJoystickCommand(translation: limitedTranslation, maxRadius: radius) } .onEnded { _ in joystickPosition .zero // 松手归位 bluetoothManager.sendCommand(X000Y000\n) // 发送停止指令 } ) } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .onAppear { bluetoothManager.startScanning() } } private func sendJoystickCommand(translation: CGSize, maxRadius: CGFloat) { // 将偏移量映射到 [-100, 100] 区间 let x Int((translation.width / maxRadius) * 100) let y Int((translation.height / maxRadius) * 100) // 注意iOS坐标系Y轴向下为正这里可能需要取反取决于你的定义 let command String(format: X%04dY%04d\n, x, -y) // 格式化指令如 X050Y-030 bluetoothManager.sendCommand(command) } }这个UI创建了一个圆形的摇杆区域手指拖动蓝色手柄时会计算出手柄相对于中心点的偏移量translation并将其映射到(-100, 100)的范围内然后格式化成X...Y...的指令字符串发送出去。当手指松开时手柄归位并发送停止指令X000Y000。4. 调试与优化 在真机上运行这个App需要Apple开发者账号或使用免费的个人团队。首次运行会请求蓝牙权限。连接上Bluno Beetle后拖动摇杆小车就应该能随之运动了。你可能需要根据小车的实际运动方向调整App中坐标映射的正负关系例如-y还是y。注意事项在真实的iOS开发中蓝牙操作是异步的且涉及状态管理需要妥善处理各种连接断开、服务发现失败的情况。上述示例代码是一个高度简化的原型用于演示核心流程。在生产级应用中你需要添加更多的错误处理、重连机制和用户提示。6. 系统联调、问题排查与性能优化当硬件、固件、App都准备好后真正的挑战——系统联调开始了。这个过程就是不断发现问题、解决问题的循环。6.1 典型问题与排查实录问题1手机搜不到“Bluno”蓝牙设备。排查首先确认Bluno Beetle已上电且蓝牙指示灯在闪烁表示处于可被发现状态。检查iPhone蓝牙是否开启并关闭再重新打开。尝试重启Beetle和手机。如果还不行检查是否有其他设备已经连接了该Bluno因为BLE设备通常一次只允许一个中心设备连接。解决确保设备处于广播状态。对于Bluno Beetle有时需要确认其固件模式正确。可以尝试用Arduino IDE通过串口监视器发送AT指令如果支持来查询或重置蓝牙模块状态。问题2App能连接但发送指令小车没反应。排查这是最常遇到的问题。采用“分段排查法”软件链路检查在App的sendCommand函数里添加打印确认指令字符串确实被正确生成和调用。在Arduino端的loop()函数里将Serial.readStringUntil(\n)读到的指令原样通过Serial.println()打印回手机需要App也具备接收显示功能看指令是否完整送达。这能验证蓝牙通信链路是否通畅。指令解析检查如果指令能收到但在Arduino串口监视器此时需通过USB连接电脑查看里看到乱码或格式不对检查App和Arduino的串口波特率是否一致都是115200。检查指令字符串的结束符\n是否添加。电机控制逻辑检查如果指令解析正确但电机不转。首先用万用表测量L9110S的电机输出端是否有电压变化。如果没有检查L9110S的VM和VCC供电是否正常控制引脚电平是否按预期变化。可以写一个简单的测试程序让电机依次正转、停止、反转来隔离蓝牙部分的问题。问题3小车运动不流畅有延迟或顿挫感。排查这通常与指令发送频率和电机控制方式有关。解决优化指令发送在iOS App中DragGesture的.onChanged回调触发非常频繁。如果每次回调都发送一条蓝牙指令可能会造成信道拥堵或Arduino处理不过来。可以加入一个简单的“节流”机制比如记录上次发送时间只有超过一定时间间隔如50毫秒才发送新指令。优化Arduino代码确保loop()函数执行一次的时间尽可能短。避免在loop()中使用delay()函数它会阻塞所有操作。如果需要进行定时操作使用millis()函数进行非阻塞计时。检查电源电机启动瞬间电流很大如果电源容量不足会导致电压瞬间跌落可能引起Arduino复位或蓝牙断开。确保使用能提供足够电流的电源并在电机电源输入端并联一个较大容量的电解电容如470uF作为储能缓冲。问题4蓝牙连接不稳定偶尔自动断开。排查BLE连接对距离和障碍物比较敏感。检查操作距离是否过远超过10米或有墙体阻隔。同时检查周围是否有强烈的2.4GHz信号干扰源如Wi-Fi路由器、微波炉。解决尽量在开阔无干扰的环境下使用。在iOS App中实现断线重连机制在BluetoothManager的centralManager(_:didDisconnectPeripheral:error:)代理方法中尝试重新连接设备。6.2 性能优化与功能扩展基础功能稳定后可以考虑以下优化和扩展增加控制模式除了摇杆可以在App中增加按钮实现一键前进、后退、左旋、右旋、加速、减速等功能。添加传感器反馈在小车上加装超声波传感器测距或红外传感器循迹将数据通过蓝牙发回手机App显示实现简单的环境感知。改进操控体验在虚拟摇杆外圈增加一个“死区”当手指在中心很小范围内移动时不发送指令或发送停止指令避免因手指微小抖动导致小车抖动。功耗优化如果使用电池供电可以在Arduino代码中加入休眠逻辑。当一段时间没有收到蓝牙指令时让电机停止甚至让Arduino进入低功耗休眠模式通过蓝牙广播的特定指令唤醒。车体优化为车轮增加橡胶套以增大摩擦力优化重心分布防止翻车甚至可以为小车加上一个手机支架用手机摄像头实现第一人称视角FPV视频传输这需要更复杂的硬件如ESP32-CAM和Wi-Fi传输。整个项目从拆开Bluno Beetle的包装到小车在手机指挥下灵活跑动大约花费了一个周末的时间。其中大部分时间其实花在了调试和优化上尤其是蓝牙通信的稳定性和电机控制的精准度。这个过程充满了“为什么不动了”的困惑和“终于跑了”的喜悦。它不仅仅是一个遥控小车更是一个涵盖了嵌入式系统、无线通信和移动开发的小型综合工程实践。希望这份详细的“作业”记录能为你点亮自己动手的灵感并帮你绕过我踩过的那些坑。