AD7606与ESP32:8通道同步采集与蓝牙SPP透传实战方案
简介一套面向毕业设计、课程设计与项目开发的完整源码工程以ESP32为主控驱动AD7606多通道并行采集数据再通过蓝牙SPP协议无线传输至接收端覆盖底层驱动、应用逻辑与通信链路的完整实现。压缩包共1600个文件、约52.4MB以obj编译中间文件、cmake工程脚本、a静态库文件为主体同时包含c/h源码、sdkconfig配置文件、elf/bin可执行文件、map映射文件及构建脚本目录结构清晰便于查看配置、重新编译与烧录调试。项目源码经过严格测试可直接参考也可根据需要在现有框架上扩展通道数、调整采样率或修改通信协议。目前已有254人学习下载适合电子与嵌入式方向的在校学生、开发者用于课题参考和二次开发。1. 从AD7606到ESP32一套可以直接跑的8通道同步采集与蓝牙SPP透传方案做电力谐波分析或振动监测时经常遇到“多通道同步采集”这个硬需求。AD7606是8路16位同步采样ADCESP32负责控制和无线传输这是毕设里很常见的组合。但要真正同时用上它的SPI接口、BUSY中断和蓝牙SPP很少有一份代码能一次跑通。我拆过这套源码里面把外设初始化和传输逻辑都打包好了可以直接烧录测数据。如果你正在做毕业设计或者课程设计想省掉踩坑时间这篇就把底层时序和数据帧结构都摊开讲清楚。下面先解决接口选型问题。2. AD7606并行/SPI接口选型与ESP32引脚连接2.1 为什么选SPI而不是并行总线AD7606原生支持三种读取方式16位并行、字节并口和串行SPI。并行接口一次读一个通道需要16根数据线加上控制线总共要二十多根ESP32的IO根本不够用而且布线混乱容易出时序问题。SPI模式只需要4根线——MOSI、MISO、SCK、CS正好匹配ESP32的VSPI或HSPI外设。虽然SPI读8个通道是逐个读出但AD7606内部是同时采样保持的读出顺序不破坏同步性。蓝牙SPP的透传速率本身就在几十Kbps量级SPI时钟跑10MHz完全不会成为瓶颈。字节并口是折中方案用两根GPIO切换高低字节但代码里要维护字节顺序状态机复杂度比SPI高一个量级。所以我最终在这套源码里用的是SPI模式这也是目前课程设计中最好解释的方案。接线时注意AD7606的DVDD接3.3V还是5V参考电压VREF默认2.5VESP32的SPI电平要匹配必要时加电平转换。2.2 引脚分配和电气连接表以ESP32 DevKitC为例我常用VSPI来挂AD7606引脚分配如下ESP32引脚GPIO编号连接AD7606引脚说明3V33V3DVDD / AVCC供电若用5V注意引脚耐压GNDGNDAGND / DGND共地GPIO1818SCKSPI时钟输入GPIO1919MISOAD7606串行数据输出DOUTGPIO2323MOSI数据输入AD7606可不用但需拉高或接GNDGPIO55CS片选低有效GPIO2727CONVST AB采样启动信号GPIO2626BUSY转换状态输出GPIO2525RESET复位低无效GPIO3333OS0/OS1/OS2过采样倍率接GND即可连接时别漏BUSY。BUSY是AD7606转换完成的标志采集程序中需要等它从高变低再读数据。很多新手在SPI模式下忽略BUSY直接延时等结果短则采数错乱长则数据漂移。更稳妥的做法是把BUSY接到ESP32的中断引脚用下降沿触发通知读取但源码里为了简化采用轮询。2.3 ESP32侧的SPI初始化代码与参数说明AD7606配置为SPI从机ESP32主机。初始化时先配置SPI总线再添加设备。打开main.c可以看到这样一段初始化代码#include driver/spi_master.h #include driver/gpio.h #define AD7606_SCK 18 #define AD7606_MISO 19 #define AD7606_MOSI 23 #define AD7606_CS 5 #define AD7606_CONVST 27 #define AD7606_BUSY 26 #define AD7606_RESET 25 spi_device_handle_t ad7606_spi; void AD7606_Init(void) { // CONVST、RESET和BUSY先作为普通GPIO控制 gpio_config_t io_conf { .pin_bit_mask (1ULLAD7606_CONVST) | (1ULLAD7606_RESET), .mode GPIO_MODE_OUTPUT, .pull_up_en GPIO_PULLUP_DISABLE, .pull_down_en GPIO_PULLDOWN_DISABLE, }; gpio_config(io_conf); gpio_set_level(AD7606_RESET, 1); // SPI总线初始化 spi_bus_config_t buscfg { .mosi_io_num AD7606_MOSI, .miso_io_num AD7606_MISO, .sclk_io_num AD7606_SCK, .quadwp_io_num -1, .quadhd_io_num -1, .max_transfer_sz 128, }; spi_bus_initialize(VSPI_HOST, buscfg, SPI_DMA_CH_AUTO); // 添加SPI设备AD7606的时序要求CPOL1, CPHA1 spi_device_interface_config_t devcfg { .mode 3, .clock_speed_hz 10 * 1000 * 1000, .spics_io_num AD7606_CS, .queue_size 8, .flags SPI_DEVICE_HALFDUPLEX, }; spi_bus_add_device(VSPI_HOST, devcfg, ad7606_spi); }这里把SPI配置成mode3即CPOL1、CPHA1。AD7606数据手册上给出的串行读取时序是CS拉低后SCK空闲为高数据在SCK上升沿移位输出所以模式3是对的。很多参考代码用模式0也能读但边界时序会紧低温或长走线时偶尔读错。max_transfer_sz设为128后续一次读4个字节够用SPI_DEVICE_HALFDUPLEX表示只有MISO方向有效因为AD7606不需要接收数据。2.4 复位与过采样配置AD7606上电后建议做一次复位脉冲让内部状态机回到确定位置。过采样倍率由OS0~OS2三个引脚决定常用组合为全接地表示2倍不对OS引脚全低表示关闭过采样也就是1倍。如果要做平均滤波可以把OS引脚接到GPIO控制。源码里默认拉低直接读原始转换值。void AD7606_Reset(void) { gpio_set_level(AD7606_RESET, 0); vTaskDelay(pdMS_TO_TICKS(1)); gpio_set_level(AD7606_RESET, 1); }RESET低电平有效复位脉冲宽度最少25ns这里延时1ms纯粹是惯性操作安全。注意复位后要等100us左右再启动第一次转换手册里有上电稳定时间。3. 8通道同步采样读取从BUSY判定到SPI读时序3.1 CONVST触发同步采样AD7606的CONVST A和CONVST B可以分别触发前4通道和后4通道但通常把两个引脚短接后共同触发8通道同步采样。ESP32在需要采样的时刻拉高CONVST然后拉低AD7606内部会同时保持8个通道的模拟输入值并开始逐次逼近转换。随后BUSY引脚拉高转换结束后BUSY自动拉低。读取时机很关键。BUSY为高时不能发起SPI读操作否则数据可能没有更新完毕。正确流程是启动转换→等待BUSY上升沿→等待BUSY下降沿→CS拉低→连续读4字节×8通道。注意有些例程等待下降沿时用死循环这在RTOS里会让其他任务饿死建议加超时。uint8_t ad7606_rd_data[32]; // 8通道 * 4字节 void AD7606_StartConv(void) { gpio_set_level(AD7606_CONVST, 1); gpio_set_level(AD7606_CONVST, 0); } int AD7606_WaitBusy(uint32_t timeout_ms) { uint32_t cnt 0; while (gpio_get_level(AD7606_BUSY) 1) { vTaskDelay(pdMS_TO_TICKS(1)); if (cnt timeout_ms) return -1; } return 0; } void AD7606_ReadChannels(void) { spi_transaction_t t { .rxlength 32 * 8, // 256位 .rx_buffer ad7606_rd_data, .length 0, // 主机只收不发 }; gpio_set_level(AD7606_CS, 0); spi_device_transmit(ad7606_spi, t); gpio_set_level(AD7606_CS, 1); }AD7606_ReadChannels里的rxlength是接收数据总长度但ESP32的SPI驱动中rxlength单位是位所以32字节要乘以8。由于是半双工length可以设为0主机不发数据只产生时钟。实际执行中CS拉低时序由驱动内部完成代码里手动拉低反而会和驱动冲突让我改成由spics_io_num自动控制更稳。若你看到源码中手动操作CS建议改成devcfg.spics_io_num控制。3.2 解析16位有符号原始值AD7606输出的是二进制补码形式每通道数据是两个字节。SPI读回的数据格式是数据线DOUT在SCK的第一个上升沿输出15位最高位D15后续依次输出到D0共16位。因为ESP32接收时按字节排列需要把每两个字节拼成一个int16_tint16_t ad7606_raw[8]; void AD7606_DataConvert(void) { for (int ch 0; ch 8; ch) { uint8_t h ad7606_rd_data[ch*2]; uint8_t l ad7606_rd_data[ch*21]; ad7606_raw[ch] (int16_t)((h 8) | l); } }这里用int16_t而不是uint16_t因为AD7606的±5V或±10V输入范围映射到-32768~32767。如果是单极性输入模式则需要用无符号处理。换算成真实电压的公式为电压 ad7606_raw * 量程 / 32768。默认2.5V基准时±5V量程下1LSB对应152.6uV。3.3 连续采集模式与定时器联动很多诊断对象不是一次采样就行比如电网谐波需要连续采集N个周期。我在这套源码里做了两种采集模式单次触发和定时循环。定时模式下用ESP32的硬件定时器产生CONVST脉冲在定时器回调中只置电平读取放在低优先级任务里避免中断里阻塞。hw_timer_t *adc_timer NULL; void IRAM_ATTR onTimer() { gpio_set_level(AD7606_CONVST, 1); gpio_set_level(AD7606_CONVST, 0); } void AD7606_StartTimer(uint32_t freq_hz) { adc_timer timerBegin(0, 80, true); // 1MHz计数 timerAttachInterrupt(adc_timer, onTimer, true); timerAlarmWrite(adc_timer, 1000000 / freq_hz, true); timerAlarmEnable(adc_timer); }timerBegin的第二个参数是分频系数80分频后计数频率为1MHz第三个参数true表示向上计数。timerAlarmWrite里的间隔传的是计数值所以1000000/freq_hz对应微秒数。比如要2kHz采样间隔就是500us。注意中断函数加了IRAM_ATTR因为ESP32的定时器中断处理代码必须放在IRAM中否则在Flash读取时会崩溃。定时器回调里不做SPI读取只触发CONVST这个设计避免在中断中占用SPI总线导致蓝牙任务卡死。4. 蓝牙SPP协议栈初始化与数据帧透传4.1 经典蓝牙SPP与BLE的选择ESP32同时支持经典蓝牙和低功耗BLE。SPPSerial Port Profile基于RFCOMM使用经典蓝牙手机端可用蓝牙串口助手直接看到数据无需单独写App。BLE需要自定义GATT服务还得在手机端开发或找特定的调试软件。毕设答辩时现场演示SPP连接速度更快兼容性也更好。所以这套源码用的是经典蓝牙SPP而非BLE。项目正文里出现的那些静态库比如libbt.a、libdriver.a、libwpa_supplicant.a其实是ESP-IDF编译后生成的组件库。其中libbt.a就是蓝牙栈的实现链接时嵌入可执行文件。源码工程通过CMake把这些库组织起来生成factory.bin烧录镜像。4.2 SPP初始化流程与连接回调使用蓝牙SPP需要三步初始化底层控制器与协议栈、配置可发现可连接、注册SPP事件回调。注意必须先做esp_bt_controller_init再esp_bluedroid_init顺序反了会直接复位。#include esp_bt.h #include esp_bt_main.h #include esp_spp_api.h void BT_SPP_Init(void) { esp_bt_controller_config_t bt_cfg BT_CONTROLLER_INIT_CONFIG_DEFAULT(); esp_bt_controller_init(bt_cfg); esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT); esp_bluedroid_init(); esp_bluedroid_enable(); esp_spp_register_callback(spp_callback); esp_spp_init(ESP_SPP_MODE_CB); }ESP_SPP_MODE_CB表示使用回调模式事件如ESP_SPP_OPEN_EVT、ESP_SPP_DATA_IND_EVT都会通过esp_spp_cb_t返回。设备名和可被发现设置如下esp_bt_dev_set_device_name(ESP32_AD7606); esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);扫描模式设置为可连接和可发现这样手机才能搜到设备。ESP_BT_GENERAL_DISCOVERABLE会让设备持续处于可被发现状态直到蓝牙栈关闭。注意默认设备名是“ESP32”多个设备同时调试时容易连错改成带有项目特征的名字会省事很多。4.3 发送采集数据的帧格式与实现SPP底层是按数据流处理的没有自带报错和分包机制。为了对端能识别通道数据我在传输前加了一个定长帧头格式如下字节偏移内容说明0-10xAA 0x55帧同步头2通道数1当前帧包含的通道数3-4帧序号从0递增用于丢帧检测5-368通道×int16原始ADC值37CRC8低8位异或校验总共38字节每采样周期发送一帧。接收端拿到后先找同步头再解析通道数最后校验CRC。发送函数如下uint8_t spp_frame[38]; uint16_t frame_seq 0; void AD7606_SendSppFrame(void) { spp_frame[0] 0xAA; spp_frame[1] 0x55; spp_frame[2] 8; spp_frame[3] (uint8_t)(frame_seq 8); spp_frame[4] (uint8_t)(frame_seq 0xFF); for (int i 0; i 8; i) { spp_frame[5 i*2] (uint8_t)(ad7606_raw[i] 8); spp_frame[6 i*2] (uint8_t)(ad7606_raw[i] 0xFF); } uint8_t crc 0; for (int i 0; i 37; i) crc ^ spp_frame[i]; spp_frame[37] crc; esp_spp_write(spp_conn_handle, sizeof(spp_frame), spp_frame); }esp_spp_write不会立即发送而是排队进入蓝牙协议栈缓冲区。采样率超过2kHz时每秒要写2000×3876KB数据经典蓝牙SPP实际有效吞吐率通常在1Mbps即125KB/s附近理论上够用但多个任务同时写会产生队列堆积。所以源码中在采样循环里加了信号量当上一次发送未完成时直接丢弃当前帧保证实时性优先。4.4 接收手机下发的控制命令SPP是双向的可以接收手机端发来的命令比如切换量程、启动停止采集。在SPP事件回调中处理ESP_SPP_DATA_IND_EVTvoid spp_callback(esp_spp_cb_event_t event, esp_spp_cb_param_t *param) { if (event ESP_SPP_DATA_IND_EVT) { uint8_t data[64]; int len param-data_ind.len; memcpy(data, param-data_ind.data, len); if (data[0] 0x01) AD7606_StartTimer(2000); else if (data[0] 0x02) AD7606_StopTimer(); } }param-data_ind.data只指向蓝牙协议栈内部的接收缓冲区不能在回调外使用。正确做法是复制到自己的缓冲区或者直接在回调内解析。回调执行时间越短越好不要在回调里调用esp_spp_write去回复大量数据否则会造成收包超时。5. 从烧录到调试总线时序、蓝牙连接与吞吐量优化5.1 用逻辑分析仪验证SPI时序拿到源码先别急着连蓝牙把SPI部分调通再说。用逻辑分析仪挂在SCK、CS、MISO上触发条件设为CS下降沿。抓包后重点看两个信号CS低电平时SCK是否有8个连续脉冲簇以及MISO在第一字节的最高位是否有数据跳变。AD7606正常工作时MISO会在CS拉低后约半个时钟周期开始输出有效位。常见问题SCK空闲时是低电平说明SPI mode配成了0改为3即可只有第一个通道有数据后面全是0多半是连续读出时CS没有在整个32字节读取期间保持低电平检查spics_io_num是否被其他GPIO干扰。5.2 蓝牙连上又立刻断开的原因连接不稳定大概率与电源有关。ESP32和AD7606同时工作时DVDD从3.3V取电通信电流峰值能达到300mA如果USB线质量差或供电能力不足蓝牙发射瞬间电压跌落超过200mV协议栈就会自动断开连接。解决办法是在VBUS和GND之间加一个470uF电解电容同时确保蓝牙天线附近没有金属材质遮挡。另一个容易忽视的点是经典蓝牙和Wi-Fi共存问题ESP32的双频天线分时复用Wi-Fi开启后蓝牙吞吐会明显下降。测试SPP时最好关闭Wi-Fi或者把Wi-Fi配置为只在空闲时扫描。5.3 采样率与SPP吞吐量的匹配计算要计算最大可持续采样率需要同时考虑AD7606转换时间和SPP发送带宽。AD7606在5V参考下最大转换速率200kSPS单次转换时间约4usSPI读32字节耗时约30us所以读取本身不是瓶颈。瓶颈在蓝牙发送队列。假设SPP实际吞吐量为80KB/s每帧38字节那么每秒最多发送2105帧即采样率上限约2kHz。源码默认配置是1kHz采样留有余量。如果想把采样率提到5kHz需要优化发送策略合并多个采样点写一次比如把5帧合成一个大包发送减少RFCOMM协议开销。实测效果明显但接收端解析逻辑要改成循环切数据。5.4 手机端快速验证命令推荐用“蓝牙串口助手”App连接后发送HEX格式的01启动采集02停止。观察接收区是否出现AA55开头的帧。如果收到AA55后数据静止不动检查CONVST定时器是否启动如果数据帧连续但数值全部为0量程设置或模拟输入接地了。我通常会把8个通道分别接不同电压比如通道0接3V通道1接电位器这样能快速判断通道顺序是否错位。最后给一个排查清单先跑AD7606自检模式把RANGE引脚拉高看8个通道是否读回相同的满量程码再关掉AD7606接蓝牙用定时器发送固定字节测SPP链路可靠性最后两者合起来跑完整工程。分步验证能让问题边界清晰比一次性联调好debug得多。本文还有配套的精品资源点击获取