当前位置: 首页 > news >正文

FPGA实战:从零实现IIC主机控制器,深入时序与状态机设计

1. 项目概述:从时序理解到硬件实现

搞FPGA的兄弟,估计没人能绕过IIC这个坎儿。无论是调个实时时钟芯片DS1307,还是读写个EEPROM存点配置参数,IIC总线就像个无处不在的“老熟人”。现在很多MCU和SoC都内置了IIC控制器,配配寄存器、调调库函数就能用,方便是方便,但总感觉隔了一层纱,知其然不知其所以然。尤其是当我们用FPGA这种“白纸”去实现它时,如果对时序的理解只停留在“起始、停止、应答”这几个名词上,那调试起来绝对是噩梦连连。

我这次的项目,就是抛开现成的IP核,用Verilog从零开始,实现一个能够向AT24C16这类EEPROM芯片写入数据的IIC主机控制器。目的很纯粹:不是追求最优化、最通用的IP,而是通过亲手搭建每一个状态、控制每一根信号线的跳变,把IIC协议那张“时序图”真正刻进脑子里。这个过程里,你会遇到几个非常具体且折磨人的问题:怎么精准产生那个100kHz或400kHz的IIC时钟(SCL)?怎么在SCL的低电平期间去改变数据线(SDA)?又怎么在需要的时候,“夺过”SCL的控制权,强制它拉高以产生起始(START)和停止(STOP)条件?这些看似基础的细节,恰恰是理解IIC总线主设备核心思想的关键。

本文将详细拆解我的实现过程,重点会放在“如何用硬件描述语言去翻译时序要求”以及“调试过程中那些让人拍大腿的坑”上。无论你是FPGA初学者想啃下IIC这块硬骨头,还是有一定经验想巩固一下底层通信协议的理解,相信这篇从实战中总结的日记都能给你带来些不一样的启发。

2. IIC协议核心思想与硬件实现难点剖析

2.1 协议再解读:不止于“起始、停止、应答”

很多人学IIC,都是从一张经典的时序图开始,记住“SCL高电平时SDA下降沿是起始,上升沿是停止”、“第9个时钟是应答位”就以为掌握了。但要用硬件实现,必须把这些“图形规则”翻译成可执行的“状态逻辑”。

首先,数据的有效性。协议规定,SDA数据线必须在SCL时钟线为高电平期间保持稳定。这意味着,SDA的电平变化,只允许发生在SCL为低电平的“窗口期”内。这个规则是总线多设备共存、避免冲突的基石。对我们的主设备设计而言,就必须有一个清晰的机制来标识“当前是否是SCL的低电平区间”,并且只在这个区间内更新要发送的数据(SDA输出)。

其次,起始(S)和停止(P)条件。它们被定义为“总线条件”,而不是简单的数据位。关键在于,它们发生的时候,SCL线必须是高电平。这就引出了一个核心矛盾:在正常数据传输时,SCL是由我们主设备产生的、周期性的时钟。但在产生S和P时,我们需要在SCL已经是高电平的前提下,再去操控SDA产生跳变。这要求我们的时钟生成逻辑必须具备“被覆盖”的能力,即当状态机需要发起S或P时,能暂时忽略内部生成的周期时钟,强制将SCL输出拉高并保持,然后操纵SDA。

最后,应答(ACK)机制。发送方(主或从)每发送完8位数据,在第9个时钟周期需要释放SDA线(输出高阻态或1),并在这个时钟周期内检测SDA线是否被接收方拉低。对于主设备来说,写操作时,主设备在第9个时钟周期是接收方(等待从机应答),需要将SDA方向设置为输入(input)并采样;读操作时,主设备在第9个时钟周期是发送方(发出应答或非应答信号),需要控制SDA输出0或1。这个方向切换(Tri-state Buffer控制)必须在正确的时钟边沿完成,否则会总线冲突。

2.2 硬件实现的三个核心挑战

