Analyzer Block Design
The analyzer block is a modular and configurable piece of hardware that performs all of the acquisition and decoding of the digital inputs. It has 4 inputs and is able to decode SPI and I2C, with a modular interface to allow more protocols to easily be supported. Additionally, it is able to record any rising and falling edges on a pin, so that all digital signals can be completely reconstructed. The block is reconfigurable at runtime, meaning that the crossbar connections, decode type, and trigger conditions can all be modified by the GUI without a hardware recompilation.
This block supports decoding I2C or SPI, or simply recording each incoming rising and falling edge to completely reconstruct a given digital signal. Each decoder receives inputs from the input crossbar, which is capable of remapping any of the four external facing inputs to the pins of the decoder. The decoders need to know which pin is the clock, data, or any other important signals to a specific protocol. The input crossbar allows the user to plug in the signals any way that they like, and using the software interface, they can let the hardware know how they connected their signals, rather than forcing the user to use specific pins for specific signals.
The SPI decoder is relatively straightforward. A transaction begins when the CS line is pulled low. With the CS low, the decoder will shift in a new value from MOSI and MISO every rising edge of SCK. When CS is brought high, if the decoder senses that an entire transaction has happened, the SPI values will be sent to the analyzer block. This means that the decoder will throw away transactions that do not send the full 8 bits, to avoid misidentifying malformed transactions. An example in ModelSim is shown below.
The above snapshot of ModelSim shows how the decoder behaves given two different SPI transactions. The first transaction shows 0xFF on both the MOSI and MISO lines. We can see that the internal “miso_value” and “mosi_value” signals update with each rising edge of SCK. When we reach the end of the transaction, both of these signals represent the values sent across the bus. When CS is pulled high, the two miso and mosi values are packed into a single result called “acquire_data,” and the “acquire_val” signal is asserted to let the analyzer block know that it should grab this decoder’s data. SImilarly, another SPI transaction immediately follows the first, this time sending 0xAA and 0x55 on the MOSI and MISO lines. Just like the previous transaction, the values of “mosi_value” and “miso_value” are updated as bits arrive, and eventually the output is latched and the valid bit asserted.
Decoding I2C was slightly less straightforward than SPI. An I2C transaction consists of four main parts: a START condition, 8 bits of data, an ACK/NACK, potentially a STOP condition. This decoder keeps track of whether the start is a repeated start or not, the 8 bit data, whether there was a NACK or an ACK, and whether a STOP was asserted.
The above waveforms from ModelSim show an example I2C sending 0xFF with a start and an ACK. We can see the “current_byte_id” increase from 0 to 7 as the rising edges of SCL clock new values on the bus. The byte value is slowly constructed throughout the transaction, resulting in a 0xFF value returned to the analyzer block. From the signals toward the button of the picture, we can see the various points at which the START, ACK, and STOP conditions are asserted.
The trigger unit can be configured to trigger on a number of different events. It can trigger on any rising or falling edge of any of the inputs, or a masked transaction value from one of the decoders. For example, this means it is possible to trigger on the following conditions:
- On any rising edge
- On the falling edge of pin 1 OR the rising edge of pins 2 and 3
- When 0x42 is send over SPI
- When the first four bits of an I2C transaction are 1
Since the trigger unit supports a mask and a condition, the set of potential triggers is quite large. Additionally, since the trigger unit’s configuration is distinct from the overall blocks configuration, it is possible to be decoding one protocol and triggering on either rising/falling edges or even another protocol.
Analyzer Clump Design
Our design allows for a number of analyzer blocks to be stamped out. This modular design was implemented to allow our design to be run on a variety of hardware platforms, with the number of decoder channels able to scale with the available hardware on the target device. We call the grouping of analyzer blocks a “clump,” and the figure below attempts to illustrate the various components of this module.
The variable number of analyzer blocks are all connected to a Round Robin Arbiter. Each of the analyzer blocks is attempting to write samples to the shared SRAM. This arbiter was sourced from the vc library provided by the ECE 4750 and ECE 5745 courses at Cornell. The arbiter takes in a bit vector of requests, which are entities requesting access to a shared resource, which is the shared SRAM in this case. The blocks only assert their request when they have data waiting in their internal buffers to write. The round robin arbiter will accept requests in a round robin fashion, attempting to be as fair as possible while ensuring that only one analyzer block is granted permission to write to the SRAM on a given cycle.
Latency Insensitive Communication
Since our analyzer blocks will be producing results at unknowable times (since the data directly corresponds to outside inputs) we felt that a latency insensitive interface that can tolerate quick bursts of data would be the most appropriate. All of the analyzer blocks share a common SRAM where they write their sample data to. This is to ensure maximum memory utilization (partitioning into multiple SRAMs could potentially be very wasteful) and reduce software complexity. The diagram below shows how we structured our interfaces.
For each ring buffer, a corresponding “Ring Buffer Manager” would keep track of its relevant metadata. Each ring buffer has a read pointer and a write pointer, where the read pointer describes where in the buffer the reader is, and the write pointer describes where the writer is. If the write pointer is behind the read pointer, then there is space to write. If the write pointer is at the read pointer, the buffer is full and we cannot write. There is similar logic for the read pointer. If the read pointer is in front of the write pointer, then we can read, but if the read pointer is just behind the read pointer, then the buffer is empty and we cannot read. The ring buffer managers take the role of disguising the ring buffer as an infinite string of memory, for easy use by both reader and writer logic.
Function Generator Design
The function generator implementation uses the onboard 3-channel DAC that is meant to drive the R, G, and B components of a VGA signal. Instead, we use each as an 8-bit DAC channel to generate arbitrary functions. Each channel has an SRAM that contains the points to output. The SRAM is controlled by software and each function generator cycle, a new sample is read out from the SRAM and outputted on the DAC. The function generator also has a prescaler that can be used to adjust the rate at which samples are read from the SRAM. This is so the user is not locked in to the sample rate of the FPGA clock, and they can choose an arbitrary prescaler value to slow the clock down. The SRAM is writeable from software and easily controllable by the GUI.
Realtime testing with PIC32
In order to test the decoding capabilities of our logic analyzer, we decided it would be best to use a PIC32 to generate realworld waveforms. Thanks to Prof. Bruce,
we were able to use the PIC32 Boards and PIC microstick from ECE4760 to make this part of the project relatively straight forward. To test correct functionality, we
required waveforms for various GPIO signals, SPI, and I2C. The PIC outputted 4 GPIO signals, all of different duty cycles and periods. For SPI, the PIC acted as the master
and transmitted values from 0-255 repeatedly and slowed it. For the I2C protocol, we ended up bitbanging specific values, as we ran into some problems using the I2C peripherals on the PIC32.
We use the following code to shift the PIC32 into slave mode:
// Open SPI Channel 2 as master (@500 kHz) 8 bit mode
SpiChnOpen(spiChn, SPI_OPEN_ON | SPI_OPEN_MODE8 | SPI_OPEN_MSTEN | SPI_OPEN_CKE_REV , spiClkDiv);
// SDO2 (MOSI) is in PPS output group 2, could be connected to RB5 which is pin 14
PPSOutput(2, RPB5, SDO2);
PPSInput(3, SDI2, RPB13); // RBP13 --> SDI2
PPSInput(3, SS2, RPA3); // RPA3 --> Chip select
The interrupt service routine code for SPI:
// CS low to start transaction
mPORTBClearBits(BIT_4); // start transaction
// test for ready
// write to spi2
WriteSPI2(count++); //send values 0-255
// test for done
while (SPI2STATbits.SPIBUSY); // wait for end of transaction
// CS high to end transaction