本文主要分为几部分,开始是老生常谈的I2C基础,然后是手撕I2C设计,最后是实际项目中使用的I2C协议:
一、I2C基础
优快云一大堆这种帖子,实际毫无意义,最详细的建议查询对应FPGA或MCU手册,有最详细的协议相关(手册是个好东西,别啥都看网上,多看手册)。
一、I2C协议核心概念
-
基本定义
。
I2C(Inter-Integrated Circuit)是一种由Philips(现NXP)开发的双线制串行通信协议,支持多主从设备连接,通过串行数据线(SDA)和串行时钟线(SCL)实现同步数据传输 -
物理层特性
- 硬件结构:SDA和SCL均为开漏输出设计,需外接上拉电阻(通常4.7kΩ)维持高电平,实现线与逻辑(任一设备拉低总线则整体为低)。
- 电气特性:支持电压范围2.5V-5.5V,空闲时SDA和SCL均为高电平。
-
协议层特性
- 同步传输:数据有效性由SCL控制,SDA在SCL高电平时保持稳定,低电平时允许变化。
- 多设备支持:通过唯一地址(7位或10位)识别从机,支持最多127个设备(7位地址模式)。
二、关键通信机制
-
起始与停止条件
- 起始信号(Start):SCL高电平时,SDA从高→低跳变。
- 停止信号(Stop):SCL高电平时,SDA从低→高跳变。
-
数据传输格式
- 数据帧:以8位(1字节)为单位传输,高位(MSB)优先,每字节后需跟随应答位(ACK/NACK)。
- 应答机制:接收方在第9个时钟周期拉低SDA为ACK,保持高电平为NACK。
-
寻址机制
- 地址帧:起始信号后紧跟7位/10位设备地址+读写位(0写/1读)。
- 广播模式:保留地址(如0000xxx)可用于全局复位或广播指令。
三、典型通信流程
-
写操作
- 主机发送起始信号→设备地址+写指令→从机ACK→数据字节→从机ACK→停止信号。
-
读操作
- 主机发送起始信号→设备地址+读指令→从机ACK→接收数据→主机回复NACK→停止信号。
-
复合操作
- 先写寄存器地址,再读数据:两次起始信号,中间不发送停止信号。
四、硬件设计与速率模式
-
速率模式
模式 速率 应用场景 标准模式 100kbps 常规传感器、EEPROM 快速模式 400kbps 高速传感器、显示器 高速模式 3.4Mbps 视频设备、存储芯片 超快模式 5Mbps 单向传输(如LED控制) (参考) -
仲裁机制
。
多主机竞争时,通过“线与逻辑”仲裁:若某主机发送高电平但检测到总线为低,则退出竞争
五、优缺点分析
- 优点:仅需两根线、支持多主从、硬件复杂度低、可靠性高(ACK机制)。
- 缺点:速率低于SPI、数据帧长度固定、多主机需额外仲裁逻辑。
六、典型应用场景
- 传感器数据采集(如温度传感器、陀螺仪)
- EEPROM存储芯片读写(如AT24C系列)
- 显示器控制(如OLED屏驱动)
二、手撕I2C要点
一、I2C协议手撕要点
-
状态机设计
I2C协议本质是有限状态机(FSM),需划分以下核心状态:- 空闲(IDLE)
- 起始位(START)
- 地址传输(ADDR)
- 数据读/写(DATA_RW)
- 应答检测(ACK/NACK)
- 停止位(STOP)
示例状态转移逻辑:在SCL低电平时切换SDA电平实现START/STOP信号
-
时钟同步策略
注意主时钟频率和SCL问题,这里我建议用计数器实现协议时钟。 -
三态门处理
SDA需实现双向通信,VHDL中通过inout
端口+使能信号控制。process(sda_enable) begin if sda_enable = '1' then sda <= sda_out; else sda <= 'Z'; end if; end process;
三、工程代码
-
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; USE IEEE.STD_LOGIC_ARITH.ALL; USE IEEE.STD_LOGIC_UNSIGNED.ALL; --USE IEEE.STD_LOGIC_signed.ALL; use WORK.ChangeWorldCtrolBoard_PACKAGE.ALL; entity i2c_master is generic(ModuleBaseADDR: std_logic_vector(7 downto 0)); Port ( sysreset : in std_logic; sysclk : in std_logic; bus_addr : in std_logic_vector(25 downto 0); bus_data_wr : in std_logic_vector(15 downto 0); bus_data_rd : out std_logic_vector(15 downto 0); bus_wren : in std_logic; --single pulse of rising_edge of BUS_WR signal of MCU. bus_rden : in std_logic; --single pulse of falling_edge of BUS_RD signal of MCU. bus_cs : in std_logic; sda : inout STD_LOGIC; scl : out STD_LOGIC); end i2c_master; architecture Behavioral of i2c_master is -- Internal Signals signal sda_i : std_logic := '1'; signal scl_i : std_logic := '1'; signal state : std_logic_vector(3 downto 0) := "0000"; signal byte_counter : integer range 0 to 9 := 0; signal bit_counter : integer range 0 to 7 := 0; signal send_data : std_logic_vector(7 downto 0) := (others => '0'); signal receive_data : std_logic_vector(7 downto 0) := (others => '0'); signal current_byte : std_logic_vector(7 downto 0) := (others => '0'); signal current_bit : std_logic := '0'; signal addr : STD_LOGIC_VECTOR(6 downto 0); signal rw : STD_LOGIC; -- '0' for write, '1' for read signal data_out : STD_LOGIC_VECTOR(7 downto 0); signal data_in : STD_LOGIC_VECTOR(7 downto 0); signal start,rise_start_en : STD_LOGIC; signal stop,I_O_Set : STD_LOGIC; signal rise_get_data_en,get_data_en,rise_send_data_en,send_data_en,start_t: STD_LOGIC; SIGNAL sclk_cnt: INTEGER RANGE 0 TO 400; --fifo信号 signal FIFO_Data_send,FIFO_Data_get: std_logic_vector(7 downto 0); signal FIFO_wrreq_send,FIFO_wrreq_get,rise_FIFO_wrreq_send: STD_LOGIC; signal LLD_FIFO_rdreq_send,LLD_FIFO_rdreq_get: STD_LOGIC; signal clear_send,clear_get : STD_LOGIC; signal numfifo_send,numfifo_get : std_logic_vector(12 downto 0); signal LLD_DATA_FIFORD_send,LLD_DATA_FIFORD_get: std_logic_vector(7 downto 0); signal LLD_FIFO_empty_send,LLD_FIFO_empty_get : STD_LOGIC; signal LLD_FIFO_full_send,LLD_FIFO_full_get,LLD_accept_state: STD_LOGIC; signal data_length_send,data_length_get :std_logic_vector(7 downto 0); signal WorkMode_send,WorkMode_get :std_logic_vector(7 downto 0); signal byteDataReceivedOK,CRC_check,send_data_req,LLD_FIFO_rden_get,LLD_FIFO_rden_send,rise_FIFO_wrreq_get :STD_LOGIC;--接收fifo使能信号 signal LLD_accept : STD_LOGIC_VECTOR(7 DOWNTO 0); begin U1: _FIFO1 port map (Data=>FIFO_Data_send,Clk=>sysclk,WrEn=>rise_FIFO_wrreq_send,RdEn=>LLD_FIFO_rdreq_send,Reset=>clear_send,Wnum=>numfifo_send,Q=>LLD_DATA_FIFORD_send,Empty=>LLD_FIFO_empty_send,Full=>LLD_FIFO_full_send); U2: _FIFO2 port map (Data=>FIFO_Data_get,Clk=>sysclk,WrEn=>rise_FIFO_wrreq_get,RdEn=>LLD_FIFO_rdreq_get,Reset=>clear_get,Wnum=>numfifo_get,Q=>LLD_DATA_FIFORD_get,Empty=>LLD_FIFO_empty_get,Full=>LLD_FIFO_full_get); U3: rising_edge_detect port map(nreset=>sysreset,clk=>sysclk,input=>LLD_FIFO_rdreq_get,output=>LLD_FIFO_rden_get); U4: rising_edge_detect port map(nreset=>sysreset,clk=>sysclk,input=>FIFO_wrreq_send,output=>rise_FIFO_wrreq_send); U5: rising_edge_detect port map(nreset=>sysreset,clk=>sysclk,input=>FIFO_wrreq_get,output=>rise_FIFO_wrreq_get); U6: falling_edge_detect port map(nreset=>sysreset,clk=>sysclk,input=>send_data_en,output=>rise_send_data_en); U7: rising_edge_detect port map(nreset=>sysreset,clk=>sysclk,input=>get_data_en,output=>rise_get_data_en); U8: rising_edge_detect port map(nreset=>sysreset,clk=>sysclk,input=>start,output=>rise_start_en); process (sysclk,sysreset) begin if sysreset = '0' then addr <= (others => '0'); rw <= '0'; start <= '0'; elsif rising_edge(sysclk) then if bus_cs ='0' and bus_wren ='1' and bus_addr(23 downto 16) =ModuleBaseADDR then case bus_addr(15 downto 0) is when x"F3FF" => --0x00000E0. FIFO_Data_send <=bus_data_wr(7 downto 0); FIFO_wrreq_send <='1'; when x"0001" => data_length_get <=bus_data_wr(7 downto 0); when x"0002" => data_length_send <=bus_data_wr(7 downto 0); when x"0003" => clear_send <=bus_data_wr(0); when x"0004" => addr <=bus_data_wr(6 downto 0); when x"0005" => rw <=bus_data_wr(0);--写使能,过非门为片选信号 when x"0006" => --0x00000E0. start <=bus_data_wr(0); when x"0007" => clear_get <=bus_data_wr(0); when x"0008" => --0x00000E0. FIFO_Data_send <=bus_data_wr(7 downto 0); FIFO_wrreq_send <='1'; when others => NULL; end case; elsif bus_cs ='1' then FIFO_wrreq_send <='0'; end if; end if; end process; process(sysclk,sysreset) begin if sysreset ='0' then LLD_accept <= x"00"; CRC_check <= '0'; elsif rising_edge(sysclk) then if (LLD_FIFO_full_get = '1') and (FIFO_wrreq_get = '1') then LLD_accept <=x"01";--溢出 elsif (LLD_FIFO_empty_get = '0') and (FIFO_wrreq_get = '0') then LLD_accept <=x"02";--完成 elsif FIFO_wrreq_get = '1' then LLD_accept <=x"03";--忙碌 elsif (LLD_FIFO_empty_get = '1') and (FIFO_wrreq_get = '0') then LLD_accept <=x"04";--空闲 elsif(CRC_check = '1') then LLD_accept <=x"05";--CRC校验失败 else LLD_accept <=x"FF";--状态返回失败 end if; end if; end process; --I2C通讯过程 --连接FIFO与I2C process (sysclk,sysreset) begin if sysreset ='0' then data_out <= (others => '0'); FIFO_Data_get <= (others => '0'); LLD_FIFO_rdreq_send <= '0'; FIFO_wrreq_get <= '0'; elsif rising_edge(sysclk) then data_out <= LLD_DATA_FIFORD_send ; LLD_FIFO_rdreq_send <= rise_send_data_en ; FIFO_Data_get <= data_in; FIFO_wrreq_get <= rise_get_data_en; end if; end process; --时钟计数器(80M分之一) PROCESS(sysclk,sysreset) BEGIN IF(sysreset = '0') THEN sclk_cnt <= 0; ELSIF(sysclk'EVENT AND sysclk = '1')THEN IF(start = '1') THEN IF(sclk_cnt = 399)THEN sclk_cnt <= 0; ELSE sclk_cnt <= sclk_cnt + 1; END IF; ELSE sclk_cnt <= sclk_cnt; END IF; END IF; END PROCESS; --时钟 PROCESS(sysclk,sysreset) BEGIN if(sysreset = '0') THEN scl_i <= '1'; elsif rising_edge(sysclk) then if start_t = '1' then if sclk_cnt = 199 then scl_i <= NOT scl_i; end if; end if; if stop = '1' and state = "0000" then scl_i <= '1'; end if; END IF; END PROCESS; --停止信号 PROCESS(sysclk,sysreset) BEGIN if(sysreset = '0') THEN stop <= '0'; elsif rising_edge(sysclk) then if (LLD_FIFO_empty_send = '1' and state = "1001") or (data_length_get = numfifo_get and state = "1001") then stop <= '1'; elsif rise_start_en = '1' then stop <= '0'; end if; END IF; END PROCESS; -- Main Process process (sysclk,sysreset) begin if sysreset ='0' then sda_i <= '1'; state <= "0000"; -- Idle byte_counter <= 0; bit_counter <= 0; send_data <= (others => '0'); receive_data <= (others => '0'); current_byte <= (others => '0'); current_bit <= '0'; get_data_en <= '0'; start_t <= '0'; LLD_accept_state <= '0'; I_O_Set <= '1'; elsif rising_edge(sysclk) then -- if sclk_cnt = 19 then --测试ack信号 -- if state = "1001" and sclk_cnt = 175 then -- sda_i <= '0'; -- end if; case state is when "0000" => -- 开始 I_O_Set <='1'; if sclk_cnt = 199 then if start = '1' and stop /= '1' then state <= "0001"; -- Start condition end if; end if; if scl_i = '1' and sclk_cnt = 200 then sda_i <= '1' ; end if; when "0001" => -- sda拉低,为端口赋值 I_O_Set <= '1'; if sclk_cnt = 150 then sda_i <= '0'; start_t <= '1'; if scl_i = '0' then byte_counter <= 0; current_byte <= (addr & rw); state <= "0010"; end if; end if; when "0010" => -- 为sda赋值发送 if sclk_cnt = 160 then if scl_i = '0' then current_bit <= current_byte(7-bit_counter); -- sda_i <= current_bit; sda_i <= current_byte(7-bit_counter); if bit_counter = 7 then bit_counter <= 0; state <= "1011"; -- Wait for ACK else bit_counter <= bit_counter + 1; end if; end if; end if; when "0011" => -- 拉高sda等待ack if sclk_cnt = 170 then if rw = '0' then send_data_en <= '1'; end if; sda_i <= '1' ;--调试9.20 state <= "1001"; end if; --when "1001" => --if sclk_cnt = 170 then -- send_data_en <= '0'; -- if sda_i = '0' then --改为0等待ack -- if scl_i = '0' then -- send_data <= data_out; -- state <= "0100"; -- end if; -- else -- state <= "1111"; -- LLD_accept_state <= '1'; -- end if; --end if; when "1001" => if scl_i = '1' then I_O_Set <='0'; if sclk_cnt = 160 then if sda = '1' then --test state <= "1111"; LLD_accept_state <= '1'; else send_data_en <= '0'; end if; elsif sclk_cnt = 175 then send_data <= data_out; state <= "0100"; end if; end if; when "0100" => -- 等待ack,并为发送端口赋值以及发送 if sclk_cnt = 180 then if scl_i = '0' then if rw = '0' then -- Write I_O_Set <='1'; current_bit <= send_data(7-bit_counter); sda_i <= send_data(7-bit_counter); if bit_counter = 7 then bit_counter <= 0; state <= "1011"; -- Wait for ACK else bit_counter <= bit_counter + 1; end if; else -- Read I_O_Set <='0'; current_bit <= sda_i; receive_data(7-bit_counter) <= sda_i; if bit_counter = 7 then bit_counter <= 0; state <= "0111"; -- send ACK else bit_counter <= bit_counter + 1; end if; end if; end if; end if; when "1011" => --为最后一位赋值 if sclk_cnt = 200 then if scl_i = '0' then sda_i <= '1'; if numfifo_send = 0 then state <= "0101"; else state <= "0011"; end if; end if; end if; when "0101" => -- 数据线拉高 I_O_Set <='0'; if sclk_cnt = 170 then if scl_i = '0' then state <= "0110"; -- Wait for SCL low end if; end if; when "0110" => -- 收到ack if sda = '0' then --test I_O_Set <='1'; sda_i <= '0'; if sclk_cnt = 170 then if scl_i = '0' then if stop = '1' then state <= "1111"; -- Stop condition LLD_accept_state <= '1'; end if; else state <= "0110"; end if; end if; end if; when "0111" => -- 保存接收数据 I_O_Set <='1'; sda_i <= '0'; if scl_i = '0' then data_in <= receive_data; get_data_en <= '1'; if stop = '1' then state <= "1111"; -- Stop condition sda_i <= '1'; else state <= "0100"; end if; end if; when "1111" => -- Stop condition I_O_Set <='1'; sda_i <= '0'; byte_counter <= 0; bit_counter <= 0; send_data <= (others => '0'); receive_data <= (others => '0'); current_byte <= (others => '0'); current_bit <= '0'; get_data_en <= '0'; start_t <= '0'; state <= "0000"; -- back to initial when others => -- Error or invalid state state <= "0000"; -- Reset to idle end case; end if; -- end if; end process; -- Assign SDA sda <= sda_i when (I_O_Set ='1') else 'Z'; scl <= scl_i; end Behavioral;
上位机通过FSMC总线发送数据到FPGA的FIFO,再通过I2C协议发送和接受下位机数据,这里根据项目需求对I2C时序和功能有一定修改,仅供参考。