公司动态

CC2530+蓝牙+Android:从零搭建无线传感器采集系统

📅 2026/9/2 4:55:01
CC2530+蓝牙+Android:从零搭建无线传感器采集系统
简介这是一份面向嵌入式与Android开发者的CC2530传感器控制上位机完整工程围绕温度DS18B20与红外HC-SR501数据采集、串口通信及Android端远程监控展开适合物联网方向学生或想要快速搭建无线传感链路的开发者参考。压缩包共78个文件、约4.45MB涵盖Android工程源码java、xml、class、可安装APK、依赖jar包及编译中间文件其中包含3个APK和多个class便于对照源码理解连接建立、协议设计、传感器驱动等实现细节。已有944人学习下载资料保留了从项目配置到打包输出的完整目录结构可直接导入开发环境查看或二次修改对掌握CC2530与Android端的数据交互流程有直接帮助。 做这套系统的时候我桌面上正好是那副经典的嵌入式教学画面CC2530 核心板插着杜邦线DS18B20 温度探头贴在散热片边上HC-SR501 人体红外模块的红灯随着手挥动闪灭旁边是一部吃灰已久的 Android 手机。目标是让手机变成这块板子的显示器实时看到温度、人体感应状态再能从手机上按一下按钮远程控制继电器。这个场景如果你也熟悉那这篇东西就是写给你的一套从零开始的简单嵌入式传感器采集系统下位机用 CC2530 裸机采集温度、红外等传感器数据通过蓝牙透传Android 上位机负责解析、显示、下发控制指令。适合谁看呢正在做嵌入式课程设计、毕业设计或者想快速搭一个手机控制传感器设备原型的同学。我会把整条链路拆开讲数据帧怎么设计、CC2530 串口怎么配、蓝牙模块怎么接、Android 端蓝牙 API 怎么用以及联调阶段最容易翻车的几个细节。不会绕弯子都是实际跑通过的方案。1. 从传感器到手机屏幕先想清楚数据怎么流1.1 一条完整链路上行和下行两件事绝大多数这种项目的核心就是两条数据流。数据上行传感器 → CC2530 的 GPIO → C 代码把数字/模拟信号读进来 → 按自定义帧协议组装 → UART 串口输出 → 蓝牙模块透传 → 手机 App 收到字节流 → 解析帧 → 更新界面上的温度、红外状态。数据下行App 按钮点击 → 蓝牙发送控制帧 → CC2530 串口中断收到 → 解析指令 → GPIO 翻转控制 LED、蜂鸣器或者继电器。很多人一上来就盯着 Android 代码写结果下位机发出来的数据压根不对后面全是白做。我的建议是先把这条链路画在纸上标清楚每个环节的数据格式再动手。1.2 为什么选 CC2530又为什么不跑协议栈CC2530 是一颗 8051 内核的 ZigBee 芯片256KB Flash、8KB RAMIO 口够多最常见的是搭配 TI 的 Z-Stack 协议栈做 ZigBee 组网。但在简单这个前提下我强烈建议先不碰协议栈把它当一颗普通单片机裸跑。原因很直接Z-Stack 的学习曲线陡工程结构复杂调试起来一半时间耗在协议上。而裸机编程就是标准的 8051 流程配时钟、配 GPIO、配串口主循环里轮询传感器。如果你手头是 STM32 甚至 ESP32也不是不行但 CC2530 有个不可替代的加分项以后想扩展成多个传感器节点时它天生支持 ZigBee 组网手机端只需要面对一个协调器节点即可。这套项目的通信链路可以直接平滑升级。1.3 为什么 Android 做上位机最省事Android 手机自带蓝牙、屏幕、交互而且蓝牙 SPP 协议对串口透传类设备非常友好不需要额外的网关硬件。你要做的只是在 Android Studio 里写一个 App通过 BluetoothSocket 和 HC-05/HC-06 这种蓝牙串口模块建立连接本质就是把手机当成一个无线串口终端。在写 App 之前可以先安装一个蓝牙串口调试助手类的工具验证链路等看到原始字节流再写正式 App效率高很多。2. CC2530 裸机采集把传感器数据装进一串字节2.1 开发环境IAR for 8051 SmartRF Flash ProgrammerCC2530 的官方开发环境是 IAR for 8051Z-Stack 官方也是用它编译的。新手不用纠结这个选择IAR 的工程管理和调试体验对 8051 内核来说足够好用。烧录工具用 TI 的 CC Debugger 或者常见的 SmartRF04EB 仿真器配合 SmartRF Flash Programmer 烧录 Hex 文件。工程里芯片型号选 CC2530F256不需要添加协议栈文件从空工程开始写。唯一的注意点是 IAR 的编译器优化级别会影响延时函数DS18B20 这种单总线器件对时序很敏感后面会详细说。2.2 传感器选型与接线这套系统里温度用 DS18B20人体红外用 HC-SR501这两个是同类传感器里最主流、资料最全的。传感器输出类型供电接 CC2530 引脚备注DS18B20单总线数字3.0~5.5VP1.0需外接 4.7k 上拉精度 ±0.5℃一根线传数据HC-SR501数字高电平4.5~20V 模块供电P1.1检测人体移动输出 3.3V TTL 电平MQ-2 烟雾可选模拟/数字5VP0.5 / ADC想扩展多路传感器时加上光敏电阻可选数字比较器输出3.3VP1.2做光照检测很便宜DS18B20 三根脚VCC、GND、DQDQ 必须接上拉电阻到 VCC否则读出来全是 0xFF。HC-SR501 三根脚VCC、OUT、GND模块上输出电压标称 3.3V TTL可以直连 CC2530 的 GPIO。老版本模块需要先上电预热模块上的两个可调电阻分别调灵敏度和延时这个在第 5 章会细说。2.3 DS18B20 单总线时序的三个关键动作DS18B20 只有一根数据线所有读写都是按位进行的时序操作核心是三个动作复位、写位、读位。复位主机把数据线拉低 480us 以上释放后 DS18B20 会在 60~240us 内把线拉低表示存在。写位主机拉低总线如果是写 0 就保持 60us 以上如果是写 1 就只在开头拉低 1~15us 然后释放。读位主机拉低 1~15us 后释放然后在短时间内采样总线电平。温度读取的完整流程是复位 → 跳过 ROM0xCC→ 启动温度转换0x44→ 等待至少 750ms → 复位 → 跳过 ROM → 读暂存器0xBE→ 连续读两个字节得到 16 位温度值。下面是一段简化代码float ds18b20_read_temp(void) { uint8_t low, high; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过 ROM ds18b20_write_byte(0x44); // 启动转换 delay_ms(800); ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读暂存器 low ds18b20_read_byte(); high ds18b20_read_byte(); int16_t raw (high 8) | low; return raw * 0.0625f; }一个很关键的坑单总线的微秒级延时会被中断打乱。如果你开启了串口接收中断串口数据进来时打断 DS18B20 读时序温度数据就会出现偶发性的 85℃ 或者负值。解决办法是在读写 DS18B20 期间临时关掉全局中断EA 0读完再恢复。我后来还发现 IAR 的优化等级也会影响延时建议把延时函数放到#pragma optimizenone的代码段里稳定复现。2.4 CC2530 串口初始化与波特率选择CC2530 的 UART0 可以映射到 P0.2/P0.3 或 P1.4/P1.5默认用备用位置 1也就是 P0.2 做 RX、P0.3 做 TX。初始化代码里需要配 PERCFG、P0SEL、U0CSR、U0UCR、U0BAUD、U0GCR 这几个寄存器void uart0_init(void) { CLKCONCMD 0x80; // 切换到 32MHz 晶振 while (CLKCONSTA 0x40); // 等待时钟稳定 PERCFG 0x00; // UART0 备用位置 1P0.2/P0.3 P0SEL | 0x0C; // P0.2、P0.3 设为外设功能 U0CSR 0x80; // UART 模式 U0CSR | 0x40; // 使能接收器 U0UCR 0x00; // 8N1无硬件流控 U0BAUD 59; // 9600 波特率 U0GCR 8; }波特率由 U0BAUD 和 U0GCR 的低 5 位共同决定常见组合是 9600 时 U0BAUD59、U0GCR8115200 时 U0BAUD216、U0GCR11按 CC2530 数据手册波特率表查最稳。项目里建议先用 9600蓝牙模块默认波特率也通常是 9600两边对齐省事。串口发数据时通过 U0DBUF 写入一个字节然后等待 U0CSR 里的 UTX_BYTE 位标志即可。2.5 帧协议为什么不能裸发温度值下位机如果直接把温度字节往串口扔比如发一个 0x1A 0x02Android 端拿到后根本不知道这一帧什么时候结束、中间有没有丢字节、数据对不对。蓝牙透传的传输环境并不稳定偶尔丢字节、粘包很正常必须有帧边界和校验。我设计了一个非常轻量的帧格式帧头0xAA 0x55两个字节用来找边界长度1 字节表示类型 数据区的字节数类型1 字节比如 0x01 表示温度、0x02 表示红外状态数据区N 字节具体内容由类型决定校验1 字节长度 类型 所有数据字节的累加和低 8 位发送函数void send_frame(uint8_t type, const uint8_t *payload, uint8_t len) { uint8_t i, sum 0; uint8_t frame[32]; frame[0] 0xAA; frame[1] 0x55; frame[2] 1 len; // 长度 类型 数据 frame[3] type; for (i 0; i len; i) { frame[4 i] payload[i]; } sum frame[2] frame[3]; for (i 0; i len; i) { sum frame[4 i]; } frame[4 len] sum; for (i 0; i len 5; i) { uart0_send_byte(frame[i]); } }温度数据我习惯放大 10 倍后用一个整数发送比如 25.3℃ 发 253避免浮点在 8051 上丢失精度Android 端再除以 10 显示。这个设计是我自己的习惯遇到大量传感器数据时整型传输比浮点省字节也省流量。3. 蓝牙透传桥接让 CC2530 的串口直接飞进手机3.1 模块选型HC-05 还是 HC-06蓝牙串口模块最常见的是 HC-05 和 HC-06。HC-06 只能做从机手机主动连接它价格便宜适合本场景。HC-05 支持主从一体可以用 AT 指令配置调试时更灵活还可以做主设备主动连接手机做蓝牙从机广播不过实际场景中用得不多。BLE 模块比如 HM-10/CC2541不建议在这个项目里用。BLE 和手机通信走的是 GATT 协议Android 端要写 BluetoothGatt 回调还要处理 Service、Characteristic、MTU、分包等一整套东西复杂度高一大截SPP 串口透传在简单二字面前是绝对的优势。3.2 接线和供电顺序HC-05/HC-06 的串口是 3.3V TTL 电平和 CC2530 的 IO 电平兼容可以直接互接。注意交叉连接CC2530 P0.2RX→ 蓝牙模块 TXCC2530 P0.3TX→ 蓝牙模块 RXGND → GND必须共地不共地串口通信必然乱码VCC → 3.3V 或模块标注的供电范围接线最容易翻车的点是蓝牙模块如果带板载稳压它可以接受 5V 供电但最好单独确认模块规格有的模块板上直接 3.3V LDO接 5V 没问题有的模块是裸芯片接 5V 会烧。我的经验是统一用 3.3V 供电别省这一步。另外一个建议先用 USB 转 TTL 模块把 CC2530 的 TX/RX 接到电脑用串口助手验证下位机数据。确认数据帧没问题后再换到蓝牙链路上来。这样把下位机问题和蓝牙问题隔离开排查时间少一半。3.3 波特率统一是第一步但为什么还会乱码CC2530 串口如果设为 9600蓝牙模块也要 9600Android App 端连接后的串口参数同样要 9600。看起来三方统一就行了但实际联调时乱码还有一个隐藏原因蓝牙模块固件默认的串口参数可能不是 9600。很多 HC-05 出厂默认 9600、停止位 1、无校验但也有部分是 38400 或 115200。上电前用 AT 指令确认一下波特率比自己盲猜省事得多。AT 指令的方式是让模块进入 AT 模式HC-05 按住板上的按键再上电指示灯慢闪即进入 AT 模式用 USB 转 TTL 接电脑串口助手发送AT返回OK就说明通了。之后用ATBAUD4或ATUART9600,0,0这类指令设置波特率具体指令格式查模块说明书。4. Android 上位机的核心蓝牙连接、帧解析、刷新与下行控制4.1 权限、UUID 与连接流程Android 端我用 Java 写核心是传统蓝牙的 RFCOMM 通道。先声明权限Android 12 及以上需要 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECTAndroid 6~11 需要 BLUETOOTH 和 BLUETOOTH_ADMIN同时运行时还要申请定位权限蓝牙扫描在旧版本系统里被归为定位相关。uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION /连接 SPP 串口服务的 UUID 是固定的00001101-0000-1000-8000-00805F9B34FB这是蓝牙串口服务SPP的标准 UUIDHC-05/HC-06 默认支持。连接的代码套路也很固定BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); BluetoothDevice device adapter.getRemoteDevice(macAddress); BluetoothSocket socket device.createRfcommSocketToServiceRecord( UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); socket.connect(); InputStream in socket.getInputStream(); OutputStream out socket.getOutputStream();连接建议放到子线程里执行不要在 UI 线程调用 connect()否则手机界面会直接卡死。设备列表就是系统已配对设备列表先让用户在系统设置里配对一次 HC-05App 里直接读配对列表即可省去蓝牙扫描的一大堆回调逻辑。4.2 数据读取子线程里的字节流逐字节喂给状态机连接成功后InputStream 的 read() 是阻塞的必须开一个专门的读线程不断把数据读到缓冲区。蓝牙串口的一个特点是你不知道自己一次能读多少字节可能一次读进来好几帧也可能一帧只读到了一半。所以绝不能读满固定长度再解析而是每收到一个字节就送给帧解析状态机。状态机维护状态找帧头、再找帧头、读长度、读数据、读校验。流程简单代码却非常实用private void handleByte(int b) { switch (state) { case STATE_WAIT_AA: if (b 0xAA) state STATE_WAIT_55; break; case STATE_WAIT_55: if (b 0x55) { state STATE_READ_LEN; } else if (b 0xAA) { /* 保持等待 */ } else state STATE_WAIT_AA; break; case STATE_READ_LEN: frameLen b; frameIndex 0; state STATE_READ_DATA; break; case STATE_READ_DATA: buffer[frameIndex] (byte) b; if (frameIndex frameLen 1) { // 数据 校验 checkAndDispatch(); state STATE_WAIT_AA; } break; } }这里frameLen是帧格式里的类型 数据区长度数据区之后再读一个校验字节两者一起收齐后做校验。校验不过直接丢弃整帧回到找帧头状态不影响后续数据。4.3 帧解析与 UI 刷新别在回调里直接改界面解析帧时按类型分发比如类型 0x01 时数据区第一个字节是温度高 8 位、第二个是低 8 位类型 0x02 时数据区第一个字节是红外状态0 或 1。UI 刷新按频率决定方式。我们这里的传感器数据量不大最简单的方式是用 runOnUiThread 直接更新 TextViewrunOnUiThread(() - { tvTemp.setText(String.format(%.1f℃, temp / 10.0)); tvPir.setText(ir 1 ? 有人 : 无人); });但如果你后面加上实时温度曲线一秒刷新几十次频繁调用 runOnUiThread 会刷到界面卡顿。更好的做法是读取线程只解析数据把结果塞进一个消息队列UI 线程通过 Handler 定时比如每秒消费一次把多个数据合并批量更新体验会顺滑很多。4.4 下行控制从手机到 CC2530App 端按钮点击后向 OutputStream 写一个同样是自定义格式的控制帧比如byte[] cmd new byte[] { (byte) 0xAA, (byte) 0x55, 0x03, // 长度类型 1 字节数据 0x10, // 类型控制指令 0x01, // 数据1 表示开继电器0 表示关 0x14 // 校验和按累加和计算 }; out.write(cmd);CC2530 串口中断收到一帧后做同样的状态机解析根据类型 0x10 执行继电器或 LED 翻转。这里有一个实际经验App 发送完尽量别立刻把按钮状态切到已开最好等 CC2530 回一条 ACK 帧再更新 UI否则蓝牙链路在断开边缘时UI 状态会和实际硬件不一致很容易误以为系统坏了。5. 联调实录五个差点让人放弃的坑5.1 乱码先查波特率别急着怀疑模块串口助手或者 App 里看到一堆乱码第一反应永远是波特率不一致。CC2530 的 U0BAUD 和 U0GCR 配置、蓝牙模块 AT 指令里的波特率、Android App 里解析时假设的波特率其实 App 不设波特率但调试助手会设三方要一致。我调试时习惯先在电脑上用 USB 转 TTL 串口助手打开故意用几个波特率去试确认下位机正常后再接蓝牙两个环节分开排查问题定位特别快。5.2 粘包和半包状态机是唯一正解蓝牙 SPP 本身是流式接口TCP 层面的粘包在串口上同样存在。下位机连续发送温度帧和红外帧时手机一次 read 可能读到 0xAA 0x55 ... 0xAA 0x55 ... 两帧连在一起也可能只读到前半帧。如果不按状态机逐字节解析直接用固定长度分割或者读到第 N 个字节就处理大概率会漏帧或错位。这个项目里我深刻体会到状态机解析虽然代码看着啰嗦但它在所有串口场景下都稳定。推荐直接把上面的 handleByte 逻辑作为公共模板存着以后换任何传感器设备都能复用。5.3 UI 卡顿蓝牙读取线程和主线程要分开新手最容易犯的问题是直接在 BluetoothSocket.connect() 之后用同一个线程一边读流一边更新 TextView。Android 不允许在非 UI 线程直接访问 View而且阻塞读会卡死主线程App 直接 ANR。所有蓝牙 IO 都必须放子线程UI 更新要回主线程。这一步不是可选项是 Android 平台的硬性要求。5.4 HC-SR501 的抽风与软件滤波HC-SR501 有两大伪故障。第一上电后有 30~60 秒的自检预热期这段时间它会随机输出几次高电平属于正常现象代码里可以加一个软启动延时上电后 60 秒内不处理红外数据。第二模块上有两个可调电阻一个调感应距离灵敏度一个调延时如果你调得太灵敏或者延时太短人来一下它就反复触发看起来像坏了一样。软件上也应该做滤波连续读到 3 次高电平才认为有人连续 3 次低电平才本文还有配套的精品资源点击获取