基于以上解读,用FPGA实现IIC主设备,尤其是写操作,会聚焦到三个具体挑战上,这也是我设计时的思考重点:

  1. 精准的时钟生成与相位控制:IIC总线速度模式有标准模式(100kbps)、快速模式(400kbps)等。我们的系统时钟(如50MHz)频率远高于IIC时钟。如何分频产生一个精准、稳定的SCL时钟?更重要的是,如何在这个慢速时钟的“低电平区间”内,找到一个稳定的“点”来改变SDA?直接使用分频时钟的下降沿?这可能会因为布线延迟导致SDA变化太靠近SCL低电平的边沿,违反建立/保持时间。一个常见的稳健做法是生成一个更高频率的“工作时钟”,用它来对SCL进行“过采样”,从而在SCL低电平的“中间位置”去改变SDA,留出充足的裕量。

  2. 总线控制权的动态切换:正常数据传输时,SCL由我们的分频计数器循环产生。但当状态机需要发起起始条件时,流程是:确保当前SCL为高 -> 控制SDA从高变低 -> 再释放SCL控制权,让其开始产生时钟。这个“确保SCL为高”和“强制拉高SCL”的操作,要求我们的SCL输出逻辑不是一个简单的分频时钟输出,而是一个多路选择器:在“空闲”和“产生S/P条件”状态下,输出强制高电平;在“数据传输”状态下,输出分频产生的周期时钟。

  3. 状态机的清晰划分:IIC一次完整的操作包含多个阶段:起始、发送设备地址(含读写位)、等待应答、发送内存地址(对于EEPROM写)、等待应答、发送数据字节、等待应答……可能还有停止。用一个大状态机(如一个大case语句)硬编码所有时钟节拍(bit)的状态,代码会极其冗长且难以修改。更好的方法是设计一个“层级化”的状态机:顶层状态机(IDLE, START, ADDR, DATA, STOP等)控制流程;底层由一个比特位计数器(bit counter)和字节计数器(byte counter)来驱动每个阶段内的时钟节拍。这样结构更清晰,也易于扩展支持读操作和多字节传输。

3. 设计思路与架构:分层状态机与时钟策略

3.1 整体架构设计

我的设计目标是一个专注于写操作的IIC主控制器,针对AT24C16。整体架构分为几个核心模块:

  • 时钟分频与使能生成模块:负责从系统时钟(sys_clk, 如50MHz)产生IIC总线时钟(SCL)的基础频率,并生成关键的“数据变化使能”信号。
  • 主控制状态机(Top-level FSM):定义整个写操作的生命周期状态,如IDLE,START,SEND_DEV_ADDR,WAIT_ACK1,SEND_WORD_ADDR_H,SEND_WORD_ADDR_L,WAIT_ACK2,SEND_DATA,WAIT_ACK3,STOP等。
  • 比特/字节发送引擎:这是一个子模块或嵌入在状态机中的逻辑。它接收顶层状态机的命令(如“发送一个字节”),然后利用底层时钟使能信号,按位将数据移出到SDA线上,并在第9个时钟周期处理应答。
  • SDA数据输出与方向控制逻辑:根据当前状态和比特位置,决定SDA引脚是输出0、输出1,还是设置为高阻输入(用于接收应答)。这通常通过一个多路选择器(MUX)实现,输出到FPGA IO引脚的三态门控制端。

3.2 核心策略:4倍频过采样与中点采样

