☰
安卓蓝牙示波器实战:从蓝牙SPP数据传输到ADC波形绘制的完整链路
2026/10/19 3:50:09 网站建设 项目流程

简介:安卓蓝牙示波器资源包面向电子爱好者、嵌入式开发者和移动端工程师,提供一套将手机当作便携测量终端的完整参考方案,适合低成本信号检测与分析场景。硬件部分包含信号采集电路、ADC与蓝牙模块,压缩包内附电路原理图及dsPIC固件代码,便于自制和调整采样精度;软件部分提供安卓应用APK及5个Java源码文件,可完成蓝牙数据解析、手机屏幕波形绘制,并支持参数设置、波形分析、存储回放等扩展功能。由于蓝牙带宽有限,实时性和精度有一定约束,但低功耗与广泛兼容性使其仍是手机连接的理想选择,开发者也在探索Wi-Fi传输以提高速度。整个zip包共8个文件,大小仅115KB,包含硬件原理图、固件压缩包、APK与5个Java源文件,结构紧凑,开源特性便于二次开发。已有2383人学习下载,适合学习蓝牙通信、Android数据可视化及移动测量技术的人群。

1. 安卓蓝牙示波器:一块屏、一个数据流、一次完整的软硬件协同实战

把一台普通安卓手机改造成一台两通道蓝牙示波器,可能吗?答案是不仅可以,而且整个链路比想象中短:单片机以固定周期采集模拟电压,通过蓝牙把数据帧推给手机App,App负责解码、存储和绘制波形。我拆过一套这样的资源和配套原理图,从焊板、烧固件到编译App,单次跑通大约需要两天时间。这套资源适合三类人:一是电子工程相关课程需要低成本实验平台的学生,二是调试电源纹波、传感器输出、马达驱动PWM等低频信号的嵌入式开发,三是对蓝牙无线数传感兴趣、想自己攥一套完整软硬件Demo的个人开发者。下面把其中最关键的数据链路、代码结构和踩坑点展开讲,让你拿到资源后不迷路。

2. 系统架构与选型:蓝牙SPP与BLE的取舍、采样率与ADC精度怎么定

2.1 整体数据链路:传感器到屏幕的每一毫秒去了哪里

先看一条信号从被测点到手机屏幕的完整路径。被测电压经过探头或杜邦线,先进入硬件板上的前端调理电路,这里的任务是分压、限幅、滤波,把未知的电压变换成单片机ADC能承受的0~Vref区间。ADC每个采样周期把模拟电压转换成数字码值,可能存在DMA缓冲区里,也可能直接被中断服务程序取走。固件里的组帧函数把最近一次的ADC结果填进自定义帧结构,加上帧头、通道标识、计数值和校验字节,然后交给串口发送。蓝牙模块(经典SPP或BLE透传)通过UART接口拿到这些帧数据,再按无线协议打包发到手机。手机端收到蓝牙串口数据后,要先做粘包和半包处理,按帧头找到完整帧,再做校验,最后取出ADC值换算成电压并交给绘图View实时刷新。

这条链路里最容易被低估的瓶颈是无线的速率和延迟。经典蓝牙SPP的实际吞吐量一般在10~30KB/s,BLE在透传模式下可达几KB/s到几十KB/s。而单片机内部ADC能做到1Msps甚至更高,所以真正的限制落在蓝牙吞吐量上。比如单通道、每个采样点用2字节表示、再加上6字节的帧头校验,数据率就是8字节/采样点。如果想传输20k采样点/秒,就是160KB/s,这个速率无论SPP还是BLE都扛不住。所以实际设计时要先算清楚可接受的有效带宽,再反推采样率上限。

2.2 选型:经典蓝牙SPP还是BLE,以及采样率的量级估算

选择蓝牙方案主要看两条:连接方式和数据吞吐量。经典蓝牙SPP就像一条虚拟串口,连接稳定、丢包少、协议栈简单,安卓侧只需要BluetoothSocket就能收发,开发最直接。BLE的优势是功耗低,广播和连接间隔可配置,但它面向的是小包突发场景,连续高速传输时容易触发丢包或流控问题。所以我的建议是:如果被测信号频率低于10kHz,且不苛求电池续航,优先用SPP;如果做的是穿戴式、需要长时间记录低频信号(比如心电图或温度曲线)的低功耗应用,选BLE,但要把采样率进一步压低到1k~5k采样点/秒。

