STM32驱动AD7656与AD5676:FSMC并行接口与SPI时序实现
2026/9/12 13:12:48 网站建设 项目流程

简介:这是针对STM32微控制器的AD7656与AD5676驱动源码压缩包,面向需要高精度模拟信号采集与输出的嵌入式开发者。项目基于16位ADC AD7656和16位DAC AD5676,通过SPI/I²C接口与STM32通信,适用于工业控制、医疗设备、电源管理、仪器仪表等场景。资源包仅含1个C文件,大小2KB,代码量精简但覆盖了AD7656的初始化与数据读取、AD5676的输出配置与转换控制等核心函数,便于直接移植或进行二次开发。描述中提到该程序为稳定版本,已经过多项目验证,在可靠性和抗干扰方面有所保障。目前已有1183人学习下载,对于正在调试16位ADC/DAC链路或需要快速完成硬件驱动的开发者来说,这份代码能提供清晰的调用思路和可复用的底层实现。

1. 从 ad7656.c 入手:AD7656 与 AD5676 在 STM32 板卡上的角色分配

拿到这个ad7656.zip时,我第一反应不是去 Keil 里点编译,而是先把板子上 AD7656 的 CONVST 和 BUSY 两根脚找出来。这个资源的核心是两块芯片:AD7656 负责把 6 路模拟信号以 16 位精度同时采进来,AD5676 负责输出 8 路 16 位模拟电压,中间由 STM32 调度。常见应用场景是伺服驱动器的电流环采样、多通道数据采集卡、或者需要同时产生多路校准电压的仪器板。适合的人群是已经在用 STM32 做控制、但对高精度 ADC/DAC 的时序接口还不太熟的工程师。ad7656.c这个文件不是初始化完就结束,真正的坑在于并行读时序里 FSMC 地址线怎么接、BUSY 下降沿怎么响应,以及 AD5676 那 24 位 SPI 帧和 STM32 外设如何对齐。

2. AD7656 接口选型与 STM32 并行时序:为什么把串行放在第二考虑

2.1 先把 AD7656 的三条路看清楚

AD7656 的数据手册里给了三种数字输出方式:并行、串行、菊花链。很多人一看到支持串行,就默认用 STM32 的 SPI 外设去接,这个选择在引脚紧张的板子上合理,但在需要保证采样率和读取稳定性的场合,并行接口往往更值得优先考虑。

并行模式下,SER/PAR 引脚拉低,DB[15:0] 作为 16 位数据总线,A0~A2 三根地址线选择当前要读哪个通道,CS 和 RD 一起控制读时序。一次读周期拿回一个通道的数据,6 个通道就是 6 个连续的读周期,总线效率最高。串行模式下 SCLK 一位一位往外移,6 通道 16 位数据需要 96 个 SCLK 才能全部读回,虽然 AD7656 的串行时钟也能跑到 18MHz 这个级别,但对比并行访问还是多了一层移位开销。菊花链更适合多片 AD7656 级联的场景,单板只放一颗的时候基本用不上,而且菊花链的调试成本比并行高不少,不建议在首个版本里引入。

所以我的建议是:MCU 选型是 STM32F103 这类不带 FSMC 的型号,老老实实走串行;如果用的是 STM32F407 及以上带 FSMC 的型号,并行接口是更稳的路线,读取速度更快,CPU 开销也更低。

2.2 并行模式的接线与 FSMC 时序配置

把 AD7656 挂到 STM32F407 的 FSMC 上,本质上是把 ADC 当作一片外部 SRAM 来读。FSMC 会提供读时序中的片选和读使能信号,我们只需要把数据线和地址线对应接好。

表格:AD7656 与 STM32F407 FSMC 引脚对应关系

AD7656 引脚STM32 引脚说明
DB[15:0]FSMC_D[15:0]16 位数据总线
A0/A1/A2FSMC_A0/A1/A2通道选择地址线
RDFSMC_NOE读使能,低有效
CSFSMC_NE4片选,地址空间 0x6C000000
CONVST_A/B/CTIM8_CH1三个 CONVST 短接后接定时器 PWM
BUSYPA6下降沿触发外部中断
VDRIVE3.3V设置数字接口电平