这是解决“何时改变SDA”问题的关键,也是我参考了许多设计后认为最稳健的方案。

  1. 生成基础IIC时钟(clk_50k):我的系统时钟是50MHz,目标IIC速度是100kHz(标准模式)。需要分频系数为 50,000,000 / 100,000 = 500。但注意,一个完整的SCL时钟周期包含高电平和低电平。为了控制占空比(通常为50%),我们需要计数到500时翻转。但我的原始代码中为了简化,直接计数到1000,用其中一段区间(如counter_div=375~875)为高电平,其他为低电平,来产生一个近似的50kHz时钟。这里第一个注意点:分频计数器的最大值和电平翻转点直接决定了SCL的频率和占空比,必须计算准确。

    // 示例:生成使能信号而非时钟,更易于同步设计 parameter SCL_PERIOD = 500; // 50MHz / 100kHz = 500 reg [8:0] scl_counter; reg scl_en; // 用于产生SCL时钟的使能信号 wire scl_out; // 最终的SCL输出 always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin scl_counter <= 0; scl_en <= 0; end else begin if (scl_counter == SCL_PERIOD - 1) begin scl_counter <= 0; scl_en <= 1'b1; // 每个SCL周期产生一个脉冲使能 end else begin scl_counter <= scl_counter + 1; scl_en <= 1'b0; end end end // 利用使能信号在合适的计数器点控制SCL高低电平
  2. 生成4倍频工作时钟(clk_200k):这是精髓所在。我们不对这个时钟进行输出,而是用它来“观察”和“控制”SCL周期。将SCL的一个周期(对应计数器0~999)平分为4个相位区间(0-249, 250-499, 500-749, 750-999)。这样,我们就能在SCL低电平的“正中间”(比如第二个相位区间的中点)和SCL高电平的“正中间”产生稳定的使能信号。

  3. 数据变化时机:规定在clk_200k的某个特定上升沿(例如,对应SCL低电平中间点的那个上升沿),去更新SDA线上要发送的下一个比特。这样,SDA的新值会在当前SCL低电平的中间位置稳定建立,并一直保持到下一个SCL低电平的中间点。这为SDA信号在SCL上升沿到来之前(建立时间)和之后(保持时间)提供了充足的稳定窗口,完全符合协议要求。

3.3 起始与停止条件的硬件实现

这是动态切换总线控制权的体现。我们不能直接用clk_50k作为SCL的输出。

  1. 构建SCL输出逻辑:SCL的输出应该由一个多路选择器决定。

    • IDLE状态:SCL应被上拉电阻拉高,我们输出高阻态(或逻辑1)即可。但在设计内部,我们可以强制SCL_out = 1'b1,表示主设备释放时钟线。
    • START状态:在发出起始条件前,需要先确保SCL为高。因此,在进入START状态后,首先强制SCL_out = 1'b1,并保持一段时间(几个系统时钟周期)。然后,在SCL为高的前提下,控制SDA_out从1变为0。之后,状态跳转到发送地址状态,此时才将SCL_out的控制权交还给分频计数器产生的周期时钟(clk_50k)。
    • STOP状态:在发送完最后一个应答位后,需要产生停止条件。此时,SCL应为低(因为刚结束第9个时钟)。我们需要先控制SCL_out从分频时钟切换到强制拉高。当SCL稳定为高后,再控制SDA_out从0变为1。完成后,回到IDLE状态。
    // SCL输出控制逻辑示例 reg scl_force_high; // 来自状态机的强制拉高信号 wire scl_generated; // 分频产生的周期时钟(0/1交替) // 多路选择器 assign scl_out = (scl_force_high) ? 1'b1 : scl_generated; // 在状态机中控制 scl_force_high always @(*) begin case (current_state) IDLE: scl_force_high = 1'b1; // 空闲时SCL高 START: scl_force_high = 1'b1; // 起始阶段强制高 SEND_ADDR, SEND_DATA: scl_force_high = 1'b0; // 数据传输时用生成的时钟 STOP_BEGIN: scl_force_high = 1'b1; // 停止条件开始,强制拉高SCL default: scl_force_high = 1'b0; endcase end

4. Verilog代码实现与关键模块解析

4.1 时钟生成模块的优化实现

