I'm going to try and run an i2c design in VHDL fabric. For self-training.
The target device is the Rohm BH1790 optical heart rate sensor.
It's a device where the i2c protocol is straightforward but strict. A good candidate for training - a RTL state machine should be able to control the flow.
What I'd like to achieve is: controlling the complete sampling part of heart rate sensor in plain VHDL/RTL, portable/independent of FPGA make. Make the raw data available.
Then, there is a second part of interpreting/filtering the raw data.
I'm going to start doing that in software, then try translating that into FPGA designs. These may be pure VHDL, or Xilinx proprietary.
If all of that is successful, I'd like to use DMA and streaming to provide that data to a program. That's definitely going to be device dependent.
The exercise is broken up in a few posts. I write as I go. This one checks out the i2c IP I'd like to use for the design.
First, I tested this sensor with Rohm's original code on an Arduino. An exercise to know how it works, and view the communication.
It's easier to design for something that you know how to operate. Because only the FPGA/VHDL part is the unknown. I understand the IC and its protocol.
Vivado / Zynq come with smart i2c IP. In two flavours*: to be used within the fabric, and a version that can be used in a MicroBlaze.
I'm after one that's implemented in fabric. I'm not using Xilinx' proprietary implementation, because I try to improve my VHDL skills in this design. Not my knowledge of Xilinx IP
*I believe there's also i2c support in the Linux/PS part of the Zynq, comparable to i2c on a Pi. That's not what I'm trying to learn here.
On the other hand, I'd like to reuse an existing VHDL i2c design.
I'd first like to implement a design that uses a working i2c IP. I'm not up to writing my own implementation of that yet.
I landed with a design posted on Digi-Key's Tech Forum. Like Avnet's element14, Digi-Key also has a community. I found this design over there.
It's written - and improved over time - by Scott Larson. Written for Quartus II (Intel/Altera).
Ideally, the end result should be cross-community, cross-distributor, cross-manufacturer.
The source code and related blog don't mention a license. I expect that writing about it, and change some parts of the code (see table below), is in line with community behaviour.
I will restrict my blogs to changes I made to the original code, and why. The ZIP attachments will contain the adapted code with link to the original in the source files.
VHDL buffer in a top design
The i2c design of Scott Larson uses an output of type BUFFER for the
A VHDL BUFFER is an output port, that can be read to and written from inside the architecture.
The consequence of BUFFER type is that it needs to be implemented all the way up in the design. Upstream ports that connect to it, need to be BUFFER too.
There are alternatives to BUFFER though. One of them is to define the flag as OUT. Then create a SIGNAL internally in the IP that can be written to and read from.
The i2c IP interface
The i2c IP has, like all IPs, a set of input and output connections.
They are very similar to the APIs you have access to in a microcontroller's i2c driver.
There are 2 variables that are set as GENERIC. clock frequency and i2c frequency.
They are set to 500 MHz and 400 kHz. You can overwrite those in a Vivado block design without wrapping or coding:
I kept the 400 kHz i2c speed. The heart rate sensor supports it.
I adapted the input clock frequency. I've set it equal to the Fabric ClocK 1 speed. That's the clock I'm attaching the IP to.
Open Drain SDA and SCL
i2c protocol uses open drain active devices, in combination with pull-up resistors.
In a constraint file, there's no way to define it. The way to do it in RTL, is by setting the output to High-impedance instead of '1' when driving an output high.
The SDA is bi-directional, input and output. But that doesn't add complexity. Implementing it is simpler than I thought.
You can just ignore any state (in or out), except output high. For output high, you assign high impedance:
sda <= '0' WHEN sda_ena_n = '0' ELSE 'Z';
Design Decision for the next post
I'm going to write a IP that reflects the BH11790 sensor.
I could have instantiated the i2c IP in its code. But I'd like to write a sensor class that's isolated from that.
The plan is that it exposes pins that can be connected to the i2c IP, and that I link them together in the block design.
It's the first time that I do this, so it may or may not be the right decision. The future will tell ...