从APB到SDA:手把手教你用Verilog搭建一个可配置的I2C Master控制器

Fax Caelestis

从APB到SDA:手把手教你用Verilog搭建一个可配置的I2C Master控制器

在数字IC设计和FPGA开发领域,I2C总线因其简单的两线制结构和灵活的多主从配置,成为连接低速外设的首选方案。无论是与EEPROM通信、读取传感器数据,还是配置显示设备参数,I2C都展现出了极高的实用价值。本文将带你从零开始,用Verilog实现一个通过APB总线控制的I2C Master控制器,重点解决实际工程中的关键问题:如何设计可配置的时钟分频、处理双向IO的PAD接口、构建高效的状态机,以及优化寄存器交互逻辑。

1. I2C协议核心与硬件设计要点

I2C协议的精妙之处在于仅用两根线(SCL时钟线和SDA数据线)就实现了完整的主从通信。与UART不同,I2C是真正的多主设备总线,这意味着我们的控制器需要处理总线仲裁和时钟同步等复杂场景。在硬件设计层面,有几个关键特性需要特别注意:

  • 开漏输出:所有I2C设备必须采用开漏输出,配合上拉电阻实现"线与"逻辑
  • 时钟同步:当多个主设备同时传输时,SCL线通过线与机制实现时钟同步
  • 地址帧格式:7位地址模式可寻址112个设备(保留地址除外),10位地址模式扩展了寻址空间

典型的I2C传输时序包含以下几个阶段:

  1. START条件:SCL高电平时SDA从高到低跳变
  2. 地址帧:7位或10位从机地址 + 1位读写方向
  3. 数据帧:8位数据 + 1位ACK/NACK
  4. STOP条件:SCL高电平时SDA从低到高跳变

提示:I2C标准模式(100kHz)和快速模式(400kHz)对上升时间要求不同,设计PAD时需要根据目标速率选择合适的驱动强度。

2. APB总线接口设计

APB(Advanced Peripheral Bus)作为ARM AMBA协议族中的低功耗外设总线,非常适合连接I2C这类低速设备。我们的控制器需要实现完整的APB接口信号:

APB信号 方向 描述
PCLK 输入 总线时钟
PRESETn 输入 低有效复位
PADDR 输入 32位地址总线
PSEL 输入 设备选择
PENABLE 输入 使能信号
PWRITE 输入 读写控制
PWDATA 输入 写数据
PRDATA 输出 读数据
PREADY 输出 传输完成
PSLVERR 输出 错误指示

寄存器组的设计直接影响控制器的灵活性。我们采用以下寄存器布局:

verilog复制module i2c_regs (
    input wire PCLK,
    input wire PRESETn,
    input wire [31:0] PADDR,
    input wire PSEL,
    input wire PENABLE,
    input wire PWRITE,
    input wire [31:0] PWDATA,
    output reg [31:0] PRDATA,
    output reg PREADY
);
    // 寄存器定义
    reg [15:0] prescale;   // 0x00: 时钟分频系数
    reg [7:0] ctrl;        // 0x04: 控制寄存器
    reg [7:0] tx_data;     // 0x08: 发送数据
    reg [7:0] rx_data;     // 0x0C: 接收数据
    reg [7:0] status;      // 0x10: 状态寄存器
    reg [7:0] command;     // 0x14: 命令寄存器
    
    // APB接口逻辑
    always @(posedge PCLK or negedge PRESETn) begin
        if (!PRESETn) begin
            // 复位逻辑
        end else if (PSEL && PENABLE) begin
            // 寄存器读写逻辑
        end
    end
endmodule

3. 时钟分频与SCL生成

I2C的时钟生成需要考虑两个关键因素:系统时钟到SCL的精确分频,以及不同模式下的时序要求。我们的设计采用可编程预分频器,支持标准模式(100kHz)和快速模式(400kHz)。

分频系数的计算公式为:

code复制分频系数 = (系统时钟频率) / (5 × 目标SCL频率) - 1

例如,当系统时钟为50MHz,目标SCL为100kHz时:

code复制分频系数 = 50,000,000 / (5 × 100,000) - 1 = 99

在Verilog中实现时,我们需要一个16位计数器:

