FPGA上VHDL实现I2C主机:STOP条件生成及时序问题
关于I2C主机STOP条件生成及时序同步的问题
问题描述
我在FPGA上使用VHDL实现I2C主机,目前遇到STOP条件无法正常生成的问题——传输过程中其他环节均正常,但总线上始终没有出现STOP条件。
根据测试所用DS1307 RTC从设备的数据手册,STOP条件的定义为:当SCL处于高电平时,SDA线从低电平跳变到高电平。我有以下疑问:
- 是否需要中断正常周期性SCL波形,手动强制SCL为高后再释放SDA?还是仅在SCL上升沿拉高SDA即可?
- 如何确保整个状态机与SCL生成同步?
- 如何保证SDA和SCL信号在状态转换间有足够的建立/保持时间?
- 是否有通用的时序规范可供遵循?
参考信息:SCL频率为100kHz
当前实现代码
library IEEE; use IEEE.STD_LOGIC_1164.ALL; use IEEE.NUMERIC_STD.ALL; entity i2c is generic ( scl_clk : integer := 500 -- SCL half-period (in system clocks) ); port ( clk : in std_logic; --100 MHz clk SDA : inout std_logic; --data SCL : out std_logic; --synch clk button : in std_logic; --button ack_ok : out std_logic --send ack after reading ); end entity; architecture execute of i2c is -------------------------------------------------------------------- -- Internal signals -------------------------------------------------------------------- signal shift_reg : unsigned(7 downto 0) := (others => '0'); --for serial transmission through SDA signal bit_cnt : integer range 0 to 7 := 7; --bit counter signal scl_count2 : integer := 0; signal scl_count : integer := 0; --synch clk counter signal scl_trig : std_logic := '0'; --trig signal signal scl_en : std_logic := '0'; -- internal signal for synch clk signal sda_bus : std_logic := '1'; -- 0 = drive SDA low, Z = release signal scl_bus : std_logic := '1'; -- 0 = drive SCL low, Z = release signal busy : std_logic := '0'; type byte_array is array (0 to 31) of std_logic_vector(7 downto 0); -- byte array for transmissions signal tx_buffer : byte_array; signal tx_len : integer range 0 to 31 := 0; --how many bits we'll send signal tx_index : integer range 0 to 31 := 0; --counter till we have gone through all bytes -- states type statetype is ( IDLE, START1, START2, SEND_BITS, NEXT_BYTE, READY_ACK, ACK_HIGH, ACK_CONFIRM, STOP1, STOP2 ); signal state : statetype := IDLE; begin -------------------------------------------------------------------- -- Open-drain outputs -------------------------------------------------------------------- SDA <= '0' when sda_bus = '0' else 'Z'; SCL <= '0' when scl_bus = '0' else 'Z'; -------------------------------------------------------------------- -- SCL clock generator -------------------------------------------------------------------- clock_div : process(clk) begin if rising_edge(clk) then if scl_count = (scl_clk - 1) then scl_count <= 0; scl_en <= not scl_en; else scl_count <= scl_count + 1; end if; end if; end process; process(clk) begin if rising_edge(clk) then if scl_count2 = (scl_clk - 1) then scl_count2 <= 0; scl_trig <= '1'; else scl_count2 <= scl_count2 + 1; scl_trig <= '0'; end if; end if; end process; -------------------------------------------------------------------- -- I2C state machine -------------------------------------------------------------------- process(clk) begin if rising_edge(clk) then -- Trigger transmission if button = '1' and busy = '0' then bit_cnt <= 7; tx_buffer(0) <= "01111000"; -- 0x3C << 1 (OLED address) tx_buffer(1) <= "10101111"; -- control byte: command tx_buffer(2) <= "00000000"; tx_buffer(3) <= "11111111"; tx_len <= 2; tx_index <= 0; shift_reg <= unsigned(tx_buffer(0)); state <= START1; end if; -- State machine case state is when IDLE => scl_bus <= '1'; sda_bus <= '1'; busy <= '0'; when START1 => busy <= '1'; if scl_trig = '1' then sda_bus <= '0'; -- Start: SDA falls while SCL is HIGH state <= START2; end if; when START2 => if scl_trig = '1' then scl_bus <= scl_en; state <= SEND_BITS; end if; when SEND_BITS => scl_bus <= scl_en; if scl_en = '0' and scl_trig = '1' then sda_bus <= shift_reg(bit_cnt); end if; if scl_trig = '1' and scl_en = '1' then if bit_cnt = 0 then state <= READY_ACK; else bit_cnt <= bit_cnt - 1; end if; end if; when READY_ACK => scl_bus <= scl_en; if scl_trig = '1' then state <= ACK_HIGH; end if; when ACK_HIGH => scl_bus <= scl_en; if scl_trig = '1' then state <= ACK_CONFIRM; end if; when ACK_CONFIRM => scl_bus <= scl_en; if scl_bus = '1' then ack_ok <= not SDA; if scl_trig = '1' then if tx_index = tx_len then state <= STOP1; else state <= NEXT_BYTE; end if; end if; end if; when NEXT_BYTE => if scl_trig = '1' then tx_index <= tx_index + 1; bit_cnt <= 7; shift_reg <= unsigned(tx_buffer(tx_index + 1)); state <= SEND_BITS; end if; when STOP1 => if scl_trig = '1' then scl_bus <= '1'; sda_bus <= '0'; state <= STOP2; end if; when STOP2 => sda_bus <= '1'; if scl_trig = '1' then busy <= '0'; state <= IDLE; end if; when others => state <= IDLE; end case; end if; end process; end architecture;
问题分析与解决方案
1. STOP条件生成的核心问题
你当前的STOP状态逻辑存在时序错误:进入STOP2时直接拉高SDA,没有给SCL高电平留足够的稳定窗口,导致总线无法捕捉到SDA的跳变。
正确做法:必须中断正常SCL周期,手动强制SCL保持高电平,再完成SDA的拉高动作,且要保证SCL高电平持续足够时间,让从设备检测到STOP条件。不能仅在SCL上升沿拉高SDA——正常SCL高电平周期只有5us(100kHz下),跳变需要发生在SCL高电平的稳定阶段,而非上升沿瞬间。
修改后的STOP状态示例:
when STOP1 => scl_bus <= '1'; -- 强制SCL保持高电平 sda_bus <= '0'; -- 确保SDA处于低电平 if scl_count = (scl_clk - 1) then -- 等待一个完整的SCL高电平周期(5us) state <= STOP2; end if; when STOP2 => scl_bus <= '1'; -- 继续保持SCL高电平 sda_bus <= '1'; -- SDA从低跳变到高,生成STOP条件 if scl_count = (scl_clk - 1) then -- 保持STOP条件足够时间 busy <= '0'; state <= IDLE; end if;
2. 状态机与SCL同步问题
你当前用了两个独立计数器scl_count和scl_count2生成scl_en和scl_trig,容易出现时序错位。应该复用同一个计数器,确保状态机触发信号与SCL时钟完全同步:
-- 合并后的SCL时钟生成和触发信号 clock_div : process(clk) begin if rising_edge(clk) then if scl_count = (scl_clk - 1) then scl_count <= 0; scl_en <= not scl_en; scl_trig <= '1'; -- 每次计数器溢出时触发状态转换 else scl_count <= scl_count + 1; scl_trig <= '0'; end if; end if; end process;
3. 建立/保持时间保证
- 建立时间:发送数据时,要确保SDA在SCL上升沿到来前至少250ns(I2C标准模式
t_SU_DAT)稳定。你当前在SCL低电平期间更新SDA的逻辑是正确的,符合规范。 - 保持时间:SDA要在SCL下降沿后至少保持0ns(
t_HD_DAT),实际实现中可留少量余量,避免信号提前变化。
4. 通用I2C时序规范(100kHz标准模式)
- SCL高电平最小时间:
t_HIGH = 4.7us - SCL低电平最小时间:
t_LOW = 4.7us - START条件:SDA下降沿到SCL下降沿最小时间
t_SU_STA = 4.7us - STOP条件:SDA上升沿到SCL下一个上升沿最小时间
t_SU_STO = 4.7us - 数据建立时间:
t_SU_DAT = 250ns,保持时间:t_HD_DAT = 0ns
内容的提问来源于stack exchange,提问作者Gaming x Juan
相关产品推荐
相关产品推荐

