FPGA驱动AD9226数据采集与UART串口传输实战:时序对齐与帧同步解析
简介面向FPGA数据采集与串口通信学习者的AD9226采集工程以Quartus 11.0完整工程呈现演示了PC机通过UART串口接收FPGA采集12位数据的全过程。工作流程为AD9226采集得到12位数据后写入深度4096的异步FIFO缓存待FIFO存满再通过串口发送至上位机串口采用8位数据、1位停止位、无校验位并完成补码转原码、截取高8位的格式转换。包内共含3496个文件除大量Quartus编译生成的cdb/hdb等中间文件外还包含AD9226芯片驱动、异步FIFO读写、串口发送控制等Verilog硬件描述语言源码以及工程配置文件、仿真脚本和说明文档整体压缩包约17.87MB。工程结构清晰模块划分完整适合正在学习FPGA数据采集、异步FIFO处理、UART通信协议的开发者参考设计直接复用驱动逻辑或在此基础上进行功能扩展。资源已有576人浏览学习具有一定学习参考价值。1. 在 FPGA 和 UART 之间这条 AD9226 链路真正难的是数据对齐PC 机通过 UART 串口从 FPGA 读回 AD9226 的采样数据看起来是一条入门链路并行 ADC 没有配置寄存器Verilog 里连读写状态机都不用写把 DATA[11:0] 接到引脚就能得到 12 位数据。真正上板后才会发现问题几乎都出在时序和吞吐上。AD9226 的输出数据跟随采样时钟变化FPGA 在哪个沿采样、UART 从哪一位开始对齐、12 位数据如何装进 8 位串口通道这三个环节任何一处没对齐串口调试助手看到的都是乱码或重复的边界值。下面按 AD9226 采集、Verilog 处理、UART 发送、Quartus 11.0 工程落地到 PC 机接收的顺序逐段展开把每一段的时序关系、参数设置和排错思路讲清楚适合用 Quartus 做过简单逻辑、但没系统调过 ADC 串行链路的开发者。2. 用 Verilog 写 AD9226 采样时序先对齐时钟沿再谈采样率2.1 AD9226 并行接口12 位数据和采样时钟的相位关系AD9226 是一个 12 位、最高 65 MSPS 的并行单通道 ADC输入模拟信号输出 D11~D0 共 12 根数据线CLK 为转换时钟。它没有 SPI/IIC 这类配置接口不给时钟就不动给了时钟就一直按节拍输出数据所以驱动逻辑比带寄存器的 ADC 简单难点集中在“数据什么时候稳定、FPGA 什么时候去取”。先把接口连接注意点整理成表多数开发板和自制板卡的接法都落在这几种组合里引脚推荐连接说明CLK接 FPGA 普通 IOFPGA 提供采样时钟时钟沿触发转换D11~D0接 12 根 FPGA IO并行数据输出数据随 CLK 变化AVDD / AVSS3.3V 模拟电源 / 地建议与数字电源做磁珠隔离DVDD / DVSS3.3V 数字电源 / 地与 FPGA 同电源时注意逻辑电平匹配SENSE按手册接模拟地或 AVDD决定输入范围和共模先核对真值表MODE按手册推荐接法配置输出格式或待机模式上电前确认SENSE 引脚必须按你的模拟输入电路去配单端 0~2V 和过零双极性输入对应不同的 SENSE 接法改的是满量程和输入阻抗。不要照抄别人的开源工程就以为通用先用信号发生器和万用表确认输入范围再决定满量程对应的码值。AD9226 内部是流水线结构数据相对采样时钟有若干个时钟周期的输出延迟而且数据往往在 CLK 的某个边沿之后才更新。工程上不需要死记手册上 tOD 是多少纳秒需要做的是把 FPGA 内的采样时刻做成“可调的”发现采样值不对或相邻值周期性跳变时把采样沿往数据稳定的方向移半拍甚至一拍问题通常立刻消失。常见做法有两种。第一种是把给 AD9226 的时钟反相用反相沿做 FPGA 采数时钟天然避开数据翻转窗口第二种是用计数器产生采样使能使能时刻落在 adc_clk 周期中间相位可以按 1/4、1/2、3/4 周期细调。确认相位正确的最简单方法是输入一个稳定的直流电压观察采样码值是否稳定如果码值周期性跳变说明采样时刻正踩在数据翻转沿上。2.2 可分频、可移相对齐的采样模块代码下面的模块同时完成两件事产生送给 AD9226 的 adc_clk在 adc_clk 周期中段采样数据输出 12 位 sample_data 和一拍 sample_valid。主时钟按 50 MHz 写换频率只改分频参数。module adc_capture #( parameter integer ADC_DIV 4 // 分频基数, adc_clk 50M/(2*ADC_DIV) ) ( input wire clk_50m, input wire rst_n, input wire [11:0] adc_data, output reg adc_clk, // 送给 AD9226 的采样时钟 output reg [11:0] sample_data, // 对齐后的 12bit 数据 output reg sample_valid // 数据有效脉冲 ); reg [7:0] div_cnt; // 分频产生 adc_clk占空比 50% always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin div_cnt 8d0; adc_clk 1b0; end else if (div_cnt ADC_DIV - 1) begin div_cnt 8d0; adc_clk ~adc_clk; end else begin div_cnt div_cnt 1b1; end end // 在 adc_clk 周期中间采样避开数据翻转窗口 always (posedge clk_50m or negedge rst_n) begin if (!rst_n) begin sample_data 12b0; sample_valid 1b0; end else if (div_cnt (ADC_DIV 1)) begin sample_data adc_data; sample_valid 1b1; end else begin sample_valid 1b0; end end endmodule逻辑说明分三段。第一段是分频计数器div_cnt 从 0 计到 ADC_DIV-1 后清零同时 adc_clk 翻转一次所以 adc_clk 一个完整周期是 2×ADC_DIV 个系统时钟频率为 50M/(2×ADC_DIV)占空比恒为 50%。第二段把 adc_data 打拍进 sample_data采样点放在 div_cnt ADC_DIV1正好位于 adc_clk 高电平或低电平的中间位置此时 AD9226 输出数据已经稳定是最安全的采样点。第三段是 sample_valid 单周期脉冲后续降采样、UART 发送模块都用这个脉冲作为“有新数据”的握手信号不靠检测 adc_data 是否变化。参数表如下参数默认值实际采样率50MHz 时钟适用场景ADC_DIV46.25 MHz与 UART 配对的常规值抽点余量足ADC_DIV212.5 MHz信号频率稍高但 UART 瓶颈仍然存在ADC_DIV83.125 MHz低频信号可缩短降采样计数器上表数值由 50M/(2×ADC_DIV) 算出换主时钟就按这个公式重算。adc_clk 不要超过 AD9226 的额定上限否则采样误差会明显变大。若采样码值周期性跳动把div_cnt (ADC_DIV 1)改成div_cnt 0或div_cnt ADC_DIV - 1对比哪组更稳定直接改参数重编译验证比在原理图上猜走线延时快得多。2.3 采样模块的仿真条件与参数微调给这个模块写仿真时务必模拟 AD9226 的输出延时不要将 adc_data 与 adc_clk 完全同步。实际芯片在 CLK 边沿之后还要延迟十几纳秒数据才稳定如果 testbench 在 adc_clk 上升沿立即改变 adc_data仿真里看不出问题上板后却会抓到毛刺。比较贴近实际的 testbench 片段// adc_clk 由例化模块输出tb 中通过层次引用取出 wire tb_adc_clk u_adc.adc_clk; // 在 adc_clk 上升沿后 10ns 更新数据模拟输出延迟 always (posedge tb_adc_clk) begin #10; tb_adc_data $random; // 或按正弦查找表驱动 end提示仿真用$random只能验证时序和帧格式验证满量程幅度与码值关系时用 12 位正弦查找表更直观。测试时把 clk 频率设成实际工程值 50 MHz观察 sample_valid 是否每隔固定拍数出现一次如果间隔抖动说明分频逻辑的复位或计数条件写错。把这一级调干净再接 UART否则后面所有乱码都会算到 UART 头上实际是采集端的采样时机错了。3. 把 12 bit 的 AD9226 数据送进 8 bit UART帧设计与发送代码3.1 UART 发送状态机分频计数、起始位、移位输出UART 发送侧只有一根 TXD 线空闲时必须保持高电平发送一帧的顺序是一个低电平起始位、8 个数据位LSB 在前、一个高电平停止位。最常见的 8N1 格式不带校验位每发一个字节实际在串口线上占 10 个位时间。波特率决定每位持续多久FPGA 端实现就是分频计数主时钟每计满 N 个输出一个 bit。给一个 50 MHz 主时钟下的分频参数表数字按N round(50_000_000 / 波特率)计算误差都落在 UART 接收容差内调试串口用 115200 最合适。目标波特率分频计数值实际波特率误差960052089600.60.006%19200260419201.20.006%115200434115207.40.006%230400217230414.70.006%UART 发送模块就是四状态机加一个移位寄存器。先写单字节发送工程初期可以只接这个模块用按键触发复位或循环发固定数先把串口链路本身确认跑通再接 ADC 数据。module uart_tx ( input wire clk, // 50 MHz input wire rst_n, input wire [7:0] tx_byte, input wire tx_start, // 单周期高脉冲请求发送 output reg txd, // 串口输出 output reg tx_busy // 忙标志高电平表示正在发送 ); parameter integer BAUD_CNT 434; // 50M/115200 reg [15:0] cnt; reg [3:0] bit_cnt; reg [7:0] data_buf; reg [1:0] state; parameter S_IDLE 2d0, S_START 2d1, S_DATA 2d2, S_STOP 2d3; always (posedge clk or negedge rst_n) begin if (!rst_n) begin txd 1b1; tx_busy 1b0; state S_IDLE; end else begin case (state) S_IDLE: begin txd 1b1; tx_busy 1b0; if (tx_start) begin data_buf tx_byte; cnt 16d0; bit_cnt 4d0; tx_busy 1b1; state S_START; end end S_START: begin txd 1b0; // 起始位拉低一位时间 if (cnt BAUD_CNT - 1) begin cnt 16d0; state S_DATA; end else begin cnt cnt 1b1; end end S_DATA: begin txd data_buf[0]; // LSB 先发 if (cnt BAUD_CNT - 1) begin cnt 16d0; if (bit_cnt 4d7) begin state S_STOP; end else begin data_buf data_buf 1; bit_cnt bit_cnt 1b1; end end else begin cnt cnt 1b1; end end S_STOP: begin txd 1b1; // 停止位 if (cnt BAUD_CNT - 1) begin cnt 16d0; tx_busy 1b0; state S_IDLE; end else begin cnt cnt 1b1; end end endcase end end endmodule这段代码是所有 UART 发送的底子S_START 拉低一位时长的低电平S_DATA 把 data_buf 的 bit0 放到 TXD 然后右移S_STOP 恢复高电平并清掉忙标志。调用方给一次 tx_start 脉冲等 tx_busy 拉低后再给下一字节即可。多位字节发送时不要在 tx_busy 为高时再拉 tx_start否则状态机会错过起始位这是手写 UART 多字节收发最常见的 bug。3.2 12 位数据的两字节帧设计与同步头UART 一次只能传一个字节AD9226 是 12 位一帧数据必须拆成两个字节。拆分方案按习惯来高字节的低 4 位放 sample_data[11:8]高位 4 位补 0低字节放 sample_data[7:0]接收端收到两字节后执行((dh 0x0F) 8) | dl即可还原 12 位原始码值。但两个裸字节不够串口是字节流PC 端开机后不一定从帧头开始收中途也可能丢字节接收端必须能从连续流里“找到头”。实际项目里先从一字节帧头 0xAA 起步对同步要求高时升级为 0xAA 0x55 双帧头。推荐帧格式帧字段长度内容帧头1 字节0xAA固定数据高字节1 字节高 4 位补 0低 4 位是 sample_data[11:8]数据低字节1 字节sample_data[7:0]3 字节一帧开销小115200 波特率下每秒约 3840 帧对应 3840 个采样点。嫌同步不稳可以在帧尾追加校验字节变成 4 字节帧代价是吞吐量降 25%对低速链路完全可接受。Verilog 端的打包逻辑架在 uart_tx 之上。下面的代码把 sample_valid 脉冲变成三次顺序发送先发帧头再发高字节最后发低字节节奏完全由 tx_busy 控制。reg tx_tx_start; reg [7:0] tx_data; reg [1:0] pack_state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin tx_tx_start 1b0; pack_state 2d0; end else begin tx_tx_start 1b0; case (pack_state) 2d0: if (sample_valid) begin tx_data 8hAA; tx_tx_start 1b1; pack_state 2d1; end 2d1: if (!tx_busy) begin tx_data {4d0, sample_data[11:8]}; tx_tx_start 1b1; pack_state 2d2; end 2d2: if (!tx_busy) begin tx_data sample_data[7:0]; tx_tx_start 1b1; pack_state 2d3; end 2d3: if (!tx_busy) begin pack_state 2d0; end endcase end end逻辑说明pack_state 为 2d0 时等待 sample_valid来新数据就发送帧头并跳到 2d1之后每个状态等待 tx_busy 拉低表示上个字节完成再启动下一字节。所有状态里 tx_tx_start 只保持当拍有效这是为了只让 uart_tx 识别一个起始脉冲避免重复启动。如果串口数据里帧头频繁重复或丢失优先检查 tx_tx_start 是不是维持了多拍。3.3 采样率与 UART 速率匹配降采样和 FIFO 缓冲采样率和传输率之间的差距是新手最容易忽略的。AD9226 以 6.25 MHz 采样115200 波特率每秒只能传约 3840 帧两者相差三个数量级没有 FIFO 能填平平均速率差必须主动降采样。常见做法是每 N 个样本取一个N 取 2 的幂用计数器实现最省资源。localparam integer DOWNSAMPLE 2048; // 每 2048 个采样点发一个 reg [10:0] ds_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin ds_cnt 11d0; end else if (sample_valid) begin if (ds_cnt DOWNSAMPLE - 1) begin ds_cnt 11d0; end else begin ds_cnt ds_cnt 1b1; end end end wire send_en sample_valid (ds_cnt DOWNSAMPLE - 1);6.25M 采样率、2048 抽 1实际发送频率约 3051 帧/秒低于 3840 的上限留有约两成余量。采样率更高或波特率更低时把 DOWNSAMPLE 上调即可。DOWNSAMPLE 必须是 2 的幂或能被计数器整除的数否则会出现最后一个点漏采。另一种做法是加深度 512 的异步 FIFOADC 始终满速采样写侧每来一个 sample_valid 写一个点读侧由 UART 的 tx_busy 拉低触发读取。好处是突发数据不丢坏处是平均吞吐量不变还要处理 FIFO 满时的策略要么停采样要么丢新样本。Quartus 里用 MegaWizard 生成单时钟 FIFO 即可不必手写双口 RAM。对这种“看波形”的需求我的建议是降采样优先等后续加滤波、触发、预处理时再上 FIFO否则排错链路会变长。4. Quartus 11.0 工程落地新建、引脚约束、编译与下载4.1 新建 Quartus 工程与器件选择文件组织和顶层例化Quartus 11.0 对应的器件范围主要是 Cyclone III、Cyclone IV 及 Max II 等。手边是 Cyclone IVQuartus 11.0 可以直接建工程是 Cyclone 10 或更新的器件需要 Quartus Prime 17.1 以上版本旧工程打开时会提示版本升级代码可以沿用IP 核和引脚约束可能要重建。先确定板子型号再决定装老版本还是升级工程避免白折腾。解压 .rar 获得的工程文件注意解压路径不要带中文和空格否则老版本 Quartus 的相对路径解析容易出问题。新建工程走 File → New Project Wizard工程名建议 ad9226_uart顶层模块也叫 ad9226_uart保持符号名一致综合器不会因为找不到顶层报错。文件组织上把采样、发送、打包拆成三个独立文件放 src 目录定位问题直接进模块。ad9226_uart/ ├── ad9226_uart.qpf ├── ad9226_uart.qsf ├── src/ │ ├── adc_capture.v # AD9226 采样 │ ├── uart_tx.v # 单字节发送 │ └── ad9226_uart_top.v # 顶层例化以上模块并完成打包 └── output_files/ # 编译输出顶层模块里把时钟、复位、ADC 数据、UART 输出引出来内部例化写好的模块。复位建议接一个上电复位计数器而不是直接只用按键否则 FPGA 刚上电 UART 会发一帧乱码这是串口项目里常碰到的“开机第一个字节乱”问题。4.2 引脚分配与约束Assignment Editor、TCL 与电平Quartus 11.0 的引脚分配在 Assignment Editor 里完成Assignments → Pins 打开引脚规划器Location 列填目标引脚号I/O Standard 列选 3.3-V LVTTL。11.0 的引脚约束基本靠手填或脚本导入建议画原理图时同步把引脚计划记下来如果等到最后再数很容易在 Fitter 阶段被引脚冲突卡住。引脚约束写成 TCL 脚本后通过 Assignments → Import Assignments 导入以 Cyclone IV 为例大致如下set_location_assignment PIN_E1 -to clk_50m set_location_assignment PIN_E3 -to rst_n set_location_assignment PIN_G1 -to adc_clk set_location_assignment PIN_D10 -to txd set_location_assignment PIN_H1 -to adc_d[0] set_location_assignment PIN_H2 -to adc_d[1] set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to clk_50m set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to adc_d[0]信号方向电平标准在开发板上常见连接clk_50minput3.3-V LVTTL板载 50 MHz 晶振rst_ninput3.3-V LVTTL按键复位按下为低adc_clkoutput3.3-V LVTTL接 AD9226 的 CLKadc_d[11:0]input3.3-V LVTTL接 AD9226 的数据线txdoutput3.3-V LVTTL接板载 USB-UART 芯片的 RXD注意 AD9226 更早手册上的输出逻辑电平可能与 3.3V LVTTL 不完全一致上板前用万用表量 D 脚电压ADC 电源 5V 但数字 IO 3.3V 时务必确认兼容再连线。开发板常用 FT232、FT231 这类 USB-UART 芯片RXD 脚由 FPGA 的 TXD 驱动Windows 下要先装 ft231x usb uart 驱动设备管理器里出现 COM 口才能用串口助手。4.3 编译下载与驱动ModelSim 替代方案与烧录命令编译流程是 Processing → Start Compilation依次执行 Analysis Synthesis、Fitter、Assembler最终在 output_files 下生成 .sof经 USB-Blaster 下载到 FPGA 就能运行。如果编译报错集中在引脚上多半是引脚号写错或约束重复回 Assignment Editor 检查有没有残留旧引脚。Quartus 11.0 自带的仿真工具是 ModelSim-Altera遇到 “cannot launch modelsim” 或仿真器无法启动先检查 Tools → Options → EDA Tool Options 里的 ModelSim 路径是否指向安装目录。路径正确仍启动不了常用替代方案有两个一是用顶层模块做时序仿真二是在线逻辑分析仪 SignalTap II。后者更推荐因为它抓的是真实引脚上的 adc_data。SignalTap 设置触发条件为 sample_valid抓一个周期的数据观察 AD9226 输出比仿真更能定位采样相位问题。下载时若 Programmer 打开文件卡顿直接用命令行烧录工具需要在 Quartus 的 bin 目录下执行或把 bin 加入 PATHquartus_pgm -c USB-Blaster -m JTAG -o P;output_files/ad9226_uart.sof-c 指定下载器名称-m 指定 JTAG 模式P 表示 Program 操作。USB-Blaster 驱动位于 Quartus 安装目录的 drivers 文件夹Windows 第一次插入设备时手动指向该目录即可完成安装。5. PC 机接收解析与上板验证技巧5.1 用 Python 解析 UART 帧并还原波形没有现成串口分析工具时用 Python 的 pyserial 写几十行脚本就能完成帧同步和数据解析。核心逻辑是不断读串口字节遇到 0xAA 才认为帧头然后取后续两个字节合成 12 位码值。import serial ser serial.Serial(COM5, 115200, timeout0.2) buf b while True: data ser.read(256) if not data: continue buf data # 找帧头丢掉前面的乱码 while len(buf) 3 and buf[0] ! 0xAA: buf buf[1:] if len(buf) 3: dh, dl buf[1], buf[2] buf buf[3:] adc ((dh 0x0F) 8) | dl print(adc / 4095.0 * 2.0) # 按 2V 满量程换算电压脚本前半段是打开串口COM 号以设备管理器为准波特率必须和 FPGA 端一致。帧头查找循环把不是 0xAA 的字节全部丢弃这一步就是 UART 通信协议里的字节流同步也是之前在 FPGA 端设计帧头的原因。取到两字节先屏蔽高字节的高 4 位再移位拼回 12 位最后按满量程换算成电压。想要直观波形把 print 换成 matplotlib 的滚动曲线绘制即可。5.2 三个必须验证的点相位、码值、帧同步上板后先别接真实传感器用信号发生器送一个稳定的直流电压比如 1.000V再依次验证三件事。第一个是采样相位。PC 端收到的码值长时间稳定在某个值附近说明相位没问题码值隔一段时间自己跳到另一个值大概率是采样时钟和数据输出沿之间还有相位差回到第 2 章的采样时刻参数往反方向微调。第二个是码值范围。直流 1V、满量程 2V 时12 位码值应在 2048 附近偏差几十个 LSB 正常读数固定在 0x000 或 0xFFF先怀疑输入超范围再查 ADC 供电和 SENSE 配置。第三个是帧同步稳定性。长时间运行观察周期性出现的毛刺点毛刺密集说明降采样计数器或打包状态机丢拍可以在 send_en 处加一个计数器对比理论帧数。实际项目里有个很好用的小技巧先不接 ADC把 txd 固定发送一组值守序列验证 UART 链路和 PC 端解析是否互为对照。这一招能把“ADC 问题”和“串口问题”彻底切开排查链路时不允许把两个未知问题混在一起调。等 UART 链路确认无误再恢复 adc_capture 的输入整个系统的排错范围就会迅速收窄到模拟前端和采样沿上。本文还有配套的精品资源点击获取