I've always used the oversampled high speed clock to treat both the I2C clk and data lines both as data.
This way I can do digital filtering to eliminate glitches on either line, and the state machine runs at the frequency of the internal logic.
Here is an I2C slave module I wrote long ago and shipped in a successful product.
It's in Verilog (you reference VHDL) but it illustrates the concept of using a high speed clock to oversample I2c clk/data.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// This module supports standard I2C bus read and write transactions as a slave target.
//
// Single and multibyte writes, with an autoincrementing slave register ptr:
// <start> <addr+wr> <ack> <ptr> <ack> [<wrdata@ptr++> <ack>]+ <stop>
//
// Single and multibyte reads, with an autoincrementing slave register ptr:
// <start> <addr+wr> <ack> <ptr> <ack> <start> <addr+rd> <ack> [<rddata@ptr++> <ack>]* <rddata@ptr++> <nack> <stop>
//
// 2006-07-01 donorth - initial version
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
`timescale 1ns / 1ns
module i2c_slave ( /*AUTOARG*/
// Outputs
sclo, sdao, datao, addr, wr, rd,
// Inputs
clk, reset, scli, sdai, i2caddr, datai
);
// global clock and reset
input clk;
input reset;
// serial i2c clock and data
input scli;
output reg sclo;
input sdai;
output reg sdao;
// own address
input [7:1] i2caddr;
// external data/addr bus and control
input [7:0] datai;
output reg [7:0] datao;
output reg [7:0] addr;
output reg wr;
output reg rd;
// slave state machine definitions
parameter SS_IDLE = 3'b000,
SS_START = 3'b001,
SS_SLAVE = 3'b011,
SS_SLAVE_ACK = 3'b010,
SS_ADDR = 3'b110,
SS_ADDR_ACK = 3'b111,
SS_DATA = 3'b101,
SS_DATA_ACK = 3'b100;
// i2c data direction type
parameter I2C_WRITE = 1'b0,
I2C_READ = 1'b1;
// for simulation
parameter TPD = 0;
// synchronized i2c bus inputs
reg [3:0] sdair, sclir;
// i2c sda start/stop and scl rising/falling states
reg sda_start, sda_stop;
reg scl_rising, scl_falling;
// i2c serial to parallel data register
reg [8:0] datas;
// i2c slave selected and r/w mode registers
reg selected, mode;
// i2c slave state machine
reg [2:0] slave_state;
// i2c slave state counter
reg [3:0] slave_cnt;
// indicate count has reached eight
wire slave_cnt_eight = (slave_cnt == 4'h3);
// 4b LFSR Counter Logic
function [3:0] LFSD4;
input [3:0] D;
begin
LFSD4 = { D[2:0], D[3] } ^ { 2'b0, D[3], 1'b0 };
end
endfunction // LFSD4;
////////////////////////////////////////////////////////////////////////////
// synchronize i2c bus inputs to clock
always @(posedge clk or posedge reset)
begin
if (reset)
begin
sclir <= #TPD (-1);
sdair <= #TPD (-1);
end
else
begin
// synchronize i2c sda/scl inputs
sclir <= #TPD {sclir[2:0],scli};
sdair <= #TPD {sdair[2:0],sdai};
end
end
wire sdain = sdair[2]; // serial shift datain
wire slaveack = (sdair == 4'b0000); // slave acknowlwdge
////////////////////////////////////////////////////////////////////////////
// i2c bus start/stop detect and clock rising/falling detect
always @(posedge clk or posedge reset)
begin
if (reset)
begin
sda_start <= #TPD 0;
sda_stop <= #TPD 0;
scl_rising <= #TPD 0;
scl_falling <= #TPD 0;
end
else
begin
// START is scl H and sda H->L
sda_start <= #TPD sclir == 4'b1111 && sdair == 4'b1100;
// STOP is scl H and sda L->H
sda_stop <= #TPD sclir == 4'b1111 && sdair == 4'b0011;
// RISING is scl L->H
scl_rising <= #TPD sclir == 4'b0011;
// FALLING is scl H->L
scl_falling <= #TPD sclir == 4'b1100;
end
end
////////////////////////////////////////////////////////////////////////////
// i2c slave state machine
always @(posedge clk or posedge reset)
begin
if (reset)
begin
slave_cnt <= #TPD (-1);
slave_state <= #TPD SS_IDLE;
end
else
begin
if (sda_start) slave_state <= #TPD SS_START; // force START
else if (sda_stop) slave_state <= #TPD SS_IDLE; // force STOP
else if (scl_falling) // only change when asserted
begin
case (slave_state)
SS_START:
begin
slave_cnt <= #TPD (-1);
slave_state <= #TPD SS_SLAVE;
end
SS_SLAVE:
begin
slave_cnt <= #TPD LFSD4(slave_cnt);
slave_state <= #TPD slave_cnt_eight ? SS_SLAVE_ACK : SS_SLAVE;
end
SS_SLAVE_ACK:
begin
slave_cnt <= #TPD (-1);
slave_state <= #TPD selected ? ((mode == I2C_WRITE) ? SS_ADDR : SS_DATA) : SS_IDLE;
end
SS_ADDR:
begin
slave_cnt <= #TPD LFSD4(slave_cnt);
slave_state <= #TPD slave_cnt_eight ? SS_ADDR_ACK : SS_ADDR;
end
SS_ADDR_ACK:
begin
slave_cnt <= #TPD (-1);
slave_state <= #TPD SS_DATA;
end
SS_DATA:
begin
slave_cnt <= #TPD LFSD4(slave_cnt);
slave_state <= #TPD slave_cnt_eight ? SS_DATA_ACK : SS_DATA;
end
SS_DATA_ACK:
begin
slave_cnt <= #TPD (-1);
slave_state <= #TPD (mode == I2C_WRITE || slaveack) ? SS_DATA : SS_IDLE;
end
default:
begin
slave_cnt <= #TPD (-1);
slave_state <= #TPD SS_IDLE;
end
endcase // case(slave_state)
end
end
end
// synthesis translate_off
reg [5*8:1] slave_state_dpy;
always @(slave_state)
casez (slave_state)
SS_IDLE: slave_state_dpy <= "IDLE ";
SS_START: slave_state_dpy <= "START";
SS_SLAVE: slave_state_dpy <= "SLAVE";
SS_SLAVE_ACK: slave_state_dpy <= "S/ACK";
SS_ADDR: slave_state_dpy <= "ADDR ";
SS_ADDR_ACK: slave_state_dpy <= "A/ACK";
SS_DATA: slave_state_dpy <= "DATA ";
SS_DATA_ACK: slave_state_dpy <= "D/ACK";
default: slave_state_dpy <= "?????";
endcase // casez(slave_state)
// synthesis translate_on
////////////////////////////////////////////////////////////////////////////
// slave address/command input process
always @(posedge clk or posedge reset)
begin
if (reset)
begin
mode <= #TPD I2C_WRITE;
selected <= #TPD 0;
end
else
begin
// compare slave i2c address for match in slave state
if (scl_falling && slave_state == SS_SLAVE && slave_cnt_eight)
begin
mode <= #TPD datas[0] ? I2C_READ : I2C_WRITE;
selected <= #TPD (datas[7:1] == i2caddr[7:1]);
end
else if (scl_falling)
begin
// selected only stays active for one state
selected <= #TPD 0;
end
else if (sda_stop)
begin
// if stop seen, deselect immediately
mode <= #TPD I2C_WRITE;
selected <= #TPD 0;
end
end
end
// synthesis translate_off
reg [2*8:1] mode_dpy;
always @(mode)
casez (mode)
I2C_READ: mode_dpy <= "RD";
I2C_WRITE: mode_dpy <= "WR";
default: mode_dpy <= "??";
endcase // casez(mode)
// synthesis translate_on
////////////////////////////////////////////////////////////////////////////
// word address processing
always @(posedge clk or posedge reset)
begin
if (reset)
addr <= #TPD 0;
else
if (scl_falling)
begin
if (slave_state == SS_ADDR_ACK)
// load new address from serial data input
addr <= #TPD datas[8:1];
else
if (slave_state == SS_SLAVE_ACK && mode == I2C_READ && selected ||
slave_state == SS_DATA_ACK && mode == I2C_WRITE ||
slave_state == SS_DATA_ACK && mode == I2C_READ && slaveack)
// increment address by one
addr <= #TPD addr + 1;
end
end
////////////////////////////////////////////////////////////////////////////
// i2c bus serial data input process
always @(posedge clk or posedge reset)
begin
if (reset)
datas <= #TPD 0;
else
if (scl_rising)
// shift i2c data bus in, always
datas <= #TPD {datas[7:0],sdain};
end
////////////////////////////////////////////////////////////////////////////
// data output processing on client bus
always @(posedge clk or posedge reset)
begin
if (reset)
datao <= #TPD 0;
else
if (scl_falling)
// clock out parallel data only on slave write complete
if (slave_state == SS_DATA && slave_cnt_eight && mode == I2C_WRITE)
datao <= #TPD datas[7:0];
end
////////////////////////////////////////////////////////////////////////////
// rd and wr signal generation
always @(posedge clk or posedge reset)
begin
if (reset)
begin
rd <= #TPD 0;
wr <= #TPD 0;
end
else
begin
// read pulse comes prior to i2c serial data required
rd <= #TPD scl_rising && mode == I2C_READ
&& (slave_state == SS_SLAVE_ACK && selected ||
slave_state == SS_DATA_ACK && slaveack);
// write pulse comes after i2c serial data is captured
wr <= #TPD scl_rising && mode == I2C_WRITE
&& (slave_state == SS_DATA_ACK);
end
end
////////////////////////////////////////////////////////////////////////////
// serial clock out (wait state) processing
always @(posedge clk or posedge reset)
begin
if (reset)
sclo <= #TPD 1; // deassert high
else
sclo <= #TPD 1; // not implemented yet
end
////////////////////////////////////////////////////////////////////////////
// serial data out / ack processing
always @(posedge clk or posedge reset)
begin
if (reset)
sdao <= #TPD 1; // deassert high
else
begin
if (slave_state == SS_SLAVE_ACK && selected ||
slave_state == SS_ADDR_ACK ||
slave_state == SS_DATA_ACK && mode == I2C_WRITE)
// assert slave ack response
sdao <= #TPD 0; // assert low
else if (mode == I2C_READ && slave_state == SS_DATA)
// assert read data response
case (slave_cnt) // 4b LFSR counter
4'hF: sdao <= #TPD datai[7];
4'hD: sdao <= #TPD datai[6];
4'h9: sdao <= #TPD datai[5];
4'h1: sdao <= #TPD datai[4];
4'h2: sdao <= #TPD datai[3];
4'h4: sdao <= #TPD datai[2];
4'h8: sdao <= #TPD datai[1];
4'h3: sdao <= #TPD datai[0];
default sdao <= #TPD 1;
endcase
else
sdao <= #TPD 1; // deassert high
end
end
////////////////////////////////////////////////////////////////////////////
endmodule // i2c_slave