i2c master from scratch. - Page 1
Forum section: electronics / general

author: paulca
author_rank: Super Contributor
timestamp: September 17, 2025, 01:09:47 pm
content: I'm trying to implement a basic I2C master on an Ice40.I think I need to address the elephant in the room sooner or later and that is the bi-directional "Open drain" signalling.I have been working on the assumption that I can reconfigure the pins such that:Module writes a 1 to the SDA Line, Wiring module switches the pin to input. Now read can happen too and nobody drives the pin high.Module writes a 0 SDA line wiring module switches it to "Output low".Another option is a shockty diode to prevent the "high drive" from feeding the sda. This has voltage level issues.Does anyone see this working or being a nightmare?For the i2c master itself I have wandered around and settled on a parallel state machine.Outer state machine is the usual "integer state" register that advances through things like "START, POP, SHIFT, RACK,STOP".Inner state machine is driven from the incoming clock at I2C rate * 4. It tracks the clock phase, RISE, HIGH, FALL, LOW. Directly drives the CLK line.So things like state=START && clk_phase==HIGH -> Drop SDA combine the two state machines.The trickiest part is keeping track of spending clock cycles and knocking the bit stream out of phase, but I have avoided perils so far.

author: BrianHG
author_rank: Super Contributor
timestamp: September 17, 2025, 02:07:00 pm
content: If you are curious how I did it:https://github.com/BrianHGinc/BHG_I2C_init_RS232_debuggerIt's a state machine controlled by a parameter table, or, a RS232 interface.Bit timing is slowed down by a period clock.

author: asmi
author_rank: Super Contributor
timestamp: September 17, 2025, 02:33:32 pm
content: Most FPGAs have bidirectional IO structures which allow setting pin to a high-impedance (often called High-Z or just Z) state, which would correspond to I2C's "1" signal if there is an ongoing transmission due to pull-up resistor on a wire, so you set it to Z to transmit "1", set it to output logic low to transmit "0".

author: bson
author_rank: Unknown
timestamp: September 17, 2025, 08:31:49 pm
content: Do yourself a favor and give it a RESET state as well, where it drives SDA high and outputs ten clocks on SCK, to unwedge a device that's stuck due to a missed clock. Then control this with a separate control bit somewhere that can be set whenever a device has stopped responding. This way code can just set it and proceed to try again; the master can just flag itself as busy during the reset to hold off the next operation.Edit: actually, don't drive SDA at all, just leave it floating and clock for a bit.

author: PCB.Wiz
author_rank: Super Contributor
timestamp: September 17, 2025, 10:44:20 pm
content: Quote from: paulca on September 17, 2025, 01:09:47 pmIf the FPGA lacks a specific open drain IO option, you can emulate that with a tristate pin, if you always drive low, and instead modulate the OE.

