1. 项目缘起:当双通道同步采样成为刚需
在嵌入式信号采集领域,我们常常会遇到一个看似简单、实则棘手的需求:如何让两个独立的ADC(模数转换器)实现真正意义上的同步采样?这里的“同步”,指的是两个ADC的采样时钟边沿在时间上完全对齐,并且转换启动信号也严格同步,从而确保采集到的两路信号在时间轴上具有可比性。这个需求在电力系统谐波分析、电机控制、振动监测、声学阵列等场景下几乎是刚需。
最近我在一个工业振动监测项目中就遇到了这个问题。项目需要同时采集来自两个正交安装的加速度传感器的信号,用于分析设备的振动模态。传感器用的是AD9220,这是一款经典的12位、20MSPS高速ADC。最初,我尝试用MCU的两个SPI接口分别去控制两片AD9220,用软件触发启动转换。结果发现,由于软件指令执行存在微秒级的抖动,以及SPI总线调度延迟,两个通道的采样时刻根本无法对齐,导致后续的相位分析完全失真。
问题的核心在于,AD9220的采样由外部时钟(CLK)驱动,而转换结果的读取则依赖于MCU提供的读时钟(通常由SPI的SCK模拟)。要实现同步,必须确保两片ADC的CLK是同源、同相的,并且启动转换的时机要精确可控。经过一番选型和评估,我最终选用了AT32F403A这款MCU,并决定利用其高级定时器(TMR)的输出比较功能来产生高精度的同步时钟,再结合DMA(直接存储器访问)来无延迟地搬运数据,从而彻底解决同步难题。这套方案不仅精度高,而且几乎不占用CPU资源,下面我就把具体的实现思路、关键配置和踩过的坑分享给大家。
2. 硬件架构与核心芯片选型解析
在动手写代码之前,理清硬件连接和芯片特性是成功的第一步。这个方案的核心是让MCU的一个硬件资源同时驱动两片ADC,并高效地取回数据。
2.1 主角登场:AT32F403A与AD9220
我选择的MCU是雅特力科技的AT32F403A,它基于ARM Cortex-M4内核,主频高达240MHz。我之所以看中它,除了性能强劲外,更关键的是其丰富且灵活的高级定时器(TMR)和DMA控制器。特别是TMR1/TMR8,支持多种输出模式,可以非常方便地生成指定频率和占空比的时钟信号。
传感器端的ADC是ADI公司的AD9220。这是一款单通道、12位分辨率、最高20MSPS采样率的逐次逼近型(SAR)ADC。它有几个关键引脚需要我们重点关注:
- CLK (Pin 1):转换时钟输入。ADC在此时钟的上升沿启动采样与转换。这是实现同步的生命线,必须由MCU提供精准、同步的时钟。
- OTR (Pin 21):输出数据就绪信号(低电平有效)。当一次转换完成,数据锁存到输出寄存器后,此引脚会拉低,提示外部控制器可以读取数据了。
- D11-D0 (Pin 9-20):12位并行数据输出总线。
- SENSE (Pin 28):模式选择。接地表示选择并行输出模式。
AD9220的工作流程很简单:在CLK上升沿,它开始对模拟输入进行采样并启动转换;经过固定的转换时间后,数据准备好,OTR引脚变低;外部控制器需要在下一个CLK上升沿到来之前,将数据读走。
2.2 同步采样的硬件连接图
要让两片AD9220(以下简称ADC1和ADC2)同步,硬件上必须确保它们的CLK来自同一个源,并且相位一致。最直接的方法就是用MCU的一个引脚同时驱动两片ADC的CLK。但这里有个细节:ADC的CLK输入有电容负载,直接并联到同一个GPIO可能会造成时钟边沿变缓,在高速时(比如10MHz以上)可能导致时序问题。稳妥的做法是,使用一个缓冲器(如74HC125)来驱动,或者确保MCU的GPIO驱动能力足够强(AT32的GPIO驱动能力不错,在10MHz以内直接驱动两片ADC通常没问题)。
我的连接方案如下:
- 同步时钟生成:使用AT32的TMR1通道1(PA8引脚)配置为PWM输出模式1,产生一个固定频率(例如5MHz)的方波。这个信号同时连接到两片AD9220的CLK引脚。
- 数据就绪信号连接:两片ADC的OTR引脚分别连接到MCU的两个外部中断引脚(例如ADC1_OTR -> PE0, ADC2_OTR -> PE1)。OTR的下跳沿将作为触发DMA传输的“请求源”。
- 数据总线连接:这是最占用引脚的部分。每片AD9220有12位并行数据线,两片就是24位。我选择使用MCU的GPIO端口A和端口B的高低位来分别承接。例如:
- ADC1_D[11:0] -> PA[11:0]
- ADC2_D[11:0] -> PB[11:0] 这样连接,当同时读取GPIOA和GPIOB的输入数据寄存器时,就能一次性获取两片ADC的共24位数据(实际上各12位),效率极高。
- DMA数据流:配置两个DMA数据流(或通道),分别服务于端口A和端口B的数据搬运。它们的触发源就分别对应上面两个OTR引脚产生的外部中断事件。
注意:在PCB布局时,连接到同一片ADC数据总线的GPIO引脚最好属于MCU的同一个端口,并且物理上连续。这样在使用DMA从
GPIOx->IDR(输入数据寄存器)搬运数据时,效率最高,也避免了位拼接的麻烦。如果不得不使用不连续的引脚,则需要通过软件进行位操作,会稍微复杂一些。
3. 核心引擎:TMR输出时钟的精确配置
同步的基石是一个精准、稳定的公共时钟源。AT32的高级定时器(TMR1/8)是完成这项任务的绝佳选择。
3.1 定时器工作模式选择与参数计算
我们的目标是用TMR1的通道1输出一个5MHz的方波时钟。为什么是5MHz?因为AD9220在5MHz时钟下,采样率就是5MSPS,对于很多中频振动信号已经足够。当然,你可以根据需求调整,但不要超过ADC的最高额定时钟频率(AD9220是20MHz)。
TMR输出时钟的本质是一个PWM波。我们需要计算三个关键参数:定时器时钟源频率(TMR_CLK)、自动重装载值(ARR)和比较捕获值(CCR)。
首先,需要确定TMR1的输入时钟。AT32F403A的APB2总线时钟(APB2_CLK)在系统时钟为240MHz时,通常是120MHz。TMR1挂载在APB2上,当APB预分频系数不为1时,定时器时钟是APB时钟的2倍,因此TMR1_CLK = 240 MHz。
接下来计算ARR和CCR值,以产生5MHz方波(占空比50%):
- 期望的PWM频率
PWM_Freq = 5 MHz - 定时器计数频率
TMR_CLK = 240 MHz - 则计数周期
Period = TMR_CLK / PWM_Freq = 240 / 5 = 48 - 在向上计数模式下,
ARR = Period - 1 = 47 - 对于50%占空比,
CCR = (ARR + 1) / 2 = 24
因此,配置TMR1->ARR = 47,TMR1->CCR1 = 24。
3.2 关键寄存器配置与代码实现
配置TMR1输出PWM,需要操作以下几个关键寄存器:
// 1. 使能TMR1时钟 RCC->APB2ENR |= RCC_APB2ENR_TMR1EN; // 2. 配置时基单元 TMR1->PSC = 0; // 预分频器为0,即不分频 TMR1->ARR = 47; // 自动重装载值,决定PWM频率 TMR1->CNT = 0; // 计数器清零 // 3. 配置通道1为PWM模式1输出 // 输出比较模式:PWM模式1 TMR1->CCMR1 |= (0b110 << TMR_CCMR_OC1M_Pos); // 输出极性:高电平有效 TMR1->CCER |= TMR_CCER_CC1E; // 设置比较值,决定占空比 TMR1->CCR1 = 24; // 4. 使能自动重装载预装载(ARPE)和主输出(MOE,针对高级定时器) TMR1->CCMR1 |= TMR_CCMR_OC1PE; TMR1->BDTR |= TMR_BDTR_MOE; // 高级定时器必须开启主输出 // 5. 启动定时器 TMR1->CR1 |= TMR_CR1_CEN;配置完成后,用示波器测量PA8引脚,应该能看到一个非常干净的5MHz、3.3V方波。这个信号将作为两片AD9220的“心跳”。
实操心得:务必使用示波器验证输出时钟的质量。检查频率是否准确、幅值是否达到标准逻辑电平(3.3V)、上升/下降沿是否陡峭(一般应小于10ns)。不理想的时钟边沿是导致ADC采样误差甚至失败的常见原因。如果边沿过缓,可以考虑降低时钟频率,或者如前所述,增加一个时钟缓冲芯片。
4. 数据搬运管家:DMA请求的巧妙触发
时钟同步解决了采样时刻对齐的问题,接下来要解决数据搬运的实时性问题。如果使用CPU轮询或中断来读取24位并行数据,在5MSPS的速率下,CPU根本忙不过来,且会引入不可控的延迟。DMA是唯一的解决方案。
但如何触发DMA传输呢?我们需要一个与数据就绪严格同步的信号。AD9220的OTR引脚正是为此而生。
4.1 将外部中断映射为DMA请求源
AT32的DMA控制器功能强大,其请求源不仅可以来自外设(如SPI、UART的TX/RX),还可以来自外部中断线(EXTI)。这正是我们需要的:将ADC的OTR引脚下降沿产生的外部中断事件,作为DMA传输的触发信号。
配置步骤如下:
- 配置GPIO为输入:将连接OTR的PE0和PE1配置为浮空输入(或上拉输入,取决于OTR的驱动能力)。
- 配置外部中断(EXTI):将PE0和PE1连接到EXTI0和EXTI1中断线,并配置为下降沿触发。
- 关键一步:配置EXTI事件映射到DMA:这不是标准库函数直接提供的,需要操作特定寄存器。在AT32中,EXTI的
EMR(事件屏蔽寄存器)和RTSR(上升沿触发选择寄存器)/FTSR(下降沿触发选择寄存器)共同决定了一个事件是否产生。当EXTI事件产生时,它可以触发中断,也可以触发事件。DMA请求关联的是事件,而非中断。 我们需要使能对应EXTI线的事件功能,并选择下降沿。// 使能EXTI0线的事件功能,下降沿触发 EXTI->EMR |= EXTI_EMR_EM0; EXTI->FTSR |= EXTI_FTSR_TR0; // 同理配置EXTI1 EXTI->EMR |= EXTI_EMR_EM1; EXTI->FTSR |= EXTI_FTSR_TR1; - 配置DMA流:我们需要两个DMA流(例如DMA1_Stream0和Stream1)。
- 外设地址:对于从GPIO读数,外设地址就是GPIO端口输入数据寄存器的地址,如
(uint32_t)&GPIOA->IDR和(uint32_t)&GPIOB->IDR。 - 存储器地址:指向我们定义的两个内存缓冲区
adc1_buffer[]和adc2_buffer[]。 - 数据宽度:外设和存储器都设置为半字(16位),因为GPIOx_IDR是32位寄存器,但我们只需要低16位(实际用了12位)。设置16位可以一次性搬运。
- 传输模式:外设到存储器。
- 循环模式:使能,这样当缓冲区填满后,DMA会自动回到开头重新开始,实现连续采集。
- 外设增量:禁止(外设地址固定为GPIOA_IDR)。
- 存储器增量:使能(每次传输后,存储器地址指向下一个数组元素)。
- 触发源:这是核心!需要将DMA流的触发源配置为“EXTI线0”和“EXTI线1”。在AT32的DMA请求映射表中,可以查到EXTI线对应的请求编号。例如,EXTI0可能映射到DMA请求
DMA_REQUEST_EXTI0。你需要查阅具体的《参考手册》来确认。
// 假设DMA_REQUEST_EXTI0的值为x DMA1_Stream0->CR &= ~DMA_SxCR_CHSEL; // 先清除通道选择 DMA1_Stream0->CR |= (x << DMA_SxCR_CHSEL_Pos); // 设置触发源为EXTI0 // 使能DMA流 DMA1_Stream0->CR |= DMA_SxCR_EN; - 外设地址:对于从GPIO读数,外设地址就是GPIO端口输入数据寄存器的地址,如
4.2 双缓冲与数据对齐技巧
为了确保数据搬运不丢失,并且方便后续处理,我采用了“双缓冲”机制和位对齐操作。
双缓冲:我定义了两个大的缓冲区,例如每个2048点。DMA配置为循环模式,当它填满半个缓冲区(1024点)时,产生一个半传输完成中断(HTIE),填满整个缓冲区时产生传输完成中断(TCIE)。在中断服务程序里,我只需切换指针,去处理已经填满的那半个缓冲区,而DMA继续向另外半个缓冲区写入数据。这样实现了采集与处理的并行,避免了数据覆盖。
数据对齐:AD9220输出12位数据,我们按16位(半字)搬运。数据存储在GPIOx_IDR的低12位。在后续处理时,需要将读取到的16位数据与0x0FFF进行“与”操作,以屏蔽掉无用的高4位。
uint16_t raw_data = adc1_buffer[i] & 0x0FFF; // 获取有效的12位ADC值对于两片ADC的数据,由于是同步采集的,adc1_buffer[i]和adc2_buffer[i]就是同一时刻两路信号的采样值,保证了严格的同步性。
5. 系统集成与软件流程设计
硬件和驱动配置好后,需要一套清晰的软件流程来管理整个采集系统。
5.1 初始化序列
系统的初始化必须遵循一定的顺序,否则可能导致时钟或DMA工作异常:
- 系统时钟初始化:配置系统时钟、AHB、APB1、APB2总线时钟。
- GPIO初始化:
- 配置TMR1_CH1(PA8)为复用推挽输出。
- 配置PA0-PA11、PB0-PB11为浮空输入(用于ADC数据总线)。
- 配置PE0、PE1为浮空输入(用于OTR信号)。
- TMR1初始化:按第3章配置,产生5MHz PWM时钟。先不要启动(CEN=0)。
- EXTI初始化:配置EXTI0和EXTI1为事件模式,下降沿触发。先不要使能。
- DMA初始化:配置两个DMA流,设置好外设地址(GPIOA_IDR, GPIOB_IDR)、内存地址、数据长度、循环模式等。最关键的是设置正确的触发请求源(EXTI0/1)。初始化完成后使能DMA流(EN=1),此时DMA处于“等待触发”状态。
- 缓冲区与变量初始化:清零ADC数据缓冲区,初始化缓冲区索引、状态标志等。
- 启动序列:这是一个关键步骤,顺序错了会导致首批数据错误。
- 第一步:启动TMR1(
TMR1->CR1 |= TMR_CR1_CEN;)。此时同步时钟开始输出,但ADC尚未开始转换(因为需要第一个CLK上升沿)。 - 第二步:短暂延时(至少一个时钟周期),确保时钟稳定。
- 第三步:使能EXTI事件(
EXTI->EMR |= ...)。一旦使能,下一个OTR下降沿就会触发DMA请求。 - 此时,系统就进入了全自动采集状态。ADC在时钟驱动下开始转换,转换完成则OTR变低触发EXTI事件,EXTI事件立即触发DMA请求,DMA将GPIO端口的数据搬运到内存。整个过程无需CPU干预。
- 第一步:启动TMR1(
5.2 主循环与数据处理
主循环变得非常简洁,CPU被解放出来进行更复杂的信号处理:
while(1) { // 检查DMA半传输/传输完成标志 if(dma_half_buffer_ready_flag) { process_buffer(half_buffer_index); // 处理前半部分或后半部分缓冲区 dma_half_buffer_ready_flag = 0; // 可以在此进行FFT、滤波、特征提取等算法 } // 其他系统任务,如通信、状态监测等 system_idle_task(); }6. 实测调试与常见问题排查
理论很美好,但调试过程总会遇到各种问题。以下是我在实现过程中遇到的一些典型问题及解决方法。
6.1 问题一:DMA毫无动静,缓冲区全是0
- 现象:程序运行后,ADC缓冲区始终为0,示波器看OTR引脚有正常跳变。
- 排查思路:
- 检查时钟:首先用示波器确认TMR1输出的时钟是否准确到达两片ADC的CLK引脚。幅值、频率、占空比是否正常。
- 检查OTR信号:用示波器同时测量ADC的OTR引脚和CLK引脚。确认在CLK上升沿之后,经过一段转换时间(查AD9220手册),OTR是否确实产生了下降沿。
- 检查EXTI事件:这是最隐蔽的一环。EXTI事件是否成功产生?一个简单的验证方法是,将EXTI配置为中断模式(而不是事件模式),并在中断服务函数里翻转一个测试引脚。如果测试引脚能随OTR跳变,说明EXTI检测到了信号。如果没反应,检查GPIO输入配置、EXTI线路映射、上下拉电阻配置(OTR是开漏输出,可能需要上拉)。
- 检查DMA触发源映射:确认
DMA_SxCR寄存器中的CHSEL字段是否设置正确,对应到了EXTI线的请求编号。这个编号需要查芯片数据手册的DMA请求映射表,非常容易配错。 - 检查DMA使能顺序:务必在使能EXTI事件之前就使能DMA流。如果顺序反了,第一个OTR事件可能发生在DMA准备好之前,从而被错过。
6.2 问题二:数据错位或跳动严重
- 现象:能采集到数据,但数值不稳定,或者两路信号看起来不同步。
- 排查思路:
- 电源与地噪声:高速ADC对电源极其敏感。务必使用高质量的LDO为模拟部分供电,并在每个ADC的电源引脚附近放置足够容量的去耦电容(如10uF钽电容 + 0.1uF陶瓷电容)。模拟地和数字地单点连接。
- 时钟抖动:用示波器的余辉模式观察TMR输出的时钟,看边沿是否有抖动。如果抖动大,检查MCU的电源,或者尝试降低输出时钟频率。也可以考虑使用定时器的“重复计数器”功能来产生更稳定的时钟。
- 数据建立保持时间:虽然我们用了DMA,但从OTR下降沿到DMA实际读取GPIO数据,仍有极短的延迟。需要确保这个延迟不会让ADC的数据输出发生变化。AD9220的数据在OTR变低后会保持稳定,直到下一个CLK上升沿。只要我们的DMA在下一个CLK上升沿前完成读取即可。在5MHz时钟(周期200ns)下,这个时间窗口是充裕的。但如果时钟频率很高(如20MHz),就需要仔细评估DMA响应时间和GPIO读取时间。
- 缓冲区溢出:如果DMA搬运速度跟不上ADC产出数据的速度,会导致缓冲区被覆盖,数据错乱。计算一下:5MSPS,每个数据16位(2字节),那么数据率是10MB/s。AT32的DMA在系统总线不拥堵的情况下,搬运一个16位数据通常只需1-2个AHB时钟周期(约4-8ns),远小于采样间隔200ns,所以带宽完全足够。但如果CPU频繁访问被DMA使用的内存区域(尤其是通过Cache),可能会与DMA争抢总线带宽,造成偶尔的延迟。可以考虑将ADC缓冲区放在DTCM(如果芯片有)或者非Cacheable的内存区域。
6.3 问题三:如何验证同步精度?
这是项目的终极验证。一个有效的方法是:
- 输入同源信号:将同一个正弦波信号通过一个运放缓冲后,同时输入到两片AD9220。
- 采集数据:运行程序,采集一段数据。
- 计算互相关:对采集到的两路数字信号序列计算互相关函数。如果完全同步,互相关函数的峰值应该出现在零延迟点。如果存在固定的时间偏差,峰值会偏移,偏移的样本点数乘以采样间隔就是同步误差。
- 观察结果:在我的实测中,使用此方案,两路信号的同步误差在±1个采样时钟周期(200ns)以内,这主要取决于两片ADC芯片自身CLK到输出延迟(tOD)的一致性,以及PCB布线等长带来的微小差异,已经达到了硬件所能实现的极限精度。
通过这套基于AT32 TMR输出时钟和DMA外部触发请求的方案,我成功实现了两片AD9220的高精度、高实时性同步采样。整个系统运行稳定,CPU占用率极低,为后续复杂的实时信号处理算法留出了充足的计算资源。这个设计模式具有很强的通用性,可以扩展到更多片ADC的同步,或者与其他需要精密时钟同步的外设配合使用。