LiFi #3: Sequence Detector
Table of Contents
Abstract
Finally getting to something more related to LiFi: The Sequence Detector. I plan on using a sequence detector in unwrapping a framed packet, by detecting the start or end bytes or any sort of command byte. In this blog I will be implementing a 4-bit sequence detector to simplify the process of the creating the state diagram.
The module can be configured to match the state machine parameters using the verilog parameters, we will also be using switch-cases to model this state change behavior. The target sequence or pattern will be (1101), the detector will be non overlapping.
There are essentially 2 types of ways to model a Finite State Machine (FSM):
- Moore Model
- Mealy Model
So without any further ado, let's get right into it!
The State Diagram
The state diagram describes the branching behavior of the state machine, in other words we go through each stage of the sequence and apply branching rules to them. The model we will be using is the Mealy model. The reason for using the Mealy model is simply because the number of states involved is the same as the length of the pattern to be detected, compared to Moore which taken an additional state before resetting.
The fraction-like elements in between the arrows indicate {input} / {output}, since we are using a non-overlapping sequence detector we find that the output is true only from state 3 to state 0, this observation can be used to optimize the module.
Speaking of the module it will have 2 inputs [clk, data] and 1 output in its basic form however in terms of the actual verilog module we will be exposing a few more ports in order to obtain greater information of the inner workings of the sequence detector. The clock registers the data into the buffer of the detector. We will also be implementing a CLR or RESET pin to act as an ENABLE for the detector.
The State Table
The state table translates the state changes into a table which we can directly implement in the FSM switch tree in our verilog module.
Here X stands for the input applied at the current state. Next state signifies the next state for a particular input and output is the result, and since we are using a non-overlapping sequence detector we will have only one true state.
We will be encoding these states in the 2 bit number, as in S0 can be 00, S1 can be 01 and so on.
Implementing the FSM
Now we come back to the world of HDL, the plan is to simulate the module using a large enough test sequence and validating the result before moving to the actual hardware testbench.
Sequence Detector module
The sequence detector needs to expose not only the result but also the state information. In order to make the module configurable I decided to make the state flags a parameter, this will enable us to define the state changes for both the inputs and also specify the output flag at the last state. The TL;DR is that we need the following port list:
-
INPUTS:
- clk
- data
- reset
-
OUTPUTS:
- result
- current_state (2bit)
-
PARAMETERS:
- X0 (8bit) - false state changes
- X1 (8bit) - true state changes
- TRIGGER - input at the last state that gives a true result
// Sequence detector module
module sequence_detector #(parameter [0:7]X0=8'b0, parameter [0:7]X1=8'b0, parameter TRIGGER=1'b0) (
input wire clk,
input wire data,
input wire reset,
output reg result,
output reg [1:0]current_state = 0
);
initial begin
current_state = 2'b00;
end
always @(posedge clk or posedge reset) begin
if (reset)
current_state = 2'b00;
else begin
result = 1'b0;
case (current_state)
0 : current_state = (data) ? X1[6:7] : X0[6:7];
1 : current_state = (data) ? X1[4:5] : X0[4:5];
2 : current_state = (data) ? X1[2:3] : X0[2:3];
3 : begin
current_state = (data) ? X1[0:1] : X0[0:1];
if (data == TRIGGER) result = 1'b1;
end
default: begin
current_state = 0;
end
endcase
end
end
endmodule
You may have noticed the parameters X0 and X1 are 8 bits in length, this is because we are trying to fit the four rows of state changes from the table, and each state change can be encoded into 2 bits, therefore (2 x {number-of-states [4]} = 8 bits).
I have implementing the input branching using the tertiary operator [?:] to shrink the code a bit. The final state 3 [11in binary] needs an additional line to set the result on equal TRIGGER. Finally for undefined inputs we clear the current_state. The reset pin fixes the current_state to S0, emulating an ENABLE pin.
The parameters X0 and X1 will be defined from the state table like so:
TestBench
The testbench will be able to create a large bitstream of say 128 bits containing random bits, eventually a group of 4 bits will contain the matched pattern which I will now call the target.
I used the verilog directive urandom
to create a seedable 32 bit random number which I repeat and concatenate to the 128 bit vector and report this number to the console, then apply a 2ns data bit send, 2ns later the clock signal gets toggled with a time period of 4ns. Which would look this:
The testbench code contains a counter for the for loop required to test all the bits in the stream along with the corresponding delays.
`timescale 1ns / 1ns
// test bench module
module testbench;
// for loop counter
integer counter;
// set the port list
reg clk = 1'b0;
reg reset = 1'b0;
reg data;
reg [0:127]sample;
wire result;
// create the wrapper
sequence_detector #(.X0((2'b00 | 2'b00 << 2 | 2'b11 << 4 | 2'b00 << 6)), .X1((2'b01 | 2'b10 << 2 | 2'b10 << 4 | 2'b00 << 6)), .TRIGGER(1)) wrapper (
.clk(clk),
.data(data),
.reset(reset),
.result(result)
);
initial begin
// place 32 bit random numbers
sample[0:31] = $urandom;
sample[32:63] = $urandom;
sample[64:127] = $urandom;
$display("----------------------------");
$display("TEST NUMBER = %128b", sample);
for (counter = 0; counter < 128; counter = counter + 1) begin
#2 data = sample[counter];
#2 clk = ~clk;
#4 clk = ~clk;
end
end
endmodule;
Notice how the parameters are defined, this is different from Blog #2, since defparam
is deprecated as a result using it would lead to spurious schematics after being sythesized. To make it readable I used shift operators in tandem with the OR bitwise operators. Also observe how the TRIGGER is set stating that when the current_state is S3 and the input is true then set the RESULT.
Let's perform a behavioral simulation using testbench as the simulation top module. Once the simulation workspace opens, select the wrapper object in the Scope
panel and clean up the graph by excluding the sample
and counter
signals. Set the simulation period to 1024 ns and simulate.
In the Tcl console below you will notice the TEST PATTERN
:
For those who may not be able to view this properly:
TEST PATTERN = 01000101010111001100110111011001000110110001010110110010100110010000000000000000000000000000000001000100010010010101101110010110
Take your time to manually check for the 1101 sequence and make a note of the number of occurences. We will have 5 matches and the waveform will look like this:
Notice the RESULT triggers 5 times as well, proving that our FSM works. You may try different seeds for urandom by passing on a seed parameter like so:
$urandom(<seed_value>)
Binary Converter
To make the circuit verbose-like in a way, I wanted to report the current_state using the 4 onboard LEDs where each LED would signal the current state number. For that we need to convert the binary encoded state to a counter-like value.
// Binary to Count module
module bin_to_count(
input wire [1:0] in,
output reg [3:0] out
);
always @(in) out <= 4'b0000 | (1 << in);
endmodule
The module is pretty self explanatory and can be perfectly written in a single line, where whenever there is a change in the binary input we need to convert the output by shifting a 1 by the binary number places. This is more aptly called a Decoder
however I prefer the term converter for this experiment.
ESP32 Real Time Testbench
I wanted to make a logic analyzer of sorts whose job is to validate the output from the FPGA board, so I used an ESP32 board [Wroom32] programmed using the Arduino framework on PlatformIO. I did this because the logic level supported by the ESP32 is 3.3V which matches the logic level of the CMOD S7 which helps since if I used an Arduino I would have to make a logic converter to support the right voltage which is unnecessary work.
The code I wrote is object oriented and scalable to higher bit length sequences and similar to our simulation testbench it creates a long test sequence. The code can be found on my GitHub page here. Follow the pin connections defined under the DEFINES
fence:
// DEFINES ========================
#define BAUDRATE 115200
#define CLK_INTERVAL 5 // clock pulse period
#define BIT_INTERVAL 2 // before clock rising edge
#define SEQUENCE_SIZE 256 // 256 bits per transfer
#define TARGET_SIZE 4
#define DATA 21
#define CLK 18
#define RESULT 19
#define RESET 22
#define START 5
#define INDICATOR 2
#define RANDOM_SEED_PIN 36
// ================================
Here the TARGET_SIZE
indicates the size of our test sequence, the RANDOM_SEED_PIN
uses the spurious ADC values from pin 36 of the ESP32 board to set the seed for the random number generator (RNG). The target sequence is defined as:
uint8_t target[TARGET_SIZE] = {1, 1, 0, 1}; // pattern to detect
You will also need a button to play or pause the test sequence. This button is to be connected to START
pin 5, in a PULLDOWN
fashion, since the pin is set to INPUT_PULLUP
.
I used the PlatformIO plugin for VSCode but it should work all the same for other editors. I have also kept a release file for the ESP32 which is precompiled and can be directly flashed on a ESP32-Wroom32 board using your flasher of choice.
Finalize
Now let's finalize the whole circuit using the top module.
// Top module
module top(
input clk,
input data,
input reset,
output result,
output [3:0] state_indicator,
output r,g,b
);
wire [1:0] state;
// create the wrapper
sequence_detector #(.X0((2'b00 | 2'b00 << 2 | 2'b11 << 4 | 2'b00 << 6)), .X1((2'b01 | 2'b10 << 2 | 2'b10 << 4 | 2'b00 << 6)), .TRIGGER(1)) wrapper
(
.clk(clk),
.data(data),
.reset(reset),
.result(result),
.current_state(state)
);
// create the output led array
bin_to_count state_display (
.in(state),
.out(state_indicator)
);
// create the result indicator
assign {r, g, b} = (result) ? 3'b101 : 3'b010;
endmodule
Here I used the RGB LEDs to indicate a positive result. The LED is pink when the result is false and green when the result is true. The state_indicator wire is connected to the 4 onboard LEDs and the result, clk, data and reset are connected to the the IO headers of the CMOD S7.
The testbench works by prompting the user to press the start button after which it creates a random bitstream of 256 bits, triggers the reset pin to clear the current_state and sends the bitstream to the FPGA board. It also runs a preliminary test before sending the bitstream to scan for any matches and saves their locations in memory, if the RESULT triggers at the right locations then it reports a TEST PASSED to the console. You can then press the START button again to repeat the test with a different bitstream.
The constrain file has the following connections made:
# required to suppress Tcl report
set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk]
## RGB LEDs
set_property -dict { PACKAGE_PIN F1 IOSTANDARD LVCMOS33 } [get_ports { b }]; #IO_L10N_T1_34 Sch=led0_b
set_property -dict { PACKAGE_PIN D3 IOSTANDARD LVCMOS33 } [get_ports { g }]; #IO_L9N_T1_DQS_34 Sch=led0_g
set_property -dict { PACKAGE_PIN F2 IOSTANDARD LVCMOS33 } [get_ports { r }]; #IO_L10P_T1_34 Sch=led0_r
## 4 LEDs
set_property -dict {PACKAGE_PIN E2 IOSTANDARD LVCMOS33} [get_ports { state_indicator[3] }]
set_property -dict { PACKAGE_PIN K1 IOSTANDARD LVCMOS33 } [get_ports { state_indicator[2] }]; #IO_L16P_T2_34 Sch=led[2]
set_property -dict { PACKAGE_PIN J1 IOSTANDARD LVCMOS33 } [get_ports { state_indicator[1] }]; #IO_L16N_T2_34 Sch=led[3]
set_property -dict { PACKAGE_PIN E1 IOSTANDARD LVCMOS33 } [get_ports { state_indicator[0] }]; #IO_L8N_T1_34 Sch=led[4]
## Dedicated Digital I/O on the PIO Headers
set_property -dict { PACKAGE_PIN L1 IOSTANDARD LVCMOS33 } [get_ports { reset }]; #IO_L18N_T2_34 Sch=pio[01]
set_property -dict { PACKAGE_PIN M4 IOSTANDARD LVCMOS33 } [get_ports { data }]; #IO_L19P_T3_34 Sch=pio[02]
set_property -dict { PACKAGE_PIN M3 IOSTANDARD LVCMOS33 } [get_ports { result }]; #IO_L19N_T3_VREF_34 Sch=pio[03]
set_property -dict { PACKAGE_PIN N2 IOSTANDARD LVCMOS33 } [get_ports { clk }]; #IO_L20P_T3_34 Sch=pio[04]
# Defaults
set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
set_property BITSTREAM.CONFIG.CONFIGRATE 33 [current_design]
set_property CONFIG_MODE SPIx4 [current_design]
Notice the first set_property CLOCK_DEDICATED_ROUTE FALSE [get_nets clk]
, this is done to suppress the warning of not using the specialized (dedicated) clock pin, I ignored this for the sake of making easy connections on the breadboard.
Let's plug in both the ESP32 devkit and the CMOD S7 and SIMP.
The synthesized schematic looks like this:
I can't show the whole internal structure properly here, but if you expand the module then you will many FSM blocks.
Finally after all this work we get a really cool looking hardware testbench!
As you can see we have a match at index 16 of the test sequence and if you follow the state report from the LEDs you can see that the result RGB LED turns green at that point. I deliberately slowed it down and reduced the test sequence for the video but you can get some really cool looking shots of it at high speed.
Conclusion
Now we can finally try this along with the Microblaze softcore, except the sequence detector there may be slightly different in terms of the target sequence and bit length, first we will be doing this via normal wires and then will be connecting the frontend for the LiFi to obtain a digital output from the transducers.
I hope you enjoyed this so far and I hope to meet you in the next one!