采样率的量级可以用一个简单式子估算:总需要的字节率 = 采样率 × 每样本字节数 × 通道数 × 协议系数。协议系数通常取1.2~1.5,是因为帧头、校验和可能引入的填补字节。假设用SPP,实际能稳定做到20KB/s,每样本占2字节,单通道,协议系数1.25,那么最大采样率就是20,000 ÷ (2 × 1 × 1.25) = 8k采样点/秒。这对应能测约4kHz带宽以内的信号。如果你的测量对象是几十赫兹的传感器输出,这个带宽绰绰有余;如果你想看高频振铃,就得降低分辨率或压缩数据。

2.3 硬件原理图的核心模块:前端调理、ADC、蓝牙接口

硬件原理图通常包含三个可复用的模块。第一个是输入保护与分压网络。被测电压可能远高于ADC参考电压,原理图上会有电阻分压器和两个二极管钳位,把输入电压限制在0~VDD之间。分压比例选择很关键,比如要测0~30V直流,而ADC参考电压是3.3V,那分压比至少要10:1。这里一个常见错误是只考虑电阻分压,没考虑ADC输入阻抗,结果分压电阻太大导致采样值偏低。正确的做法是加一级电压跟随器(运放构成),让信号源阻抗与ADC隔离。

第二个模块是ADC采样电路。单片机的ADC引脚需要一只0.1uF的小电容滤除高频噪声,同时保证采样保持电路有足够的充电时间。有些原理图会在ADC引脚前串联一个几十到几百欧的电阻,跟电容构成低通滤波器,截止频率要算得比目标信号高,否则会把你要看的细节滤掉。还要注意ADC参考电压的来源,如果直接接单片机LDO输出,精度只有1%左右,要想波形幅值准确,就得用独立基准电压源。

第三个模块是蓝牙接口。经典蓝牙模块多数用UART,RX/TX交叉连接,还要处理电平匹配。很多蓝牙模块是5V逻辑,单片机是3.3V,需要加电平转换或者分压。原理图里XC6152类降压芯片给整个板子供电,蓝牙模块的供电和地要单独走线,避免功率电路干扰到模拟前端。这部分的布局走线会直接影响波形噪声,我第一版的板子就是因为蓝牙模块紧挨着模拟输入走线,导致屏幕上出现了规律的毛刺。

3. 固件端实现:ADC连续采样、组帧发送与流量控制

3.1 定时器触发ADC采样:用DMA还是中断

固件端首先要解决的是ADC采样时机。轮询模式只在主循环里循环读ADC,采样间隔不稳定,波形画出来会带有时间轴抖动。正确的做法是用定时器触发ADC转换,并用DMA把结果搬进内存,这样采样间隔精确到微秒级,CPU还能同时处理组帧和蓝牙发送。

下面是一个常见单片机(以标准外设库为例)的配置思路,代码内核参考如下:

