简介:面向嵌入式开发者的DSP28335与AD7606采样程序源码包,聚焦多通道同步采样场景,适用于电力监测、工业数据采集等对实时性和精度要求较高的项目。整个工程围绕完整采样链路提供精简代码,从硬件引脚映射与外设初始化开始,实现SPI通信模块的初始化与数据读取函数,并利用BUSY信号编写中断服务程序以确保采样触发实时性,主程序框架理顺了系统初始化与转换启动流程,结构清晰、易于移植。压缩包共3个文件,包含1个inscode工程代码文件、1个HTML说明页面和1个gitignore版本管理配置,整体仅9KB,轻量实用;目前已有92人学习。此外,资源还给出了关键参数配置说明、调试技巧和性能优化方案,并针对典型调试问题提供了解决思路,能够协助开发者快速搭建稳定的DSP28335+AD7606采样系统。
1. DSP28335与AD7606采样程序:从12位到16位,外挂采样的正确打开方式
做电力电子调试的人多半会遇到一个尴尬:DSP28335片内ADC是12位,采样保持通道虽然多,但实际精度和抗干扰能力在大电压大电流现场经常不够用——同一段母线电压,用内部ADC采出来和用精密万用表差出几十个LSB是常有的事。与其在片内ADC上反复滤波和校准,不如外挂一块16位、8通道同步采样的AD7606。这套采样程序正是为这个组合准备的:DSP28335做主控,AD7606做模拟前端,并行读取、中断触发、DMA搬运都有现成代码可以抄。适合做逆变器、电机驱动、电能质量监测这类需要同时抓多路波形的人,也适合刚把28335从点灯进阶到真实采集的新手。
2. 接口选型与初始化:XINTF总线映射与上电时序的细节账
2.1 三种数据接口怎么选:并行、串行、字节模式的真实代价
AD7606提供三种读取接口:16位并行、8位字节并行和串行SPI。很多人一上来就选SPI,因为引脚少、接线简单。低速采集场景下SPI确实够用,但放到DSP28335上要想清楚:串行模式下,8通道全部读出需要至少8次SPI事务,每次16位,还要加上帧同步和各通道切换的空闲时间,很难跑满200kSPS。而DSP28335自带16位外部总线,走并行几乎不占额外资源,反而是最顺手的一种接法。
字节模式是8位单片机时代的产物,通常是为了省GPIO才用的。DSP28335的外部数据总线本来就是16位,把16位数据拆成两次8位读,等于多一次握手、多一倍总线占用。所以这套程序直接用16位并行模式,这是最简单也最能逼近芯片指标的一种接法。
实际画PCB时,并行模式的代价是占掉16根数据线和五六根控制线。如果你的板子CPU资源够、GPIO紧张,可以把数据线直接连到DSP的XD0~XD15上,控制线单独引GPIO,这样读取时用XINTF地址访问,配置时用GPIO控制,两不耽误。
2.2 引脚连接与XINTF映射:把AD7606映射成一个外部内存地址
接线是这套方案的核心。AD7606的数据线DB0~DB15直接接到DSP28335的XD0~XD15,CS接XZCS6n,RD接XRD,这样AD7606就出现在DSP的Zone6地址空间里。完整接线对照如下:
| AD7606引脚 | DSP28335引脚 | 电平方向 | 说明 |
|---|---|---|---|
| DB0~DB15 | XD0~XD15 | 双向 | 16位并行数据总线 |
| CS | XZCS6n | 输出 | Zone6片选,选通一次读周期 |
| RD | XRD | 输出 | 外部读信号,下降沿锁存数据 |
| RESET | GPIO某脚 | 输出 | 上电复位,必须给出低→高脉冲 |
| CONVST | GPIO某脚 | 输出 | 上升沿启动一次同步转换 |
| BUSY | GPIO某脚 | 输入 | 转换期间为高,结束后拉低 |
| FRSTDATA | GPIO某脚 | 输入 | 第一通道数据有效时拉高 |
| RANGE | GPIO某脚 | 输出 | 选择±5V还是±10V量程 |
| OS0~OS2 | GPIO三脚 | 输出 | 设置过采样倍率 |
把CS接在XZCS6n上之后,程序里读写地址0x100000就会自动产生完整读时序,RD的下降沿、建立时间和跟踪时间全由XINTF控制器管理,不需要再用GPIO模拟波形。
这里有个容易想糊涂的点:AD7606不是按地址选通道的。数据手册的并行读时序是“每给一个RD下降沿,芯片内部把当前通道的数据放到总线上,指针自动切到下一通道,连续给8个RD脉冲就把8个通道读完了”。所以在DSP端,8次读操作访问的是同一个地址,而不是8个不同的地址。那些把XA0~XA2接过去想“一次选一个通道”的人,往往会发现通道数据完全对不上。
// 读取一个通道的原始数据 uint16_t adc_read_raw(void) { // volatile告诉编译器:每次访问都是真实的外部总线事务 return *(volatile uint16_t *)0x100000; }这里的关键在volatile关键字。如果不加,编译器在开启优化后可能把同一地址的多次读取合并成一次,最终读到的只有第一通道数据。CCS工程里-O2优化档位最容易踩这个坑。
2.3 初始化时序:复位、量程与过采样配置代码
AD7606最容易被忽略的是初始化顺序。数据手册要求上电后必须给RESET一个低脉冲,等芯片内部状态机稳定后再开始配置。实际项目里我会把低脉冲宽度放宽到10us,宁可慢一点,也不让它带着不确定状态进入首次转换。
复位之后,量程和过采样这两个静态配置也要在此时一次性设好。RANGE拉高选±5V,拉低选±10V;OS0~OS2的组合决定过采样倍率。首次调试建议把过采样先关掉,拿到的是一档未经数字滤波的原始数据,方便对照示波器找问题。
#define ADC7606_BASE 0x100000 // Zone6基地址 void AD7606_Reset(void) { GPIO_SetPin(AD7606_RESET, 0); // RESET拉低 DELAY_US(10); // 满足最小脉冲宽度 GPIO_SetPin(AD7606_RESET, 1); // 释放复位 DELAY_US(20); // 等待内部逻辑稳定 } void AD7606_Config(int os0, int os1, int os2, int range) { // 过采样组合:000=1倍,001=2倍,010=4倍,011=8倍,100=16倍 GPIO_SetPin(AD7606_OS0, os0); GPIO_SetPin(AD7606_OS1, os1); GPIO_SetPin(AD7606_OS2, os2); // 量程:1代表±5V,0代表±10V GPIO_SetPin(AD7606_RANGE, range); // 等待配置稳定 DELAY_US(5); }除了这些,XINTF本身的时序也要一起配好。XINTF总线时钟一般设150MHz,一个周期约6.7ns。AD7606的RD低电平脉宽要求最少约20ns,但总线负载一重,20ns很容易变成临界值,我把读激活时间设成8个周期,约53ns,留出两倍以上余量,读取稳定性会明显改善。
// XINTF Zone6时序配置实例 void Init_XINTF_Zone6(void) { // 读时序:建立2周期,激活8周期,跟踪2周期 XintfRegs.XTIMING6.bit.XRDLEAD = 2; XintfRegs.XTIMING6.bit.XRDACTIVE = 8; XintfRegs.XTIMING6.bit.XRDTRAIL = 2; // 写时序:如果不用XINTF写功能,可以保持和读一致 XintfRegs.XTIMING6.bit.XWRLEAD = 2; XintfRegs.XTIMING6.bit.XWRACTIVE = 8; XintfRegs.XTIMING6.bit.XWRTRAIL = 2; // 数据总线宽度:1代表16位 XintfRegs.XTIMING6.bit.XSIZE = 1; }初始化顺序有个经验:先配XINTF时序,再做AD7606复位和配置,最后才允许主循环里调用转换函数。如果反过来,第一次启动转换时XINTF还没来得及稳定,BUSY下降沿后读回来的数据很可能是一堆乱码。
3. 读取与缓存:从查询等待到DMA双缓冲的完整链路
3.1 BUSY下降沿与外部读时序:软件查询和中断两种读取方式
AD7606启动转换的入口很简单:给CONVST一个上升沿,8个通道同时开始采样,芯片内部自行完成采样保持和ADC转换,整个过程中BUSY引脚保持高电平。转换完成后BUSY拉低,这时才能去读取数据。
最简单的读取方式是查询BUSY:
uint16_t g_adc_buf[8]; // 全局缓冲区 void AD7606_ReadAll(void) { // 启动一次同步转换 GPIO_SetPin(AD7606_CONVST, 0); DELAY_US(1); GPIO_SetPin(AD7606_CONVST, 1); DELAY_US(1); GPIO_SetPin(AD7606_CONVST, 0); // 等待BUSY下降沿,转换完成 while (GPIO_GetPin(AD7606_BUSY) == 1); // 连续读取8个通道,源地址固定 for (int i = 0; i < 8; i++) { g_adc_buf[i] = *(volatile uint16_t *)ADC7606_BASE; } }这段代码的耗时大头在最后的8次读取,每次XINTF读周期按前面的配置约53ns,8次总耗时不超过0.5us。真正拖后腿的是while(BUSY)这一句,它在转换期间一直占着CPU。如果主循环里还有SCI通信、PWM占空比更新、上位机协议解析等任务,查询方式会挤掉这些任务的时间片。
更合适的做法是把启动和等待拆开:需要采样时先发CONVST,然后去做别的事,等BUSY下降沿触发外部中断,在中断里读取数据。这样等待转换的时间CPU是空闲的,可用于处理其他实时任务。
interrupt void XINT1_ISR(void) { // 把8通道数据从总线搬到全局数组 for (int i = 0; i < 8; i++) { g_adc_buf[i] = *(volatile uint16_t *)ADC7606_BASE; } // 置位新数据标志,主循环轮询这个标志 g_adc_ready = 1; // 清除中断标志,允许下一次触发 XintRegs.XINT1CR.bit.INTFLG = 1; }用中断方式有一个前提:BUSY引脚必须接到带外部中断功能的GPIO上,通常是XINT1~XINT7对应的那几个复用脚。画原理图时就要把这个对应关系定好,而不是上电之后靠改程序解决。如果板子已经做错了,中断方案只能退化成查询方案,采样率上限会打折扣。
3.2 数据拼接与通道映射:8个通道与FRSTDATA的正确用法
每次读回来的16位原始码,要明确它的格式。AD7606输出的是双极性二进制补码,正满量程对应0x7FFF,负满量程对应0x8000,中间0对应0V。不是在16位里只取低12位或低8位,所有16位都有效,读出来是什么就存什么。
读取8通道时,每触发一次RD,总线数据自动切到下一通道,顺序固定为V1~V8。因此缓冲区数组的索引天然对应通道号,不需要额外查表映射。
FRSTDATA这个引脚我一般会接上,但不是每次都读它。它只在两个场景里有价值:一是读取过程中被高优先级中断打断,重新调度后你不知道当前总线上的通道号;二是芯片内部状态出问题,指针漂了,需要靠FRSTDATA重新对齐。如果每次转换后一口气读满8次、中间没有外部打断,FRSTDATA不参与逻辑也完全没问题。
把原始码转成实际电压的代码:
float adc_to_voltage(uint16_t raw, int range_5v) { // ±5V量程时1LSB约152.6uV,±10V量程时1LSB约305.2uV float lsb_mv = range_5v ? 0.1526f : 0.3052f; // 二进制补码转为有符号数 int32_t code = (raw & 0x8000) ? (int32_t)raw - 0x10000 : (int32_t)raw; return code * lsb_mv; // 返回电压值,单位mV }这段转换逻辑里最容易出错的是LSB数值。±5V量程对应10V全跨度除以65536,得152.6uV左右;±10V量程对应20V除以65536,得305.2uV左右。如果你把量程搞错,采样出来的电压会差一倍,而且往往不容易发现——低频正弦波形状完全一样,只有幅值不对。
3.3 双缓冲与DMA搬运:采样任务里不要让CPU一直忙
在开关电源或电机控制场景中,数据更新周期只有几十微秒,如果每个周期都靠CPU中断去读8个通道,系统压力很大。更稳的做法是用DMA把数据从XINTF地址直接搬到内存缓冲区,CPU只在缓冲区切换时介入一次。
DMA方案的核心配置是把源地址固定指到0x100000,目标地址指向RAM缓冲区,每次触发搬运8个字。触发源选外部中断,也就是BUSY下降沿经过XINT映射后触发DMA。
// DMA通道0配置示例 void ADC_DMA_Init(void) { // 源地址固定指向XINTF Zone6 DMA->SRC_BEG_ADDR = ADC7606_BASE; DMA->SRC_ADDR = ADC7606_BASE; // 目标地址指向缓冲区A,搬运后地址递增 DMA->DST_BEG_ADDR = (Uint32)g_adc_buf_a; DMA->DST_ADDR = (Uint32)g_adc_buf_a; // 每次触发搬运8个字,源地址不变,目标地址步进 DMA->BURST_SIZE = 8; DMA->SRC_BURST_STEP = 0; DMA->DST_BURST_STEP = 1; // 触发源:XINT1事件,具体位域写法按CCS头文件配置 // DMA传输完成后触发中断,切换缓冲区指针 }DMA搬运结束后会发出传输完成中断,在中断里只需要把缓冲区指针从A切到B,同时置一个标志位。A/B两个缓冲区交替使用,主循环处理A数据时,DMA在往B填新数据,不会因为读取和计算抢同一个数组而丢帧。
有一个限制要提前知道:DSP28335的DMA触发源不能直接接GPIO,必须通过外部中断XINT或某个片上外设事件中转。所以电路设计时就要把BUSY引到XINT映射的引脚上,否则只能退而求其次用软件触发DMA,效率和查询方式相差不大。
4. 采样程序常见问题排查:四条实测踩坑记录
4.1 现象一:采样值末几位不停跳变,波形毛刺很多
代码逻辑看起来没问题,接线也核对了,但采集回来的正弦波总有毛刺,把波形放大看,低几位每次差好几个码。
原因:AD7606的AVCC和DVCC共用一个电源,模拟电源上混入了数字开关噪声;另外XINTF读周期太短,数据总线翻转时采到了中间电平。
解决:把AVCC和DVCC分开供电,模拟电源回路加磁珠和100nF退耦电容;同时把读激活时间从默认几个周期加长到8个周期。实测把这两个改完后,毛刺能降低一个数量级。
4.2 现象二:通道数据错位,V3显示的是V1的信号
程序连续读8次,但波形显示某个通道的数据对应到了另一个通道的输入信号上,而且错位规律固定。
原因:读取过程被高优先级中断打断,打断点恰好落在两次读操作之间。中断返回来继续读时,AD7606内部通道指针已经往前走了几个通道,读取序列整体错开。
解决:读取代码段里临时关闭可屏蔽中断,或把读取放进一个不可打断的临界区:
unsigned short old_state = int_disable(); for (int i = 0; i < 8; i++) { g_adc_buf[i] = *(volatile uint16_t *)ADC7606_BASE; } int_restore(old_state);如果是DMA方案,不存在这个问题,因为DMA传输不依赖CPU中断响应。
4.3 现象三:上电后偶尔读不到数据,RESET一下又好了
这个故障最磨人——板子正常上电,程序能启动,但偶尔读回来的数据全是0或全是0xFFFF,复位一次就恢复。
原因:上电时序里AD7606和DSP28335的复位节奏没对齐。DSP启动速度快,AD7606还没完成内部上电复位,DSP就已经开始配置引脚、发CONVST,导致AD7606进入不确定状态。
解决:在DSP主程序入口处加一段固定延时,等AD7606完全上电后再执行复位和配置。延时不用太讲究,100ms足够,只消耗启动阶段一次。
void main(void) { // 系统时钟、GPIO初始化... DELAY_MS(100); // 等待AD7606完成上电复位 Init_XINTF_Zone6(); AD7606_Reset(); AD7606_Config(0,0,0,1); // 过采样关闭,量程±5V // 进入主循环... }4.4 现象四:采样率测出来只有4kSPS,离200kSPS差太远
用定时器固定频率触发采样,逻辑上没问题,但示波器上看到实际采样周期比设定值大了几十倍。
原因:定时器中断里塞了太多东西,中断处理函数本身跑不完就重复进入;或者过采样倍率设得太高,转换时间本身就不支持这个采样率。比如64倍过采样下,单片AD7606的实际最高采样率只有几千SPS,想跑200kSPS物理上不成立。
解决:先检查过采样配置,再统计中断服务程序最长执行时间。用一个IO翻转来测:进入中断置高、出中断置低,示波器看高电平宽度。如果中断负载超过20%,就要考虑改DMA方式,或者把数据处理挪到主循环。
5. 采样率调优与验证:把程序跑到极限并确认数据可信
5.1 时间开销到底花在哪:从XINTF读周期倒推采样率上限
要判断一段采样程序能不能逼近200kSPS,先算一笔时间账。单个XINTF读周期包含建立、激活、跟踪三段,总线时钟150MHz时,配置成8周期激活加2周期建立、2周期跟踪,一次读约12个时钟周期,折合80ns左右。8个通道连续读约640ns,加上AD7606的转换时间约4us,理论周期约4.7us,对应采样率约210kSPS。
但实际运行时还有中断响应时间、软件判断和缓冲区切换开销,所以查询方式能稳定跑到的采样率大约180~190kSPS。如果需求只有50kSPS,还能接受;如果非要顶满200kSPS,查询方式就比较勉强,DMA是必选项。
如果发现采样率怎么都上不去,先别急着优化读取逻辑,看一下过采样倍率是否设高了。过采样会让芯片内部累积多次采样再输出一个码,实际吞吐率成倍下降。参考对应关系如下:
| 过采样档位 | 过采样倍率 | 参考采样率上限(8通道) |
|---|---|---|
| 000 | 1x | 约200kSPS |
| 001 | 2x | 约100kSPS |
| 010 | 4x | 约50kSPS |
| 011 | 8x | 约25kSPS |
| 100 | 16x | 约12.5kSPS |
| 101 | 32x | 约6kSPS |
| 110 | 64x | 约3kSPS |
这个表是折算参考值,不同温度、不同基准源下会有浮动,但量级很明确:过采样倍率每翻一倍,采样率上限就大约砍一半。调试初期为了跑速度,先把这档设在000,等波形确认没问题了,再根据噪声需求逐步加过采样。
5.2 测量中断负载:用示波器直接看IO翻转宽度
想知道采样程序占了多少CPU时间,不需要跑性能分析器。在读取函数入口把某脚拉高、出口拉低,示波器看高电平宽度就是这段代码的实际执行时间。
interrupt void XINT1_ISR(void) { GPIO_SetPin(DEBUG_IO, 1); // 波形起点 for (int i = 0; i < 8; i++) { g_adc_buf[i] = *(volatile uint16_t *)ADC7606_BASE; } g_adc_ready = 1; GPIO_SetPin(DEBUG_IO, 0); // 波形终点 XintRegs.XINT1CR.bit.INTFLG = 1; }如果示波器抓出的高电平宽度是2us,而采样率目标是100kSPS,也就是周期10us,那么中断本身占CPU约20%,主循环还剩80%。这个比例算健康。如果高电平宽度超过4us,建议马上改DMA方案,把读取耗时从微秒级压到接近零。
5.3 波形还原验证:50Hz正弦输入下的三个动作
采样程序改完以后,我不会直接上真实电压信号,而是先接一个信号发生器,给V1通道加50Hz、峰值为量程80%左右的正弦波,然后按三步验证。
第一步,用串口把原始码送出来,检查最大值和最小值是否大致对称。±5V量程下,正峰应在0x7FFF附近,负峰应在0x8000附近。第二步,把采集数据导入上位机画波形,看是否平滑正弦、频率和相位是否连续。第三步,把两个通道接同一个信号源,验证同步性:两通道波形应完全重合,相位差不大于一个采样周期。
// 把原始码转成电压数组,方便上位机画图 void adc_formatted_output(uint16_t *raw_buf, float *voltage_buf, int len) { for (int i = 0; i < len; i++) { // 假设量程为±5V,调用转换函数 voltage_buf[i] = adc_to_voltage(raw_buf[i], 1); } }这三个动作做完,基本可以确认采样率和精度两个维度没有明显问题。剩下就是上真实信号测试,那部分和模拟前端设计有关,不属于程序排查范围。
6. 顺手把调试链路补全:串口一帧数据同步波形不是难事
采集程序跑通后,大部分人会被一个问题卡住:怎么把数据弄到电脑上看波形?我习惯的做法是走SCI串口,波特率921600,8通道原始码打包成16字节一帧,加帧头帧尾和校验。这样既方便上位机解析,又不会因为浮点格式转换拖慢采样。
// 串口发送一帧采样数据 void adc_send_frame(void) { uint8_t frame[18]; frame[0] = 0xAA; // 帧头 frame[1] = 0x55; // 帧头,同步字节流 // 8通道原始码,低字节在前 for (int i = 0; i < 8; i++) { frame[2 + i * 2] = (uint8_t)(g_adc_buf[i] & 0xFF); frame[3 + i * 2 + 1] = (uint8_t)(g_adc_buf[i] >> 8); } // 累加和校验,只保留低8位 uint8_t sum = 0; for (int i = 0; i < 18; i++) { sum += frame[i]; } frame[17] = sum; for (int i = 0; i < 18; i++) { scia_send_byte(frame[i]); } }上位机解析时,先找0xAA 0x55的帧头,再从数据段拼回原始码。这套帧格式很朴素,但胜在解析容易、出错能发现。波特率921600按10bit/byte折算,每秒约能传92000字节,18字节一帧约等于每秒5000帧,对应采样率大约能做到60~80kSPS。如果要跑更高采样率,要么把帧里塞更多采样点,要么换USB虚拟串口。
这个串口调试链路我到现在还在用。后来养成的习惯是:每改一次采样参数,先跑一遍50Hz正弦、用串口抓一段波形、量一下频率和幅值偏差,确认没问题再放到真实环境里去接电压电流。这个动作看起来不起眼,却能省掉很多藏在时序毛刺里的玄学问题。希望这次的程序拆解和踩坑记录能帮到你,尤其是刚把AD7606挂上28335总线、还在为数据和波形打架的那部分人。
本文还有配套的精品资源,点击获取