Programming Microblaze Subsystems from Jupyter

In the Base I/O overlays that accompany the PYNQ release Microblazes are used to control peripherals attached to the various connectors. These can either be programmed with existing programs compiled externally or from within Jupyter. This notebook explains how the Microblazes can be integrated into Jupyter and Python.

The Microblaze is programmed in C as the limited RAM available (64 KB) limits what runtimes can be loaded - as an example, the MicroPython runtime requires 256 KB of code and data space. The PYNQ framework provides a mechanism to write the C code inside Jupyter, compile it, load it on to the Microblaze and then execute and interact with it.

The first stage is to load an overlay.


In [1]:
from pynq.overlays.base import BaseOverlay

base = BaseOverlay('base.bit')


Now we can write some C code. The %%microblaze magic provides an environment where we can write the code and it takes a single argument - the Microblaze we wish to target this code at. This first example simply adds two numbers together and returns the result.


In [2]:
%%microblaze base.PMODA

int add(int a, int b) {
    return a + b;
}

The functions we defined in the magic are now available for us to interact with in Python as any other function.


In [3]:
add(4,6)


Out[3]:
10

Data Motion

The main purpose of the Python bindings it to transfer data between the host and slave processors. For simple cases, any primitive C type can be used as function parameters and return values and Python values will be automatically converted as necessary.


In [4]:
%%microblaze base.PMODA

float arg_passing(float a, char b, unsigned int c) {
    return a + b + c;
}

In [5]:
arg_passing(1, 2, 3)


Out[5]:
6.0

Arrays can be passed in two different way. If a type other than void is provided then the data will be copied to the microblaze and if non-const the data will be copied back as well. And iterable and modifiable object can be used as the argument in this case.


In [6]:
%%microblaze base.PMODA

int culm_sum(int* val, int len) {
    int sum = 0;
    for (int i = 0; i < len; ++i) {
        sum += val[i];
        val[i] = sum;
    }
    return sum;
}

In [7]:
numbers = [i for i in range(10)]
culm_sum(numbers, len(numbers))
print(numbers)


[0, 1, 3, 6, 10, 15, 21, 28, 36, 45]

Finally we can pass a void pointer which will allow the Microblaze to directly access the memory of the host processing system for transferring large quantities of data. In Python these blocks of memory should be allocated using the Xlnk.cma_array function and it is the responsibility of the programmer to make sure that the Python and C code agree on the types used.


In [8]:
%%microblaze base.PMODA

long long big_sum(void* data, int len) {
    int* int_data = (int*)data;
    long long sum = 0;
    for (int i = 0; i < len; ++i) {
        sum += int_data[i];
    }
    return sum;
}

In [9]:
from pynq import Xlnk
allocator = Xlnk()

buffer = allocator.cma_array(shape=(1024 * 1024), dtype='i4')
buffer[:] = range(1024*1024)

big_sum(buffer, len(buffer))


Out[9]:
549755289600

Debug printing

One unique feature of the PYNQ Microblaze environment is the ability to print debug information directly on to the Jupyter or Python console using the new pyprintf function. This functions acts like printf and format in Python and allows for a format string and variables to be passed back to Python for printing. In this release on the %d format specifier is supported but this will increase over time.

To use pyprintf first the appropriate header needs to be included


In [10]:
%%microblaze base.PMODA
#include <pyprintf.h>

int debug_sum(int a, int b) {
    int sum = a + b;
    pyprintf("Adding %d and %d to get %d\n", a, b, sum);
    return sum;
}

In [11]:
debug_sum(1,2)


Adding 1 and 2 to get 3
Out[11]:
3

Long running processes

So far all of the examples presented have been synchronous with the Python code with the Python code blocking until a result is available. Some applications call instead for a long-running process which is periodically queried by other functions. If a C function return void then the Python process will resume immediately leaving the function running on its own.

Other functions can be run while the long-running process is active but as there is no pre-emptive multithreading the persistent process will have to yield at non-timing critical points to allow other queued functions to run.

In this example we launch a simple counter process and then pull the value using a second function.


In [12]:
%%microblaze base.PMODA
#include <yield.h>

static int counter = 0;

void start_counter() {
    while (1) {
        ++counter;
        yield();
    }
}

int counter_value() {
    return counter;
}

We can now start the counter going.


In [13]:
start_counter()

And interrogate its current value


In [14]:
counter_value()


Out[14]:
311849316

There are some limitations with using pyprintf inside a persistent function in that the output will not be displayed until a subsequent function is called. If the buffer fills in the meantime this can cause the process to deadlock.

Only one persistent process can be called at once - if another is started it will block the first until it returns. If two many processes are stacked in this way a stack overflow may occur leading to undefined results.

Creating class-like objects

In the C code typedefs can be used to create psuedo classes in Python. If you have a typedef called my_class then any functions that being my_class_ are assumed to be associated with it. If one of those functions takes my_class as the first argument it is taken to be equivalent to self. Note that the typedef can only ultimately refer a primitive type. The following example does some basic modular arithmetic base 53 using this idiom.


In [15]:
%%microblaze base.PMODA

typedef unsigned int mod_int;

mod_int mod_int_create(int val) { return val % 53; }
mod_int mod_int_add(mod_int lhs, int rhs) { return (lhs + rhs) % 53; }

We can now create instances using our create function and call the add method on the returned object. The underlying value of the typedef instance can be retrieved from the .val attribute.


In [16]:
a = mod_int_create(63)
b = a.add(4)
print(b)
print(b.val)


typedef mod_int containing 14
14

Coding Guidelines for Microblaze Interfacing Code

There are some limitations to be aware of in the Jupyter integration with the Microblaze subsystem in particular the following things are unsupported and will result in the function not being available.

  • structs or unions of any kind
  • Pointers to pointers
  • returning pointers

All non void* paramters are passed on the stack so beware of passing large arrays in this fashion or a stack overflow will result.