mikroBus is an add-on board standard from Mikroe. Most commonly implemented by Mikroe's "Click" boards, mikroBus enables developers to plug a wide variety of components in to a development board through a compact hardware interface. Development boards typically have somewhere between one and three mikroBus sockets. In the case of small development boards that have only one socket, connecting to multiple external modules can be a hassle. With one Click board socketed in, you may not have any I/O left for connecting another required device. The mikroBus I/O Expander, nicknamed mikroBridge, is an example of how to solve this problem by using an FPGA to create a bridge between multiple mikroBus sockets.
The mikroBus standard requires that pins be reserved for I2C, SPI, and UART communication as well as five GPIO pins. The standard pin assignments for the GPIO pins are Analog, Reset, SPI Chip Select, Interrupt, and PWM. However, these pins are generally reassigned to suit the component on the add-on board. Communication pins (ex. SCL, TX, SCK, MOSI, etc.) are left unconnected when unused and are not reassigned. Therefore, a bridging system must take this variability into account to accommodate as many add-on boards as possible.
Additional information about the mikroBus standard can be found on Mikroe's website: mikroBus Standard Specification
The final system was implemented with a Microchip IGLOO2 FPGA. Source files may be found below and include a Libero project, VHDL, test benches, and a KiCad project with gerber files.
The remainder of this article walks through the architecture of the mikroBridge system with summaries of each building block.
The functionality of the mikroBridge is twofold. It needs to act simultaneously as a multiplexer and an I/O expander. The requirements are simple:
- Any signal or communication bus must be able to be routed directly to the corresponding pin(s) on any or all of the expansion sockets. (Multiplexing)
- Pins used as outputs on the expansion sockets must be writable during run-time while not selected by the mux.* (I/O Expansion)
- Pin direction must be programmable during run-time for non-communication pins.
It is expected that frequently changing signals (ex. PWM) will be directly routed while less frequently changing signals (ex. RST) will be controlled indirectly through the I/O expansion capabilities.
*One exception to the above is the analog pin, AN. The implementation described here uses an external analog switch which does not have the capability to drive the pins that aren't selected. However, such a circuit could be developed with additional external components.
Simplified Block Diagram
This section provides a brief overview of the individual components used to build up the functionality specified above. Most implement fairly standard behavior (muxes, serial communication, etc). Special functions are noted where necessary.
Bidirectional I/O Primitive
Related mikroBus Signals: SCL, SDA, CS, RST, INT, PWM
At the I/O pin level, a bidirectional buffer is critical. Because the 5 GPIO pins can essentially be used for any functions, the mikroBridge must accommodate them as both inputs and outputs. Bidirectional signals are also required for the I2C communication bus. This is the only component of the design that is dependent on silicon manufacturer as each toolchain handles instantiation of bidirectional buffers differently.
The above image is a BIBUF as it appears in Microchip's Libero design tool. Y is the logic level on the I/O pad. D is the level that will be driven on the pad if E is high. If E is low, Y can be read as an input. In the case of the I2C pins, SCL and SDA, the D input of the bidirectional buffers is tied low because I2C lines are only pulled low and never driven high.
Multiplexer / Demultiplexer
Related mikroBus Signals: MISO, RX, TX
For the communication signals that cannot be reassigned, muxes and demuxes are used to route the signals between the host and outputs. They have two generic parameters for configuring the width, N and SEL_WIDTH. N sets the width of the i or q ports for a mux or demux, respectively. SEL_WIDTH sets the width of the sel port. SEL_WIDTH must be specified such that the number of available addresses is greater than or equal to N. For example, if N is 4 then SEL_WIDTH must be greater than or equal to 2. Addresses outside of N will result in a default value on the ports, '0' for the mux or defaults for the demux.
The demuxes have 2 additional ports for added flexibility. The first special parameter is the defaults port. This port defines the default or idle value of the q side outputs while they are not currently selected. This allows lines to idle high instead of low. The second special parameter is demux_pass_all. If '1', a special condition is created when sel = "11" that will pass the input on i to all q outputs. The most common use for this is sending a reset signal to multiple devices at the same time.
Bidirectional I/O Multiplexer
Related mikroBus Signals: SCL, SDA, CS, RST, INT, PWM
Each of the GPIO pins that can be arbitrarily assigned requires it's own bidirectional mux/demux to control it's direction (input or output) and how it is routed to the host mikroBus interface. In the image above, the i-prefixed signals represent the host side while the q-prefixed signals are the bridged side. The directions are typically chosen such that the bridge side directions are the inverse of the host side. A direction value of '1' indicates an output and '0' indicates input. This block then acts as a typical mux or demux depending on the configuration with two special demux parameters described above.
Example: If CS is an output from the host, set i_dir to '0' (input) and set q_dir to "11" (outputs). The CS signal is then passed from i to one of the q outputs, depending on sel. SPI CS lines are active low and need to idle high so set demux_defaults = "11".
Related mikroBus Signals: SCL, SDA
I2C is inherently addressable so it doesn't require muxing like SPI and UART do, but it still must be routed to each of the expansion sockets. In this hardware design, the FPGA is an intermediary on the bus and acts as a repeater, duplicating the signals from the I2C master for each of the slaves. The I2C bus could have been physically connected to each socket allowing this step to be skipped, but passing it through the FPGA provides greater flexibility in the long run.
In order to create a pass-through effect on the I2C signals, the module must know whether the master or the slave is driving a signal. When the slave is driving a line, the Y output of the BIBUF is '0' and the output enable (E) is also '0' indicating that the line is being pulled low externally. The corresponding output enable on the master side is then set to '1' to pull that line low. In all other situations, the master input Y is inverted and connected to the output enable of the slave side BIBUFs, copying the level on the master line to the slave line.
Care should be taken with regard to I2C pull-up resistors. In the initial tests for this system, the FPGA's internal pull-ups were used in the absence of dedicated external ones. The value of the weak internal pull-ups is around 10k ohms resulting in a rise time of ~1us on the I2C lines. This is very slow relative to the rest of the system and requires compensation to avoid errors. To do this, a 2-bit shift register is added on the output enable signals to create a delay. Choosing a clock frequency of 1 MHz matches the total delay with the rise time of 1us, which ensures the signal is stable when sampled. It is required for both bits of the shift register to be '0' before deciding that the line is stable and being pulled low externally.
Additionally, the output enable signals from the internal I2C interface tend to cause metastability issues when crossing from the higher frequency clock domain of the control system into the 1 MHz clock domain of the pass-through module. Similar to the output delay buffers above, a 2 bit synchronization chain is used on the int_slave_scl_e and int_slave_sda_e signals to ensure stability.
Without the input synchronization and rise time compensation, the internal I2C controller ends up in invalid states and the SDA line may stick low until it is reset.
A modified RAM block is used to hold all of the settings for the system such as GPIO directions, mux selects, demux defaults, etc. There's nothing special happening here except for the individual signals being broken out at the top level of the module to connect to the muxes.
While the memory is partitioned into single byte words to match the incoming serial communication, most of the settings are only a few bits and are sliced as necessary from the lowest bits of the byte. This enables the host to read back the entire byte that was written into memory over the serial interface which can be helpful for debugging. It also allows the design to be easily extended to more than two mikroBus sockets if necessary.
The memory map is provided below:
Default values are chosen according to the mikroBus default assignment and assume reset signals are active low. They may be easily adjusted as needed in the source file.
A breakdown of each "register" setting is included below.
This register sets the position of the muxes/demuxes. Except for AN_SEL, each _SEL register follows this format.
|0x00||Route to Socket 1|
|0x01||Route to Socket 2|
|0x02||Route to internal RAM interface (SPI and UART)|
|Others||Unused / default|
For the AN_SEL setting,
|0x00||Off / not connected|
|0x01||Route to Socket 1|
|0x02||Route to Socket 2|
This register sets the level of the demuxed outputs when not selected. All of the _DEMUX_DEFAULTS registers follow this format.
|X||X||X||X||X||Internal RAM Interface*||Default for Socket 1||Default for Socket 0|
*For SPI or UART interfaces only.
This register determines which demuxes use the special "pass all" SEL value of 0xFF. A value of '1' for a demux enables the pass all function.
These registers set the pin direction for the bidirectional GPIO pins. It is tied directly to the output enable of the BIBUF. All of the _DIR registers follow this format.
A '1' indicates that signal is an output from the FPGA.
A '0' indicates that signal is an input to the FPGA.
|X||X||X||X||X||Host||Socket 1||Socket 0|
Related mikroBus Signals: SCL, SDA
I2C is the primary way of interfacing with the mikroBridge as it is naturally addressable, which makes it always available. It does not need to be muxed to the internal controllers like SPI and UART do.
This module implements basic I2C communication with 7-bit addressing and no clock stretching. The slave address of the module can be configured by a generic. By default the slave address is 0x25.
To transmit, tx_data is loaded into the send buffer on a rising clock edge when tx_load is high. When a byte is received, including the slave address byte, rx_ready is pulsed to signal valid data on the rx_data port.
Instead of using inout ports, the internal I2C slave is set up to explicitly use the BIBUF signals. This method adds an extra level of clarity when trying to create the pass-through effect described above.
Related mikroBus Signals: CS, SCK, MOSI, MISO
SPI is an optional method of interfacing with the mikroBridge. To connect to the internal SPI controller, spi_sel must be set to "10". By default the internal SPI slave operates in Mode 0, but can be changed using generics.
The SPI slave operates nearly identical to the I2C slave. Data is loaded into the send buffer on a rising clock edge when tx_load is high and rx_ready is pulsed after each received byte.
Related mikroBus Signals: TX, RX
UART is another optional method for interfacing with the mikroBridge. To connect to the internal UART controller, uart_sel must be set to "10". Baud rate and other parameters are configurable using generics. Remember to set the correct system clock frequency to get the correct baud rate. By default, the baud rate is set to 19,200 with a system clock of 25 MHz.
The VHDL for the UART is borrowed from another eeWiki page. More details about its operation can be found here: UART (VHDL)
Serial to RAM Interface
Related mikroBus Signals: SCL, SDA, CS, SCK, MOSI, MISO, TX, RX
Each of the serial protocols has a unique wrapper used to communicate with the config RAM. This wrapper layer allows the interaction with the RAM to be virtually identical regardless of the chosen serial protocol. Whenever the underlying serial interface receives a byte, the wrapper layer routes it to the correct port of the RAM using a simple state machine. The command structure is outlined below.
The mikroBus host can read and write the RAM with a 2-byte command consisting of an address and data. The address byte is a 7-bit address with a read/write bit in the uppermost position. The data byte is just a single ordinary byte. Address bits beyond the actual addressable space of the config RAM may be treated as "don't care" and will be sliced to match the width of the RAM's addr port.
Command Byte Format
For write operations R/W = '0' and for read operations R/W = '1'. See the following examples for more details.
|Protocol||Slave Address + I2C R/W||R/W | RAM Address||RAM Data|
|Protocol||Slave Address + I2C R/W||R/W | RAM Address||Slave Response|
|I2C||0x4B||0x82||Value at 0x02 (INT_SEL)|
|SPI||N/A||0x82||Value at 0x02 (INT_SEL)|
|UART||N/A||0x82||Value at 0x02 (INT_SEL)|
Related mikroBus Signals: SCL, SDA, CS, SCK, MOSI, MISO, TX, RX
The interface controller is one level above the serial to RAM interfaces in the hierarchy. It is a wrapper that takes the three serial modules from above and merges them into a unified interface to the RAM. It allows the mikroBus host to use any of the three interfaces and will automatically switch to the correct one if activity is detected. Recall that SPI and UART must be muxed to the internal controllers (sel = "10") before trying to communicate with the RAM.
In the unlikely case of a conflict, an arbitration process assigns control of the RAM bus based on the following priority (highest to lowest): I2C > SPI > UART.
Prior to physical implementation, the mikroBridge system was simulated using Modelsim. The complete Libero project includes a master test bench for the top level mikroBridge system as well as some smaller test benches for sub-modules.
The test bench is not completely exhaustive, but covers the majority of use cases. Each serial interface is tested to ensure that it can read and write to the config RAM. Each of the possible positions for the routing muxes is also verified. A TCL .do script is included to automatically format the wave view with separate sections for each module for easier viewing.
A PCB implementing a 1:2 mikroBridge design using a Microchip IGLOO2 FPGA was developed in KiCad.
This PCB design does not fit the mikroBus standard specification because there isn't a way to fit two full sockets onto any of the standard sizes. However, since this is primarily intended for use with single socket development boards, it is an acceptable deviation.
For testing, the mikroBridge PCB was paired with an AVR-IoT development board which has a single mikroBus socket. An Atmel Studio project was developed to mirror the simulation test bench and verify the behavior of the final implementation. Like the main simulation test bench, the AVR version steps through all possible routing scenarios and verifies that each interface can read and write the config memory. A header file is included in the project with #defines for the RAM memory map.
On the development board, SW0 is used to advance through the test cases and SW1 resets to the initial state. LEDs are used to indicate if a memory read/write was successful. Verification of the signal routing requires an oscilloscope.
Note: I2C pull-up resistors (R13-R16) on the expansion sockets were not present in the initial version of the PCB and were added later. Internal pull-ups on the IGLOO2 were used for testing with degraded performance.
Libero project with VHDL source files, test benches, etc: mikrobridge_wiki.zip
KiCad schematic and layout source files, BoM, and gerbers for the board can be downloaded here: mikrobridge_3.3.20.zip
AVR test bench source: ClickBridgeTest.zip
The mikroBridge is a useful development tool for expanding I/O limited development boards. It demonstrates the application of an FPGA as a flexible I/O expander. This basic structure leaves the door open for additional customizations to suit an even wider variety of applications.
There were a number of interesting lessons to be learned in the development process, particularly regarding the routing of I2C signals through the FPGA. Accounting for timing and signal stability was critical to get the I2C communication to function properly.
While the IGLOO2 implementation above is effective for demonstration and learning, it is far from a cost-optimized solution. Other options exist which could reduce the cost and size of the final product. With the current design, most Click add-on boards are compatible, but there is still room for improved flexibility especially on the analog signal pins.
For comments, feedback, or questions please visit Digi-Key's TechForum: TechForum
Or via email at: firstname.lastname@example.org.