FSMC 的初始化在 HAL 库里的关键代码段如下:

FSMC_NORSRAM_TimingTypeDef timing = {0}; SRAM_HandleTypeDef hsram; timing.AddressSetupTime = 3; // 地址建立时间,单位 HCLK 周期 timing.DataSetupTime = 5; // 数据建立时间,单位 HCLK 周期 timing.BusTurnAroundDuration = 0; timing.ClockDivision = 0; timing.DataLatency = 0; timing.AccessMode = FSMC_ACCESS_MODE_A; hsram.Instance = FSMC_NORSRAM_DEVICE; hsram.Extended = FSMC_NORSRAM_EXTENDED_DEVICE; hsram.Init.NSBank = FSMC_NORSRAM_BANK4; // NE4 hsram.Init.DataAddressMux = FSMC_DATA_ADDRESS_MUX_DISABLE; hsram.Init.MemoryType = FSMC_MEMORY_TYPE_SRAM; hsram.Init.MemoryDataWidth = FSMC_NORSRAM_MEM_BUS_WIDTH_16; hsram.Init.BurstAccessMode = FSMC_BURST_ACCESS_MODE_DISABLE; hsram.Init.WaitSignalPolarity = FSMC_WAIT_SIGNAL_POLARITY_LOW; hsram.Init.WrapMode = FSMC_WRAP_MODE_DISABLE; hsram.Init.WaitSignalActive = FSMC_WAIT_TIMING_BEFORE_WS; hsram.Init.WriteOperation = FSMC_WRITE_OPERATION_DISABLE; hsram.Init.WaitSignal = FSMC_WAIT_SIGNAL_DISABLE; hsram.Init.ExtendedMode = FSMC_EXTENDED_MODE_DISABLE; hsram.Init.AsynchronousWait = FSMC_ASYNCHRONOUS_WAIT_DISABLE; hsram.Init.WriteBurst = FSMC_WRITE_BURST_DISABLE; HAL_SRAM_Init(&hsram, &timing, NULL);

这里有个非常容易踩的坑:AddressSetupTimeDataSetupTime的单位是 HCLK 周期,不是纳秒。HCLK 在 168MHz 时一个周期约 6ns,AddressSetupTime = 3对应约 18ns,DataSetupTime = 5对应约 30ns。如果把数据手册上的纳秒数值直接填进去,实际时序会被放大一百多倍,读数据会慢得离谱,但程序又不会报错,排查起来很费劲。

注意:FSMC 的时序字段在 HAL 库里的单位是 HCLK 周期。改主频后这些值必须重新换算,否则采样率会跟着变。

2.3 CONVST 触发方式决定了采样率稳定性

AD7656 不是自由运行型 ADC,每次转换必须由 CONVST 引脚上的上升沿启动。启动后 BUSY 拉高,转换结束后 BUSY 自动拉低,这个下降沿就是我们读取数据的时机。

软件触发最简单,GPIO 拉低再拉高就算一次,但 GPIO 翻转的时序会被中断、分支预测影响,产生抖动。数据采集场景里采样间隔抖动会直接变成频谱上的噪声,所以我一般不用软件翻转,而是用定时器 PWM 输出直接接 CONVST 引脚。PWM 由硬件产生,不受 CPU 阻塞影响,触发间隔完全均匀。

下面是一段用 TIM8 输出 50kHz 采样触发信号的初始化代码:

htim8.Instance = TIM8; htim8.Init.Prescaler = 168 - 1; // 168MHz / 168 = 1MHz 计数时钟 htim8.Init.CounterMode = TIM_COUNTERMODE_UP; htim8.Init.Period = 20 - 1; // 1MHz / 20 = 50kHz 采样率 htim8.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim8); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 1; // 1 个计数周期的高电平脉冲 sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim8, &sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim8, TIM_CHANNEL_1);

Prescaler 和 Period 共同决定了采样率:先分频到 1MHz,再计数 20 个周期产生一个上升沿,最终得到 50kHz。AD7656 的转换时间典型值在 2.9us 左右,理论上限能到 250kSPS 以上,50kHz 留了足够裕量。要调采样率只改 Period 就行,不需要动分频系数。Pulse 设为 1 是为了让 CONVST 高电平宽度足够窄,同时保证满足触发要求。

