ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android APP控制51单片机智能小车:蓝牙串口通信与PWM调速实战

Android APP控制51单片机智能小车:蓝牙串口通信与PWM调速实战 简介这是一个基于 Android 设计 APP 控制 51 单片机多功能智能小车的完整项目包属于课程设计/单片机类高分资源面向计算机、自动化、电子信息、物联网等专业在校生以及需要完成毕设、课设或初期项目演示的开发者。包体共 133 个文件、压缩后约 3.74MB核心内容包括 Android 客户端 Java/XML 源码、单片机端 C/H 底层驱动、可直接安装验证的 APK、工程配置文件以及说明文档并附有界面截图与工程目录树整体结构清晰便于从 APP 界面到小车控制逻辑逐层对照学习。目前已有 114 人学习下载。该项目已通过 95 分答辩评审核心代码经过运行测试可直接在现有功能上修改扩展对入门者而言也能借助其中的前后端通信实现、硬件驱动接口和完整配置思路快速理解 Android 与 51 单片机联合开发的典型流程。1. 拿到这个高分项目先看清“手机—蓝牙—单片机”这条链路周五答辩前夜手机蓝牙搜不到小车模块51单片机主板上的电源灯却亮着这是做“基于Android设计APP控制51单片机多功能智能小车”课程设计时几乎必然遇到的一幕。这个高分项目包里放着可直接安装的APK成品、Android工程缓存resources.ap_、jarlist.cache这一类以及单片机端的main.c源码和配套文档。资源拿过来之后重点不是“能跑”而是搞清楚Android Studio编译出来的APP、HC-05蓝牙模块、51单片机三者之间的通信链路因为答辩老师最喜欢沿着这条链路往下问。下面按工程实际顺序拆解串口通信协议、PWM调速、Android蓝牙收发、Keil5烧录四个环节并给出能直接抄的代码和调试参数适合课设答辩党、单片机初学者和想把资料改造成毕业设计的读者。2. 串口通信协议与蓝牙透传帧同步、命令字与校验设计2.1 为什么课程设计选“51 HC-05 Android SPP”课程设计的选型逻辑很实际。51单片机STC89C52或AT89S52在智能小车项目里有三个不可替代的优势寄存器少、开发资料多、Keil5 C51工具链稳定对“多功能智能小车”这种低速移动平台8位机处理一个蓝牙串口中断和两路PWM完全够用。Android端的作用是提供图形界面把按钮点按转换成字节流蓝牙模块只是透明传输通道不解析内容手机发给它的字节原样出现在51单片机串口单片机回传的数据也原样回到手机。相比WiFi方案ESP8266透传或自建TCP蓝牙SPP方案不需要组网、不需要配置IPHC-05模块十几块钱属于课程设计普遍采用、答辩老师又最容易接受的方案。文件里会出现两个APKKyr1eSmartCarControl.apk和Carcontrol.apk以及两个同名main.c。经验来看Kyr1eSmartCarControl.apk命名更完整可以当作较新的版本先装重复的main.c多半是固件在不同阶段留下的备份解压后按文件修改时间选新的那份编译。项目里出现的resources.ap_、jarlist.cache、.classpath这些文件是Eclipse ADT时代构建Android工程时生成的中间产物拿到Android Studio里一般不会直接参与构建所以不要被它们干扰。2.2 指令帧定义0xAA帧头、命令字、异或校验51单片机串口收到的是一个字节一个字节进入SBUF的数据流如果没有帧格式一旦第一个字节丢失后面所有指令都会错位。因此协议设计要解决三个问题帧同步、命令表达、错误检测。常见做法是采用“帧头命令字数据区校验字”的固定长度帧本项目的蓝牙控制指令帧可以简化为4字节字节位置名称取值说明0帧头FRAME_HEADER固定0xAA接收方用它重新对齐同步1命令字CMD0x00停止0x01前进0x02后退0x03左转0x04右转0x05调速2数据DATA速度值0~100或辅助参数用不到时填0x003校验CHECK第1字节与第2字节异或即CMD^DATA校验字用异或而不是累加和原因是单片机里异或运算只需要一条指令累加和还要考虑进位处理在中断里执行会拉长响应时间。帧头固定为0xAA是因为它的二进制是10101010与常见的0xFF、0x00相比更不容易在总线空闲态或乱码流中被误判。数据区虽然只有1字节但足够表达0~100的速度档位以及蜂鸣器、灯光这类扩展功能的开关状态。生成帧的C代码放在51端#define FRAME_HEADER 0xAA #define CMD_STOP 0x00 #define CMD_FORWARD 0x01 #define CMD_BACKWARD 0x02 #define CMD_LEFT 0x03 #define CMD_RIGHT 0x04 unsigned char txFrame[4]; void buildFrame(unsigned char cmd, unsigned char data) { txFrame[0] FRAME_HEADER; // 帧头 txFrame[1] cmd; // 命令字 txFrame[2] data; // 速度或辅助参数 txFrame[3] cmd ^ data; // 异或校验 }这段代码在后面调试时可以直接复制到Keil5里。需要注意buildFrame只是做发往手机的回显或状态上报真正控制电机不需要它接收端校验逻辑更重要见下一节。参数上如果命令字和数据区的长度将来要扩展帧格式从4字节改成6字节状态机同步逻辑也要同步调整不要只改一边。单片机端的串口初始化也要一并确认void UART_Init_9600() { SCON 0x50; // 串口方式18位UARTREN1允许接收 TMOD 0x0F; // 保留定时器0原有配置 TMOD | 0x20; // 定时器1工作方式28位自动重装 TH1 0xFD; // 11.0592MHz晶振9600波特率 TL1 0xFD; TR1 1; // 启动波特率发生器 ES 1; EA 1; }重装值0xFD的来源是11.0592MHz晶振这个频率专门为了让9600和115200等波特率的误差为0而设计换成12MHz晶振后同样的0xFD会产生明显偏差蓝牙透传后误码率剧增。因此main.c里的串口常量明确按11.0592MHz配置后面提到的PWM定时器初值则按12MHz计算两者并不冲突因为用的是两个不同的定时器。2.3 串口中断状态机从一个字节到一个完整控制指令接收端更需要在意字节边界。不要在中断里等整帧到达否则缓冲区管理复杂而且容易死锁常见做法是写一个状态机每收到一个字节就改变一次状态。下面这段代码可以直接作为main.c的串口中断服务函数unsigned char rxBuf[4]; unsigned char rxState 0; void UART_ISR() interrupt 4 { unsigned char byte; if (!RI) return; // 没有接收到数据 RI 0; // 清接收中断标志 byte SBUF; // 读串口数据 switch (rxState) { case 0: // 等待帧头 if (byte 0xAA) { rxBuf[0] byte; rxState 1; } break; case 1: // 已收到帧头等待命令字 rxBuf[1] byte; rxState 2; break; case 2: // 已收到命令字等待数据区 rxBuf[2] byte; rxState 3; break; case 3: // 已收到数据区等待校验并执行 rxBuf[3] byte; if ((rxBuf[1] ^ rxBuf[2]) rxBuf[3]) { executeCommand(rxBuf[1], rxBuf[2]); } rxState 0; // 无论校验是否通过都回到空闲 break; } }这段代码的核心思路是只有当第一个字节匹配0xAA时才进入状态1否则一直停留在状态0。蓝牙模块上电时偶尔会吐出几个乱码字节状态机会自动丢弃它们下一个正确帧头到来后立刻恢复同步不需要在main函数里做复位处理。校验失败时直接丢弃整帧不做任何容错性执行目的是避免电机误动作造成安全隐患。状态机的性能足够因为每收到一个字节最多执行4次比较和赋值在11.0592MHz晶振下用时远小于下一位的到来间隔。提示如果把帧头改成0xFF这类全1字节在总线空闲时可能被误认为有效数据所以0xAA是更稳妥的选择。这也是很多串口协议把帧头设计成0xAA或0xA5的原因。3. 51单片机 main.c 实战PWM调速、指令执行与循迹避障3.1 L298N电机驱动与定时器PWM调速参数计算小车的动力部分通常用L298N驱动模块既能驱动两个直流电机又自带使能逻辑输入引脚直接兼容TTL电平。L298N与51的接口常见接法是P1口低四位接IN1~IN4P1^4和P1^5分别接ENA、ENB。IN1和IN2控制左电机方向IN3和IN4控制右电机方向ENA和ENB用PWM波控制有效电平的时间比例。电机正转、反转和刹车的逻辑组合如下表功能IN1IN2IN3IN4ENA/ENB前进1010PWM调速后退0101PWM调速左转1001两侧PWM差速右转0110两侧PWM差速刹车1111无PWMPWM的频率不是越高越好也不是越低越好。频率太高电机线圈感抗会让平均电流上不去驱动力不足频率太低电机会发出明显啸叫而且小车运行不平稳。10ms周期、100个档位的方案也就是100Hz的PWM在低速小车上是个标准起点。定时器初值基于12MHz晶振计算每档0.1ms初值0xFF9C如果换成11.0592MHz晶振直接套用这个初值会产生定时误差左右轮转速不一致的直接表现就是小车跑不直。unsigned char pwmTick 0; unsigned char pwmDutyLeft 60; // 左轮占空比 60% unsigned char pwmDutyRight 60; // 右轮占空比 60% void Timer0_ISR() interrupt 1 { TH0 0xFF; // 12MHz晶振100us定时初值高位 TL0 0x9C; // 100us定时初值低位 pwmTick; if (pwmTick 100) pwmTick 0; // 100 * 100us 10msPWM频率100Hz ENA (pwmTick pwmDutyLeft) ? 1 : 0; ENB (pwmTick pwmDutyRight) ? 1 : 0; }这段代码把ENA和ENB当作普通IO口直接赋值是51端最常见的做法。pwmDutyLeft和pwmDutyRight是全局变量后面executeCommand里可以通过修改它们实现调速。参数调整的核心是左右轮对称性问题同一个占空比下左侧电机和右侧电机的机械特性可能不同实际行驶会向一侧偏所以要在实测中微调两个初值而不是一味追求左右一致。调试时可以先在代码里把占空比固定为80让小车直线跑一段距离观察偏移方向后再回调。3.2 executeCommand把命令字转换成四个IO引脚电平在2.3小节的校验成功分支里调用了executeCommand这个函数才是真正驱动电机逻辑的地方。它做的事很简单根据命令字设置IN1~IN4的电平再按数据区内容更新PWM占空比。用switch比用一串if else更清晰void executeCommand(unsigned char cmd, unsigned char data) { switch (cmd) { case CMD_STOP: IN1 1; IN2 1; IN3 1; IN4 1; // 刹车 break; case CMD_FORWARD: IN1 1; IN2 0; IN3 1; IN4 0; if (data 10 data 100) { // 数据保护 pwmDutyLeft data; pwmDutyRight data; } break; case CMD_BACKWARD: IN1 0; IN2 1; IN3 0; IN4 1; if (data 10 data 100) { pwmDutyLeft data; pwmDutyRight data; } break; case CMD_LEFT: IN1 1; IN2 0; IN3 0; IN4 1; // 左轮正转右轮反转 pwmDutyLeft 40; pwmDutyRight 60; break; case CMD_RIGHT: IN1 0; IN2 1; IN3 1; IN4 0; // 左轮反转右轮正转 pwmDutyLeft 60; pwmDutyRight 40; break; default: break; } }代码里的data范围保护值得单独说明。如果把手机端的调速滑块直连到PWM占空比用户拖到0时电机完全截止但拖到200时占空比超过100%程序里如果没做边界处理pwmDutyLeft就会大于pwmTick的判断范围导致电机一直全速。把输入限制在10~100之间既避免最小占空比下电机转不起来又防止溢出是工程实现里常见的“输入合理化”处理。转向的实现方式有两种一种是转弯半径较大的两轮同向差速另一种是原地转向。上面左转代码让左轮正转、右轮反转电机对顶小车可以原地掉头适合狭窄场地演示但原地转向对电机和电源的瞬时电流冲击更大电池电压不足时会出现突然复位。答辩演示场合建议把速度调低一些。3.3 循迹、避障传感器如何并入现有主循环“多功能”这三个字通常落在一组传感器上循迹用红外反射传感器避障用红外避障模块或超声波测距。它们不需要占用串口只需要占用几个普通IO口所以可以在main函数的循环里轮询。以两路循迹传感器为例接P2.0和P2.1黑线反射率不同传感器输出电平也不同代码可以这样组织sbit TRACK_LEFT P2^0; // 左循迹传感器 sbit TRACK_RIGHT P2^1; // 右循迹传感器 void trackLine(void) { if (TRACK_LEFT 1 TRACK_RIGHT 0) { // 左侧偏离黑线向右纠偏 pwmDutyLeft - 5; pwmDutyRight 5; } else if (TRACK_LEFT 0 TRACK_RIGHT 1) { // 右侧偏离黑线向左纠偏 pwmDutyLeft 5; pwmDutyRight - 5; } if (pwmDutyLeft 20) pwmDutyLeft 20; if (pwmDutyRight 100) pwmDutyRight 100; }这段代码只做增量调节每次轮询只调整一个很小的量避免小车在赛道上来回大幅摆头。实际项目中循迹模式和蓝牙遥控模式一般不能同时生效我会加一个mode全局变量mode为0时蓝牙指令直接控制电机mode为1时主循环只跑trackLine蓝牙接收只用于切换模式。这个做法很朴素但比在中断里切来切去可靠得多也容易在答辩现场讲清楚。4. Android端蓝牙控制Android Studio导入与指令发送4.1 解压后先区分APK与Android工程缓存文件安卓端的文件需要分类看待。Kyr1eSmartCarControl.apk和Carcontrol.apk是编译好的安装包想快速演示的直接装前者resources.ap_、jarlist.cache、.classpath、index.db这些是Eclipse ADT或本地索引生成的中间产物在Android Studio里打开工程时会被忽略一部分但其中.classpath记录了原工程的源码目录和依赖库如果原工程没有gradle配置反而能从中看到src和libs的相对路径。我的常见做法是在Android Studio里新建project把原工程中com开头的Java目录、res目录、AndroidManifest.xml按原路径复制进去再让Android Studio自动生成gradle配置。这个过程比折腾Eclipse工程导入快得多也避开了旧构建工具的版本问题对课程设计时间紧张的人更友好。Android SDK的版本选择有讲究。targetSdkVersion如果定在22蓝牙权限模型还是旧规则在Android 12以上手机也能跑但只能用于演示无法上架应用市场如果定在33需要处理运行时权限。课程设计场景建议targetSdkVersion用33正好把Android 12新的蓝牙权限模型一起演示出来显得项目有更新价值。4.2 Android 6到Android 13的蓝牙权限差异蓝牙权限是这套代码里最容易让新手在答辩现场翻车的点。Android 6以上扫蓝牙设备必须申请定位权限Android 12以上新的BLUETOOTH_SCAN和BLUETOOTH_CONNECT取代了旧的BLUETOOTH和BLUETOOTH_ADMIN。AndroidManifest.xml里需要同时保留两组权限因为老版本系统不认识新权限新版本系统又建议用新权限uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION /权限申请不能只在Manifest里写Android 6以上还需要运行时弹窗确认。Java里封装一个通用方法if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{ Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.BLUETOOTH_SCAN }, 1001); } } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { if (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{ Manifest.permission.ACCESS_COARSE_LOCATION }, 1002); } }代码按系统版本分了两段实际上Android 6~11走ACCESS_COARSE_LOCATIONAndroid 12以上走两个新蓝牙权限。需要注意即使在Android 12上如果应用没有声明neverForLocation标志扫描蓝牙时系统仍可能把它解释为需要定位服务所以部分测试机上还要同时打开系统的“位置信息”开关。这一点在演示前一定要确认否则会出现“权限全给了还是扫不到HC-05”的情况。4.3 BluetoothSocket发送4字节控制帧的Java实现蓝牙连接使用SPP协议UUID 00001101-0000-1000-8000-00805F9B34FB是串口服务标准UUID。连接成功后发送控制指令的核心代码与2.2小节的帧格式完全对应private BluetoothSocket socket; private OutputStream outputStream; public void sendCommand(byte cmd, byte data) { if (socket null || !socket.isConnected()) { Log.e(CarControl, 蓝牙未连接请先完成配对); return; } byte[] frame new byte[4]; frame[0] (byte) 0xAA; // 帧头 frame[1] cmd; // 命令字 frame[2] data; // 数据区 frame[3] (byte) (cmd ^ data); // 异或校验 try { outputStream.write(frame); outputStream.flush(); Log.d(CarControl, TX: String.format(%02X %02X %02X %02X, frame[0], frame[1], frame[2], frame[3])); } catch (IOException e) { Log.e(CarControl, 发送失败: e.getMessage()); } }sendCommand里先判断socket是否连接再构造4字节帧最后写入发送流。这里必须强调write方法的参数是byte[]不能直接写字符串“AA015051”否则手机端发出去的是一串ASCII字符0x41、0x41、0x30、0x3151端按照0xAA帧头匹配时一个都匹配不上。车不动的时候先通过logcat里打印的TX日志确认发出去的字节是不是AA开头这个排查几乎能挡住一半的问题。按钮与指令的映射关系建议在界面设计文档里画清楚答辩时直接展示这张表按钮/操作命令字数据区完整帧十六进制前进0x010x50AA 01 50 51后退0x020x50AA 02 50 52左转0x030x00AA 03 00 03右转0x040x00AA 04 00 04停止0x000x00AA 00 00 00加速档0x050x14AA 05 14 11从表里能看出完整帧的计算并不需要电脑帮忙异或结果当场手算也行。答辩时如果老师问“为什么前进是AA 01 50 51”可以直接把0x01和0x50的二进制异或过程写出来这比讲界面的按钮布局更有说服力。5. Keil5 烧录、串口助手联调与参数对齐5.1 Keil5 C51编译与STC-ISP烧录要点Keil5默认安装时不带C51编译器需要额外勾选C51支持包。新建工程后选择Atmel AT89C52或STC对应型号选项里要打开“Create HEX File”否则烧录软件拿不到可执行文件。烧录用STC-ISP先选择芯片和COM口点击下载后给目标板重新上电。整个过程最关键的是下载时序STC单片机要在冷启动瞬间进入下载模式如果板子上P3.0、P3.1接了蓝牙模块下载时最好把蓝牙的RXD、TXD拔掉避免模块拉低串口电平导致下载失败。5.2 去掉蓝牙模块先用PC串口助手验证固件蓝牙模块容易被误判为故障点但最常见的问题其实是它两侧的波特率不一致。有效的排错方法是把HC-05从电路上断开用USB转TTL模块直接接51单片机串口。接线是USB转TTL的TXD接单片机P3.0RXD接P3.1GND共地。打开PC串口助手设为9600波特率、8个数据位、1个停止位、无校验发送十六进制帧AA 01 50 51。如果小车前进说明单片机端固件链路完整问题只出在蓝牙模块或手机端如果小车没反应就要查串口中断是否配置正确、PWM占空比变量是否被意外清零。这里有个先后逻辑先用PC串口助手绕开蓝牙把固件链路隔离出来验证再装回蓝牙模块就不会在“手机问题还是固件问题”之间反复横跳这也是整个项目调试里信息量最大的一个动作。5.3 HC-05配对参数与联调排错顺序HC-05有两种工作模式AT模式和透传模式。按住模块上的按键再上电指示灯慢闪进入AT模式此时串口助手发送ATUART9600,0,0可以设置通信波特率发送ATNAMECar51可以改蓝牙名称。如果不在AT模式下手机串口助手里发出的字符会直接透传到51单片机这相当于一个检测手段向手机串口助手发一个0xAA单片机端能通过状态机收到才算打通。联调到这一步把常见问题和处理顺序列成一张表答辩现场照顺序排查思路很清晰现象排查点处理思路手机搜不到HC-05模块是否进入配对模式重新上电确认指示灯慢闪能配对但车不动发送的帧格式对不对用蓝牙串口助手发AA 01 50 51验证APP安装后闪退缺少运行时权限检查BLUETOOTH_CONNECT和定位权限行驶轨迹明显跑偏左右轮PWM初值不一致在main.c中微调pwmDutyLeft/Right负载时单片机复位电源电压不足换大容量电池电机驱动与逻辑共地最后补一个有价值的演示技巧在executeCommand入口处增加一个帧计数变量frameCount每收到一帧有效指令就自增然后通过串口把计数回传给手机。unsigned int frameCount 0; void executeCommand(unsigned char cmd, unsigned char data) { frameCount; // 帧计数用于联调定位 // 原有电机控制逻辑保持不变 switch (cmd) { // ... } }手机端logcat里看到回传计数持续增长就能立刻区分“手机没发出去”“单片机没收到”“单片机收到但执行失败”三种情况。这个回传链路还可以延伸成状态上报把电量、避障传感器状态一并编码进数据区结尾就落在“收到一帧回显一帧链路状态一眼可知”这个调试闭环上。本文还有配套的精品资源点击获取
返回列表