verilog复制module i2c_clock_gen (
    input wire clk,
    input wire reset_n,
    input wire [15:0] prescale,
    output reg scl_out,
    output reg scl_en
);
    reg [15:0] counter;
    reg [2:0] phase;  // 5相位计数器
    
    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            counter <= 0;
            phase <= 0;
            scl_out <= 1'b1;
            scl_en <= 1'b1;
        end else begin
            if (counter == prescale) begin
                counter <= 0;
                phase <= phase + 1;
                
                case (phase)
                    0: begin scl_out <= 1'b1; scl_en <= 1'b1; end // SCL高电平
                    1: begin /* 保持高电平 */ end
                    2: begin scl_out <= 1'b0; scl_en <= 1'b0; end // SCL下降沿
                    3: begin /* 保持低电平 */ end
                    4: begin scl_out <= 1'b1; scl_en <= 1'b1; end // SCL上升沿
                endcase
            end else begin
                counter <= counter + 1;
            end
        end
    end
endmodule

注意:I2C规范要求SCL高电平时间(tHIGH)和低电平时间(tLOW)必须满足最小要求,标准模式下分别为4.0μs和4.7μs。设计时需要根据系统时钟频率确保分频后的时序符合规范。

4. 双向SDA处理与PAD设计

I2C的SDA线是典型的双向开漏信号,在RTL设计中需要特别注意三态控制。我们采用以下接口信号:

  • sda_out: 主设备输出数据
  • sda_in: 主设备输入数据
  • sda_en: 输出使能信号(0表示驱动,1表示高阻)
verilog复制module i2c_pad (
    inout wire sda_io,    // 物理SDA引脚
    input wire sda_out,   // 内部输出数据
    output reg sda_in,    // 内部输入数据
    input wire sda_en     // 输出使能
);
    // 三态驱动逻辑
    assign sda_io = sda_en ? 1'bz : sda_out;
    
    // 输入同步逻辑
    always @(*) begin
        sda_in = sda_io;
    end
endmodule

在实际FPGA实现中,需要特别注意:

  1. 同步采样:SDA输入信号需要与系统时钟同步,避免亚稳态
  2. 毛刺滤波:添加消抖逻辑,通常2-3个时钟周期的滤波足够
  3. IO约束:在综合约束文件中正确设置SDA引脚的IO标准(如I2C电平)

5. 状态机设计与协议实现

I2C主控制器的核心是一个精心设计的状态机,需要处理以下主要状态:

  1. IDLE:等待启动传输
  2. START:生成START条件
  3. ADDR:发送从机地址和读写位
  4. DATA_TX:发送数据字节
  5. DATA_RX:接收数据字节
  6. ACK:处理ACK/NACK
  7. STOP:生成STOP条件

状态机的Verilog实现框架:

