I have worked on many embedded systems during my career. Some of those projects used a processor while others required custom approaches such as using a field programmable gate array (FPGA). I’ve been using FPGAs since the early 1990’s where initially they where configured using schematic entry. Later we moved to VHDL to create behavioral descriptions of the circuits. Today a more common FPGA hardware description language is Verilog.
For an upcoming proof of principle project I determined that a processor only solution wasn’t flexible enough to meet some of the strict timing requirements as well as not supporting potential system changes based on testing.
Some of the project requirements included the measurement of multiple input signal properties, controlling a digital to analog converter (DAC), and sending the measurement results to another computer via USB.
While searching for a simple FPGA development board and programming environment I discovered the Alchitry FPGA board set and development environment with the hardware being sold by SparkFun.
The current hardware offering has three different FPGA development boards. Two of the boards use the AMD (Xilinx) Artix 7 FPGA (Au and Au+) and the third board uses the Lattice iCE40 HX (Cu). Each board has different resources but all include a connector system for adding additional boards that add a USB 3.0 interface (Ft Element), multiple IO types (Io Element), and an IO breakout (Br Prototype Element). For my project the Au FPGA Development Board with the Br Prototype Element Board gave me the resources and connectivity that I needed. The picture below shows the SparkFun Alchitry Au FPGA kit that includes the Au, Io, and Br boards plus a female header set.

The Alchitry Labs development environment is an abstraction layer called Lucid to help simplify FPGA programming. The Alchitry Labs IDE actually converts Lucid structure into Verilog before interfacing with the FPGA vendor’s tools to create your FPGA image. The Alchitry Labs IDE has an extensive library of components (e.g., I2C Controller) as well has having an interface to the FPGA vendor’s tools to create specialized IP cores (e.g., clocking module).
One of the project’s features is to control a DAC. For that task I decided to use an Adafruit MCP4725 breakout board that has an I2C interface. The MCP4725 is a 12-bit DAC that supports a 3.4Mbps fast mode I2C interface. This requires the FPGA to operate as an I2C controller. The Alchitry Au has a QWIIC/STEMMA QT connector to support a 3V3 I2C connectivity and the IDE has an I2C controller component.
In this I2C controller example I am using Architry Labs Version 1.2.7. For my project there are basically four steps to control an I2C DAC:
- Add the Alchitry I2C Controller component (Lucid source file)
- Define I2C FPGA pinout (interface constraint file)
- Create MCP4275 module to write new DAC value (voltage)
- Create a test driver to verify operation
To add a predefine component in the IDE use Project > Add Components. A popup window appears where you select under Protocols the I2C Controller. This adds a file, i2c_controller.luc, under Components in the IDE left pane.

To define the pinout you need to know which pins are assigned to SCL and SDA. The Au environment as an IO abstraction that matches the Br pinout, which is show below (from the SparkFun website). You can either add the pinout to an existing Constraints file (alchitry.acf) or create a new one and add the lines below. I did not add a pullup resistor to each line since the MCP4725 breakout already has pullup resistors. I am using the Au QWIIC connector on Au board requiring the signals to be assigned as scl = A24 and sda = A23. The letter is the bank and the number is the pin within the bank (e.g., A23 is bank A, pin 23, lower right of bank A).
pin scl A24;
pin sda A23;