void adc_timer_init(void) { // 定时器分频后得到 1MHz 计数频率 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler = 72 - 1; // 假设系统主频72MHz,分频到1MHz tim.TIM_Period = 10 - 1; // 1MHz除以10,得到100kHz触发频率 tim.TIM_ClockDivision = TIM_CKD_DIV1; tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &tim); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); }

这段代码把定时器更新事件作为ADC外部触发源。分频器和周期值决定了触发频率,调整这两个参数就能改采样率。比如想要20k采样,就把周期改成50(1MHz÷50=20kHz)。注意定时器不能产生太快的中断,否则主程序会被打满,所以更推荐的成熟方案是配合DMA。

void adc_dma_start(void) { DMA_InitTypeDef dma; dma.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; dma.DMA_MemoryBaseAddr = (uint32_t)adc_buf; dma.DMA_DIR = DMA_DIR_PeripheralSRC; dma.DMA_BufferSize = ADC_BUF_LEN; // 比如128次转换 dma.DMA_PeripheralInc = DMA_PeripheralInc_Disable; dma.DMA_MemoryInc = DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.DMA_Mode = DMA_Mode_Circular; // 循环采集 dma.DMA_Priority = DMA_Priority_High; DMA_Init(DMA1_Channel1, &dma); DMA_Cmd(DMA1_Channel1, ENABLE); }

这里的DMA缓冲区用了循环模式,每采满一整个缓冲区后触发一次中断,固件在中断里把缓冲区数据打包发送。之所以不用半传输中断,是为了保证每帧数据都是连续的片段,不会出现新旧数据混在一起的错位。另一个好处是CPU只需在缓冲区满时工作一次,采样间隙完全空闲,留给蓝牙推帧用。

3.2 数据帧协议设计:帧头、通道、计数值与校验

蓝牙无线传数不像有线串口那么可靠,偶尔会出现字节丢失或乱序。因此每一帧数据必须有明确的边界和校验。我常用的帧格式是6字节帧头 + 每通道2字节数据。

以单通道、批量16个点为例,帧结构可以设计成:

typedef struct __attribute__((packed)) { uint8_t head[2]; // 固定0xAA 0x55 uint8_t channel; // 0:CH1, 1:CH2 uint8_t seq; // 包序号,用来检测丢包 uint16_t length; // 本帧数据字节数 uint16_t samples[]; // ADC原始值,每个点2字节 uint8_t checksum; // 前所有字节和取低8位 } DataFrame;

发送侧这么填充和校验:

void build_frame(uint8_t ch, uint16_t *buf, uint16_t len) { frame.head[0] = 0xAA; frame.head[1] = 0x55; frame.channel = ch; frame.seq = seq++; frame.length = len * 2; memcpy(frame.samples, buf, len * 2); uint8_t sum = 0; uint8_t *p = (uint8_t *)&frame; for (uint16_t i = 0; i < sizeof(frame) - 1; i++) { sum += p[i]; } frame.checksum = sum; }

seq包序号非常重要。手机端只要发现序号不连续,就能判断无线链路丢了包,然后决定是补发还是丢弃这帧。length字段用来做半包和粘包拆分的依据,checksum用来过滤传输误码。协议里没有用复杂CRC,是因为低波特率下误码率并不高,一个累加校验足够挡住绝大多数随机错误,但要注意累加校验会把字节顺序错误也掩盖掉,所以帧头用了AA 55这种非对称模式,能在很大程度上避免误对齐。

3.3 发送节奏与串口波特率:别让蓝牙成为瓶颈

蓝牙模块和单片机之间是UART,默认波特率一般有9600、38400、115200几档。如果单片机一次发一大包数据,蓝牙模块内部缓冲区可能撑爆,所以帧长和发送间隔必须算好。比如蓝牙模块串口波特率是115200,实际有效数据率约11.5KB/s(去掉起始位停止位),而ADC采样率20k、每点2字节,那么单通道就需要40KB/s,明显超了。解决思路有三个:降低采样率、降低分辨率(8位)、或者压缩数据。

我一般把发送放在一个独立的环形缓冲队列里,主循环从队列里取帧,通过串口发送函数推给蓝牙,而不是在DMA中断里直接阻塞发送。原因是DMA中断频率高且不能长时间占用,否则会饿死定时器中断。代码核心逻辑如下:

#define TX_RING_SIZE 4096 uint8_t tx_ring[TX_RING_SIZE]; uint16_t tx_head = 0, tx_tail = 0; void send_frame(DataFrame *p, uint16_t len) { // 将frame字节写入环形缓冲,注意处理回绕 for (uint16_t i = 0; i < len; i++) { tx_ring[tx_head] = p->bytes[i]; tx_head = (tx_head + 1) % TX_RING_SIZE; } } void uart_dma_send_task(void) { // 查询DMA是否空闲,空闲时从环形缓冲取数据并启动一次DMA发送 }

这个设计防止了串口发送阻塞采样任务。如果环形缓冲已满,说明手机端处理不过来,此时宁可丢弃最新的帧也不能把旧数据写入覆盖掉,因为波形看到的“卡顿”总比“错乱”容易接受。实际调试时我会用蓝牙模块的指示灯频率粗略判断是否过载——指示灯闪烁太快说明数据量接近上限,闪烁规律性变差则说明发送节奏不稳定。

4. 安卓App实现:蓝牙连接、数据解析与波形绘制

4.1 权限配置与设备发现:经典蓝牙的连接流程

安卓侧开发要先在AndroidManifest里申请权限。Android 6以上需要动态申请定位权限才能扫描蓝牙设备,Android 12以上还要申请BLUETOOTH_SCAN和BLUETOOTH_CONNECT。代码中用运行时权限请求处理扫描接口,这一步漏掉的话,设备列表永远是空的。

连接代码走标准BluetoothAdapter流程:

BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); adapter.startDiscovery(); BluetoothDevice device = adapter.getRemoteDevice(macAddress); BluetoothSocket socket = device.createRfcommSocketToServiceRecord(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")); socket.connect();

这个UUID是串口服务(SPP)的标准UUID。很多新手卡在连接失败上,原因往往是扫描和连接不允许并发,连接前必须调用cancelDiscovery()。另外,为了防止蓝牙服务适配器被系统回收,很多资料建议获取一个守护的BluetoothAdapter实例,但更稳定的做法是让连接操作放在后台线程,并在Activity的onPause里断开连接,避免界面切换导致socket泄漏。

连接建立后,数据读取用独立线程:

InputStream input = socket.getInputStream(); byte[] buffer = new byte[1024]; int bytes; while ((bytes = input.read(buffer)) != -1) { if (bytes > 0) { dataQueue.offer(Arrays.copyOf(buffer, bytes)); } }

这里用的是阻塞式InputStream,因为蓝牙socket的读取本身是阻塞的,放到普通线程里最直观。千万不能直接在主线程里读数据,否则界面一卡一顿,波形直接糊掉。

4.2 接收线程与环形缓冲:避免UI卡顿的设计

原始字节流到达后,App要做拆帧、校验、转电压、再丢给UI绘制。如果把这些工作全部挤在读取线程里,一旦有半包数据,线程就被占住,后续数据来不及收,导致进一步丢包。更合理的结构是用生产者消费者模式:读取线程只负责把原始字节写入一个线程安全的环形队列,同时另外起一个解析线程从队列里取数据,拆出完整帧。

private RingBuffer<byte[]> rawRing = new RingBuffer<>(256); private void parseLoop() { while (!stopFlag) { byte[] chunk = rawRing.poll(50, TimeUnit.MILLISECONDS); if (chunk == null) continue; parseBytes(chunk); } }

parseBytes内部维护一段残留缓冲区,每次把新数据追加进去,然后循环查找帧头、解析长度、校验、取出样本。解析结果放入一个波形数据数组,使用ArrayDeque存放最近几千个点,绘制线程通过invalidate()触发重绘。这种分层设计让UI线程永远只做绘制,不做IO和解析,就算蓝牙数据稍微抖动,波形也不会掉帧。

4.3 自定义View画波形:缩放、暂停、触发基线

波形显示是App里最直接的脸面。用系统View的onDraw里画Path即可,核心思路是维护一个采样点数组,每次解析到新数据就滚动数组,再在onDraw里把数组映射为屏幕x/y坐标。

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Path path = new Path(); float stepX = getWidth() / (float) waveData.length; for (int i = 0; i < waveData.length; i++) { float y = getHeight() - (waveData[i] / 4096f) * getHeight(); if (i == 0) { path.moveTo(0, y); } else { path.lineTo(i * stepX, y); } } canvas.drawPath(path, wavePaint); }

波形显示还要支持比例缩放。最简单的实现是定义样本数量变量pointsPerScreen,当用户双指缩放时改变这个值,刷新绘制。暂停功能则是停止更新waveData数组,但保留接收线程继续回放数据到后台缓冲,这样恢复绘制时能直接补画最近一段。触发基线,也就是触发电平,可以画一条横向虚线,比较跨过基线的相邻点来标记起始位置。这个功能在观察周期信号时特别有用,否则波形在屏幕上快速滚动,人眼很难捕捉到稳定的相位。

5. 联调避坑:波形乱飞、数据丢包、电压超量程的五个典型现场

5.1 现象:刚连上就疯狂掉线

联调第一天,手机App可以搜索到模块,连接后大约一两秒就断开,有时候根本连不上。反复配对后偶尔成功,但一传输数据瞬间掉线。

原因有两个。第一是连接过程中没有调用cancelDiscovery(),蓝牙还处在扫描状态,占用了RFCOMM通道。第二是单片机上电后蓝牙模块进入AT配置模式或者频繁广播,干扰了连接建立。解决方法是连接前强制取消扫描,并检查模块状态指示灯,确认模块已经进入数据传输模式。控制代码顺序如下:

adapter.cancelDiscovery(); if (socket != null) { try { socket.close(); } catch (Exception e) {} } socket = device.createRfcommSocketToServiceRecord(uuid); socket.connect();

之后就稳定了。

5.2 现象:波形是锯齿或者随机跳变

屏上正常输入一个直流电压,显示的却是锯齿状波形,或者数值随机跳。

原因排查发现,模拟前端的地线和数字地没有分开铺铜,蓝牙模块发射瞬间的电流波动通过共地耦合到了ADC输入。另一个原因是Vref直接用3.3V LDO供电,负载变化导致参考电压波动,ADC读数自然跟着抖。解决方法是地平面做单点连接,模拟地和数字地通过磁珠或0欧电阻连接;Vref换成专门的基准源,或者在ADC采样前加一颗100uF电解电容和0.1uF瓷片电容并联。如果还抖,则检查探头屏蔽层,我后来给两根测试线加了双绞屏蔽线,锯齿立刻消失。

5.3 现象:采样率标称20k,实际只有1k

设置定时器触发频率是20kHz,App端显示实际更新速率只有1kHz,波形明显迟钝。开始怀疑蓝牙吞吐,后来发现瓶颈在固件端发送逻辑上。

工程里用了串口轮询发送,每次发送一帧数据要等待发送寄存器清空,如果波特率只有9600,那么一帧96字节需要100毫秒,实际每秒只能发10帧,每帧16个点,只有160个点每秒,完全达不到20k。解决方法是把波特率提高到115200以上,并且配置成使用DMA发送,或者把帧内样本数放大,一帧发256个点,这样帧发送开销被摊薄,有效数据率才上去。我最终把一帧样本数改成256,波特率提到450000,实际能跑到16k采样点/秒,足够看10kHz之内的波形。

5.4 现象:正常测量时单片机发热明显

给板子供电后,环境温度正常,但单片机芯片摸起来烫手。

原因是蓝牙模块的VCC被误接到了单片机的IO口上,而这个IO口被配置成了推挽输出高电平。蓝牙模块工作电流可以达到几十毫安,IO口根本带不动,于是内部压降变大,热量集中。解决方法是严格区分电源引脚和信号引脚,蓝牙模块供电从3.3V电源轨走,中间加一个10uF去耦电容。这个现象说明原理图和PCB检查要过一遍“电源流向图”,每一路电流从哪来回哪去。

5.5 现象:电池供电下波形噪声很大

用USB供电时波形很干净,改用电池供电后噪声幅度增加了至少一个量级。

原因不是蓝牙模块,而是电池电压不稳定。在电池供电状态下,蓝牙发射时电流瞬间增大,电源轨跌落再反弹,AD转换正好发生在这个波动窗口就会采到毛刺。解决方法是硬件端在电源输入端并入一个大电容(例如470uF)和一个小电容(0.1uF)形成高低频去耦阵列;固件端则把ADC触发点错开蓝牙发射时刻。更绝的做法是采用恒流源或电源管理芯片做稳压,但从实测角度看,一颗470uF电容已经能解决95%的噪声问题。

6. 进阶调试技巧:用回调式波形标记和离线数据回放校准测量误差

波形显示出来只是第一步,真正好用还要加上测量辅助功能。推荐两个我经常用到的进阶技巧:回调式峰值标记和离线数据回放校准。

回调式峰值标记不是简单地画一条最高点线,而是实时监视新到的样本,当检测到连续三个点的变化趋势从升转降时,就把这个点的位置用一条竖直光标标出来,同时计算相邻两个峰之间的时间差得到频率。代码里用一个简易状态机:

if (prevSample < currentSample && nextSample < currentSample) { peakIndex = index; peakValue = currentSample; // 触发UI更新标记 }

之所以叫“回调式”,是因为峰值检测事件通过回调接口抛给Activity,Activity只负责更新界面标注,不参与波形数据存储。这样即使每秒有几千个样本,UI也能轻松响应。

离线数据回放校准是每次测量时同时把原始ADC值和对应时间戳写入文件,格式用CSV:

timestamp,adc_value,voltage 1630000000,2048,1.652 1630000001,2050,1.653

在电脑上加载这些CSV,先用已知直流电压表数据做线性回归,求出实际增益和偏置,再把修正后的参数写回App的配置文件。这个习惯帮我发现了一块板子上的微小电阻焊错问题,ADC看起来正常,电压却整体偏低0.08V。校准流程也很简单:输入0V、1V、2.5V三档标准电压,记录每档的ADC均值,然后用两点拟合出斜率和截距。

从那以后我每次拿到新板子,都会强制走一遍“跑波形、存CSV、回放校准”的三步流程。校准完成后,再回来看实时屏,波形幅度误差能控制在2%以内,比只看读数可靠得多。这套蓝牙示波器资源里带的正是一个能在真实项目里反复用的软硬件底座,把通信协议和波形绘制都跑通之后,你还可以继续往里面加协议解码、频谱分析甚至双通道时延测量,希望帮到你。

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

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

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

立即咨询