简介面向DSP嵌入式开发者这份源码包提供了基于TMS320F28335与AD7606高精度ADC的完整采样程序可解决多通道同步采集、SPI高速传输与中断实时读取等工程实现难题。包内仅3个文件以inscode源码工程、html说明文档和gitignore配置为主压缩包大小9KB结构紧凑且便于直接检视。资源覆盖硬件连接配置中的引脚映射与外设初始化、SPI通信模块的初始化与数据读取函数以及BUSY信号触发的中断服务程序其中SPI读写函数针对时序与片选做了细致处理BUSY中断则保障转换完成后及时读取结果。主程序框架清晰展示系统初始化与转换启动流程并附有关键寄存器配置说明、调试技巧、性能优化方案和典型调试问题解决方法。开发者可据此快速搭建AD7606数据采集链路或借助DSP28335的片上外设中断写法排查采样数据跳变、时序不稳定或SPI通信异常等问题。目前已有92人学习对入门同步采样设计及后续二次开发具有较高参考价值。1. DSP28335与AD7606采样程序为什么这对组合总出现在电压电流采集项目里DSP28335 配 AD7606 的采样程序几乎是电力电子、电能质量这类项目跑不掉的一套组合DSP 负责浮点运算和闭环控制AD7606 负责把 8 路电压电流同时落到 16bit 数字量。很多人照着例程改完却发现波形不对、通道错位、采样率上不去问题大多不在控制算法而在 ADC 驱动本身。这篇文章从硬件接线讲到驱动源码再讲到 DMA 和避坑目标是让你拿到思路后能在自己的板子上把 8 通道数据稳定读回来并且知道每一条配置背后的理由。我假定你已经跑通了 F28335 的 GPIO 和 SCI 基础例程因为采样程序里的引脚操作、寄存器写法都建立在熟悉开发环境的前提上。如果还没点亮过 LED建议先花半天把工程模板和烧录流程跑通否则后面查问题会分不清是代码问题还是环境问题。2. AD7606 与 F28335 接线接口模式、引脚分配与上电时序2.1 接口模式怎么选并行、字节并行还是串行AD7606 对外提供三种数字接口16 位并行、8 位字节并行、串行。很多第一次用的人默认选并行因为读起来直接、速度快但代价是占掉 F28335 一大片 GPIO还要处理数据总线方向。如果你做的是三相电压电流采样DSP 还要留引脚给 PWM、编码器、通信并口往往不现实。我一般用串行模式AD7606 的 SDO 一根数据线接到 F28335 的 SPISOMI 或普通 GPIO加上 SCLK、CS、BUSY、CONVST、RESET、RANGE、OS 引脚总共不超过 10 根线对引脚紧张的控制板非常友好。串行模式下 16 位数据逐位移出速度上限取决于 SCLK对电力电子采样场景来说完全够用。并行、字节并行、串行三种模式的选型可以对号入座接口模式占用引脚数读取速度适合场景16 位并行约 20 根最快单次读 16bit高速故障录波、多板同步8 位字节并行约 12 根中等分两次读 16bit不想用串行但 GPIO 略紧张串行约 8 根受 SCLK 限制约 200kSPS 上限大部分控制板、信号调理板常用方案串行模式还有一点好处F28335 的 SPI 模块字长最大支持 16 位刚好和 AD7606 的单通道输出宽度对齐不需要拼字节。后面第 4 章讲硬件 SPI 时你会有体会。2.2 串行模式接线表与供电参考电压设计先给一组我常用的引脚分配F28335 的 GPIO 大多可以复用你按自己板子调整即可关键是方向别搞反。F28335 引脚AD7606 引脚方向说明GPIO0CONVST_A / CONVST_B 短接输出启动转换上升沿有效GPIO12BUSY输入转换中为高下降沿表示数据就绪GPIO1RESET输出低电平复位脉冲GPIO2CS输出片选低有效GPIO3SCLK输出移位时钟GPIO4DOUT输入串行数据输出GPIO5FRSTDATA输入第一通道数据开始输出时变高GPIO6RANGE输出低 ±5V高 ±10VGPIO7/G8/G9OS0/OS1/OS2输出过采样倍率配置供电部分要特别注意两路电源AVCC 接 5VVDRIVE 接 3.3V。VDRIVE 是数字接口电平参考接 3.3V 就是为了和 F28335 的 GPIO 直接对接不用加电平转换芯片。REFIN/REFOUT 引脚上电后量一下正常应该是 2.5V 左右这是片内基准的输出悬空或短路都会让采样值异常。「注意」AD7606 的 PARALLEL/SER/BYTE 三个引脚决定接口模式。串行模式下要把 SER 拉高、PARALLEL 拉低如果这两个引脚悬空器件可能进入未知模式读回来的数据全是乱的。硬件上最好用电阻固定电平不要依赖软件初始化。2.3 复位与初始化的关键时序AD7606 上电后建议先做一次复位让内部状态机回到已知位置。RESET 引脚最小低脉冲宽度手册给的是纳秒级但程序里给足余量不会有坏处。下面是我的初始化代码放在工程初始化阶段调用。#define AD_CONVST GpioDataRegs.GPADAT.bit.GPIO0 #define AD_RESET GpioDataRegs.GPADAT.bit.GPIO1 #define AD_CS GpioDataRegs.GPADAT.bit.GPIO2 #define AD_SCLK GpioDataRegs.GPADAT.bit.GPIO3 #define AD_RANGE GpioDataRegs.GPADAT.bit.GPIO6 #define AD_OS0 GpioDataRegs.GPADAT.bit.GPIO7 #define AD_OS1 GpioDataRegs.GPADAT.bit.GPIO8 #define AD_OS2 GpioDataRegs.GPADAT.bit.GPIO9 #define AD_BUSY GpioDataRegs.GPADAT.bit.GPIO12 #define AD_DOUT GpioDataRegs.GPADAT.bit.GPIO4 #define AD_FRSTDATA GpioDataRegs.GPADAT.bit.GPIO5 void AD7606_Init(void) { EALLOW; // GPIO0/1/2/3/6/7/8/9 配置为输出 GpioCtrlRegs.GPAMUX1.all 0x0000; GpioCtrlRegs.GPADIR.all | 0x03CF; // GPIO12/4/5 配置为输入 GpioCtrlRegs.GPADIR.all ~0x1030; EDIS; // 复位脉冲低电平保持 100us AD_RESET 0; DELAY_US(100); AD_RESET 1; DELAY_US(10); AD_RESET 0; // 默认 ±5V 量程无过采样 AD_RANGE 0; AD_OS0 0; AD_OS1 0; AD_OS2 0; // 片选拉高时钟拉低等待第一次转换 AD_CS 1; AD_SCLK 0; }代码里先把 GPIO 复用寄存器全部清零再按位设置方向避免默认复用功能把引脚抢走。GPADIR 的写法用了掩码输出引脚和输入引脚分开处理改引脚分配时不会误伤其他外设。RESET 脉冲给 100us 是为了兼容不同板子上的电源稳定时间实际 AD7606 要求远小于这个值。「注意」RESET 拉高后建议等至少 10us 再开始配置 RANGE 和 OS 引脚因为器件从复位释放到内部逻辑稳定需要一点时间。如果你在复位后立刻启动转换偶尔会出现第一帧数据异常这就是复位等待不够的典型表现。3. 驱动源码拆解用 GPIO 模拟 SPI 把 8 通道数据读回来3.1 为什么优先 GPIO 模拟而不是 DSP 的 SPI 外设F28335 的 SPI 外设确实能接 AD7606但有两个问题让新手容易翻车时钟极性和相位一旦配错读回来的数据是反的或是错位的另外硬件 SPI 需要把发送缓冲和接收缓冲的关系理解清楚才能持续产生时钟。GPIO 模拟没有这些黑匣子时序自己控制出问题用示波器一量就知道是哪一步不对所以调试阶段我建议先用 GPIO 模拟跑通数据链路。代价是速度。GPIO 模拟每条 SCLK 周期需要几条指令来翻转电平和读取数据16 位 × 8 通道一共 128 个时钟周期整体读一次大约要几十微秒。这决定了 GPIO 模拟适合采样率在几千赫兹以下的场景比如温度、压力、慢速电量监测。如果你的目标是几百 kSPS直接跳到第 4 章用硬件 SPI。3.2 读取流程与核心代码转换和读取是两个动作CONVST 引脚上升沿启动一次采样AD7606 内部 8 个 ADC 同时采样并开始转换BUSY 拉高转换完成后 BUSY 拉低数据锁存在输出寄存器里等待外部读取。读取时 CS 拉低SCLK 逐位把数据从 DOUT 移出先移出的是通道 1 的 16 位数据然后是通道 2直到通道 8。下面这段代码是核心读取函数我把它放在定时器中断或主循环里调用。Uint16 AD7606_ReadOneChannel(void) { Uint16 val 0; Uint16 i; AD_CS 0; // 拉低片选开始一次数据输出 AD_SCLK 0; for (i 0; i 16; i) { AD_SCLK 1; DELAY_US(1); val (val 1) | AD_DOUT; // SCLK 高电平期间采样 DOUT AD_SCLK 0; DELAY_US(1); } AD_CS 1; // 拉高片选结束本次数据输出 return val; } void AD7606_ReadAllChannels(Uint16 *buf) { Uint16 i; // 等待 BUSY 下降沿表示转换完成 while (AD_BUSY 1); // 可选等待 FRSTDATA 变高确保当前输出从通道 1 开始 // 如果只用单芯片且读取紧跟 BUSY 下降沿可不查这个引脚 // while (AD_FRSTDATA 0); for (i 0; i 8; i) { buf[i] AD7606_ReadOneChannel(); } }逻辑说明ReadOneChannel 函数一次读取 16 位高位在前正好对应 AD7606 串行输出的 MSB-first 格式。循环里先拉高 SCLK再读 DOUT这个顺序很关键因为器件的输出数据在时钟边沿后稳定高电平期间读取是安全的。如果你用示波器看到数据有规律地错位把采样点改到 SCLK 下降沿之后再读通常是极性判断反了。参数说明DELAY_US(1) 是我在 150MHz 主频下调试用的保守值一条延时大约 1us8 通道读一次约 128us对应采样率大约 7kSPS。如果只采集 50Hz 工频信号这个速度绰绰有余想提高速度就逐步减小延时但减到 0 之前要确认 DOUT 在 SCLK 高电平期间已经稳定否则会出现随机跳变。3.3 原始码值到电压的换算AD7606 输出的是二进制补码±5V 量程下满量程对应 ±32768所以 1LSB 5V / 32768 ≈ 0.1526mV。换算逻辑如下。#define ADC_FULL_SCALE 32768.0f void AD7606_ConvertToVoltage(Uint16 *adc_buf, float *volt_buf) { Uint16 i; int16_t raw; for (i 0; i 8; i) { raw (int16_t)adc_buf[i]; // 转换成有符号数 volt_buf[i] (float)raw * 5.0f / ADC_FULL_SCALE; // ±5V 量程 } }注意强制转换成 int16_t 这一步不能省否则 0xFFFF 会被当成 65535 而不是 -1负半周电压会算成接近满量程的正值。这个坑我第一次调的时候踩过波形看起来像整流桥输出追了半天才发现是符号扩展没做。4. 采样不占 CPUSPI 硬件DMA 双缓冲的关键配置4.1 用硬件 SPI 替换 GPIO 模拟GPIO 模拟读一个通道要手动翻转 16 次 SCLK硬件 SPI 则在写发送缓冲时自动产生时钟程序只需要往 SPITXBUF 写一个任意值同时从 SPIRXBUF 读到 AD7606 的串行数据。F28335 的 SPI 有几项配置直接决定和 AD7606 能不能正常通信字长要设 16 位、时钟极性要匹配、主从模式要对。下面这段初始化代码我标注了每项的作用。void AD7606_SpiInit(void) { EALLOW; // 关掉 SPI 模块才能改配置 SpiaRegs.SPICTL.bit.SPIINTENA 0; // 字长 16 位时钟空闲为高时钟极性选择 SpiaRegs.SPICCR.all 0x003F; // SPICHAR16, CLKPOL1 // 主机模式相位选择 SpiaRegs.SPICTL.all 0x000E; // MASTER1, CLK_PHASE1 // 波特率 LSPCLK / (SPIBRR 1)按实际外设时钟调整 SpiaRegs.SPIBRR 0x0007; // 使能 SPI 模块和主模式 SpiaRegs.SPICCR.bit.SPISWRESET 1; SpiaRegs.SPICTL.bit.MASTER 1; SpiaRegs.SPICTL.bit.SPIINTENA 1; EDIS; }时钟极性和相位的组合是这节最容易翻车的点。AD7606 的串行数据在 SCLK 边沿更新主机最好在另一个边沿采样。我给的配置是时钟空闲高、相位 1配合 DOUT 在下降沿更新、上升沿采样这是我在某块模拟项目 X 采集板上验证过的组合。如果你换了时钟极性发现通道顺序不对或数据镜像优先检查这里的 CLKPOL 和 CLK_PHASE。4.2 BUSY 中断触发读取中等采样率下的均衡方案硬件 SPI 之后读取 8 个通道变成了 8 次 SPI 读写每次大约 1.6us10MHz SCLK比 GPIO 模拟快了近一个数量级。但主循环里做这件事依然会阻塞控制算法。我一般把转换完成信号做成中断BUSY 下降沿触发外部中断在中断里一次性读完 8 个通道。Uint16 adc_buf[8]; volatile Uint16 adc_ready 0; interrupt void XINT1_ISR(void) { Uint16 i; // 等待 BUSY 确认下降沿避免中断标志误触发 while (AD_BUSY 1); AD_CS 0; for (i 0; i 8; i) { SpiaRegs.SPITXBUF 0x0000; // 写任意值产生 SCLK while (SpiaRegs.SPICTL.bit.BUSY 1); // 等待传输完成 adc_buf[i] SpiaRegs.SPIRXBUF; } AD_CS 1; adc_ready 1; }中断读取的优点是采样时刻由硬件边沿决定不受主循环抖动影响对电压电流相位计算特别重要。中断服务程序里读 SPI 是同步方式虽然占用 CPU但总时长可控在采样率不超过 20kSPS 时 CPU 占用率可以接受。4.3 DMA 双缓冲把数据搬运彻底交给硬件如果采样率要推到 AD7606 的 200kSPS 上限中断里同步读 SPI 会占用大量 CPU这时候需要 DMA 把 SPI 接收数据直接搬到内存。F28335 的 DMA 和外设之间可以通过外设事件触发传输SPI 接收 FIFO 有数据时就能触发一次 DMA 搬运。DMA 方案的核心是两个通道配合一个通道把虚拟数据写到 SPITXBUF用来产生 SCLK另一个通道把 SPIRXBUF 的内容搬到内存缓冲区。下面这段是关键的 DMA 通道配置片段。extern Uint16 adc_buf[8]; extern Uint16 spi_tx_dummy[8]; // 全 0 的数组用于产生时钟 void AD7606_DmaConfig(void) { EALLOW; // DMA 通道 1源地址指向虚拟数据目的地址指向 SPI 发送缓冲 DmaRegs.CH1.SRC_BEG_ADDR (Uint32)spi_tx_dummy; DmaRegs.CH1.SRC_END_ADDR (Uint32)(spi_tx_dummy 7); DmaRegs.CH1.DST_BEG_ADDR (Uint32)SpiaRegs.SPITXBUF; DmaRegs.CH1.DST_END_ADDR (Uint32)SpiaRegs.SPITXBUF; DmaRegs.CH1.BURST_SIZE 7; // 每次触发搬运 8 个字0~7 DmaRegs.CH1.TRANSFER_SIZE 0; // 单次突发 DmaRegs.CH1.SRC_BURST_STEP 1; DmaRegs.CH1.DST_BURST_STEP 0; // 目的地址固定指向同一寄存器 // DMA 通道 2源地址指向 SPI 接收缓冲目的地址指向采样数组 DmaRegs.CH2.SRC_BEG_ADDR (Uint32)SpiaRegs.SPIRXBUF; DmaRegs.CH2.SRC_END_ADDR (Uint32)SpiaRegs.SPIRXBUF; DmaRegs.CH2.DST_BEG_ADDR (Uint32)adc_buf; DmaRegs.CH2.DST_END_ADDR (Uint32)(adc_buf 7); DmaRegs.CH2.BURST_SIZE 7; DmaRegs.CH2.TRANSFER_SIZE 0; DmaRegs.CH2.SRC_BURST_STEP 0; DmaRegs.CH2.DST_BURST_STEP 1; // 触发源选择外设事件具体位定义参考 DSP2833x 头文件 DmaRegs.CH1.MODE 0x0002; // 外设事件触发 DmaRegs.CH2.MODE 0x0002; EDIS; }逻辑说明DMA 通道 1 把 8 个虚拟 0 数据依次写到 SPITXBUF每写一次 SPI 就自动产生 16 个 SCLK 周期AD7606 移出 16 位数据到 SPIRXBUFDMA 通道 2 在 SPI 接收事件触发下把 SPIRXBUF 搬到 adc_buf。两个通道的突发长度都是 8正好对应 8 个通道。「注意」F28335 DMA 的寄存器布局在不同版本的库头文件里字段名略有差异上面代码是配置思路实际编译时以你工程里的 DmaRegs 结构体定义为准。DMA 方案调试难度明显高于中断读取我第一次调通花了整整一个下午后来发现是触发源选择位写错了导致 DMA 从不启动。如果你时间紧先用 4.2 的中断方案把功能跑通再考虑 DMA。5. AD7606 采样避坑记录数据错位、钳位与采样率超限5.1 通道数据错位读回来的第一个通道不是通道 1现象把 8 个通道接上不同电压读取结果发现通道顺序整体旋转通道 1 的电压出现在通道 3 或通道 5 的位置。原因读取时机没对齐。BUSY 下降沿之后数据虽然锁存但如果你在下降沿后等了一段时间才拉低 CSAD7606 可能已经开始输出或输出了部分通道更常见的是没接 FRSTDATA用多片级联或总线共享时失去帧同步参考。解决单芯片应用下严格保证 BUSY 下降沿后立刻读取CS 拉低后连续读完 8 个通道中间不要拉高 CS。如果芯片布局导致 BUSY 到 DSP 的走线太长、下降沿被延迟把 FRSTDATA 也读出来做判断读取前等待 FRSTDATA 拉高再开始。5.2 数据一直钳在满量程附近量程配置和实际输入不匹配现象输入信号明明只有 2V读回来的数据却接近 0x7FFF 或 0x8000而且波形削顶。原因RANGE 引脚配置成了 ±5V但输入信号是 ±10V 范围或者反过来信号太小而量程太大导致分辨率不足。另一种可能是输入前端有运放偏置把信号抬到了超出量程的范围。解决确认 RANGE 引脚电平低电平对应 ±5V高电平对应 ±10V。用信号发生器给一个已知直流电压对比换算值是否线性。如果确认是硬件偏置问题先处理信号调理电路不要靠软件缩放掩盖否则小信号段误差会很大。5.3 采样率设到 300kSPS 但波形毛刺严重现象程序里用定时器触发 CONVST触发周期对应 300kSPS结果波形全是毛刺数据连续性差。原因AD7606 最大吞吐率是 200kSPS超限后转换时间不够BUSY 还没下降你就发下一个 CONVST 上升沿内部状态机被打乱。另外如果开了过采样上限还要再除以过采样倍率。解决先把过采样倍率基线算清楚。无过采样时最大 200kSPS过采样 4 倍时最大 50kSPS过采样 16 倍时最大 12.5kSPS。触发周期必须大于转换时间与读取时间之和给余量。下表是常用配置下的吞吐率上限OS[2:0]过采样倍率最大吞吐率0001x200kSPS0012x100kSPS0104x50kSPS0118x25kSPS10016x12.5kSPS10132x6.25kSPS11064x3.125kSPS5.4 复位后读回全零接口模式引脚没固定现象程序下载后第一次读取全零手动复位一次后恢复。原因AD7606 的 PARALLEL/SER/BYTE 引脚悬空上电时状态不确定器件进入了并行模式而不是串行模式。串行模式下数据从 DOUT 输出并行模式下数据总线是高阻F28335 自然读到全零。解决硬件上把 SER 用上拉电阻固定为高PARALLEL 用下拉电阻固定为低BYTE 任意固定。这样即使软件初始化再乱器件模式也不会漂移。5.5 GPIO 模拟读数据时偶发跳变采样点落在数据翻转边上现象8 通道里某个通道的数据偶尔跳变跳变幅度是 1 或 2 个 LSB频率随机示波器看 DOUT 波形正常。原因GPIO 模拟时 SCLK 延时太短读取 DOUT 的时刻刚好落在数据翻转边沿附近。AD7606 串行输出在 SCLK 边沿后有一段建立时间读早了数据还没稳定。解决在拉高 SCLK 和读取 DOUT 之间插入几个 NOP 或 DELAY_US(0.1)把采样点往数据稳定的窗口里推。如果用了硬件 SPI把 SPI 时钟从最大降一半试试通常能消除这类随机误码。6. 进阶用法动态量程与 SCI 实时上传采样值6.1 过采样与量程动态切换什么时候改 RANGE 才安全AD7606 的 RANGE 引脚在转换过程中不能随意切换只能在一次转换结束到下一次 CONVST 上升沿之间的空闲窗口里改。我习惯把量程切换封装成一个函数并在调用处保证当前 BUSY 为低。void AD7606_SetRange(Uint16 range) { if (range 1) { return; } // 确保当前没有转换正在进行 while (AD_BUSY 1); AD_RANGE range; // 0 ±5V, 1 ±10V DELAY_US(10); // 等待引脚电平稳定 }动态切换量程后电压换算系数要同步切换否则数据会偏离真实值。±5V 时 1LSB 约为 0.1526mV±10V 时约为 0.3052mV换算代码里建议用一个全局变量保存当前量程换算时查表。6.2 SCI 帧格式与发送代码把 8 通道数据送往上位机调试采样程序最直接的验证方式是把数据发到上位机绘图。F28335 的 SCI 模块配置为 115200bps、8 位数据位我这里自定义一个简单帧格式帧头 2 字节、长度 1 字节、16 字节数据、2 字节校验和共 21 字节。void SCI_SendByte(Uint8 byte) { while (SciaRegs.SCICTL1.bit.TXRDY 0); SciaRegs.SCITXBUF byte; } void SCI_SendAdcFrame(Uint16 *adc_buf) { Uint16 i; Uint16 sum 0; SCI_SendByte(0xA5); SCI_SendByte(0x5A); SCI_SendByte(0x08); // 数据长度 8 通道 for (i 0; i 8; i) { SCI_SendByte((adc_buf[i] 8) 0xFF); SCI_SendByte(adc_buf[i] 0xFF); sum adc_buf[i]; } SCI_SendByte((sum 8) 0xFF); SCI_SendByte(sum 0xFF); }发送时先发高字节再发低字节上位机按同样顺序拼接。校验和是简单的 16 位累加能挡住大部分串口传输错包。SCI 属于慢速外设115200bps 下 21 字节约耗时 1.8ms对应最大上传频率约 500Hz用来验证低频信号完全够用。6.3 验证时序的三个习惯示波器比打印更有用我最后想分享一个排查采样的习惯先用示波器量 SCLK、CS、BUSY 三根线的波形再决定改代码还是改硬件。很多时候程序看着没问题实际是 CS 时序不对或 SCLK 毛刺太多这些只有示波器能量到。验证新板子我通常按三步走第一步BUSY 和 CS 分别接示波器观察转换周期是否稳定有没有频率抖动第二步用信号发生器给通道 1 输入 1kHz 正弦波把数据发到上位机看波形是否干净第三步把 RANGE 切换功能打开切换到 ±10V 量程再测一遍满幅信号。三步都通过这套采样程序才算真正落地。动态量程和 SCI 上传是两个独立的增强功能我建议顺序上先做 SCI 再动量程切换因为上位机波形能直观反映量程切换是否正确。整个方案做到这里你手里已经有了一套能读、能传、能换量程的 AD7606F28335 采样基础后面无论是做故障录波还是闭环控制都能直接往上叠。希望帮到你。初次接触这套组合时我也在通道顺序和时钟极性上反复折腾过走通之后回看大部分问题都出在没把时序当成一个整体来看。按文章顺序搭一次你会少吃很多没必要的亏。本文还有配套的精品资源点击获取