3. ad7656.c 的驱动骨架:FSMC 读取、BUSY 中断与补码换算

3.1 驱动文件里应该有的函数划分

ad7656.c拿到手之后,先别急着往工程里拖,确认里面是否有几类基本函数:初始化、触发转换、读取结果、换算。ADC 驱动最忌讳把所有逻辑堆在一个函数里,初始化归初始化,中断归中断,数据读取与业务换算分开,后面接 PID 或者上位机协议时才不会被采样代码干扰。

一个可用的驱动骨架大致如下:

void AD7656_Init(void); void AD7656_StartConversion(void); void AD7656_ReadAllChannels(uint16_t buf[6]); int16_t AD7656_ToSigned(uint16_t raw);

AD7656_Init负责配置 BUSY 引脚的外部中断、FSMC 外设时钟和 CONVST 触发源。AD7656_StartConversion在软件触发模式下才需要,使用定时器 PWM 触发时直接由硬件完成。AD7656_ReadAllChannels是核心读取函数,放在 BUSY 下降沿中断里调用。AD7656_ToSigned负责把原始码值转换为有符号数,因为 AD7656 的输出是二进制补码格式。

3.2 通道选择与补码换算的实现细节

并行模式下,A0~A2 决定当前 DB 总线上是哪一个通道的数据。FSMC 的 16 位数据宽度下,C 语言uint16_t指针的地址步进是 2 字节,这正好对应 FSMC_A0 的翻转。假设 A0/A1/A2 依次接 FSMC_A0/A1/A2,那么从基地址开始,读第 0 个 uint16_t 是通道 0,第 1 个是通道 1,依此类推。代码写起来非常干净:

static volatile uint16_t * const AD7656_BASE = (volatile uint16_t *)0x6C000000; void AD7656_ReadAllChannels(uint16_t buf[6]) { for (int i = 0; i < 6; i++) { buf[i] = AD7656_BASE[i]; } }

这 6 次连续读取每次都会在总线上产生一次完整的 CS/RD 读周期,读取完成后立即返回。要注意AD7656_BASEvolatile修饰不能去掉,否则编译器可能优化成直接从寄存器里取值,读出来永远是同一个结果,这种 bug 非常隐蔽。总线宽度为 16 位时,指针加 1 对应地址偏移 2,是因为 FSMC 内部会把 HADDR 右移一位后输出到外部地址线,这里不需要手动乘 2,但如果直接操作字节地址指针,就必须自己处理对齐。

AD7656 的转换结果是二进制补码格式,直接把uint16_t当无符号数用会导致零点附近的符号错乱。换算成电压的标准做法是先转int16_t

int16_t AD7656_ToSigned(uint16_t raw) { return (int16_t)raw; } float AD7656_ToVoltage(uint16_t raw, float fullScale) { int16_t signedVal = (int16_t)raw; return (float)signedVal * fullScale / 32768.0f; }

举例来说,量程设为 ±10V 时,fullScale传 10.0f,读回 0x8000 转为int16_t后是 -32768,乘 10 除 32768 得到 -10V;读回 0x0000 就是 0V 附近。这里有个容易搞错的地方:AD7656 的 0x0000 并不一定对应 0V,必须先按补码解释再做线性映射,顺序反了结果就差一个满量程偏移。

3.3 连续采样时最容易出现的竞争条件

用 PWM 自由运行触发 AD7656 时,硬件层面不存在等待问题,但软件读取如果不注意时序,会出现一种典型故障:BUSY 下降沿中断还没执行完,下一次 CONVST 上升沿已经来了,导致正在读的数据其实是 ADC 正在转换中的无效数据。表现就是偶发的跳变值,幅度很大,不像噪声,更像脏数据。