原始代码中使用了一个计数器(counter_div)同时生成clk_50kclk_200k,并通过比较器设定高低电平区间。这种方法直观,但将时钟作为寄存器输出可能在某些综合工具中产生毛刺或时序问题。更推荐的方法是生成时钟使能信号,并在系统时钟同步下控制SCL和SDA寄存器。

以下是优化后的核心代码思路:

module iic_clock_gen ( input wire sys_clk, input wire sys_rst_n, input wire iic_enable, // 使能时钟生成 output reg scl_out, output wire sda_change_pos, // SDA数据变化正沿(在SCL低电平中点) output wire scl_sample_pos // SCL采样正沿(在SCL高电平中点) ); parameter SYS_CLK_FREQ = 50_000_000; parameter IIC_CLK_FREQ = 100_000; localparam DIVIDER = SYS_CLK_FREQ / (IIC_CLK_FREQ * 4); // 4倍过采样,计算每相位计数 reg [15:0] phase_counter; reg [1:0] phase; // 0,1,2,3 对应SCL周期的4个相位 // 相位计数器 always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin phase_counter <= 0; phase <= 0; end else if (!iic_enable) begin phase_counter <= 0; phase <= 0; end else begin if (phase_counter == DIVIDER - 1) begin phase_counter <= 0; phase <= phase + 1; // 每计数DIVIDER次,进入下一相位 end else begin phase_counter <= phase_counter + 1; end end end // 根据相位生成SCL和使能信号 always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin scl_out <= 1'b1; // 空闲时SCL为高 end else if (iic_enable) begin // 假设相位0和1为SCL低电平,相位2和3为SCL高电平 scl_out <= (phase >= 2) ? 1'b1 : 1'b0; end else begin scl_out <= 1'b1; end end // 关键:在相位1的末尾(SCL低电平中点)产生SDA变化使能 assign sda_change_pos = (phase_counter == DIVIDER-1) && (phase == 1); // 关键:在相位3的末尾(SCL高电平中点)产生采样使能(用于读取SDA) assign scl_sample_pos = (phase_counter == DIVIDER-1) && (phase == 3); endmodule

关键点解析

  • phase计数器将每个SCL周期分为4个相等的相位,实现了4倍过采样。
  • sda_change_pos信号在phase==1结束时(即SCL低电平的中间点)产生一个系统时钟周期的高脉冲。我们的状态机应该在这个脉冲到来时,更新SDA输出寄存器为下一个要发送的比特。
  • scl_sample_pos信号在phase==3结束时(即SCL高电平的中间点)产生脉冲。用于在SCL高电平稳定期间采样SDA输入线,读取从设备的应答或数据。这是读取操作的关键,对于写操作,我们主要用SDA_change_pos

4.2 主控制状态机设计

状态机设计采用经典的三段式(状态声明、状态转移、状态输出),清晰且利于综合。

module iic_master_write ( input wire sys_clk, input wire sys_rst_n, input wire start, // 外部触发开始一次写操作 input wire [6:0] dev_addr, // 7位从设备地址 input wire [15:0] word_addr, // 16位内存地址(针对AT24C16) input wire [7:0] data_in, // 要写入的数据 output reg iic_busy, // 总线忙标志 output reg sda_out, // SDA数据输出 output reg sda_tri_en, // SDA三态使能,0-输出,1-高阻(输入) output reg scl_out, // 连接到时钟生成模块的使能信号 output reg clk_gen_en, input wire sda_change_pos, input wire scl_sample_pos ); // 状态定义 localparam [3:0] S_IDLE = 4'd0, S_START = 4'd1, S_SEND_ADDR = 4'd2, S_WAIT_ACK1 = 4'd3, S_SEND_ADDR_H = 4'd4, // 发送内存地址高字节 S_WAIT_ACK2 = 4'd5, S_SEND_ADDR_L = 4'd6, // 发送内存地址低字节 S_WAIT_ACK3 = 4'd7, S_SEND_DATA = 4'd8, S_WAIT_ACK4 = 4'd9, S_STOP = 4'd10; reg [3:0] current_state, next_state; reg [3:0] bit_cnt; // 0-7 发送数据位,8为应答位 reg [7:0] shift_reg; // 移位寄存器,存放正在发送的字节 reg ack_received; // 应答接收标志 reg scl_force_high; // 强制SCL高电平信号 // 状态转移逻辑(第一段) always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) current_state <= S_IDLE; else current_state <= next_state; end // 状态机组合逻辑(第二段) always @(*) begin next_state = current_state; case (current_state) S_IDLE: if (start) next_state = S_START; S_START: if (scl_sample_pos) next_state = S_SEND_ADDR; // 等待起始条件建立 S_SEND_ADDR: if ((bit_cnt == 4'd7) && sda_change_pos) next_state = S_WAIT_ACK1; S_WAIT_ACK1: if (scl_sample_pos) next_state = (ack_received) ? S_SEND_ADDR_H : S_STOP; // 无应答则停止 S_SEND_ADDR_H: if ((bit_cnt == 4'd7) && sda_change_pos) next_state = S_WAIT_ACK2; S_WAIT_ACK2: if (scl_sample_pos) next_state = (ack_received) ? S_SEND_ADDR_L : S_STOP; S_SEND_ADDR_L: if ((bit_cnt == 4'd7) && sda_change_pos) next_state = S_WAIT_ACK3; S_WAIT_ACK3: if (scl_sample_pos) next_state = (ack_received) ? S_SEND_DATA : S_STOP; S_SEND_DATA: if ((bit_cnt == 4'd7) && sda_change_pos) next_state = S_WAIT_ACK4; S_WAIT_ACK4: if (scl_sample_pos) next_state = (ack_received) ? S_STOP : S_STOP; // 假设总是成功,可跳回S_SEND_DATA发下一字节 S_STOP: if (scl_sample_pos) next_state = S_IDLE; // 等待停止条件建立 endcase end // 比特计数器控制 always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) bit_cnt <= 0; else begin case (current_state) S_SEND_ADDR, S_SEND_ADDR_H, S_SEND_ADDR_L, S_SEND_DATA: begin if (sda_change_pos) begin if (bit_cnt == 4'd8) bit_cnt <= 0; else bit_cnt <= bit_cnt + 1; end end S_WAIT_ACK1, S_WAIT_ACK2, S_WAIT_ACK3, S_WAIT_ACK4: begin if (scl_sample_pos) bit_cnt <= 0; // 应答位采样后清零计数器 end default: bit_cnt <= 0; endcase end end // 移位寄存器与SDA输出控制(第三段 - 输出逻辑) always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begin shift_reg <= 8'h00; sda_out <= 1'b1; sda_tri_en <= 1'b0; // 默认输出模式 ack_received <= 1'b0; scl_force_high <= 1'b1; clk_gen_en <= 1'b0; iic_busy <= 1'b0; end else begin // 默认值 scl_force_high <= 1'b0; clk_gen_en <= 1'b1; // 非IDLE状态使能时钟生成 iic_busy <= 1'b1; case (current_state) S_IDLE: begin sda_out <= 1'b1; sda_tri_en <= 1'b0; scl_force_high <= 1'b1; // 空闲时强制SCL高 clk_gen_en <= 1'b0; // 关闭时钟生成以省电 iic_busy <= 1'b0; end S_START: begin scl_force_high <= 1'b1; // 起始条件,强制SCL高 if (sda_change_pos) sda_out <= 1'b0; // 在SCL高期间拉低SDA end S_SEND_ADDR: begin sda_tri_en <= 1'b0; // 输出模式 if (sda_change_pos) begin if (bit_cnt == 0) shift_reg <= {dev_addr, 1'b0}; // 装载地址+写位 sda_out <= shift_reg[7]; // 发送最高位 shift_reg <= {shift_reg[6:0], 1'b0}; // 左移 end end S_WAIT_ACK1: begin sda_tri_en <= 1'b1; // 释放SDA(高阻),准备读取ACK if (scl_sample_pos) begin // 假设sda_in是输入的SDA信号,这里需要从端口读取 // ack_received <= ~sda_in; // ACK为低电平有效 ack_received <= 1'b1; // 简化,假设总是收到ACK end end // ... 其他发送状态类似 S_SEND_ADDR S_SEND_DATA: begin sda_tri_en <= 1'b0; if (sda_change_pos) begin if (bit_cnt == 0) shift_reg <= data_in; // 装载要写的数据 sda_out <= shift_reg[7]; shift_reg <= {shift_reg[6:0], 1'b0}; end end S_STOP: begin scl_force_high <= 1'b1; // 准备停止条件,先强制SCL高 sda_tri_en <= 1'b0; // 当前SDA应为低(来自ACK位或最后一位) if (scl_sample_pos) sda_out <= 1'b1; // SCL高后,SDA从低变高 end endcase end end // SCL输出最终选择 assign scl_out = scl_force_high ? 1'b1 : (clk_gen_en ? /* 来自clock_gen的scl_out */ : 1'b1); endmodule

代码要点与避坑指南

  1. 状态划分要细:将发送地址、发送数据、等待应答都分开成独立状态,逻辑更清晰。发送内存地址高字节和低字节也分开,便于处理16位地址。
  2. 比特计数器是关键bit_cnt从0计数到7,发送8位数据;在第8个sda_change_pos(对应bit_cnt==7)后,状态跳转到等待应答。在等待应答状态,bit_cnt被清零,为下一个字节发送做准备。
  3. SDA方向控制:在发送状态(S_SEND_*),sda_tri_en=0,FPGA引脚配置为输出。在等待应答状态(S_WAIT_ACK*),sda_tri_en=1,FPGA引脚配置为高阻输入,同时内部上拉电阻将总线拉高,此时从机可以拉低SDA表示应答。务必在FPGA约束文件中正确设置SDA引脚为双向(inout)
  4. 应答采样:在S_WAIT_ACK状态,利用scl_sample_pos(SCL高电平中点)去采样SDA输入线。如果读到0,表示ACK有效。实际设计中需要将sda_in端口信号同步到系统时钟域再采样,防止亚稳态。
  5. 起始/停止条件的顺序:代码中体现了:START状态先强制SCL高并保持,再在合适的时机拉低SDASTOP状态先确保SCL高(可能来自上一个ACK位的低电平后的自动拉高,或强制拉高),再拉高SDA

5. 仿真、调试与常见问题实录

5.1 仿真环境搭建与关键观察点

理论设计完成,必须通过仿真验证。我使用ModelSim/QuestaSim,编写了一个简单的AT24C16行为模型作为从设备。仿真中要重点观察以下几点:

  1. 时序参数:测量SCL时钟频率是否准确为100kHz(周期10us)。测量START条件中,SDA下降沿时SCL是否稳定为高;STOP条件中,SDA上升沿时SCL是否稳定为高。
  2. 数据建立保持时间:放大看SCL上升沿附近,SDA信号是否稳定(无毛刺),其变化点是否只在SCL低电平期间。利用sda_change_posscl_sample_pos的标记,检查它们是否出现在预期位置。
  3. 应答周期:在第9个SCL周期,主设备的SDA是否变为高阻(表现为信号线变蓝或高阻态Z),从设备模型是否在此时将SDA拉低。主设备是否在SCL高电平期间正确采样到了这个低电平。
  4. 状态机流转:观察状态跳转是否与比特计数器、使能信号对齐。特别是在每个字节发送完(bit_cnt==7)和应答采样后,状态是否正确跳转。

一个典型的仿真错误:SDA的变化刚好卡在SCL的上升沿。这大概率是因为数据更新使能信号(sda_change_pos)的生成与SCL的边沿对齐了,而不是在SCL低电平中点。解决方法就是坚持使用4倍频过采样,在低电平的中间相位去更新SDA。

5.2 上板调试问题与解决方案

仿真通过后,烧录到FPGA开发板,连接真实的AT24C16芯片,用逻辑分析仪(如Saleae)抓取波形。这里才是“坑”最多的地方。

  1. 问题一:总线无响应,SCL或SDA始终为高。

    • 排查:首先检查物理连接,上拉电阻(通常4.7kΩ)是否接好。用万用表测量SDA和SCL线在不通信时的电压,应为电源电压(如3.3V)。如果为低,可能存在硬件短路或FPGA引脚配置错误(冲突输出0)。
    • 检查FPGA引脚约束:确认SDA引脚被定义为inout类型,并且在综合实现后,该IOB确实被配置为三态门。在Vivado/Quartus的Pin Planner中仔细核对。
    • 检查内部上拉:有些FPGA需要在代码中或约束中使能内部弱上拉。可以尝试在代码中,当SDA为高阻时,对其赋值为1'bz,并依赖外部上拉电阻。
  2. 问题二:能抓到起始信号,但地址发送后无应答(ACK)。

    • 排查:逻辑分析仪显示主设备发送完8位地址(7位地址+1位写0)后,在第9个时钟周期,SDA线没有被拉低。
    • 可能原因1:从设备地址错误。AT24C16的7位设备地址是1010(A6-A3) +A2A1A0(引脚电平)。确认你的硬件连接(A2,A1,A0引脚接GND还是VCC)与代码中dev_addr设置是否一致。常见错误:忽略了地址的最低位是读写位,发送时应该是{dev_addr, 1'b0}
    • 可能原因2:SDA方向切换时机不对。逻辑分析仪查看第9个SCL周期,主设备的SDA输出是否真的变成了高阻(波形显示为中间电平或明显变弱)。如果还是稳定的高电平,说明SDA_tri_en信号没有在第9个时钟周期到来前及时变为1。检查状态机在bit_cnt==7时的跳转,以及SDA_tri_enS_WAIT_ACK状态是否被正确设置为1。
    • 可能原因3:从设备忙。EEPROM在写入周期内(Page Write或Byte Write后)会不响应,直到写入完成。确保两次写操作之间有足够的延时(AT24C16的页写入周期最长5ms)。可以在发送STOP条件后,主设备延迟一段时间再发起下一次操作。
  3. 问题三:写入数据后,读回的数据不正确。

    • 先单独测试读操作:如果读逻辑也有,先确保读功能正确。
    • 排查写地址:AT24C16的地址是16位(2字节)。确认你发送的内存地址高字节和低字节顺序正确,并且地址值没有超出芯片容量范围。
    • 检查页边界:AT24C16的页大小为16字节。如果你连续写入的数据跨越了页边界(例如从地址15开始写10个字节),从地址16开始的数据会回卷到当前页的开头(地址0)覆盖之前的数据。这是EEPROM的常见特性。你的驱动需要处理页边界,在跨页时重新发送起始条件和新的内存地址。
    • 用逻辑分析仪对比波形:将一次成功的写操作波形(从起始到停止)完整保存下来,与官方数据手册的时序图逐段对比,特别是地址、数据位的值,以及各个信号之间的时间关系。

5.3 性能优化与扩展思考

  1. 支持多字节页写入:当前设计是单字节写入。要支持页写入,需要在状态机中增加一个循环。在发送完第一个数据字节并收到ACK后,不发送STOP,而是继续装载下一个数据字节,跳回S_SEND_DATA状态。同时,内部需要有一个字节计数器,并检查地址是否到达页边界(地址低4位是否为1111),到达后必须结束当前页写入。
  2. 添加读操作支持:读操作更复杂,涉及“伪写”发送地址、重复起始条件(Repeated Start)、发送设备地址(读位)、接收数据并发送应答/非应答。状态机需要增加S_SEND_RESTART,S_SEND_READ_ADDR,S_READ_DATA,S_SEND_ACK_NAK等状态。SDA的方向控制逻辑也会更频繁地切换。
  3. 参数化设计:将系统时钟频率、IIC总线速度、从设备地址宽度、数据地址宽度等定义为模块参数,提高代码复用性。
  4. 添加超时与错误恢复:当前状态机假设从设备总是应答。实际中应添加超时机制,如果在S_WAIT_ACK状态等待一段时间(如几个SCL周期)后仍未收到ACK,则跳转到错误处理或STOP状态,避免总线死锁。

通过这个从零实现IIC主设备的过程,我最大的体会是,通信协议的理解一定要落到具体的时钟边沿和信号电平上。看十遍协议文档不如动手写一次状态机,调一次波形。当你用逻辑分析仪看到自己代码产生的波形与数据手册严丝合缝时,那种对协议透彻掌握的感觉,是调用任何现成库函数都无法带来的。这个项目虽然只实现了基础的写功能,但它为你打开了一扇门,之后再去实现SPI、UART,甚至更复杂的协议,你都会拥有清晰的思路和调试的信心。

http://www.jsqmd.com/news/968288/

相关文章:

  • OBS多平台推流终极指南:3步实现一键多平台直播
  • 农夫划船带狼羊菜过河的Python互动动画游戏(含源码和可执行程序)
  • 如何将CAJ格式文献快速转换为PDF:caj2pdf开源工具终极指南
  • 海口市有哪些官方授权的CPPM注册职业采购经理培训机构? - 众智商学院课程中心
  • 抖音无水印视频下载全攻略:douyin-downloader轻松搞定
  • 西电XDOJ 2023期末C语言真题实战包:数组操作、字符串处理、数学建模与信号解调全涵盖
  • 滚动页面时自动贴边的侧边栏JS工具(带节流和自适应高度)
  • 从“记住我”到“控制你”:Shiro 550漏洞实战复现与一键检测脚本分享
  • 99%的工程师都不知道,PCB板失效的原因
  • 3分钟掌握NFC卡片管理:Windows平台最强Mifare工具完全指南
  • 强力指南:如何用PySD快速构建系统动力学模型
  • LaserGRBL:从零开始掌握专业激光雕刻控制软件
  • 如何快速实现Switch手柄PC适配:3层架构深度解析
  • Android应用里每秒跑一次的随机数生成小demo(带完整源码)
  • [智能体-301]:Chroma向量数据库详解,包括主要接口,代码示例
  • 从网页IM状态集成到现代客服组件:原理、演进与实战
  • Intel TBB 2019 Update 8(2019年6月5日发布)Windows全功能开发包
  • Java电商项目沙箱支付全流程演示包(含下单、签名、回调模拟)
  • 2026年宁波市PMP培训机构哪家好?官方授权R.E.P.报考指南 - 众智商学院课程中心
  • 掌握Windows与Office智能激活解决方案:KMS_VL_ALL_AIO专业指南
  • JavaWeb 全套教程 乱码问题 85-88
  • 串口通信:查询与中断模式详解及实战应用
  • VCC、VDD、VEE、VSS电源符号的起源、区别与PCB设计实战
  • STM32L431 STOP模式实测:LPUART收数据或RTC定时都能唤醒,功耗稳、响应快
  • Windows体检套餐配置工具:C#写的桌面程序,增删项目+自动算总价
  • 如何快速单独编译LibreDWG的dwg2dxf工具:轻量级CAD文件转换方案
  • 保姆级教程:用端口转发搞定跨网段打印机共享(潘多拉/Padavan固件实测)
  • 2026年佛山市PMP培训机构哪家好?官方授权R.E.P.报考指南 - 众智商学院课程中心
  • 工程师职场生存指南:从技术实力到沟通表达与职业网络构建
  • 星露谷物语模组开发终极指南:用SMAPI打造你的专属农场