author: mtwieg
author_rank: Frequent Contributor
timestamp: September 18, 2025, 11:28:07 am
content: We have usedthis I2C master implementation from opencoreswith great success. Includes vhdl and verilog implementations.For interacting with SDA/SCL, the module has three ports for each signal: one input for reading the level on the pin, one output for controlling the direction of the pin, and one signal for controlling the level on the pin (when it's an output). The user would decide how to use these signals to control the actual FPGA pin on the top level.I've never used ice40, but I'd be very surprised if it didn't allow for bidirectional open-drain io, with no external hardware (aside from a pullup resistor).

author: paulca
author_rank: Super Contributor
timestamp: September 18, 2025, 12:05:10 pm
content: I set mild goals. As a minimum I only need it to read 9 registers from an RTC. So I'm hoping I can get away with a "happy path impl", maybe with a single "ERROR" state, flag and bit code at most.I have to wire up 2 FIFOs, in transmit and the receive.The simplist idea is to pre-prepare the transaction, then run it to completion or error.* CPU loads the address+read word and then the register address word into the transmit FIFO.* CPU sets how many bytes it would like to receive before stopping the slave in the RxCnt reg* CPU "starts" the transactionWhen the BUSY flag clears and the ERROR flag is clear, the receive FIFO should contain the received bytes.This allows the I2C master to operate at a speed completely decoupled from the CPU and concurrently to it.As its mostly a learning project, I'm going to carry on in "sim land" until I get it working, while still running the synthesis periodically to check.The Ice40 IP comes with the SB_IO macro(?) and it does have control over input and output partially independantly. I don't think it will do automatic "OD low with High-Z high", I doubt it. So I need to test if I can change those macros on the fly and how to trigger same.Also, BrianHG opened a can of worms I need to explore first. Tasks. Not used them before. GPT generated me a test bench with a few at one point, but I didn't want to make assumptions that they "look" like functions, but I expect are rather "conditionally activated logic statements". If this condition, then "include Task X here".

author: asmi
author_rank: Super Contributor
timestamp: September 18, 2025, 07:31:55 pm
content: Also make sure you don't forget to support clock stretching by slaves (some real-life ones actually do use this feature). And - while you're at it - arbitration for multi-master support. It's all in specs.

author: BrianHG
author_rank: Super Contributor
timestamp: September 18, 2025, 10:09:05 pm
content: Ok Paulca,1) maybe first you should decide on a strategy and first concentrate on an I2C master transmitter topology.Here is what I have done: (This has yet to implement a slowing down for I2C speeds)My stupid method:Code:program_counter + 1'b1; // Think of this as a super-default action for the program counter if it is not set to anything else below.case (program_counter)default :0: beginif (~input_run) program_counter <= program_counter; // wait here until the input control 'input_run' = 1.end1: begin // output enable & set data to highi2c_data_oe <= 1;i2c_clk_oe <= 1;i2c_data_out <= 1;i2c_clk_out <= 1;end2: begin // set clock lowi2c_data_oe <= 1;i2c_clk_oe <= 1;i2c_data_out <= 1;i2c_clk_out <= 0;end// wait an additional 4 clock cycles6: begin // set clock highi2c_data_oe <= 1;i2c_clk_oe <= 1;i2c_data_out <= 1;i2c_clk_out <= 1;end// wait an additional 2 clock cycles8: begin // back to tristate busi2c_data_oe <= 0;i2c_clk_oe <= 0;i2c_data_out <= 1;i2c_clk_out <= 1;end// wait an additional 2 clock cycles10: begin // go back to start, wait for next 'input_run' commandprogram_counter <= 8'd0;endendcaseNow, the above illustration is not to show you a functional code, but a simple frame to generate a sequence of events like an assembly program which you may engineer yourself anyway you like.2) As for my "task", task just groups a number of code lines you wish to use multiple times with an optional input parameter. For example, above, even though I didn't need to, I did set these 4 in a group:Code:i2c_data_oe <= 1;i2c_clk_oe <= 1;i2c_data_out <= 1;i2c_clk_out <= 1;I could have made a separate task called 'drive_i2c_bus' like so:Code:task drive_i2c_bus(bit oe,bit dat,bit clk); begini2c_data_oe <= oe && ~dat; // Simulate an open drain output pin by toggling the OE control linei2c_clk_oe <= oe;i2c_data_out <= dat; // Simulate an open drain output pin, you may also place a 1'b0 here.i2c_clk_out <= clk;end; endtaskThis would simplify my first code to:Code:program_counter + 1'b1; // Think of this as a super-default action for the program counter if it is not set to anything else below.case (program_counter)default :0: beginif (~input_run) program_counter <= program_counter; // wait here until the input control 'input_run' = 1.end1: drive_i2c_bus(1,1,1); // Output enable2: drive_i2c_bus(1,1,0); // Set clock low// wait an additional 4 clock cycles6: drive_i2c_bus(1,1,1); // Set clock high// wait an additional 2 clock cycles8: drive_i2c_bus(0,1,1); // Back to tristate bus// wait an additional 2 clock cycles10: begin // go back to start, wait for next 'input_run' commandprogram_counter <= 8'd0;endendcaseAnother smart task would be a JSR and RET task routines:Code:task JSR([7:0] code_position); begin // Jump to a sub routine.program_counter_return_address <= program_counter + 1'b1;program_counter <= code_position;end; endtasktask RET(); begin // Return to original program program counter.program_counter <= program_counter_return_address;end; endtaskNow, in your above 'case (program_counter)', you may:Code:... bla bla bla....4: beginJSR(8'd20); // jump to position 20 in the sequencerend... bla bla bla....20: begin... bla bla bla....end21: begin... bla bla bla....end22: beginif (finished) RET(); // return from this subroutine (in this case, goto sequence position 5)else program_counter <= 8'd20; // goto position 20, loop around.endThis has 1 level deep stack, but, you can easily make your own 2,3,4 level deep subroutine stack.Don't yet think about loops and delays to slow down the I2C, for now, make a simple I2C transmitter that works at speed. Then worry about making a flag with a period timer to slow down the master 'program_counter' during mid I2C transactions.

author: macboy
author_rank: Super Contributor
timestamp: September 27, 2025, 04:07:38 pm
content: The clock SCL most also be open collector style and not directly driven. This is because the i2c specifications allow devices to stretch the clock by holding it low, in order to provide itself time to respond to a request. The Master must monitor to clock line after setting it "high" to wait for it toactuallygo high. In most cases this will happen immediately but if the devices stretches the clock it could be some time. Some higher speed variations (official or not) of the spec may allow the clock to be driven directly, and many devices will never stretch the clock, so there are exceptions to this rule as usual. In general though, both data and clock are open collector and the Master must verify the state of the clock before proceeding.I remember about 10 years ago, using a Bus Pirate to talk to an i2c device. The device stretched the clock and the Bus Pirate couldn't handle it. So I fixed the code to add clock stretching (just a few lines of code), got it working great, and posted about it on their forums. They claimed my fix was impossible because the microcontroller on the BP had a bug in the i2c peripheral and couldn't support clock stretching. I had to explain thattheirsoftware was bit banging the protocol so the peripheral bug was irrelevant. They didn't believe me and I don't think I even shared the fix. It couldn't have possibly worked after all.

