资讯动态

FPGA驱动PCF8591实现I2C接口AD采样:Verilog状态机设计实战

发布时间:2026/9/9 12:24:07 来源:尧图企业网站定制
简介一份基于FPGA的PCF8591 AD采样程序使用Verilog语言实现主要面向电子设计竞赛选手、FPGA课程学生以及嵌入式系统开发者帮助解决利用PCF8591完成模拟信号采集、数码管显示和串口通信的综合设计问题。程序通过I2C总线配置PCF8591对0~3.3V范围内的模拟电压进行8位采样并将采样结果并行驱动数码管显示同时通过UART串口发送至上位机覆盖了I2C协议状态机、数码管译码、异步串行通信等关键技术。压缩包共145个文件约4.15MB包含可综合的Verilog源码.v、Quartus工程配置.qpf/.qsf、下载文件.sof/.pof以及备份与说明文档.bak/.txt/readme工程可直接打开并烧录验证。目前已有1802人学习下载对于希望快速熟悉FPGA数据采集链路和I2C外设控制的读者这份资源提供了从代码设计、仿真验证到硬件调试的完整参考具有很好的实践价值。 PCF8591这颗芯片做嵌入式或者FPGA开发的朋友应该不陌生一颗带I2C接口的8位AD/DA转换芯片四路模拟输入一路模拟输出价格便宜、接口简单经常被拿来当ADC入门芯片用。但真到了FPGA上用Verilog去驱动它和新唐、ST的MCU里直接调库完全是两码事——I2C总线时序需要你自己用状态机逐bit抠读回来的数据还要处理通道切换、旧数据丢弃这些细节踩坑点其实不少。这篇博客我从项目设计的角度把PCF8591在FPGA上的AD采样完整实现思路、Verilog核心代码、仿真验证以及实际调试中遇到的问题一次讲清楚适合正在做FPGA入门项目、或者想搞清楚I2C协议在FPGA里到底怎么落地的人参考。1. 项目思路与整体方案设计1.1 为什么选PCF8591芯片特性与项目适配性先说选型逻辑。FPGA本身不带模拟前端要做AD采样要么用开发板上的专用ADC芯片比如AD9226这种高速并行ADC要么自己外接一颗低速串行接口ADC。PCF8591属于后者8位分辨率、逐次逼近型、I2C接口最大400kHz时钟采样率虽然不高典型几k到几十k级别但胜在协议简单、外围电路只需一个上拉电阻非常适合做FPGA的I2C通信练习和低速数据采集场景。对比几种常见方案方案接口类型分辨率FPGA工作量适用场景PCF8591I2C8bit需实现I2C主机状态机低速ADC采集、I2C协议学习AD9226并行12bit无需协议时序简单高速数据采集、图像采集SPI接口ADC如MCP3008SPI10bit需实现SPI主机中低速、多通道采集从这个表能看出来PCF8591的核心价值其实不在性能而在它把I2C协议的所有关键要素都走了一遍写寄存器、读数据、重复起始、ACK应答一套流程练下来I2C协议在FPGA里基本就通了。1.2 系统架构与采样流程设计我在设计这个项目时把整个系统拆成了三个层次顶层模块pcf8591_top负责时钟输入、复位、通道选择、采样启动信号以及最终的8位采样结果输出可以直接接LED或数码管显示。I2C主机控制器这是核心模块负责按I2C协议生成SCL时钟、控制SDA数据线上的数据发送和接收内部是一个大状态机。应用逻辑层简单点的话就是个触发信号生成器复杂点可以在后面接FIFO或者串口发送模块。更关键的是采样流程设计。PCF8591不是你把地址发过去就能立刻拿到当前通道的转换值它有一个内部工作周期主机发送控制字节后芯片才开始对选定通道进行采样转换而且第一次读回来的数据往往是上一次转换的残留值。所以完整的读取流程必须做成发送启动条件发送器件地址写方向0x90等待ACK发送控制字节配置通道和输入模式等待ACK发送重复起始条件Restart发送器件地址读方向0x91等待ACK读取第一个字节并回ACK触发转换数据丢弃读取第二个字节并回NACK有效数据说明PCF8591上电后真正的转换结果在这里发送停止条件这一步是很多人容易忽略的我第一次做的时候只读了一个字节结果每次上电读到的第一组数据都是乱码后来翻数据手册才搞明白第二个字节才是有效转换结果。后面代码里我会把这个流程完整写进状态机。2. I2C时序分析与时序参数计算2.1 PCF8591读写时序逐段拆解I2C协议在FPGA里实现核心就是把时序拆成状态的转移。PCF8591的数据手册里给了两个关键时序表标准模式100kbit/s和快速模式400kbit/s。我用的是标准模式因为FPGA里分频到400kbit/s虽然没问题但为了兼容性和稳定性100kbit/s更保险。一次完整的数据字节传输时序上要经历这么几个阶段STARTSCL保持高电平期间SDA由高变低发送8位数据SCL低电平期间SDA准备好数据SCL上升沿后芯片采样第9个时钟接收ACK主机释放SDA线芯片拉低表示应答STOPSCL高电平期间SDA由低变高在FPGA里SDA的跳变必须严格控制发生在SCL低电平期间否则会被芯片误判为启动或停止条件。这是写I2C时序最需要注意的一个点必须在代码里用状态顺序来保证而不是靠延迟碰运气。2.2 寄存器与控制字节配置要点PCF8591的器件地址是固定的高四位1001跟着是三位硬件地址A2、A1、A0一般接地为000最后一位是读写方向位。所以写地址是0x90读地址是0x91。控制字节是整个配置的灵魂各bit的含义要搞清楚Bit位含义取值说明7DA输出使能1使能AOUT模拟输出6输入模式0四路单端输入1差分输入5自动增量1每次转换后通道自动14:3通道选择00~11对应AIN0~AIN32:1测试模式00正常工作0保留置0单端四路输入模式下选择AIN0控制字节就是8b0000_0000如果需要DAC同步输出就加上0x80变成8b1000_0000。这里有个常见的坑不少例程写0x40那个是选择差分输入模式初学者直接抄板子配置的是单端输入读出来的数据对不上号排查半天发现是控制字的问题。2.3 时钟分频设计与时序参数表我用的开发板晶振是50MHz要得到100kHz的SCL一个完整SCL周期需要500个系统时钟周期也就是250个周期拉高、250个周期拉低。分频系数计算SCL频率 系统时钟 / (2 * 分频计数) 100kHz 50MHz / (2 * 250)在代码里我定义了一个参数SCL_DIV 250再定义一个半周期计数器从0数到249翻转一次SCL。这里额外多说一句I2C标准规定数据建立时间最短250ns、保持时间最短100ns100kHz的SCL半周期是5us远远满足要求所以不需要额外做建立保持时间补偿时序上是安全的。如果后续要用400kHz模式半周期变成1.25us依然能满足但不建议再往上跑了PCF8591的最大时钟就是400kHz。3. Verilog关键模块实现与代码解析3.1 顶层模块接口设计顶层模块把I2C主机封装成一个可复用的控制器对外暴露的接口要简洁清晰module pcf8591_top( input clk, // 50MHz系统时钟 input rst_n, // 低电平复位 input start, // 采样启动信号 input [1:0] ch_sel, // 通道选择 00~11 output reg [7:0] adc_data, // 采样结果 output reg adc_done, // 采样完成标志 inout i2c_sda, // I2C数据线 output i2c_scl // I2C时钟线 );需要特别注意的是i2c_sda这个inout端口。FPGA内部无法直接读外部引脚电平来同时驱动它必须用三态门控制assign i2c_sda (sda_out_en) ? sda_out : 1bz;sda_out_en为高时FPGA主动拉低SDA因为I2C是开漏结构输出数据时只拉低或释放不主动拉高为低时释放总线此时可以读取外部输入sda_in。这个三态控制如果写反了总线就会被FPGA一直钳住芯片收不到任何有效数据。3.2 I2C主控制器状态机实现核心状态机的设计是整个项目的重中之重。状态划分要细但不要过度复杂我用的状态序列如下localparam IDLE 5d0, START_A 5d1, SEND_ADDR_W 5d2, WAIT_ACK1 5d3, SEND_CTRL 5d4, WAIT_ACK2 5d5, RESTART 5d6, SEND_ADDR_R 5d7, WAIT_ACK3 5d8, READ_DATA1 5d9, // 第一字节丢弃 SEND_ACK 5d10, // 主机回ACK READ_DATA2 5d11, // 第二字节有效数据 SEND_NACK 5d12, // 主机回NACK STOP 5d13, DONE 5d14;每个状态内部再细分bit计数。以发送字节为例我用一个cnt_bit计数器在0~7之间循环状态每个SCL周期推进一位。发送字节的伪代码逻辑是always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_bit 0; scl_half_cnt 0; end else if (scl_half_cnt SCL_DIV - 1) begin scl_half_cnt 0; scl_reg ~scl_reg; if (scl_reg 1b0) begin // SCL从低到高发送下一位 if (cnt_bit 8) begin sda_out tx_data[7 - cnt_bit]; cnt_bit cnt_bit 1; end end end else begin scl_half_cnt scl_half_cnt 1; end end注意这个if (scl_reg 1b0)的判断它的含义是SCL刚完成一次从低到高的翻转后主机可以改SDA准备下一位数据。实际调试时这个位置很容易出问题如果数据更新时机不对芯片采到的就是错误bit。接收字节和发送字节的框架类似区别在于cnt_bit小于8时主机释放SDA读数据第9个周期根据当前状态决定回ACK还是NACK。ACK和NACK的区别就是主机是否拉低SDAACK拉低NACK释放芯片看到NACK后知道主机不再读取会释放总线。3.3 数据接收与转换结果输出有效数据收到后我加了一个简单的同步寄存逻辑always (posedge clk or negedge rst_n) begin if (!rst_n) adc_data 8b0; else if (state READ_DATA2 bit_cnt 7 scl_half_cnt SCL_DIV - 1) adc_data rx_data; endadc_done标志在进入DONE状态时拉高持续一个系统时钟周期方便外部逻辑后续触发FIFO写或者串口发送。如果只是点灯验证直接把adc_data接到8个LED上看电压变化即可。4. 仿真验证与上板调试实录4.1 Testbench编写与仿真波形分析Verilog代码写完后不能直接上板先仿真把时序调通才是高效率的做法。Testbench的核心任务有两个生成50MHz时钟、给start一个启动脉冲然后观察SDA和SCL波形。initial begin clk 1b0; forever #10 clk ~clk; end initial begin rst_n 1b0; start 1b0; #100 rst_n 1b1; // 复位释放 #200 start 1b1; #20 start 1b0; end // inout端口需要在仿真时加入上拉 pullup(pull_sda); assign pull_sda 1b1; assign i2c_sda pull_sda;这里有个Modelsim仿真的实际坑i2c_sda在testbench里必须同时处理pullup和驱动否则仿真波形里SDA一直是不定态X。实际的做法是把DUT和testbench之间的inout连接用一个wire然后单独加pullup(sda_wire);。仿真波形里要重点检查的几个点启动条件是否在SCL高电平时SDA由高变低第一个字节0x90之后芯片是否有ACK在第9个时钟SDA拉低重复起始条件是否出现读地址0x91之后数据线上是否读到芯片返回的数据如果波形里看到SDA在应该释放的时段变成0多半是三态控制逻辑有问题优先查sda_out_en的时序。4.2 Modelsim/Vivado仿真常见坑与调试技巧仿真验证这块我遇到过最蹊跷的问题是用Vivado自带的仿真器跑波形完全正常换到Modelsim里就各种时序怪象。后来发现是Modelsim默认对inout端口处理方式不同需要在testbench里显式处理三态总线的上拉否则高阻态会显示成红色锯齿状电平。另外还有一个通用技巧在testbench里用文本方式打印关键状态跳转比单靠肉眼盯波形效率高得多always (posedge clk) begin if (state ! prev_state) begin $display(Time:%0t State changed to %0d, $time, state); prev_state state; end end这样仿真完看一眼打印信息就能定位状态机卡在哪一步不用反复放大缩小波形窗口。4.3 上板实测流程与数据验证仿真通过后开始上板实测。我的流程是先不接任何模拟输入把0V和3.3V分别接到AIN0上观察读回来的8位数据是否接近0x00和0xFF用这种方式验证基本通路。然后接一个电位器调整电压时观察adc_data是否同步变化。如果手头没有电位器用一根杜邦线悬空也行但悬空状态下读到的数据会乱跳这是正常的不是代码问题。我用的是Vivado环境加ILA集成逻辑分析仪来抓内部信号抓sda、scl、state、adc_data这几个关键信号比外接逻辑分析仪方便。ILA触发条件设成adc_done上升沿就能稳定抓到每次采样完成时的完整总线时序。5. 常见问题与排查技巧速查5.1 I2C无ACK、读不到数据的排查路径实际项目中最常见的问题就是总线无应答数据读不出东西。我把排查经验总结成了一张速查表现象可能原因处理方式SCL有波形但一直无ACKSCL频率超出芯片400kHz上限检查分频参数实测降速到100kHzSDA一直是低电平三态门方向控制写反检查sda_out_en极性读地址后返回0xFF器件地址配置错或SDA未释放检查A2~A0硬件连接数据有值但固定不变通道配置字写错核对控制字节bit4:3首次上电读到乱码未丢弃第一个字节旧数据按两字节读取流程操作SCL和SDA的上拉电阻也要确认很多开发板自带了4.7kΩ上拉但如果你是自己搭的板子忘了加上拉电阻会导致I2C总线根本无法工作这是硬件上的硬性要求不是软件能绕过的。5.2 采样值跳变与精度问题的处理技巧8位ADC本身分辨率有限0~3.3V映射到0~255每一步代表大约12.9mV所以数据跳动1~2个LSB都是正常的。但如果你发现数据跳变幅度有几十甚至上百那就要从以下几个角度排查电源噪声。PCF8591的VREF最好直接用3.3V稳压电源不要用FPGA的IO口供电。输入信号源阻抗过高。PCF8591的采样电容充电时间有限如果信号源内阻太大采样值会偏低且不稳加一个运放跟随器或者至少一个0.1uF电容到地。数字噪声耦合到模拟输入。采样线尽量短远离SCL和SDA走线。如果想做简单的滤波可以在FPGA里对连续多次采样取平均或者做滑动窗口滤波。最简单的做法是连续采8次求和后右移3位相当于8点平均在低速场景下效果非常明显。如果后续想做得更平滑可以试试滑动窗口滤波本质上就是一个移位寄存器累加器实现复杂度也不高。最后再分享一个实际操作中的心得PCF8591这个项目虽然看起来简单但它把FPGA开发里最核心的几个基本功都串起来了inout端口的三态控制、分频时钟设计、大状态机的编写与调试、时序仿真验证方法。做完这个项目再去做SPI接口的ADC、I2C接口的EEPROM甚至去读其他带I2C总线的传感器套路都是相同的——无非是换个控制字、换个寄存器配置。如果手头资源允许建议在这个基础上做两个扩展一是加一个FIFO连续采集多通道数据配合上位机画波形二是把I2C控制器改写成参数化模块数据位宽和地址位宽可配这样以后复用起来更灵活。我踩过的最大一次坑就是控制字那一位的配置错误差分和单端模式搞反整整查了一个下午才发现问题。所以还是那句话做这种接口类项目第一件事永远是熟读数据手册把每一个寄存器的bit位含义都弄明白再动手写代码不迟。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价