CC2530+蓝牙+Android:从零搭建无线传感器采集系统
2026/9/24 10:48:26 网站建设 项目流程

简介:这是一份面向嵌入式与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 RAM,IO 口够多,最常见的是搭配 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 Programmer

CC2530 的官方开发环境是 IAR for 8051,Z-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、DQ,DQ 必须接上拉电阻到 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 后释放,然后在短时间内采样总线电平。

温度读取的完整流程是:复位 → 跳过 ROM(0xCC)→ 启动温度转换(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 optimize=none的代码段里,稳定复现。

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 备用位置 1:P0.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 时 U0BAUD=59、U0GCR=8,115200 时 U0BAUD=216、U0GCR=11,按 CC2530 数据手册波特率表查最稳。项目里建议先用 9600,蓝牙模块默认波特率也通常是 9600,两边对齐省事。

串口发数据时,通过 U0DBUF 写入一个字节,然后等待 U0CSR 里的 UTX_BYTE 位标志即可。

2.5 帧协议:为什么不能裸发温度值

下位机如果直接把温度字节往串口扔,比如发一个 0x1A 0x02,Android 端拿到后根本不知道这一帧什么时候结束、中间有没有丢字节、数据对不对。蓝牙透传的传输环境并不稳定,偶尔丢字节、粘包很正常,必须有帧边界和校验。

我设计了一个非常轻量的帧格式:

  • 帧头: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.2(RX)→ 蓝牙模块 TX
  • CC2530 P0.3(TX)→ 蓝牙模块 RX
  • GND → GND(必须共地,不共地串口通信必然乱码)
  • VCC → 3.3V 或模块标注的供电范围

接线最容易翻车的点是:蓝牙模块如果带板载稳压,它可以接受 5V 供电,但最好单独确认模块规格,有的模块板上直接 3.3V LDO,接 5V 没问题;有的模块是裸芯片,接 5V 会烧。我的经验是统一用 3.3V 供电,别省这一步。

另外一个建议:先用 USB 转 TTL 模块把 CC2530 的 TX/RX 接到电脑,用串口助手验证下位机数据。确认数据帧没问题后,再换到蓝牙链路上来。这样把"下位机问题"和"蓝牙问题"隔离开,排查时间少一半。

3.3 波特率统一是第一步,但为什么还会乱码

CC2530 串口如果设为 9600,蓝牙模块也要 9600,Android App 端连接后的串口参数同样要 9600。看起来三方统一就行了,但实际联调时乱码还有一个隐藏原因:蓝牙模块固件默认的串口参数可能不是 9600。很多 HC-05 出厂默认 9600、停止位 1、无校验,但也有部分是 38400 或 115200。上电前用 AT 指令确认一下波特率,比自己盲猜省事得多。

AT 指令的方式是让模块进入 AT 模式:HC-05 按住板上的按键再上电,指示灯慢闪即进入 AT 模式,用 USB 转 TTL 接电脑串口助手,发送AT,返回OK就说明通了。之后用AT+BAUD4AT+UART=9600,0,0这类指令设置波特率,具体指令格式查模块说明书。

4. Android 上位机的核心:蓝牙连接、帧解析、刷新与下行控制

4.1 权限、UUID 与连接流程

Android 端我用 Java 写,核心是传统蓝牙的 RFCOMM 通道。先声明权限,Android 12 及以上需要 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT,Android 6~11 需要 BLUETOOTH 和 BLUETOOTH_ADMIN,同时运行时还要申请定位权限(蓝牙扫描在旧版本系统里被归为定位相关)。

<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

连接 SPP 串口服务的 UUID 是固定的00001101-0000-1000-8000-00805F9B34FB,这是蓝牙串口服务(SPP)的标准 UUID,HC-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-05,App 里直接读配对列表即可,省去蓝牙扫描的一大堆回调逻辑。

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 直接更新 TextView:

runOnUiThread(() -> { tvTemp.setText(String.format("%.1f℃", temp / 10.0)); tvPir.setText(ir == 1 ? "有人" : "无人"); });

但如果你后面加上实时温度曲线,一秒刷新几十次,频繁调用 runOnUiThread 会刷到界面卡顿。更好的做法是读取线程只解析数据,把结果塞进一个消息队列,UI 线程通过 Handler 定时(比如每秒)消费一次,把多个数据合并批量更新,体验会顺滑很多。

4.4 下行控制:从手机到 CC2530

App 端按钮点击后,向 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 次低电平才

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询