author: KrudyZ
author_rank: Frequent Contributor
timestamp: September 27, 2025, 05:04:05 pm
content: All you need to know WRT the IO cell is documented in section 4 of this document:https://www.latticesemi.com/view_document?document_id=52206Specifically, you want to configure your SB_IO output pin function to mode #3 "PIN_OUTPUT_TRISTATE" and the input to mode #1 "PIN_INPUT".This would results in:defparam IO_PIN_INST.PIN_TYPE - 6'b101001;if you are using Verilog or the equivalent in the generic map for VHDL.You then wire up the macro to read what's on the external pin from D_IN_0, wire up D_OUT_0 to a constant low and connect the signal you want to drive out to OUTPUT_ENABLE. This will switch the pin between low and tri-state.No need for dynamic configuration, which isn't even supported, since the pin type definition is a parameter, not a port.

author: BrianHG
author_rank: Super Contributor
timestamp: September 28, 2025, 02:41:47 am
content: Quote from: macboy on September 27, 2025, 04:07:38 pmOk, Ok, Ok.....In my example, just change:Code:task drive_i2c_bus(bit oe,bit dat,bit clk); begini2c_data_oe <= oe && ~dat; // Simulate an open drain output pin by toggling the OE control linei2c_clk_oe <= oe;i2c_data_out <= dat; // Simulate an open drain output pin, you may also place a 1'b0 here.i2c_clk_out <= clk;end; endtaskTo:Code:task drive_i2c_bus(bit oe,bit dat,bit clk); begini2c_data_oe <= oe && ~dat; // Simulate an open drain output pin by toggling the OE control linei2c_clk_oe <= oe && ~clk;i2c_data_out <= 1'b0; // Simulate an open drain output pin, you may also place a 1'b0 here.i2c_clk_out <= 1'b0;end; endtask

author: nctnico
author_rank: Super Contributor
timestamp: September 29, 2025, 08:53:13 am
content: When implementing I2C on a microcontroller or FPGA the approach to use is to only drive pins low. So set the output to 0 and only change between tri-state (allow the line to be pulled up) or output on (line low). And there is clock stretching to be considered as well.

author: AndyC_772
author_rank: Super Contributor
timestamp: September 29, 2025, 09:28:06 am
content: Solve the problem you actually have, not the most complex possible version of it.If you're designing a product which uses a particular I2C device, produce an interface for that device.If you don't need clock stretching - and you almost certainly don't, because it's a very rare feature that few devices use - then don't implement it.Same for multiple masters; if your product incorporates exactly 1 master, don't waste time and effort supporting hypothetical extra parts that simply don't exist.Add extra features later,ifand when you need them.

author: mikeselectricstuff
author_rank: Super Contributor
timestamp: September 29, 2025, 09:41:58 am
content: Quote from: AndyC_772 on September 29, 2025, 09:28:06 amExactly. Clock-stretching is rare - the only time I've ever seen it is when talking to another MCU acting as a slave, where the stretching allows for the time for the MCU to supply the data to the I2C peripheral. Devices like EEPROMs, crypto engines and anything else that can take significant time typically use status polling to avoid hogging the bus.Multi-master is probably not worth using as even if the low-level interface supports it, higher-level functionality like slave device internal state will likely not.A single-master system can happily drive the clock high & low, which might be an advantage if getting max speed is an issue, though you'd probably be running the slave put of spec if you need to do that.

