ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android App遥控51单片机智能小车:蓝牙通信与PWM调速实战

Android App遥控51单片机智能小车:蓝牙通信与PWM调速实战 简介面向单片机课程设计的高分项目方案基于Android端APP与51单片机实现多功能智能小车控制压缩包内提供完整工程源码与详细说明文档覆盖Android客户端、单片机C程序、APK安装包及界面素材。资源共133个文件其中以Java/XML为主的Android源码、以main.c为代表的单片机程序、PNG图片、APK安装文件以及项目配置文件构成主体整体仅3.74MB便于快速下载和二次开发。项目本身经过测试运行且获得高分答辩认可可帮助在校学生理解APP端与51单片机的交互流程熟悉小车控制功能的扩展与排错思路适用于课程设计、毕业设计或初期项目演示。已有114人学习/下载具备较好的实践参考价值。1. Android App 遥控 51 单片机智能小车不止是蓝牙控车而是一条完整软硬链路很多人一看到“Android App 控制 51 单片机多功能智能小车”就默认是老课设一个 HC-05 蓝牙模块、一个 L298N、四个轮子和一块 STC89C52 板子手机发送字符单片机跑起来就算完事。真正动手做会发现花时间最多的根本不是让轮子转而是把协议定清楚、把掉线抓回来、把传感器数据和 PWM 调速放到同一个中断里不打架。这个项目好就好在它把移动端开发、嵌入式硬件和无线通信串在了一条链路上适合课程设计也适合拿去打工创赛、电子设计竞赛的底层车体部分。文章按系统架构、51 端固件、Android App、联合调试四层展开给出一套可以直接落到 Keil 和 Android Studio 的方案。2. 系统架构与通信协议蓝牙选型、命令帧设计和 51 单片机硬件规划2.1 控制链路与通信选型为什么不用 Wi-Fi也不用红外遥控多功能智能小车从数据流向看就是手机端把人机交互翻译成字节选一个无线通道送到 51 单片机单片机解析字节后操作电机、舵机和传感器。通信方式里红外的抗干扰差转个角度就丢命令不适用于运动控制Wi-Fi 虽然能传摄像头画面但 51 单片机接 ESP8266 做 TCP 透传时AT 指令缓冲区和协议栈占用资源调试周期明显变长。常见的做法是用经典蓝牙模块 HC-05它工作在蓝牙 2.0 SPP 模式Android 端用 BluetoothSocket 连接后就是一条虚拟串口字节流稳定延迟几十毫秒一次发 7 到 20 个字节的车控帧非常从容。Android 端很多人纠结要不要换 BLE先明确一个边界如果只是遥控、传传感器数据经典蓝牙足够了如果要在 App 里持续收 20Hz 以上的超声波波形再看低功耗蓝牙的 notification 机制。BLE 每次写特征值都可能有 MTU 限制Android 系统的 BLE 连接参数协商还会被厂商改反而是 HC-05 这类经典蓝牙在不同手机上表现更一致。另一点要注意HC-06 只能做从机而 HC-05 可配置主从后面想加一个蓝牙配对按钮、或做车与车之间的数据中继HC-05 更灵活。2.2 帧协议设计7 字节命令帧与 8 字节应答帧控制小车最忌讳的是裸发一个字符‘F’或‘0x01’因为无线环境里的随机干扰可能把错误字符当成命令而且单字符无法携带速度、舵机角度这类多参数。我一般会设计一个定长小帧帧结构如下字段长度取值说明帧头20xAA 0x55同步头用来找帧边界类型10x01 控制帧 / 0x02 查询帧区分遥控命令和传感器读取模式与方向1bit0-2 模式bit3-6 方向bit7 保留前进、后退、左转、右转速度10-100左右轮基础占空比校验1前面 5 字节累加取低 8 位避免误帧为什么不用 JSON 或文本协议STC89C52 只有 256 字节内部 RAM字符串解析会引入大量的 strcmp中断里处理不了放在主循环又容易丢字节。定长二进制帧可以直接用带状态的串口中断逐字节吃进数组每个字节处理时间小于波特率间隔9600 波特率下每字节大约 1.04ms51 单片机在 12MHz 晶振下完全跑得过来。应答帧做成 8 字节帧头同样 0xAA 0x55后面带传感器状态、电池电压和校验。这里有个容易被忽略的设计点上位机和下位机的校验算法要一致且下位机收到合法帧后必须回一个应答否则 Android 端根本不知道命令已经生效。如果出现校验错误单片机应该直接丢弃并累加错误计数不回错误帧也不要复位避免形成应答风暴。2.3 引脚分配与硬件共地规则51 单片机 GPIO 不够时的扩展思路多功能小车的基础硬件包括单片机最小系统、L298N 电机驱动、HC-05 蓝牙、超声波 HC-SR04、两个 TCRT5000 循迹传感器和一个 SG90 舵机想做摄像头云台时用。常见引脚规划如下外设引脚连接说明蓝牙HC-05 TXD 接 P3.0 (RXD)RXD 接 P3.1 (TXD)交叉连接GND 必须与单片机共地L298N IN1-IN4P1.0-P1.3控制电机正反转L298N ENA/ENBP1.4, P1.5接定时器输出 PWM不要直接接高电平超声波Trig 接 P2.0Echo 接 P2.1Echo 返回高电平宽度需要定时器捕获循迹左传感器接 P2.2右传感器接 P2.3输出高低电平1 表示黑线SG90 舵机P2.4PWM 周期 20ms占空比 0.5ms-2.5ms最关键的硬件坑是共地。L298N 的逻辑电源和电机电源共用一组 12V 或者 9V通过板载 5V 稳压给单片机供电时电机启动瞬间会把电压拉低导致单片机复位。我的做法是电机驱动和单片机分别独立供电然后用一根杜邦线把两边的 GND 连起来。至于 GPIO 不够可以用 P1 输出控制 74HC595 扩展引脚然后把并行 IO 换成串行方式写多出来的 P1.6 和 P1.7 还能接两个编码器输入。3. 51 单片机端固件Keil 工程、串口状态机和 PWM 调速实现3.1 Keil5 安装与工程配置要点芯片选择、晶振和烧录设置Keil5 安装教程里最容易被跳过的是 C51 器件包只装了 MDK 的话看不到 51 单片机。安装完要额外勾选 Keil C51 支持包打开项目时才选得到 Atmel AT89C52 或 STC 系列。工程建立后用 Keil 的 Options for Target 做三件事把 Xtal 改成 11.0592MHz这个值不是随便选的它能让波特率发生器计算误差接近 0Memory Model 选 Small因为默认 compact 或 large 会引入额外指针操作Output 标签页勾选 Create HEX File烧录时才有目标文件。烧录器用 STC-ISP 即可需要留意的是 STC 单片机默认使用内部 IRC 时钟很多板载 11.0592MHz 晶振并没有真正被启用。如果发现串口乱码先别急着改代码用 STC-ISP 把“输入用户程序运行时的时钟源”选为外部晶振或内部 IRC 对应的 11.0592MHz。51 单片机的引脚功能里P3.0/P3.1 同时承担串口和第二组定时器烧录时尽量用 USB-TTL 的 RXD/TXD 交叉连接不要占用蓝牙模块的串口引脚否则容易出现下载程序失败。3.2 定时器 0 生成双路 PWML298N 调速的两个边界条件L298N 的 IN1-IN4 控制方向ENA/ENB 是使能端。如果 ENA/ENB 直接接高电平小车只有一个速度做不了避障和循迹时的左右轮差速。因此需要用定时器生成两路独立 PWM。51 单片机没有硬件 PWM 外设常见做法是定时器中断里按周期切片翻转输出#define PWM_PERIOD 100 volatile unsigned char duty_left 0; volatile unsigned char duty_right 0; unsigned char pwm_cnt 0; void timer0_isr() interrupt 1 { TH0 0xFF; // 重载计数值大约每 12 个机器周期进一次中断 TL0 0x9C; if (pwm_cnt PWM_PERIOD) { pwm_cnt 0; } ENA (pwm_cnt duty_left) ? 1 : 0; ENB (pwm_cnt duty_right) ? 1 : 0; }上面的代码利用定时器 0 产生周期约 1kHz 的脉宽调制波形。PWM_PERIOD 取 100占空比精度为 1%。每次进入中断后计数器加一当前计数值小于目标占空比就输出高电平。要注意 L298N 存在启动死区占空比低于 20% 时电机可能不转不是代码没跑而是驱动力矩不够我在速度映射时会写map_speed 20 speed * 80 / 100让滑条 0 到 100 映射到 20% 到 100% 的占空比。还有第二边界是电流。PWM 频率太低电机噪音大频率太高L298N 的开关损耗上升且 51 单片机中断压力大。1kHz 到 10kHz 是电机驱动比较合适的范围。若用 STC15 系列可以直接用 PCA 模块生成 PWM不需要频繁进中断但如果手里是普通 STC89C52建议老老实实用定时器 0 定时器 1 交替负载。另一点是 ENA/ENB 对应的是左右轮不是前后轮差速转弯靠的是让两侧轮子的占空比不同而不是只反转一个电机。3.3 串口中断状态机与工作模式仲裁避障、循迹和遥控不打架串口解析不能放在主循环里轮询因为当蓝牙持续给数据时RC 和 ACK 之间的间隙很短主循环一旦被超声波测距的while(Echo 1);卡住SBUF 会溢出。我用串口中断加状态机逐字节处理unsigned char rx_state 0; unsigned char rx_buf[8]; unsigned char rx_index 0; void uart_isr() interrupt 4 { unsigned char tmp; if (RI) { RI 0; tmp SBUF; switch (rx_state) { case 0: if (tmp 0xAA) rx_state 1; break; case 1: if (tmp 0x55) { rx_state 2; rx_index 0; } else if (tmp ! 0xAA) rx_state 0; break; default: rx_buf[rx_index] tmp; if (rx_index 5) { unsigned char sum 0; for (unsigned char i 0; i 5; i) sum rx_buf[i]; if (sum rx_buf[4]) { execute_command(rx_buf); } rx_state 0; } break; } } else if (TI) { TI 0; } }状态机处理的顺序是“两个 0xAA 重同步”加“5 字节载荷后校验”。代码里数组和索引都用了全局变量因为 Keil C51 在中断内声明局部变量会占用固定内存地址函数重入会带来不可预期问题。执行execute_command时根据帧里类型字段设置全局模式变量遥控模式、避障模式、循迹模式互斥。三个模式在同一个主循环里仲裁结构大致是如果模式是避障先用超声波测距距离小于 30cm 时自动优先转弯否则执行最近一次遥控方向循迹模式则完全忽略遥控方向只根据两个 TCRT5000 传感器输出做差速。这里有一个很容易被忽视的细节手动遥控时蓝牙帧速度更新频率可能在 20Hz 左右而避障时超声波单次测量要 20ms 以上如果把测距放在主循环会拖慢对终极命令的响应。所以速度指令在中断里先写进速度变量主循环只负责调用电机更新函数这样遥控的实时性就不会被传感器拖住。4. Android App 开发蓝牙权限、Socket 连接和摇杆控件实现4.1 Android 12 及以上蓝牙权限适配从 AndroidManifest 到运行时请求Android App 端第一个坑是权限模型。面向 Android 11 以下时只需要 BLUETOOTH、BLUETOOTH_ADMIN、ACCESS_FINE_LOCATIONtargetSdk 31 以上就必须增加 BLUETOOTH_SCAN、BLUETOOTH_CONNECT。这些权限不能只写进 AndroidManifest还要在代码里运行时请求。uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /Android 12 的车载蓝牙扫描不再强制要求位置权限但国产手机厂商普遍还会在系统层保留位置限制。代码里先把ActivityCompat.requestPermissions请求一次全部权限然后在回调里判断BLUETOOTH_CONNECT是否被拒绝。需要特别注意neverForLocation这个标记加了以后系统才知道你不是用来做地理定位的部分手机会减少隐私弹窗次数。真机上不要用模拟器测蓝牙因为模拟器不暴露宿主机的蓝牙适配器。4.2 BluetoothSocket 可靠读写连接线程、心跳包和异常重连经典蓝牙连接使用 UUID00001101-0000-1000-8000-00805F9B34FB这是 SPP 服务的标准 UUID。连接不是 UI 线程的事必须在子线程里执行否则BluetoothDevice.createRfcommSocketToServiceRecord几秒钟的网络延迟会把界面卡死。常见写法如下public class BtConnectThread extends Thread { private BluetoothSocket socket; private BluetoothDevice device; private UUID uuid UUID.fromString(00001101-0000-1000-8000-00805F9B34FB); public void run() { try { BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); device adapter.getRemoteDevice(macAddress); socket device.createRfcommSocketToServiceRecord(uuid); socket.connect(); startReadLoop(); } catch (IOException e) { try { // 部分国产 SoC 需要使用反射方法才能连上 socket device.createRfcommSocket(1); socket.connect(); } catch (IOException fallbackException) { Log.e(BtCar, connection failed, fallbackException); } } } }这里第一段startReadLoop会一直等待输入流因此蓝牙数据的读取也是一个死循环。建议把读到的字节按帧头帧尾重新组包不要直接按固定长度读因为 Socket 流不保证一次把 7 个字节全部交给你。接下来是发送命令的方法可以用OutputStream.write(commandBytes)加flush()如果写线程和 UI 线程并发需要加一个synchronized块锁住输出流。手动遥控中UI 控件会高频发送控制帧这么做没必要也不安全触摸事件每秒可能产生 60 次回调蓝牙模块的缓冲区和 51 主循环不一定能跟得上。我一般把命令发送节流到 20Hz用SystemClock.elapsedRealtime()判断距离上次发送是否超过 50ms超时才真正发送这样既能保证不掉帧也符合大多数 51 单片机的处理速度。App 还应该在连接断掉后自动重试重试前先调用BluetoothAdapter.cancelDiscovery()否则系统中的设备发现机制会阻塞新连接。4.3 摇杆控件与传感器回显把触摸坐标映射成左右轮占空比App 界面不需要写复杂自定义 View用 SurfaceView 或普通 ImageView 都能做摇杆。触摸事件核心是拿到手指位置和圆心位置的偏移量private int[] computeJoystickValue(float touchX, float touchY, float centerX, float centerY) { double angle Math.atan2(touchY - centerY, touchX - centerX); int distance (int) Math.hypot(touchX - centerX, touchY - centerY); if (distance maxRadius) distance maxRadius; int speed distance * 100 / maxRadius; int direction (int) Math.toDegrees(angle); if (direction -135 || direction 135) { // 后退同时带左右差速 } else if (direction 45) { // 右转 } else if (direction -45) { // 左转 } else { // 前进 } return new int[]{speed, rightSpeed}; }参数说明angle 的 0 度对应正右方顺时针增长。为了符合直觉通常在 start 和 stop 事件里记录“原点”然后在 move 事件里计算偏移。摇杆大步推时把速度设为最大值 100小车启动电流会很大因此代码要加上一个速度斜坡每 20ms 把当前速度向目标速度靠拢 5 个单位防止轮胎打滑或单侧堵转。传感器的回显通过蓝牙应答帧完成。51 端每隔 500ms 主动上报一次距离和循迹状态App 端定义readPacketCallback在组包完成后解析数据并更新 TextView。这里推荐用原子变量或者 Handler 把子线程读到的数据切回主线程更新 UI不要直接在线程里调用setText。比较省事的方式是runOnUiThread它内部会创建一个消息队列不会因为频繁更新 UI 抛CalledFromWrongThreadException。5. 联合调试、丢帧排查与高分验收技巧5.1 先用 USB-TTL 空跑 MCU再连蓝牙项目最容易出现的故障是所有功能分开测都正常连在一起惨不忍睹。正确顺序是先把 51 单片机上的蓝牙模块断开用 USB-TTL 连接 P3.0/P3.1在电脑端打开串口助手手动发送AA 55 01 01 64 05观察电机是否动作。如果动了说明 MCU 侧协议和电机逻辑没问题再把 USB-TTL 换成 HC-05在手机端用一个串口透传工具发送同样的帧看小车反应。分层的目的是把问题隔离在“MCU 固件”、“硬件接线”和“Android App”三段里而不是全都搅在一起。串口助手发送后如果没有任何反应优先检查波特率。HC-05 模块的常见默认波特率是 38400 或 9600买到手先发 AT 指令ATUART?查询再把 51 端代码改成一致并且注意 AT 指令需要按住模块上的按键进入 AT 模式。遵循“先通角色后通协议”的顺序调试可以避免在错误波特率下做无意义的逻辑分析。5.2 掉线、丢速和超声波误触发的三个排查方向现象直接原因处理方式连接几秒后自动断开HC-05 从机掉线后没有重新配对在 App 的 broadcast receiver 监听BluetoothDevice.ACTION_ACL_DISCONNECTED后自动重连车轮转速忽快忽慢PWM 占空比写入不连续电机电源被拉低给 L298N 供电处并联 1000uF 电容测电压速度斜坡超声波测距偶发 0 或满值Echo 引脚持续高电平超时没做超时退出测距函数加超时上限如 30ms 未回退则判定为异常App 端发送指令后小车不动命令帧校验差额计算错误或 byte 转 int 出现符号扩展用0xFF byte进行位运算避免负数参与累加手机搜不到 HC-05蓝牙模块处于 AT 模式或没有配对授权退出 AT 模式确保模块重启后红灯慢闪在这些常见雷区里“符号扩展”是最隐蔽的。Java 端 byte 是带符号类型0xFF 会变成 -1如果直接累加校验会得到错误结果。所以组包时每个字段都要data[i] commandBytes[i] 0xFF。同理接收 51 上报的传感器温度或距离时大于 127 的数值要用int v b 0xFF恢复成正数否则 UI 会显示负数。5.3 再进一步摄像头循迹、手机端避障决策和协议状态上报如果目标是拿高分一定要在“遥控”这层之外加一个智能相关的能力。低成本方案是在手机端用 OpenCV 处理摄像头图像识别前方车道线再把“左偏、右偏、直行”的指令通过蓝牙发给 51 单片机单片机只做底层电机控制。此时上位机变成“决策端”下位机变成“执行端”项目就从一个蓝牙遥控作品升级成了感知-决策-执行的车规级架构。我的建议是保留 USB 口作为日志输出用第二条串口把 PID 计算后的目标占空比、当前速度、传感器状态以 CSV 格式打印到上位机。验收时候考官问任何实时数据都可以指着屏幕说出具体数值。文档里把设计文档、关键代码注释、封装测试数据三样做齐比单纯高大上的 UI 更经得住深挖。最后还要把 Android 的蓝牙连接异常、51 端协议自检错误、电池电压过低都做成可提示的状态码这三个状态能让答辩演示中即使出错也显得有条理。本文还有配套的精品资源点击获取
返回列表