verilog复制module i2c_master_fsm (
    input wire clk,
    input wire reset_n,
    input wire [7:0] command,
    input wire [7:0] tx_data,
    output reg [7:0] rx_data,
    output reg busy,
    output reg int_status,
    // 其他控制信号...
);
    // 状态定义
    typedef enum {
        ST_IDLE,
        ST_START,
        ST_ADDR,
        ST_DATA_TX,
        ST_DATA_RX,
        ST_ACK,
        ST_STOP,
        ST_ERROR
    } i2c_state_t;
    
    reg [2:0] state;
    reg [2:0] next_state;
    reg [3:0] bit_counter;
    reg [7:0] shift_reg;
    reg ack_received;
    
    // 状态转移逻辑
    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            state <= ST_IDLE;
            // 其他复位逻辑...
        end else begin
            state <= next_state;
            
            case (state)
                ST_IDLE: begin
                    if (start_condition) begin
                        next_state <= ST_START;
                        shift_reg <= {command[7:1], 1'b0}; // 地址 + 写
                    end
                end
                ST_START: begin
                    // 生成START时序
                    next_state <= ST_ADDR;
                end
                // 其他状态处理...
            endcase
        end
    end
    
    // 输出逻辑
    always @(*) begin
        case (state)
            ST_IDLE: begin
                sda_en = 1'b1;
                scl_en = 1'b1;
            end
            // 其他输出控制...
        endcase
    end
endmodule

6. 验证策略与测试用例

一个可靠的I2C控制器需要全面的验证。我们建议采用分层验证策略:

  1. 模块级验证:使用Verilog仿真测试每个子模块

    • 时钟分频器的精度测试
    • 状态机的所有转移路径测试
    • 寄存器接口的读写测试
  2. 系统级验证:模拟实际I2C从设备

    • EEPROM读写测试
    • 传感器数据读取测试
    • 错误条件测试(总线冲突、无应答等)
  3. FPGA原型验证:使用逻辑分析仪抓取实际信号

    • 时序参数测量(建立时间、保持时间)
    • 信号完整性检查

以下是一个典型的测试用例序列:

verilog复制initial begin
    // 初始化
    i2c_reset();
    
    // 测试1:写入EEPROM
    i2c_set_prescale(99);  // 100kHz @ 50MHz
    i2c_enable();
    i2c_start();
    i2c_send_addr(0xA0, 0);  // EEPROM写地址
    i2c_send_data(8'h00);    // 内存地址高字节
    i2c_send_data(8'h10);    // 内存地址低字节
    i2c_send_data(8'h55);    // 测试数据
    i2c_stop();
    
    // 测试2:从EEPROM读取
    i2c_start();
    i2c_send_addr(0xA0, 0);  // EEPROM写地址
    i2c_send_data(8'h00);    // 内存地址高字节
    i2c_send_data(8'h10);    // 内存地址低字节
    i2c_start();             // 重复START
    i2c_send_addr(0xA0, 1);  // EEPROM读地址
    i2c_read_data(1);        // 读取带NACK
    i2c_stop();
end

7. 性能优化与高级功能

在基本功能实现后,可以考虑以下优化和扩展:

  1. 时钟拉伸支持:检测SCL被从设备拉低的情况
  2. 多主仲裁:实现总线竞争检测和优雅退出
  3. DMA接口:添加直接内存访问支持,减少CPU开销
  4. 时钟同步:支持多个主设备之间的时钟同步
  5. 10位地址扩展:支持10位从机地址模式

一个实用的优化是添加FIFO缓冲,减少APB总线交互频率:

verilog复制module i2c_fifo #(
    parameter DEPTH = 8
)(
    input wire clk,
    input wire reset_n,
    input wire [7:0] data_in,
    input wire wr_en,
    input wire rd_en,
    output wire [7:0] data_out,
    output wire full,
    output wire empty
);
    reg [7:0] mem [0:DEPTH-1];
    reg [3:0] wptr, rptr;
    reg [3:0] count;
    
    always @(posedge clk or negedge reset_n) begin
        if (!reset_n) begin
            wptr <= 0;
            rptr <= 0;
            count <= 0;
        end else begin
            case ({wr_en, rd_en})
                2'b10: if (!full) begin
                    mem[wptr] <= data_in;
                    wptr <= wptr + 1;
                    count <= count + 1;
                end
                2'b01: if (!empty) begin
                    rptr <= rptr + 1;
                    count <= count - 1;
                end
                2'b11: begin
                    mem[wptr] <= data_in;
                    wptr <= wptr + 1;
                    rptr <= rptr + 1;
                end
            endcase
        end
    end
    
    assign data_out = mem[rptr];
    assign full = (count == DEPTH);
    assign empty = (count == 0);
endmodule

在实际项目中,我发现最常遇到的问题集中在时序控制和异常处理上。特别是在多主环境中,总线仲裁失败后的恢复流程需要特别注意。建议在状态机中添加专门的错误处理状态,确保在任何异常情况下都能安全回到IDLE状态。

内容推荐

【流体力学基础】:从质量守恒到连续性方程的物理直觉
本文深入浅出地解析了流体力学中的连续性方程,从质量守恒的生活直觉出发,通过交通流类比和会计记账法的比喻,帮助读者建立物理直觉。文章详细介绍了控制体概念、质量收支计算,以及从积分形式到微分方程的推导过程,并提供了管道系统设计和可压缩流动的实战应用案例,助力工程师掌握流体分析的核心理念。
Ubuntu 20.04下RTL8156网卡驱动实战:从编译到巨型帧配置全解析
本文详细解析了在Ubuntu 20.04系统下为RTL8156网卡安装驱动并配置巨型帧的全过程。从驱动源码获取、编译安装到DKMS方案部署,再到巨型帧(Jumbo Frame)的深度优化与性能测试,为工业视觉等高性能网络应用场景提供完整解决方案。特别针对MTU 9000配置、驱动兼容性等常见问题给出实战经验。
ESP8266 OTA 实战指南:从Arduino IDE到Web服务器的无线固件升级
本文详细介绍了ESP8266 OTA无线固件升级的实战指南,涵盖从Arduino IDE到Web服务器的多种实现方式。通过具体代码示例和最佳实践,帮助开发者掌握安全可靠的无线更新技术,提升物联网设备的远程维护效率。
【神经网络】从MLP到Transformer:编码器-解码器、注意力与残差连接的演进与融合
本文系统梳理了神经网络从MLP到Transformer的技术演进历程,重点解析了编码器-解码器架构、注意力机制和残差连接等关键技术突破。通过对比分析各技术的优势与局限,揭示了Transformer如何融合这些创新成为当前最强大的序列建模架构,为开发者理解现代深度学习模型提供技术洞见。
华为机试Python通关秘籍:三道高频题详解与避坑指南
本文详细解析华为机试Python高频题目,包括字符串处理、考勤判断和书本堆叠问题,提供多种解题思路和优化方案。特别针对华为机试的评分机制和时间限制,给出实用的应试策略和避坑指南,帮助开发者高效通关。
避坑指南:用Netcat传输树莓派视频流时如何解决卡顿问题(实测UDP/TCP对比)
本文详细解析了在树莓派Zero上使用Netcat传输视频流时遇到的卡顿问题,通过对比UDP与TCP协议的性能差异,提供了一套完整的参数调优方案。特别针对4G网络环境,优化了Netcat命令和播放器设置,显著减少卡顿次数。文章还介绍了网络诊断工具和高级稳定性增强技巧,帮助开发者提升视频传输的可靠性。
从C3D到自注意力LSTM:花样滑冰视频动作质量评估的算法演进与实践
本文探讨了从C3D到自注意力LSTM的花样滑冰视频动作质量评估算法演进与实践。通过分析长视频时序建模、关键动作筛选和小样本训练等技术挑战,详细介绍了C3D特征提取、LSTM时序建模及自注意力LSTM的创新应用,最终实现与人类裁判评分87.2%的吻合度。文章还展望了多模态融合的未来发展方向。
华为设备BGP联盟实战:从原理到配置的深度解析
本文深入解析华为设备BGP联盟技术,从原理到配置实践全面讲解。通过实际案例展示如何利用BGP联盟解决大型网络连接数爆炸问题,详细说明华为设备上联盟ID与成员AS的配置方法,以及联盟EBGP的特殊处理机制,帮助网络工程师高效部署和管理复杂网络架构。
别光盯着SQL!当MyBatis报'No more data to read from socket'时,你的排查路线图可能错了
本文深入分析了MyBatis抛出'No more data to read from socket'异常的系统化排查方法,涵盖应用层连接池配置、中间件代理问题、操作系统TCP参数调优及数据库服务端设置。通过实战案例和高级诊断技巧,帮助开发者快速定位并解决这一常见但棘手的数据库连接问题。
松下A6伺服调试实战:从‘嗡嗡’异响到丝滑运行的增益调整避坑指南
本文详细介绍了松下A6伺服调试实战,从‘嗡嗡’异响到丝滑运行的增益调整避坑指南。通过分析不同频段的噪音特征,定位伺服系统问题环节,并提供三步消音法和参数调整策略,帮助工程师快速解决异响问题,提升设备运行稳定性与效率。
从开源项目到可运行Demo:我是如何修复ecsheet并打包成可部署Jar的
本文详细记录了如何将开源项目ecsheet修复并打包成可部署的Spring Boot应用。ecsheet是一个基于Java的多人协同在线编辑Excel工具,通过引入乐观锁机制解决并发编辑冲突,优化性能并支持多数据库配置,最终生成可执行Jar文件。
HandyControl 3.2.0资源字典深度解析:如何像换衣服一样轻松切换应用皮肤?
本文深入解析HandyControl 3.2.0资源字典系统,教你如何轻松实现WPF应用皮肤切换。通过动态资源绑定和皮肤管理器设计,开发者可以快速构建支持多色系切换的专业级应用界面,提升用户体验。文章详细介绍了Default、Dark等预置色系的使用方法,并提供了自定义皮肤开发的完整实践方案。
5G PUCCH DTX检测:从功率估计到半盲算法的性能演进与挑战
本文深入探讨了5G PUCCH DTX检测的技术演进与挑战,从传统功率估计到半盲算法的性能提升。重点分析了物理上行控制信道(PUCCH)在DTX检测中的核心问题,包括信道估计误差、相位信息浪费等,并介绍了半盲算法如何通过联合信道估计显著降低漏检率和虚警率。文章还展望了深度学习与多维信息融合等未来优化方向,为5G通信系统的可靠性提升提供技术参考。
Android 11 应用更新:从后台下载到静默安装的完整实现
本文详细解析了在Android 11上实现应用更新的完整流程,包括后台下载、文件访问适配和静默安装。重点介绍了使用DownloadManager确保下载稳定性,通过FileProvider解决文件共享问题,并适配Android 11的安装权限要求。文章还提供了实用的代码示例和兼容性处理技巧,帮助开发者高效完成APK更新功能开发。
为什么我劝你别轻易升级?Jetson Orin Nano 坚守 Ubuntu 20.04 的 ROS 生态考量
本文探讨了Jetson Orin Nano在机器人开发中坚守Ubuntu 20.04的重要性,分析了ROS生态与系统版本的深度耦合关系。文章指出盲目升级到Ubuntu 22.04可能导致驱动不兼容、性能下降等问题,并提供了降级评估框架和混合部署策略,帮助开发者平衡系统稳定性和新特性需求。
LIBERO终身学习实战:5分钟搞懂如何实现你自己的防遗忘算法(以EWC为例)
本文详细介绍了在LIBERO框架下实现弹性权重固化(EWC)算法以解决终身学习中的灾难性遗忘问题。通过核心思想解析、代码实现剖析和实战调优建议,帮助开发者快速掌握EWC算法在机器人操作任务中的应用,显著提升模型在多任务学习中的表现。
从眼科到皮肤科:SS-OCT技术是如何革新临床诊断的?聊聊它的应用现状与未来
本文探讨了SS-OCT技术在眼科、皮肤科等临床诊断中的革命性应用。通过微米级分辨率和实时动态成像能力,SS-OCT在青光眼诊断、黑色素瘤检测和心血管介入等领域展现出显著优势,大幅提升诊断准确率。文章还分析了技术挑战与未来发展方向,如芯片化探头和动态血流量化技术。
在VMware Workstation 17 Pro中部署Raspberry Pi Desktop:从镜像获取到系统配置的完整指南
本文详细介绍了在VMware Workstation 17 Pro中部署Raspberry Pi Desktop的完整流程,从镜像获取到系统配置。通过虚拟机方案,开发者无需真实树莓派硬件即可搭建开发环境,享受硬件零成本、环境隔离和高效编译等优势。文章包含镜像下载、VMware配置、系统安装及开发环境搭建等实用指南,特别适合初学者快速上手树莓派开发。
别再死记硬背‘漂亮老男人’了!华为/华三设备BGP选路13条原则实战配置与避坑指南
本文详细解析了华为/华三设备BGP选路的13条原则,重点介绍了Local_Preference、AS_Path、MED等关键属性的实战配置技巧,帮助工程师在多出口架构中优化流量走向。通过真实案例和排错指南,解决BGP选路中的常见问题,提升网络性能。
从芯片手册到实际电路:手把手教你理解74LS90的BCD码计数模式与八进制应用
本文详细解析74LS90芯片的BCD码计数模式与八进制应用,从芯片手册解读到实际电路搭建,涵盖8421BCD码计数器构建、八进制改造及Multisim仿真验证。通过实战案例,帮助读者深入理解数字电路设计中的关键技术与应用场景。
已经到底了哦
精选内容
热门内容
最新内容
别再死记硬背公式了!用OpenCV的getPerspectiveTransform函数5分钟搞定图像透视变换
本文详细介绍了如何使用OpenCV的getPerspectiveTransform函数快速实现图像透视变换,无需死记硬背复杂公式。通过5行核心代码,即可矫正倾斜文档、车牌等图像,适用于文档数字化、车牌识别等多种场景,大幅提升工作效率。
HUAWEI DevEco Device Tool实战排障指南:从环境搭建到烧录成功
本文详细介绍了HUAWEI DevEco Device Tool从环境搭建到烧录成功的实战排障指南。针对Python版本冲突、权限问题、驱动识别等常见问题,提供了具体解决方案和优化建议,帮助开发者高效完成HarmonyOS设备开发任务。
FPGA仿真入门:用Quartus 20.1.1和ModelSim SE 10.6d跑通你的第一个LED测试程序
本文详细介绍了如何使用Quartus Prime 20.1.1和ModelSim SE 10.6d进行FPGA仿真,从环境配置到LED测试程序的完整流程。通过创建Verilog模块、构建测试平台和运行RTL仿真,帮助初学者快速掌握FPGA开发的核心技能,特别适合需要学习FPGA仿真和Quartus工具的新手。
用 xv6 的 Lab1 理解 Unix 哲学:管道、进程与组合命令的实战演练
本文通过MIT 6.S081课程的xv6 Lab1实验,深入解析Unix哲学中的管道、进程与组合命令设计理念。从sleep、pingpong到primes等工具的实现,展示了模块化、组合性与简洁性的核心思想,帮助开发者理解现代Unix-like系统的设计精髓与应用实践。
PyAutoGui图像定位实战:从基础定位到性能优化的核心方法
本文深入探讨PyAutoGui图像定位的核心方法,从基础的`locateOnScreen`到高效的`locateCenterOnScreen`应用,详细解析三种定位方法的性能差异与优化策略。通过实战案例分享如何提升定位精度与速度,包括多显示器环境处理和动态内容匹配等高级技巧,助力开发者构建更稳定的自动化测试解决方案。
从状态机到实战配置:手把手带你理解MIPI M-PHY的HS/LS模式切换与避坑指南
本文深入解析MIPI M-PHY协议中的HS/LS模式切换机制,提供从状态机原理到实战配置的完整指南。通过详细的状态迁移分析、配置流程避坑技巧和示波器调试方法,帮助工程师掌握高速/低速模式切换的关键技术,特别适用于移动设备和汽车电子设计。
手把手教你用YOLOv8搭建PCB元件识别Web应用(附完整代码与数据集)
本文详细介绍了如何使用YOLOv8构建PCB元件智能检测系统,涵盖环境配置、数据集处理、模型训练优化及Web应用部署全流程。通过实战指南和完整代码,帮助开发者快速掌握深度学习在电子元件识别中的应用,提升PCB检测效率与准确性。
IDA Python Runtime初始化报错:多版本环境冲突的根源与隔离启动方案
本文深入分析了IDA Pro在初始化Python运行时环境时遇到的多版本冲突问题,提供了详细的解决方案和隔离启动脚本。通过定制化批处理脚本,可以有效避免Python 2和Python 3环境冲突,确保IDA稳定运行,特别适合逆向工程和恶意软件分析场景。
你的量化策略回测不准?可能是K线周期数据没对齐!用Python检查并修复通达信数据(实战案例)
本文探讨了量化策略回测中K线周期数据对齐问题,通过Python实战案例解析通达信5分钟数据特性,并提供时间轴校准四步法和数据质量检查工具,帮助开发者避免回测与实盘表现差异。重点解决时间戳错位、休市时间处理等常见陷阱,提升量化交易策略的准确性。
RTL8211E、RTL8211EG-VB-CG选型与接口实战:MII、RMII、RGMII到底该怎么接?
本文深入解析RTL8211E系列PHY芯片的选型与接口设计,重点对比MII、RMII、RGMII等千兆网络接口的优劣,并提供实际PCB设计规范和调试技巧。针对RTL8211E-VB-CG、VL-CG、EG-VB-CG等不同型号,给出电源架构、信号完整性和EMI优化的专业建议,帮助工程师在工业控制和嵌入式系统中实现稳定可靠的千兆以太网连接。