To create the MCP4725 module add a new file, File > New File. In the popup add the file name and select Lucid Source and then Create File.
The module interface is listed below. The signal vdata[12] is the 12-bit voltage value and write is a single clock signal (high = write) that starts the DAC write over I2C. The module has feedback to indicate if the module can accept a new value via the busy signal. Busy HIGH indicates the module is current writing a DAC value and cannot accept a new value.
module mcp4275 (
input clk, // clock
input rst, // reset
input vdata[12], // 12-bit voltage data to write
input write, // write value to mcp4275
output busy, // busy indicator 1 = busy
inout sda, // i2c data
output scl // i2c clock
)
This module has the I2C controller (qwiic) that uses a FSM (state) to send the data (voltage value) to the MCP4725 as well as a memory element to save the vdata input in case it changes during FSM execution (dff newvalue[12]).
.clk(clk) {
.rst(rst) {
fsm state = {IDLE, START, ADDRESS, BYTE2, BYTE3, STOP}; // mcp4725 states write
i2c_controller qwiic(.sda(sda)); // i2c controller
dff newvalue[12]; // value to be written to DAC
}
}
For this example we are only performing a single write of a 12-bit value to the DAC over I2C. Future enhancements include creating a fully supported MCP4724 interface that includes writing and reading from the MCP4725 EEPROM. For now we are only interested in changing the DAC voltage value. To start we assign some of the default signal values in the always block. The module is busy when not in the IDLE state.
always {
busy = state.q != state.IDLE;
// i2c signals and connections
qwiic.start = 0;
qwiic.stop = 0;
qwiic.write = 0;
ack_write = qwiic.ack_write;
scl = qwiic.scl;
qwiic.read = 0;
qwiic.data_in = 8hxx;
qwiic.ack_read = 0;
The core of this module is the FSM that executes the steps required to write the new voltage value to the DAC. The description on how to use the I2C Controller component is found in the Component source code. As a summary, to use the I2C Controller you set the start signal to high for 1 clock cycle and then write or read as many bytes as desired. Once finished, set the stop signal high for 1 clock cycle.
To write to the MCP4725 we are using the fast mode write method has shown in the datasheet in Figure 6-1. This method transmits 3 bytes – Byte 1: the 7-bit address with the R/W bit set to 0, Byte 2: the upper 4-bits of data with the command bits (C2, C1 = 0) and normal power (PD2, PD1 = 0), and Byte3: lower 8-bits of data.
The FSM Lucid code is shown below. The FSM is IDLE until the module input write bit is set to 1, which saves the input data to a 12-bit DFF and then moves to the next state START. If the controller isn’t busy, the I2C controller start bit is set to 1 and the next state is ADDRESS. When the controller isn’t busy, each of the three bytes is sent in order. Once all three bytes have been transmitted, the I2C controller stop is issued. There are two items to note. First, in each non IDLE state the controller is check for busy before writing the next byte or stopping the controller. Second, although available, the write ack is never checked. I2C has support an acknowledgement after each byte is written to the peripheral.
// fsm for updating mcp4275 output
// using fast mode write method (datasheet Figure 6-1)
case(state.q) {
state.IDLE:
// send new dac data
if(write) {
// transmit new DAC data over i2c
state.d = state.START;
newvalue.d = vdata; // capture new DAC data
}
state.START:
// start i2c controller
if(!qwiic.busy) {
state.d = state.ADDRESS;
qwiic.start = 1;
}
state.ADDRESS:
// if not busy write 1st byte, i2c address, write = 0
if(!qwiic.busy) {
state.d = state.BYTE2;
qwiic.write = 1;
qwiic.data_in = 8b11000100; // address 0x62 (<< 1)
}
state.BYTE2:
// if not busy write 2nd byte, C2,C1, PD1, PD2, upper 4 data bits
// C2 = C1 = 0 (Fast Mode Command), PD1 = PD2 = 0 normal power
if(!qwiic.busy) {
state.d = state.BYTE3;
qwiic.write = 1;
qwiic.data_in = c{4b0000, newvalue.q[11:8]};
}
state.BYTE3:
// if not busy write 3rd byte, lower 8 data bits
if(!qwiic.busy) {
state.d = state.STOP;
qwiic.write = 1;
qwiic.data_in = newvalue.q[7:0];
}
state.STOP:
// stop i2c controller
if(!qwiic.busy) {
state.d = state.IDLE;
qwiic.stop = 1;
}
}
The final item is to test the design. At the top level I created a counter. Whenever the 12-bit counter value changes, the value is written to the DAC. This creates a sawtooth waveform since the counter wraps around from 0xFFF to 0. The code for the test driver is shown below.
module au_top (
input clk, // 100MHz clock
input rst_n, // reset button (active low)
output led [8], // 8 user controllable LEDs
input usb_rx, // USB->Serial input
output usb_tx, // USB->Serial output
inout sda, // i2c data
output scl // i2c clock
) {
sig rst; // reset signal
.clk(clk) {
// The reset conditioner is used to synchronize the reset signal to the FPGA
// clock. This ensures the entire FPGA comes out of reset at the same time.
reset_conditioner reset_cond;
.rst(rst) {
counter dacoutput(#SIZE(12), #DIV(8)); // test data driver, voltage 12-bit value
dff newvalue[12](#INIT(12h555)); // value to be written to DAC
mcp4275 voltage(.sda(sda)); // write new voltage value
}
}
always {
reset_cond.in = ~rst_n; // input raw inverted reset signal
rst = reset_cond.out; // conditioned reset
led = 8h00; // turn LEDs off
led = newvalue.q[11:4]; // upper 8 bits of DAC voltage value
usb_tx = usb_rx; // echo the serial data
voltage.write = 0;
voltage.vdata = newvalue.q;
scl = voltage.scl;
if(newvalue.q != dacoutput.value && voltage.busy == 0) {
// transmit new DAC data over i2c
newvalue.d = dacoutput.value; // capture new DAC data
voltage.write = 1;
}
}
}
The results were as expected. The only issue I encountered was related to my original FSM structure using signals to interface to the I2C controller. Most of the FSM states were optimized out, thus not executing the desired sequence. That problem was solved by interfacing directly with the I2C controller component signals. The output waveform is shown below. The sawtooth period matches the expected rate (100MHz clock / 256 (counter #DIV(8)) / 2^12 (counter size) = 95.367Hz).

Improvements to the DAC module include making a fully featured MCP4725 interface as well as modifying (creating my own) I2C controller to support the 3.4Mbps fast mode I2C interface. Currently the I2C controller operates at a single frequency based on the user specified divider. To enter the fast mode I2C a command is sent at the “normal I2C rate” then scl is increased to 3.4MHz.
I found both the hardware and Lucid programming environment to be excellent, as well as Alchitry support. The Au development board provides plenty for hardware resources and IO. The SparkFun boards are well made and of high quality. The hardware costs are very reasonable even for a hobbyist. The Lucid programming environment does simplify FPGA behavioral programming and interfaces well with the vendor’s FPGA tools. The Alchitry cores support the majority of a user’s needs. I recommend this board set and programming environment for both hobbyist and professionals developers.