我处理这个问题通常有两个方案。方案一是保证 PWM 周期大于“转换时间 + 6 通道读取时间 + 中断响应时间”,50kHz 采样率下周期 20us,转换 2.9us,读取最多几百纳秒,余量充足。方案二是在 BUSY 中断里读完后,用软件再触发下一次转换,这样采样间隔被固定在中断处理完成之后,逻辑上不会重叠:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == BUSY_PIN) { AD7656_ReadAllChannels(adcBuf); AD7656_StartConversion(); // 软件触发下一轮 } }

AD7656_StartConversion就是 GPIO 拉低再拉高,产生一次 CONVST 上升沿。这个方案把采样率精确控制在“中断处理结束后的下一个时刻”,对后续做 FFT 或同步采样非常友好。代价是采样率不再由定时器单独决定,而是受中断响应时间影响,实际测试时需要用示波器测一下 BUSY 的下降沿间隔是否均匀。

注意:两种触发方式不要同时用。如果定时器 PWM 一直在输出,软件又去拉 CONVST,触发频率会变成两者叠加,采样间隔完全混乱。

4. AD5676 SPI 驱动:24 位帧、LDAC 同步与基准的一个坑

4.1 24 位帧结构拆开看

AD5676 是 16 位 DAC,但它的 SPI 输入移位寄存器是 24 位宽。这 24 位由三段构成:高 4 位是命令位,中间 4 位是通道地址,低 16 位是 DAC 数据。具体到写入 DAC 寄存器这种最常用的操作,命令位通常是0011,通道地址从 0 到 7 对应 DAC_A 到 DAC_H。低 16 位数据是二进制原码格式,0x0000 对应 0V,0xFFFF 对应满量程附近。

位段DB[23:20]DB[19:16]DB[15:0]
含义命令位DAC 通道地址16 位 DAC 数据

这个结构和 STM32 的 SPI 外设放一起看,问题就来了。STM32 的 SPI 可以配置成 8 位或 16 位帧模式,如果图省事直接配成 16 位模式,一次只能发一个 16 位数据帧,命令位和通道地址要不发不进去,要不被当成另一个帧的开头,DAC 输出必然错乱。标准做法是把 SPI 配成 8 位模式,连续发送 3 个字节组成一个完整 24 位帧。

4.2 SPI 的 8 位帧模式与 SYNC 引脚控制

AD5676 的 SYNC 引脚类似于片选,低电平期间 SCK 上每 8 个时钟接收一个字节,24 个 SCK 后把 SYNC 拉高,一个完整帧结束。需要注意的是,SYNC 必须在 SCK 空闲时拉高,否则 DAC 会把下一帧的第一个字节误认为当前帧的剩余部分,造成数据整体错位。

static void AD5676_Write(uint8_t cmd, uint8_t ch, uint16_t data) { uint8_t frame[3]; frame[0] = (cmd << 4) | (ch & 0x0F); // 命令位 + 通道地址 frame[1] = (data >> 8) & 0xFF; // 数据高 8 位 frame[2] = data & 0xFF; // 数据低 8 位 HAL_GPIO_WritePin(SYNC_GPIO_Port, SYNC_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, frame, 3, 10); HAL_GPIO_WritePin(SYNC_GPIO_Port, SYNC_Pin, GPIO_PIN_SET); }

cmd是 4 位命令,ch是通道号 0~7,data是 16 位 DAC 数据。HAL_SPI_Transmit的超时时间给了 10ms,SPI 速度在几 Mbps 时一帧数据只需要几微秒,这个超时只用于异常保护。SYNC_Pin拉低的一瞬间,DAC 开始采样 SCK 上的数据,所以 SYNC 的 GPIO 配置要设成推挽输出且初始电平为高,避免上电瞬间产生假片选。

SPI 的极性和相位配置以数据手册为准,不同 ADI 的 DAC 芯片要求不完全一样。我常用的配置是 CPOL=0、CPHA=1,即 SCK 空闲为低、第二个边沿采样。如果发现 DAC 输出毛刺或者个别通道值不对,先试着把 CPHA 或 CPOL 取反,配合逻辑分析仪观察 SCK 与 DIN 的相对位置,通常能找到原因。

4.3 LDAC 引脚实现多通道同步输出

AD5676 的每一个通道内部都有输入寄存器和 DAC 寄存器。默认情况下,写入的数据先进入输入寄存器,是否立即反映到输出引脚取决于 LDAC 引脚的电平。LDAC 一直拉低时,写入立即生效,简单但对多通道同步更新不利。

需要多个通道同时改变输出电压时,正确做法是先把 LDAC 拉高,依次写入所有要更新的通道,最后给 LDAC 一个低脉冲,让所有通道在同一时刻锁存并输出:

HAL_GPIO_WritePin(LDAC_GPIO_Port, LDAC_Pin, GPIO_PIN_SET); AD5676_Write(0x3, 0, dacVal0); AD5676_Write(0x3, 1, dacVal1); AD5676_Write(0x3, 2, dacVal2); HAL_GPIO_WritePin(LDAC_GPIO_Port, LDAC_Pin, GPIO_PIN_RESET); HAL_GPIO_WritePin(LDAC_GPIO_Port, LDAC_Pin, GPIO_PIN_SET);

低脉冲宽度有最小时间要求,GPIO 连续拉低再拉高在几十纳秒级别,通常够用,但为了保险可以在两次写之间加一条__NOP()。这个机制对电机驱动和波形同步输出很有用,三个通道的电压变化发生在同一个时钟沿,不会出现先后错开导致的中间状态。

4.4 外部基准电压与换算公式的一个坑

AD5676 本身不带内部基准,满量程输出直接由 VREF 引脚的外接基准决定。设基准电压为 VREF,DAC 输出范围约是 0 到 2×VREF,换算关系为:

Vout = (data / 65535.0) * 2 * VREF

比如 VREF = 2.5V 时,满量程约 5V,0x8000 对应约 2.5V。如果买的是 AD5676R,内部自带 2.5V 基准,可以省掉外部基准芯片,但程序里的换算系数要按内部基准计算,不能想当然认为满量程是某个固定值。

注意:AD5676 上电复位后所有 DAC 寄存器清零,输出应该接近 0V。如果上电后发现输出在中间电平,先量 VREF 引脚电压,多半是基准电路没工作,而不是 SPI 配置的问题。

5. 闭环验证:DAC 扫描接 ADC,用逻辑分析仪定位时序问题

5.1 用阶梯波做自检回环

把板卡上的 AD5676 通道 0 输出用跳线直接连到 AD7656 的 V1 输入,不需要外接信号源就能验证两大芯片的链路。程序里让 DAC 按 0x0000、0x2000、0x4000、0x6000、0x8000、0xA000、0xC000、0xE000、0xFFFF 九级阶梯输出,每一级停留 10ms,同时让 ADC 连续采 32 次取平均,通过串口打印码值。

正常情况下,ADC 读回的码值应该随 DAC 输出近似线性递增,相邻两级的差值接近均匀。如果哪一级跳变异常,问题大概率出在对应的位线或者基准上。这种方法比单纯量电压更有效,因为它把 ADC 和 DAC 的线性度一起检查了。

5.2 常见异常现象与定位方向

异常现象优先排查方向
ADC 读值不随 DAC 台阶变化模拟链路虚焊、基准电压未建立
读值全为 0xFFFF 或 0x0000FSMC 读时序不满足、通道地址线接反
所有通道读值完全相同A0/A1/A2 地址线没接或接地
偶发大幅跳变BUSY 中断与下一次 CONVST 竞争
低位数固定为 0引脚接触不良或 SPI 位序配置错误

排查时先用逻辑分析仪抓 CS、RD、BUSY 三根信号,确认一个采样周期内出现了 6 次读脉冲,且每次读脉冲都发生在 BUSY 下降沿之后。这比反复看模拟量盲猜快得多。

5.3 换到 APM32 或其它引脚兼容 MCU 时怎么处理

这两年不少项目把主控从 STM32 换到 APM32 这类引脚兼容的国产型号,AD7656 和 AD5676 的驱动代码基本可以平移,但有两个地方必须核对:FSMC 的 bank 基地址是否一致,以及 HAL 库中时序参数字段的单位是否变化。APM32 的库在 FSMC 初始化接口上和 STM32 有细微差异,直接把SRAM_HandleTypeDef相关代码搬过去可能会编译报错,需要对照厂商 SDK 里的例程做适配。

最稳妥的验证办法是保持 5.1 的阶梯波回环测试不变,换完芯片后跑一遍同样的程序,对比每一级 ADC 读值是否在预期范围内。这个闭环测试通过,基本可以确认数字接口、模拟链路和基准三部分都没问题。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询