LiCoRICE Quickstart
Welcome to LiCoRICE! We’re excited to help you bring the principles of realtime computing to your conventional hardware. This guide provides instructions on how to build a simple model from scratch. We’ll start out with a model that just prints output to our terminal each tick and progressively add more complexity until we’re reading in external outputs, piping them through the realtime system, and writing them back out.
0. Prerequisites
This guide assumes that you’re working in a BASH shell on a POSIX-compliant system and have followed the LiCoRICE installation instructions.
1. Simple Hello World
This first hello world example will create a LiCoRICE model that outputs a message once a second 10 times. This is one of the simplest models we can construct with just one module. We’ll write everything from scratch.
Create a workspace
In this example, we’ll assume you’re using ~/licorice_quickstart
as your workspace.
export LICORICE_WORKING_PATH=~/licorice_quickstart
mkdir $LICORICE_WORKING_PATH
The above sets the LICORICE_WORKING_PATH
environment variable and creates the directory. This tells LiCoRICE where to look for model and module files. Feel free to change the path to anything that’s convenient on your system.
Specify the model
Create the model file:
touch $LICORICE_WORKING_PATH/quickstart-1.yaml
Open the created file add the following:
config:
tick_len: 1000000 # tick length in microseconds (1s)
num_ticks: 10 # number of ticks to run for
source_init_ticks: 1 # number of source ticks run before time t=0
modules:
counter_simple:
language: python
constructor: true
This specifies a LiCoRICE model that has a single module, counter_simple
which will run 10 times over 10 seconds and must complete its execution once every 1 second.
Generate modules
licorice generate quickstart-1 -y
This should generate two files: $LICORICE_WORKING_PATH/counter_simple.py
and $LICORICE_WORKING_PATH/counter_simple_constructor.py
.
Write modules
Open the constructor ($LICORICE_WORKING_PATH/counter_simple_constructor.py
) and add the following:
counter = 0
Then open the parser ($LICORICE_WORKING_PATH/counter_simple.py
) and add the following:
print(f"Hello World! Tick: {counter}", flush=True)
counter += 1
The constructor will create a variable counter
and set it to 0
before realtime execution starts. Then, each tick, the value of counter
will be output and incremented. We interpolate a Python f-string with the counter value and flush stdout so that the output appears in our terminal immediately.
Run LiCoRICE
In general, only one command (go
) needs to be issued to parse, compile, and run a model, but these commands can also be issued individually if need be:
licorice go quickstart-1 -y
If everything worked, you should see the following among the output in your terminal:
Hello World! Tick: 0
Hello World! Tick: 1
Hello World! Tick: 2
Hello World! Tick: 3
Hello World! Tick: 4
Hello World! Tick: 5
Hello World! Tick: 6
Hello World! Tick: 7
Hello World! Tick: 8
Hello World! Tick: 9
2. Pass a Signal
The first example showed you how to set up a simple LiCoRICE model with one module. Here, we’ll split that module in two and use a signal to pass data from the first module to the second. The behavior of this model will be exactly the same, but we’re able to see how LiCoRICE can pass data from a generator process to a printer process each tick.
Update model config
Copy over the model config to a different file:
cp $LICORICE_WORKING_PATH/quickstart-1.yaml $LICORICE_WORKING_PATH/quickstart-2.yaml
First, add another top-level block called signals with our signal definition as follows:
signals:
tick_count:
shape: 1
dtype: int32
This defines a NumPy array that can be shared between our models.
Now, add another module nested under modules:
with the following info:
modules:
tick_counter:
language: python
constructor: true
out:
- tick_count
...
The tick_counter
module will have a constructor and is responsible for outputting the tick_count
signal.
Next, change the name of the counter_simple
module to counter
so we can generate a new set of module files and remove its constructor. It also must take tick_count
as an input:
modules:
...
counter:
language: python
in:
- tick_count
This model now defines two modules with a signal passed between them.
Generate modules
Go ahead and generate a new set of module files to use for this model.
licorice generate quickstart-2 -y
This should generate three files: two module files and one constructor.
Write modules
Open the tick_counter
constructor ($LICORICE_WORKING_PATH/tick_counter_constructor.py
) and add the following:
counter = 0
This is basically doing the job of the constructor from the previous example.
Then open the tick_counter
parser ($LICORICE_WORKING_PATH/tick_counter.py
) and add the following:
tick_count[:] = counter
counter += 1
Instead of printing the counter
variable directly in the module as before, we pass it along as a LiCoRICE signal that can be read by the counter
module.
Finally, open the counter
and add the following line:
print(f"Hello World! Tick: {tick_count[0]}", flush=True)
The functionality of this model is the same as quickstart-1
, but the logic of the counter-simple
module is split between one module that keeps track of the tick number and one that outputs over stdout. A signal passes information between these two processes. This has the advantage of allowing us to create multiple modules that will read from the signal in parallel.
Run LiCoRICE
Run the new model:
licorice go quickstart-2 -y
Now you should see the same output as the quickstart-1
model in your terminal.
3. Add Logging
LiCoRICE also allows you to log signals so that the entire history of a model’s run can be examined after the fact.
Update model config
Copy over the model config to a different file:
cp $LICORICE_WORKING_PATH/quickstart-2.yaml $LICORICE_WORKING_PATH/quickstart-3.yaml
Open up the quickstart-3.yaml
model file and set the log
flag on the tick_count
signal:
signals:
tick_count:
shape: 1
dtype: int32
log: true
And then add a logger module (sink) which will be responsible for writing this signal to disk:
modules:
...
logger:
language: python
in:
- tick_count
out:
name: log_sqlite
args:
type: 'disk'
save_file: './data' # TODO allow user to write local path here
Run LiCoRICE
Run the new model:
licorice go quickstart-3 -y
You should see the same output as the quickstart-2
model in your terminal, but there should also be a SQLite database file that was created in the LiCoRICE output directory.
Examine the results
To examine the SQLite database file, run:
sqlite3 $LICORICE_WORKING_PATH/quickstart-3.lico/out/data_0000.db "select * from signals;"
And you should see the value of the tick_count
variable over time:
0
1
2
3
4
5
6
7
8
9
4. Output an External Signal
So far, we’ve only dealt with internal modules and signals. These pass data and perform computation within LiCoRICE and aren’t meant to interact with external processes or devices. In this model, we’ll see how to output a digital signal from LiCoRICE over parallel port that can be read by an oscilloscope.
Prerequisite Hardware
To output and view our parallel port external signal, we’ll need some specific hardware:
PC with two empty PCIe slots
2x PCIe parallel port adapter. We recommend cards that work automatically with the Linux kernel and don’t require a separate driver such as this one.
oscilloscope. You’re welcome to use any oscilloscope that you have on hand, but for our examples, we use the Hantek DSO2D10 (docs) since it’s fairly inexpensive and has a signal generator and persist function which lets us monitor our
tick_count
signal over time.
Hardware Setup
If the two parallel port cards are not already installed in your computer, open up your PC’s case and plug them in. If you’re using the recommended card above with a low-profile expansion slot, you’ll have to remove the bracket and serial port and install the included low-profile bracket. After installing the two parallel ports in your PC, you should see that ls /dev/parport*
returns /dev/parport0 /dev/parport1
. If this isn’t the case, the easiest solution is unfortunately to use different parallel port adapters. If you’re unfamiliar with installing PCI-e cards, feel free to watch this video.
Once your PC is set up correctly, you’ll need to connect the male side of each parallel cable to your PC and the female sides to breakout boards. Then, you can loosen the screws on the breakout board to insert jumper wires to the breakout board. For each breakout board, connect one jumper wire to one of the GND pins (pin 25) and one jumper wire to one of the data pins (pin 9). Tighten breakout board screws. Parallel port pinout for reference.
Finally, connect your BNC oscilloscope probes with the probe connected to pin 9 on each breakout board and the black alligator clip connected to pin 25 on each breakout board. We’ll connect channel 1 to the breakout board connected to our first parallel port (/dev/parport0
) and channel 2 to /dev/parport1
. If you’re not sure which is which, connect it either way and change it after viewing the program output.
Permissions
Make sure the user running LiCoRICE can access the port by adding them to the lp
group as follows:
sudo usermod -aG lp <user>
Then log out and back in for the changes to take effect. Note that you’ll have to reset environment variables such as $LICORICE_WORKING_PATH
after restarting your session.
Update model config
Copy over the model config to a different file:
cp $LICORICE_WORKING_PATH/quickstart-3.yaml $LICORICE_WORKING_PATH/quickstart-4.yaml
Add a parallel port sink that will take in tick_count
and output the result over our connected parallel port:
modules:
...
parallel_writer:
language: python
in:
- parallel_out
out:
name: parport_out
args:
type: parport
addr: 1
This creates a sink process which uses the in-built parport
driver outputting over /dev/parport1
.
We’ll also add a parallel_toggle
module under modules
that will create the signal which controls the parallel port:
modules:
parallel_toggle:
language: python
constructor: true
out:
- parallel_out
...
We then need to define the parallel_out
signal as follows:
signals:
...
parallel_out:
shape: 1
dtype: uint8
log: true
And add it as an input to the logger:
logger:
...
in:
- tick_count
- parallel_out
...
Add parallel_toggle
module files
The default behavior of the parport
driver is to take the value of the input signal and write it to the specified parallel port. For that to work, we’ll need to set the parallel_out
signal in our parallel_toggle
parser each tick.
Let’s generate parser and constructor files for our new module:
touch $LICORICE_WORKING_PATH/parallel_toggle.py $LICORICE_WORKING_PATH/parallel_toggle_constructor.py
In the constructor, add the following line:
toggle_switch = 0b00000000
In the parser add the following lines:
parallel_out[:] = toggle_switch
if toggle_switch == 0b00000000:
toggle_switch = 0b10000000
else:
toggle_switch = 0b00000000
The 0b
syntax allows us to set each bit individually in the unsigned 8 bit integer signal parallel_out
. Here we set only the first bit high (pin 9, data bit 7), but we could just as easily set all the bits high with 0b11111111
Run LiCoRICE
Turn on your oscilloscope and run:
licorice go quickstart-4 -y
You should see the same output in the terminal as the quickstart-3
model and a SQLite database in the model output directory, but now you should also see that the green trace (channel 2) on the oscilloscope screen jumps up and down each second. If the output is over channel 1, feel free to switch the BNC probes so that the output signal is on channel 2.
View the oscilloscope output as a square wave
Since the LiCoRICE model only runs for 10 ticks over 10 seconds, we don’t have a lot of time to modify the settings on the oscilloscope to see what’s going on. Start by commenting out the num_ticks
argument in quickstart-4.yaml
so that the model runs indefinitely and decrease the tick length so we have a 10Hz square wave:
config:
tick_len: 50000 # tick length in microseconds (50ms)
#num_ticks: 10 # number of ticks to run for
...
Run the model again and while it’s running, adjust the oscilloscope to view the square wave output. Start by turning off channel 1 using the CH1 MENU
button and adjust the horizontal scaling using the SEC/DIV
knob until the division length shows as 50ms in the topbar. Adjust the vertical position and scale for channel 2 until you see the full signal. A 1V division should be sufficient. If you’d like to stop running the model, you can do so by typing Control-C in the terminal.
Setting a trigger
Using the TRIG MENU
button, make sure that an Edge Type trigger is set on the CH2 Source and that Slope is set to Rising. Then, use the trigger LEVEL
knob to set the trigger to the midpoint of the signal. You should now see something like this:
5. Drive Output from an External Input
In the last example, we generated a 10Hz square wave in LiCoRICE and output it over an external channel. Here, we will use an external parallel port input to drive our parallel_out
variable which will be output over a second parallel port cable.
Update model config
Copy over the model config to a different file:
cp $LICORICE_WORKING_PATH/quickstart-4.yaml $LICORICE_WORKING_PATH/quickstart-5.yaml
Add a source that will read our parallel port input:
modules:
parallel_reader:
language: python
in:
name: parport_in
args:
type: parport
addr: 0
schema:
data:
dtype: uint8
size: 1
max_packets_per_tick: 1
out:
- parallel_in
...
Similarly to the parport
sink driver, the default parport
source driver behavior is to populate the parallel_in
variable with the data read over parallel port in each tick.
We’ll also change the parallel_toggle
module definition to take parallel_in
as an input signal and not use the constructor. Rename the module to parallel_through
so we can use a different module file:
modules:
...
parallel_through
...
constructor: false
in:
- parallel_in
...
...
Lastly, add the parallel_in
signal which will have a similar definition as the parallel_out
, just without logging.
parallel_in:
shape: 1
dtype: uint8
Update toggler module
Open a new file named $LICORICE_WORKING_DIR/parallel_through.py
and update it with the following:
parallel_out[:] = parallel_in
Set up the oscilloscope function generator
First, we’ll need to output a signal over our oscilloscope that will drive LiCoRICE. Use a 10Hz square wave:
Connect a BNC to Jaw clip line cable to the EXT TRIG/GEN OUT
port on the oscilloscope. Connect the black alligator clip to ground on the /dev/parport0
breakout board and the red alligator clip to pin 9. There should be enough room on the jumper cable terminals to connect the channel 1 probe as well. Press the WAVE GEN
button on the scope to turn on the waveform generator and set Wave: Square, Frequency: 10.000Hz, Amplitude: 3.300V, and Offset: 0.000V. You should see the oscilloscope-generated signal on channel 1.
Run LiCoRICE
Run the model:
licorice go quickstart-5 -y
You should see all the same outputs as in the previous examples, but now there should be two similar traces on the oscilloscope. Since the oscilloscope and LiCoRICE are both operating on the same clock, the two signals will not necessarily be in phase with each other. To see better phase alignment, try setting the LiCoRICE tick_len
to a lower number, say 10000 (10ms ticks) or 1000 (1ms ticks). Note that operating at such a fast clock rate without a realtime kernel patch (1ms) may cause a timing violation.
Oscilloscope view
To visually see the latency introduced by LiCoRICE on the oscilloscope, change the trigger Source to CH1 and make sure the LEVEL
knob is set correctly. You should see something like this:
Manipulate the signal
We’ve taken the input signal and replicated it at the output, but what if we want to modify it? We can change the parallel_through
module to the following:
parallel_out[:] = ~parallel_in
This uses the bitwise NOT operator ~
to flip the bits in the parallel_in
signal. After doing this, you should see the paralell_out
signal inverted on the oscilloscope:
Conclusion
If you’ve gotten this far, congrats! You’ve finished the LiCoRICE quickstart and have learned how to input a signal into a realtime system, manipulate it, log it, and output it back. To learn more about LiCoRICE and work through the rest of the examples, check out the full tutorial.