
简介这是一份面向Android开发初学者与物联网爱好者实战学习的手机遥控智能小车完整源码围绕Android客户端与小车端之间的Socket、蓝牙通信展开帮助读者理解移动设备如何向硬件下发前进、后退、转弯等控制指令。压缩包共27个文件约54KB包含2个java源文件、12个class字节码、3个xml布局与配置、3个png图标资源以及apk安装包、dex、ap_、properties、classpath等构建产物覆盖从源码到可运行应用的完整结构。目前已有1604人学习下载。通过研读这份源码读者可以掌握Android端界面按钮与速度滑块的设计、Socket连接的建立与数据收发、蓝牙适配器与蓝牙Socket的连接管理以及小车端微控制器解析指令驱动电机的思路同时了解连接超时、指令可靠性等稳定性处理方式为后续扩展到避障、路径规划等场景打下基础。1. 从手机到车轮一套能跑通的 Android 遥控小车源码长什么样很多人第一次接触「Android 手机遥控智能小车」这个方向都是被一个很朴素的场景勾住的手里有台旧手机桌上有块开发板和两个电机想让它们连起来动一动。真动手才发现卡人的从来不是写一个按钮而是手机发出的指令怎么穿过 Wi-Fi 或蓝牙落到单片机串口上再变成电机驱动能识别的电平。这套源码解决的正是这条链路——它把 Android 端的操作界面、通信协议、下位机的指令解析和电机控制打包在一起让你不用从零搭架子。适合两类人一类是做过单片机但没碰过 Android 的嵌入式工程师另一类是写过 App 但没驱动过硬件的移动开发者。下面按「先看懂结构、再动手复现、最后避坑」的顺序拆开讲中间会给出可直接抄的代码和参数。2. 拆开源码看结构Android 端、通信层、下位机三段怎么分工拿到一份 Android 遥控小车源码别急着点运行。先把它当成三段流水线来看手机端负责采集触摸或重力感应数据并编码通信层负责把数据送到板子下位机负责解码并驱动电机。三段之间靠一套约定好的指令格式粘合这套格式就是整份源码的骨架。2.1 工程目录与模块划分常见的目录结构大致是这样不同作者命名会有差异但职责基本固定目录/文件职责关键内容app/src/main/java/.../ui界面与交互摇杆控件、按钮、方向键盘app/src/main/java/.../comm通信封装Socket 或蓝牙 Socket 的收发线程app/src/main/java/.../protocol指令编码把方向、速度打包成字节firmware/下位机固件串口解析、PWM 输出、电机正反转docs/协议说明指令表、波特率、引脚定义看目录时重点确认两件事通信方式是 Wi-FiTCP/UDP还是蓝牙经典蓝牙 SPP 或 BLE以及下位机是哪种板子。这两点决定了你后面要改哪些参数。Wi-Fi 方案通常手机开热点或连同一路由器板子跑 TCP Server蓝牙方案则是手机作为主机去连板子的蓝牙模块。源码里一般会有一个Config或Constants类集中放 IP、端口、UUID、波特率先找到它。2.2 指令协议手机和下位机之间的「暗号」协议是这套源码最该先读懂的部分。一个够用的遥控协议不需要复杂常见做法是用固定帧头加动作码加校验。下面是一个典型的编码实现方向用单字节表示速度用 0 到 100// protocol/CommandBuilder.java public class CommandBuilder { public static final byte HEAD (byte) 0xAA; // 帧头下位机靠它对齐 public static final byte TAIL (byte) 0x55; // 帧尾防止半包误判 // direction: 0停 1前 2后 3左 4右speed: 0~100 public static byte[] build(byte direction, byte speed) { byte[] frame new byte[5]; frame[0] HEAD; frame[1] direction; frame[2] speed; frame[3] (byte) (direction ^ speed); // 简单异或校验 frame[4] TAIL; return frame; } }逻辑说明帧头帧尾负责在字节流里定位一帧的边界方向码和速度码是有效载荷异或校验用来丢弃传输中被干扰的坏帧。参数上speed建议限制在 0 到 100映射到下位机 PWM 占空比时再按电机驱动的能力缩放。下位机对应的解析逻辑要严格按「找帧头 → 读够长度 → 校验 → 执行」的顺序任何一步不满足就丢弃当前字节继续找帧头否则一旦错位就会连续误动作。// firmware/uart_parse.c 片段 void parse_byte(uint8_t b) { static uint8_t buf[5]; static uint8_t idx 0; if (idx 0 b ! 0xAA) return; // 没等到帧头直接丢 buf[idx] b; if (idx 5) { idx 0; if (buf[4] ! 0x55) return; // 帧尾不对 if ((buf[1] ^ buf[2]) ! buf[3]) return; // 校验不过 motor_apply(buf[1], buf[2]); // 真正驱动电机 } }这段解析的关键在于「状态机」思路用一个静态索引记录当前收到第几个字节只有帧头对齐后才开始计数。很多新手把它写成一次性读一整包结果串口分包时直接崩掉。参数上波特率要和 Android 端发送侧一致常见是 9600 或 115200两边不一致时现象是电机乱动或完全不动。2.3 通信线程与主线程的边界Android 端最容易翻车的地方是把网络收发放在主线程一卡就 ANR。源码里通常会把 Socket 收发放进独立线程再用 Handler 或 LiveData 把状态回传 UI。下面是一个精简的发送线程写法// comm/SendThread.java public class SendThread extends Thread { private final BlockingQueuebyte[] queue new LinkedBlockingQueue(); private Socket socket; private OutputStream out; public void send(byte[] frame) { queue.offer(frame); } // 非阻塞入队 Override public void run() { try { socket new Socket(192.168.4.1, 8080); // 板子热点默认地址 out socket.getOutputStream(); while (!isInterrupted()) { byte[] frame queue.take(); // 没数据就阻塞不空转耗电 out.write(frame); out.flush(); } } catch (Exception e) { // 通知 UI 连接断开 } } }逻辑说明用阻塞队列把「UI 产生指令」和「线程发送指令」解耦UI 只管入队发送线程按顺序出队写 Socket避免多线程同时写一个流导致粘包错乱。参数上192.168.4.1和8080是示例实际要按你板子的热点网段和监听端口改queue.take()在没有指令时会挂起线程比轮询省电这点在手机端尤其重要。3. 动手复现从编译固件到手机连上跑起来看懂结构之后复现的路径其实很线性先把下位机固件烧进去并确认电机能转再编译 Android 端连上去最后联调。顺序反了会很难定位问题——手机连不上时你分不清是网络问题还是板子根本没在跑。3.1 下位机固件烧录与电机自检先确认板子的串口和电机驱动引脚。常见做法是固件里留一个自检函数上电后让两个电机正转一秒、反转一秒确认接线和供电没问题再进入接收指令的主循环。// firmware/main.c 片段 int main(void) { uart_init(115200); // 与 Android 端保持一致 motor_init(); // 配置 PWM 引脚和方向引脚 self_test(); // 正反转各一秒验证硬件 while (1) { if (uart_has_byte()) { parse_byte(uart_read_byte()); } } }逻辑说明self_test()是排错的第一道关如果自检时电机不转后面所有通信调试都是白费。参数上PWM 频率一般取 1kHz 到 20kHz太低电机会啸叫太高部分驱动芯片发热。供电要单独给电机一路别和板子逻辑电源混用否则电机一启动板子就复位这个现象非常典型。3.2 Android 端编译与连接参数修改用 Android Studio 打开工程后先改Config里的连接参数再编译安装。Wi-Fi 方案要确保手机连的是板子热点或同一路由器蓝牙方案要先在系统设置里配对。// config/Config.java public class Config { public static final String MODE WIFI; // 或 BLUETOOTH public static final String HOST 192.168.4.1; // 板子 IP public static final int PORT 8080; // 板子监听端口 public static final String BT_UUID 00001101-0000-1000-8000-00805F9B34FB; // SPP }逻辑说明把连接方式抽成一个常量切换 Wi-Fi 和蓝牙时只改这一处不用翻遍代码。参数上蓝牙 SPP 的 UUID 是标准串口服务 UUID多数模块通用Wi-Fi 的 IP 要看你板子热点默认网关常见是192.168.4.1端口要和固件里listen的端口一致。改完先跑一次连接测试界面上能看到「已连接」再动车。3.3 联调先发单帧再上摇杆联调阶段别一上来就用摇杆连续发。先做一个「单次发送」按钮点一下发一帧前进确认电机动作正确再切到摇杆的连续发送模式。// 摇杆回调里做节流避免每帧触摸都发 private long lastSend 0; private static final long MIN_INTERVAL 50; // 毫秒 public void onJoystickMove(byte dir, byte speed) { long now System.currentTimeMillis(); if (now - lastSend MIN_INTERVAL) return; // 节流 lastSend now; sendThread.send(CommandBuilder.build(dir, speed)); }逻辑说明摇杆的触摸回调触发频率很高不节流会把通信队列塞满表现为操作延迟越来越大。MIN_INTERVAL取 50 毫秒大约对应 20Hz 的指令频率对遥控小车足够也能给下位机留出执行时间。参数上如果发现动作一顿一顿可以适当调小如果延迟明显调大。4. 避坑与排查遥控小车联调时最常翻的五个车这一章按「现象 → 原因 → 解决」记录几个高频问题都是我在复现这类源码时真实遇到过的。现象一手机显示已连接但电机完全不动。原因多半是协议对不上比如 Android 端发的是文本指令而下位机在等二进制帧或者波特率不一致。解决先用串口助手手动发一帧十六进制数据确认下位机能动再回头查 Android 端的编码函数重点看帧头帧尾和校验字节。现象二电机乱转、时好时坏。原因是串口分包导致帧错位解析状态机没做帧头对齐。解决检查parse_byte是否在没等到帧头时直接丢弃字节而不是无条件往缓冲区里塞。另外确认发送侧有没有多线程同时写同一个流。现象三电机一启动板子就复位或手机断连。原因是电机和逻辑电路共用电源启动瞬间电流拉低电压。解决电机驱动单独供电共地但不共电源必要时在电机两端加滤波电容。这个坑和代码无关纯硬件但排查时最容易被忽略。现象四操作延迟越来越大最后卡死。原因是摇杆回调没有节流指令堆积在队列里。解决按 3.3 的节流写法限制发送频率或者用「只保留最新一帧」的策略队列满时丢弃旧帧而不是阻塞。现象五蓝牙能配对但连不上。原因是用了 BLE 的 UUID 去连经典蓝牙 SPP两者不通用。解决确认模块是经典蓝牙还是 BLE经典蓝牙用00001101-...这个 SPP UUIDBLE 则要走 GATT 服务发现流程不能混用。提示每次改动通信参数后先断开重连再测试很多「改了没生效」其实是旧连接还挂着。5. 进阶把定速遥控改成带反馈的闭环以及一个验证习惯基础版跑通后这套源码还能往上加东西。最常见的是加反馈在下位机读编码器或电流把实际速度回传给手机显示形成最简单的闭环。做法是在协议里加一类「上行帧」下位机定时发Android 端解析后更新界面。// firmware/feedback.c 片段每 100ms 上报一次速度 void report_speed(uint8_t speed) { uint8_t frame[5] {0xBB, speed, 0x00, 0x00, 0x55}; // 上行帧头用 0xBB frame[3] frame[1] ^ frame[2]; uart_send(frame, 5); }逻辑说明上行帧用不同的帧头这里用0xBB和下行帧区分Android 端的接收线程按帧头分流避免上下行混在一起解析。参数上上报周期 100ms 是个折中太频繁会占满串口带宽太慢界面显示会滞后。Android 端对应加一个解析分支把速度值通过 Handler 刷到 TextView 或自定义仪表盘上。再进一步可以做速度闭环手机发目标速度下位机用 PID 根据编码器反馈调整 PWM。这一步不用改协议只改下位机的motor_apply把开环的set_pwm(speed)换成 PID 输出即可。验证闭环有没有生效有个简单办法用手按住车轮看上报速度是否下降、PWM 是否自动加大如果两者都不动说明反馈链路没通。我自己的习惯是每次改完协议或引脚定义都强制走一遍「自检 → 单帧 → 摇杆 → 反馈」这四步不跳步。跳过自检直接联调最后往往要花几倍时间回头查硬件。这套源码的价值不在于它多完整而在于它把这条链路摊开给你看改哪里、为什么改都有迹可循。希望帮到你。本文还有配套的精品资源点击获取