
简介一套基于ESP32与AD7606的蓝牙SPP数据采集传输方案适合毕业设计、课程设计与嵌入式项目开发。工程源码经过严格测试覆盖ADC采集、SPI通信、蓝牙串口协议配置等关键环节并预留扩展接口便于二次开发。压缩包共1600个文件以构建中间文件obj、a、cmake、ninja等为主也包含核心C/H源码、sdkconfig配置文件、bin固件及编译脚本整体约52.4MB其中链接脚本、map文件与编译日志还可辅助排查工程报错帮助理解ESP-IDF的完整构建流程。目前已有254人学习下载适合有一定单片机基础、希望快速上手AD7606多通道同步采样及经典蓝牙无线传输的开发者参考也可作为课程报告或毕业设计的技术支撑。1. 为什么要用ESP32做AD7606的蓝牙SPP数据采集做本科毕业设计或课程设计时最难的不是选一个ADC而是让“采集”这件事显得完整有传感器前端、有模拟量到数字量的转换、有处理器参与、还要能无线回传数据。用ESP32采集AD7606并通过蓝牙SPP传输恰好覆盖了这几层。AD7606是8通道同步采样的16位ADCESP32则自带了双核CPU和经典蓝牙二者结合起来既避开了用STM32还要外挂蓝牙模块的布线麻烦又能把整条链路放在一块板子上调试。这个方案的最大价值是“同步”。普通ADC多通道数据是轮询扫描得到的各通道之间存在时间差做三相电分析或多通道振动监测时会带来相位误差AD7606在同一个CONVST脉冲到来时把8个通道同时锁存ESP32再按SPI接口把结果逐通道读走才能保证通道间数据对得上。蓝牙SPP则解决了无线传输的问题手机端装一个通用的SPP串口助手就能直接看到波形数据比写专用App方便得多也更容易在答辩现场演示。项目本身适合电子、自动化、测控类的毕设也适合想跑通“采集-处理-无线传输”完整链路的开发者当模板。2. AD7606的8通道同步采样原理与ESP32接线设计2.1 AD7606为什么适合做多通道同步采集AD7606是ADI公司推出的16位逐次逼近型ADC单芯片内集成了8个采样保持放大器和16位ADC内核。它的关键特性在于8个通道共用同一个CONVST触发信号当CONVST引脚收到一个上升沿8个通道同时进入保持状态然后各自开始模数转换。由于转换在同一时刻启动通道间的最大时间偏差只在皮秒量级这就保证了“同步”的工程语义。对比常见的ADS1256或STM32内置ADCADS1256虽然也是高精度但它是多路复用式输入采集通道1和通道2天然有先后顺序STM32内部ADC同样存在注入通道之间的微小延时。如果题目是“多路同步数据采集系统”用普通方案很难解释通道间相位差而AD7606的方案在答辩时一句话就能说清楚。另一个好处是输入范围。AD7606的模拟输入支持±5V和±10V双极性范围通过RANGE引脚的电平直接切换不用额外搭衰减和偏置电路。对毕业设计来说直接采集传感器输出的双极性信号比给其他ADC芯片加前端调理电路省很多事。2.2 ESP32与AD7606的SPI接线表AD7606支持并行、字节和串行三种接口模式。ESP32引脚资源虽然不少但并行接口要占十几根IO在PCB布局上不划算所以实际项目中几乎都用串行SPI模式。串行模式下需要将PAR/BYTE/SEL引脚拉低或根据芯片版本接对地然后按下面的表接线信号AD7606引脚ESP32引脚作用VIOVIO3.3V数字接口电源决定输出的逻辑电平SCLKSCLKGPIO18SPI时钟输入CSCSGPIO5片选低电平有效DOUTADOUTAGPIO19 (MISO)串行数据输出ABUSYBUSYGPIO34转换忙标志高电平表示正在转换CONVSTCONVST_A / CONVST_BGPIO32启动转换脉冲RESETRESETGPIO33复位RANGERANGEGPIO25±5V/±10V量程选择DOUTB直接接VIO不接单DOUT模式全部8通道从DOUTA输出注意VIO引脚的用法。AD7606的核心供电是5V但VIO引脚是数字接口的电平参考把它接到3.3V后DOUTA输出的高电平就是3.3VESP32的GPIO可以直接读取。如果这里接了5V就必须加电平转换芯片否则会烧损坏ESP32的输入引脚。SPI接线推荐用ESP32的默认VSPI或FSPI任意一组只要代码里SPI外设编号和引脚一致就行。图上GPIO5、18、19只是示例实际开发板只要避开BOOT、EN、ADC和JTAG引脚即可例如GPIO4、16、17也是常用选择。2.3 CONVST、BUSY与SPI读出的时序关系从触发一次转换到拿到8个通道的数据时序可以拆成4步:CONVST引脚产生一个上升沿脉冲8个通道同时进入采样保持。BUSY引脚拉高AD7606内部开始逐通道转换此时不能发起SPI读操作。BUSY拉低表示转换结束。这既是“数据准备好”的标志也是SPI读数据的起始条件。主机拉低CS在SCLK驱动下AD7606按MSB先行原则把16位结果串行送出DOUTA依次输出通道V1到V8的数据。代码里最容易被忽略的是第二步的等待。有的ESP32例程只做SPI读取不检查BUSY一旦输入的采样率超过了芯片转换时间读到的高位数据就可能混入上一次转换结果。AD7606在OS0-OS2全为0时转换时间大约4微秒这个时间虽然短但在高频连续采样时仍然需要软件上做一次死循环等待我一般会加超时保护防止BUSY异常拉高时任务卡死uint32_t timeout_cnt 0; while (gpio_get_level(busy_pin) 1) { if (timeout_cnt 10000) return -1; }过采样模式由OS0、OS1、OS2三个引脚配置。默认全低表示关闭过采样得到的是原始采样率将OS引脚按二进制组合从1倍提高到64倍时芯片内部会对同一采样点做多次测量并滤波等效分辨率提高但输出速率会按过采样倍数向下折算。毕业设计里如果信号本身比较干净建议先关闭过采样把采样率跑满数据解释也更直观。3. 蓝牙SPP通道的搭建与ESP32发送代码3.1 为什么毕业设计要选SPP而不是BLEESP32的蓝牙部分同时支持经典蓝牙BR/EDR和低功耗蓝牙BLE。BLE优点在于功耗低、广播方便但它的数据传输基于GATT服务手机端需要自己实现客户端代码或者找第三方调试App去连接特定UUID的服务体验上绕了不少路。蓝牙SPP是经典蓝牙的串口配置文件它的特点是“连上就像一个虚拟串口”。手机装一个通用的蓝牙串口助手配对后就能像操作普通串口一样收发数据天然兼容计算机端的串口调试工具。对课程设计和毕业设计来说演示环节最怕的是“代码能跑但现场连不上”SPP把连接复杂度降到最低所以我一般建议选SPP。代价是它比BLE耗电但毕设板卡通常外接5V供电功耗指标并非重点。3.2 基于ESP-IDF的SPP服务初始化代码以下代码基于ESP-IDF 5.x通过Bluedroid协议栈的spp API注册一个从机服务设备名设为“ESP32_AD7606”便于手机识别#include esp_bt.h #include esp_bt_main.h #include esp_spp_api.h static esp_spp_cb_param_t *spp_param; static uint32_t spp_handle 0; static bool spp_connected false; void spp_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { switch (event) { case ESP_SPP_SRV_OPEN_EVT: spp_handle param-srv_open.handle; spp_connected true; ESP_LOGI(SPP, client connected); break; case ESP_SPP_CLOSE_EVT: spp_connected false; ESP_LOGI(SPP, client disconnected); break; case ESP_SPP_WRITE_EVT: esp_spp_write(spp_handle, 0, NULL); // 释放发送缓冲 break; default: break; } } void spp_service_init(void) { esp_bt_controller_mem_release(ESP_BT_MODE_BTDM); esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_bt_controller_init(bt_cfg)); ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BTDM)); ESP_ERROR_CHECK(esp_bluedroid_init()); ESP_ERROR_CHECK(esp_bluedroid_enable()); ESP_ERROR_CHECK(esp_spp_register_callback(spp_callback)); esp_spp_cfg_t spp_cfg { .mode ESP_SPP_MODE_CB, .enable_l2cap_ertm true, .tx_buffer_size 0, }; ESP_ERROR_CHECK(esp_spp_init(spp_cfg)); ESP_ERROR_CHECK(esp_spp_start_srv( ESP_SPP_SEC_NONE, ESP_SPP_ROLE_SLAVE, 0, ESP32_AD7606)); }这部分代码有几个参数要单独说明。第一处是esp_bt_controller_mem_release(ESP_BT_MODE_BTDM)它告诉协议栈只分配经典蓝牙所需的内存不再给BLE预留缓冲能节省大约几十KB的RAM对于同时跑采集任务的项目空间紧张时很关键。第二处是enable_l2cap_ertm trueERTM是增强重传模式开启后蓝牙链路层对丢失的分组会重传无线环境差时不容易丢字节代价是增加少量延迟和数据冗余。esp_spp_start_srv的参数里安全等级设成了ESP_SPP_SEC_NONE即不强制配对和加密方便手机快速连接。正式的工业应用通常改成ESP_SPP_SEC_AUTHENTICATE要求认证后才能连上。3.3 发送数据时的缓冲管理细节采集任务把一帧8通道数据准备好后通过下方函数发出bool spp_send_packet(uint8_t *buf, uint16_t len) { if (!spp_connected || spp_handle 0) return false; esp_err_t ret esp_spp_write(spp_handle, len, buf); return (ret ESP_OK); }这里最容易踩坑的是发送缓冲区生命周期。esp_spp_write是异步接口函数返回只代表数据进入协议栈内部队列不代表已经发到手机端。如果传入的是局部数组或栈上缓冲区函数返回后数据仍可能被协议栈引用此时栈帧释放就会导致发送内容非法。因此我在采集任务里用了一个静态数组作为发送缓冲区每填满一帧就调用一次发送。实际调试中还会发现两个采集周期之间如果连续调用esp_spp_write两次第一次的数据可能还滞留在协议栈队列里第二次写入会被拒收此时可以增加一个“上一帧发送完成”的信号量在ESP_SPP_WRITE_EVT事件里释放。这也是上面回调里写esp_spp_write(spp_handle, 0, NULL)的原因用一次零长写触发协议栈释放之前帧的缓冲再配合信号量做发送节流。4. ESP32读取AD7606的源码实现与任务拆分4.1 用SPI外设读取AD7606的最小代码段ESP32的SPI主机驱动配置较为繁琐我通常把它封装成一个ad7606_init函数把设备初始化、GPIO初始化都收敛到一处#include driver/spi_master.h #include driver/gpio.h #define PIN_SCLK 18 #define PIN_MISO 19 #define PIN_CS 5 #define PIN_BUSY 34 #define PIN_CONV 32 spi_device_handle_t adc_spi; void ad7606_init(void) { gpio_config_t io { .pin_bit_mask (1ULL PIN_BUSY) | (1ULL PIN_CONV), .mode GPIO_MODE_INPUT_OUTPUT, .pull_up_en GPIO_PULLUP_DISABLE, .pull_down_en GPIO_PULLDOWN_DISABLE, }; gpio_config(io); gpio_set_level(PIN_CONV, 0); spi_bus_config_t bus { .miso_io_num PIN_MISO, .mosi_io_num -1, .sclk_io_num PIN_SCLK, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 64, }; spi_bus_initialize(SPI2_HOST, bus, SPI_DMA_CH_AUTO); spi_device_interface_config_t dev { .mode 3, .clock_speed_hz 20000000, .spics_io_num PIN_CS, .queue_size 4, }; spi_bus_add_device(SPI2_HOST, dev, adc_spi); }spi_device_interface_config_t里的mode 3对应CPOL1、CPHA1也就是SCLK空闲为高电平、在第二个边沿采样数据。AD7606数据手册的时序图明确要求模式3写成模式0读回来的低字节会倒序。clock_speed_hz设置成20MHz在多数场景下稳定无误再往上提速时要考虑杜邦线长度和接触电阻过长飞线跑40MHz很容易出现误码。4.2 连续采样的完整读取函数读取一帧8通道数据需要128个SCLK周期因为每个通道16位、单DOUT模式下DOUTA依次送出8个通道。实际代码里不需要手动翻转SCLKESP32的SPI外设会用一次DMA或轮询事务自动生成脉冲typedef struct { int16_t ch[8]; } ad7606_frame_t; int ad7606_read_frame(ad7606_frame_t *frame) { gpio_set_level(PIN_CONV, 1); gpio_set_level(PIN_CONV, 0); // CONVST上升沿触发转换 uint32_t timeout 0; while (gpio_get_level(PIN_BUSY) 1) { if (timeout 20000) return -1; } uint8_t rx_buf[16] {0}; spi_transaction_t t { .length 128, .rxlength 128, .rx_buffer rx_buf, }; spi_device_polling_transmit(adc_spi, t); for (int i 0; i 8; i) { frame-ch[i] (int16_t)((rx_buf[i * 2] 8) | rx_buf[i * 2 1]); } return 0; }这段代码里spi_transaction_t的length和rxlength必须写成128而不是16。如果只写length16SPI主机只产生16个时钟周期只能读到第一个通道的数据。这里128比特恰好对应8×16位AD7606会在CS低电平期间按顺序把8个通道全部推出。关于字节顺序AD7606输出的每个通道都是高位在前。SPI主机收到的第一个比特进入rx_buf的高位区所以组合时用8把高字节放前面这个顺序和芯片数据手册的时序图是对应的。4.3 用FreeRTOS双任务隔离采集和发送项目里我给ESP32分配了两个任务分别跑在两个CPU核心上static void adc_task(void *arg) { ad7606_frame_t frame; while (1) { if (ad7606_read_frame(frame) 0) { uint8_t *p (uint8_t *)frame; xQueueSend(adc_queue, p, pdMS_TO_TICKS(2)); } } } static void bt_task(void *arg) { ad7606_frame_t frame; while (1) { if (xQueueReceive(adc_queue, frame, portMAX_DELAY) pdTRUE) { if (spp_connected) { spp_send_packet((uint8_t *)frame, sizeof(frame)); } } } }xQueueSend把16字节的帧数据放入队列bt_task从队尾取出发送。这个队列天然起到了双缓冲的作用当蓝牙链路突然拥堵时采集任务不会被阻塞队列会把来不及发的数据暂存起来。队列深度一般设成32左右太深会导致发送延迟变大太浅则可能丢帧。如果用xQueueSendToFront则优先发最新数据适合需要实时波形的场合。两个任务用xTaskCreatePinnedToCore分别绑定到核心0和核心1采集任务在高优先级蓝牙发送任务稍低。ESP32的Wi-Fi/蓝牙协议栈默认跑在核心1上如果发送任务也绑核心1会与协议栈争抢CPU导致吞吐下降我习惯把采集任务绑核心0、发送任务绑核心1让协议栈和发送逻辑使用同一个核心减少跨核切换。5. 蓝牙SPP带宽与采样率如何匹配、防止数据堆积5.1 先算出瓶颈在哪一侧AD7606理论上能跑到100kSPS串行模式8通道每通道16位意味着每产生1个采样周期就有16字节数据。如果全速工作1ms内会产生160KBytes数据这个量远超过蓝牙SPP的传输能力。ESP32的经典蓝牙SPP有效数据吞吐通常只有几十KB/s量级实际用手机串口助手接收时持续传输能达到20到30KB/s已经算表现稳定。因此在设计阶段必须先算一遍数。例如设定8通道同步采样率1kHz每秒产生1kHz × 8通道 × 2字节 16KB/s这个数据量在蓝牙SPP的稳定区间内手机端刷新波形时不会频繁断流。如果把采样率提高到5kHz数据量变成80KB/s必然在发送队列里越积越多最终老化导致ESP32复位或看门狗超时。5.2 用定时器触发的方式压低采样率上面第4章的代码是“采样任务里不停的发CONVST”属于全速运行实际项目里我不推荐直接这么用因为抖动和无谓的CPU占用都很明显。更稳的做法是把CONVST脉冲交给定时器产生把采样率固定到目标值#define SAMPLE_RATE_HZ 1000 void timer_isr(void *arg) { gpio_set_level(PIN_CONV, 1); gpio_set_level(PIN_CONV, 0); } void ad6020_start_timer(void) { const esp_timer_create_args_t args { .callback timer_isr, .name adc_conv, }; esp_timer_handle_t timer; esp_timer_create(args, timer); esp_timer_start_periodic(timer, 1000000 / SAMPLE_RATE_HZ); }esp_timer_start_periodic的单位是微秒1kHz对应1000微秒。脉冲的占空比不用特意控制AD7606只需要在上升沿触发一次转换我在中断里连续拉高拉低即可。5.3 过采样当降速工具用会让数据更好看如果把OS引脚设为2倍过采样则内部转换完成一次数据需要的时间加倍实际输出速率相应减半。这样可以在不改变定时器周期的情况下天然地把数据率降到500Hz同时每个输出值都是两次转换的平均噪声更低。毕业设计展示通道波形时加上过采样后的曲线更平滑很容易给答辩老师留下正面印象。记住一个准则先利用过采样把率降到目标值附近再用采样率作为微调参数。不要直接靠vTaskDelay把采样做主调整因为FreeRTOS的软件定时误差在毫秒级对同步采集来说偶然的时钟抖动会体现为波形毛刺。5.4 验证传输完整性的最简方法调试SPP链路是否丢数据的办法很多最省事的是在PC端写一个Python脚本从虚拟串口读取统计每秒收到的帧数和CRC错误数import serial ser serial.Serial(COM5, 460800, timeout1) total 0 errors 0 while True: data ser.read(16) if len(data) 16: total 1 checksum sum(data[:-1]) 0xFF if checksum ! data[-1]: errors 1 if total % 100 0: print(fframes{total} errors{errors})发送端在拼接蓝牙包的载荷时最末尾加上一字节校验和即前15字节之和取低8位。Python脚本里做同样的计算两者一致说明这一帧完整到达。用这个脚本连续跑10分钟如果error数一直是0就说明SPP链路在当前的采样率下是可靠的如果error不断增长第一件事不是调代码而是把采样率减半重新测一遍先排除带宽瓶颈。本文还有配套的精品资源点击获取