author: paulca
author_rank: Super Contributor
timestamp: September 29, 2025, 10:41:34 am
content: Yeah. I am already well ahead on that. It's my engineering ethos anyway. Work out what needs done. Do it. DO NOT work out what you COULD do and do that. It just makes everyone elses job, including your own harder later.The goal is to read the date and time from an RTC.Mostly it's just an FPGA learning project. I could just buy a parallel load I2C bus master IC and set it up.The downside is, it's blocking the project which wants an RTC and that links to "OS Design" work I'm currently doing. I have choosen the RTC to my first engagement with system tasks/scheduling etc.I have only got as far as the start condition working. Not been able to spend a lot of time on it. So excuse me if I go quiet for a week and may come back bounding with questions soon.Thanks to KrudyZ for the doc reference. Thanks for pointing out the tristate mode. Allowing the pin to be set "low" as a constant and just flip the OE to drive it low and "release" it high.What is however sounding more and more appealing in general, for the OS Design project, to unblock it, is to just use the FPGA to handle the "WAIT" signal necessary to interface an MCU with the Z80. The FPGA can simple identify the IO request to the MCU and pull the wait line low. When the MCU feels like it can respond it can pull a "RESET WAIT" line on the FPGA to release the CPU. Then I can just use the I2C/SPI etc. on the MCU and not bother with the FPGA. It's just a mater of project sequencing and as it's hobby, it's more about what I want to do next, as opposed to what would the best thing be. I don't even need a big FPGA or much IO for that. The SugarNano would probably do.The future concepts that relate to FPGA are for the next project in mind, the 68000 CPU. I'm pushing a lot of stuff to it. The plan is to use the FPGA as a "front side bus controller". Wrap the 68k in 3v3 shifters and front it with the FPGA. The main feature will be glorified dynamic address translation, aka TLBs and virtual memory for multi-tasking. It sounds complicated, but its really just a set of LUTs and a "Load" "Erase" "Append" interface to the CPU in supervisor mode only.

author: langwadt
author_rank: Super Contributor
timestamp: September 29, 2025, 11:10:09 am
content: Quote from: BrianHG on September 28, 2025, 02:41:47 amor simplyi2c_data_out <= dat ? 1'bz : 1'b0;i2c_clk_out <= clk ? 1'bz : 1'b0;and simulation use tri1 instead of wire for clock and data

author: zapta
author_rank: Super Contributor
timestamp: September 29, 2025, 12:28:46 pm
content: For open drain output you can also use the ICE40 SB_IO primitive. it's full of options.

author: Gleeson
author_rank: Contributor
timestamp: September 29, 2025, 04:46:26 pm
content: Yes, exactly, good advice here. Keep it as simple as possible.

author: nctnico
author_rank: Super Contributor
timestamp: September 29, 2025, 04:53:44 pm
content: Quote from: mikeselectricstuff on September 29, 2025, 09:41:58 amI wouldn't be so quick to dismiss having clock stretching. There are quite a few devices out there with a microcontroller under the hood which might service an ISR every now and then causing the device to not respond every now and then. I have spend a couple of days trying to hunt down a problem on a bit-banged I2C bus which was caused by a device doing a clock stretch once in an hour. IOW: without supporting clock stretching, it is very hard to prove your design is robust & reliable. And supporting clock stretching is easy enough to implement: only start counting down the next clock period after the SCL line went high. Bonus points for signalling when this isn't happening so the software can try doing a bus reset procedure instead of moving on with wrong values.

author: BrianHG
author_rank: Super Contributor
timestamp: September 29, 2025, 07:59:43 pm
content: Quote from: langwadt on September 29, 2025, 11:10:09 amEfinix FPGAs do not support tristate or 1'bz.That is the only reason why my code looks the way it does. My original code had what you recommended.

author: langwadt
author_rank: Super Contributor
timestamp: September 29, 2025, 08:30:41 pm
content: Quote from: BrianHG on September 29, 2025, 07:59:43 pminteresting, I've never used Efinix. Is it only IO or can they generally not figure out inferring primitives from code?

author: BrianHG
author_rank: Super Contributor
timestamp: September 29, 2025, 11:00:24 pm
content: Quote from: langwadt on September 29, 2025, 08:30:41 pmYup, their compiler cant handle bidirs.You need to manually hard-wire their primitives.

author: AndyC_772
author_rank: Super Contributor
timestamp: October 01, 2025, 06:12:44 pm
content: Efinix parts, or at least their tools, do have their quirks. IMHO the worst of it is the VHDL only describes the 'core' of the device, not the complete functionality from pin to pin - there's a separate I/O ring which can latch signals independently of the core. This is different from other vendors, where the code describes how the complete device behaves, including the behaviour of any buffers or latches attached directly to pins.On the other hand, they work well, tech support from the manufacturer is excellent, and they're much cheaper than the incumbents.I switched from Altera a few years ago. Haven't looked back.

author: mopplayer
author_rank: Contributor
timestamp: October 12, 2025, 05:27:40 pm
content: Quote from: AndyC_772 on October 01, 2025, 06:12:44 pmThat aren't quirks, just gimmicks, like saying “Quantum mechiasm”, in fact there are no any special points compared to other Chinese FPGAs. When using near full of LUT, their speed of devices step quickly down and unstable. You get what you pay for.The Chinese FPGAs, including EDA tools, are restricted by the US government, which intends to inhibit China from producing military rockets and destructive UAVs. Therefore, the leading Chinese company, Pango, has been cutted off supplement for their high-end FPGA (TSMC mauaferuted) and EDA technology(Synplify Pro).Elitestek(Effinx, outside of China) is the same as other Chinese FPGA companies, like ANLOGIC, GOWIN, XAIC and Hercules(tier 2~3).