Introduction: CPE 542: Getting Started Making Making Software for Heterogenous Computing on the IMX7ULP UCOM Board

Preface

In the world of embedded systems, a common decision designers must make is whether to use a beefy, more power-hungry processor core capable of running an operating or a simpler microcontroller core that can at best host some middleware like RTOS. As the field and its needs has evolved; however, it has become increasingly more desirable for embedded systems at the edge to be able to be optimized to do both tasks that require a full featured applicaiton processor like video processing alongside simpler tasks like controlling some electronic device like a motor.

Presenting: heterogenous embedded systems!

These systems, at the cost of more complexity and die area give you both applicaton core(s) and programmable logic (like FPGA fabrics) and/or additional microcontroller cores on the same die.

Although they introduce this complexity, used correctly, they can allow for lots of application optimization and bring about greater power savings and lower system complexity compared to solutions that use a standalone module for application processing and a standalone module for a micrcontorller. (like a Raspberry Pi for example talking over Serial to a Arduino).


Project: Build Software for the iMX7ULP uCOM Board!

For this project, we will get our hands dirty by developing software for a Heterogenous System on Chip created by Embedded Artists and NXP called the iMX7ULP. It is an ultra-low power device featuring an ARM Cortex-A7 as the applications processor for applications that need an opearting system and an ARM Cortex-M4 and a Real-Time processor for applications that don't. In order to interface with the board, we will use the iMX7ULP uCOM Developer’s Kit V2.

Supplies

Hardware

  1. iMX7ULP uCOM Developer's Kit V2 (includes the IMX7ULP board already)
  2. Extra USB micro B to A USB OTG Cable for programming
  3. USB micro to A cable
  4. A PC capable of running Ubuntu 18.04 (and optionally Windows 10/11)

Software

  1. Ubuntu 18.04
  2. (Optional Windows 10/11)
  3. A Serial Console like minicom or PuTTY
  4. MCUXpresso SDK for iMX7ULP M4 Core
  5. UUU tool, Bootloader, and Prebuilt Kernels
  6. NXP Universal Update Utility for Windows (if flashing using Windows)
  7. ARM GNU Toolchain 12.2 Rel 1
  8. Google Repo Tool
  9. Yocto Bitbake (will be installed in later stops)
  10. A code editor or IDE like Visual Studio Code

Additional References/Resources

  1. How to set up Universal Update Utility (uuu) on Windows (emteria.com)
  2. iMX7ULP uCOM Datasheet
  3. Yocto Guide from Embedde Artists: Introduction | Embedded Artists Developer Site
  4. Yocto Tutorial - 29 Kernel Development | Out of Tree Kernel Module

Step 1: Download Needed Software

  1. Obrain an Ubuntu 18.04 Linux environment can be computer directly running Ubuntu 18.04 Ubuntu 18.04 running in WSL if the host machine runs Windows 10 or 11
  2. Obtain the boatloader and UUU tool scripts. The UUU or Universal Update Utility essentially allows us to flash the software we write or pre-made examples on to the iMX7 cores. The version that need to be downloaded is Kernel Version 4.14.98 for the i.MX7 ULP uCOM Board/Kit. (or just click this link uuu_imx7ulp_ucom_4.14.98.zip ).
  3. If the host machine is Windows, UUU will NOT work in WSL. It is necessary to install the application on the Windows side. Follow this tutorial to do so: https://emteria.com/kb/uuu-windows
  4. If host machine is on Linux, install UUU there: https://emteria.com/kb/uuu-linux
  5. Download the SDK for the M4 core on the iMX7. SDK version 2.15.00 is prebuilt for you at this link: https://mcuxpresso.nxp.com/download/2cdd8a0b0c0ce856fc93a01db3539d9d
  6. Download Version 12.2 Rel1 of the ARM GNU toolchain from this site: https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
  7. Make sure to download the version for your host mahcine archetecture and make sure the target is a AArch32 bare-metal target (arm-none-eabi). In my case, since I'm installing the toolchain in WSL on an x86_64 host, I downloaded this version and put it in my WSL machine home directory: "arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi.tar.xz" . It is super important you download a 'arm-none-eabi' compiler and not a compiler like "arm-none-linux-gnueabihf" due to hard-coded compiler names in the Cmake configurations

Step 2: Set Up the ARM GNU Toolchain

The ARM GNU Toolchain contains the cross-compilers necessary to make binaries for the M4. In this step, we will install it. First extact the tar file

lap@LAP-SLS2:~$ tar -xvf arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi.tar.xz

Then move the extracted folder into the /usr/ directory

lap@LAP-SLS2:~$ sudo mv arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi /usr/

After that, some environment variables need to be setup so that CMake knows where to find the cross-compiler. Append the paths of the toolchain to your .bashrc file similar to how I do it below. Note I am on an Intel machine so I see the 'x86_64' part in the name, but this will vary based on your machine archtecture.

export ARMGCC_DIR=/usr/arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi
export PATH=$PATH:/usr/arm-gnu-toolchain-12.2.rel1-x86_64-arm-none-eabi/bin

Note you may need to install CMake

$ sudo apt-get -y install cmake

Now that this is setup, we are ready to start making M4 images!

Step 3: Start Making the Ping Pong Demo

For the next few steps our goal will be to get the processors to talk to each other over a protocol called RPMsg. This protocol uses Virtual IO and ring buffers to allow different processors to address each other and send and recieve messages to each other. To demonstrate this we will flash the software needed to do a Ping-Pong demo. In this demo, one core is going to send a value to the other core. Then the recepient core will increment it and send it back to the sender. The process will repeat for 100 times.

Step 4: Compile the M4 Binary

If in Windows, be sure to move the SDK folder into WSL Ubuntu 18.04

In the downloaded MCUXpresso SDK folder, navigate to the following directory: '../SDK_2_15_000_EVK-MCIMX7ULP/boards/evkmcimx7ulp/multicore_examples/rpmsg_lite_pingpong_rtos' Feel free to browse the codebase if desired. You will see the ping-ponging on the M4 side of the ping pong demo happens

 while (msg.DATA <= 100U)
    {
        (void)PRINTF("Waiting for ping...\r\n");
        (void)rpmsg_queue_recv(my_rpmsg, my_queue, (uint32_t *)&remote_addr, (char *)&msg, sizeof(THE_MESSAGE),
                               ((void *)0), RL_BLOCK);
        msg.DATA++;
        (void)PRINTF("Sending pong...\r\n");
        (void)rpmsg_lite_send(my_rpmsg, my_ept, remote_addr, (char *)&msg, sizeof(THE_MESSAGE), RL_BLOCK);
    }


Then navigate from the current directory into the ARMGCC directory. You will notice in that directory a couple of scripts. These scripts allow you to compile the source code into a binary .bin file and leverage CMake. Give execute permissions to the clean.sh and build_flash_release.sh scripts. It is important to use the flash release because the M4 does not use an SD card and uses the flash memory onboard the module.

$ chmod +x clean.sh
$ chmod +x build_flash_release
$ ./clean.sh
$ ./build_flash_release.sh

Be sure to run the clean script before building a binary to clear the files made in the last build. You will see in this linked video, this process.

SB3_Compiling_M4_Image_pt1.mp4


Step 5: Package the Binary Into an Image

Now it's time to package the binary into an image file that uuu can flash onto the board.

In the amrgcc/flash_release directory you will now find a file named 'imx7_ulp_m4_demo.bin'.

Do the following to create an image.

From the flash_release directory

$ cp imx7ulp_m4_demo.bin ~/imx7_SDK_2_15_4_29/tools/imgutil/evkmcimx7ulp/

Move to the directory with the image creation tool

$ cd ~/imx7_SDK_2_15_4_29/tools/imgutil/evkmcimx7ulp/

In that directory you will find an existing binary named sdk20-app.bin. Go ahead and overwrite that binary with the binary we just moved into it.

$ mv imx7ulp_m4_demo.bin sdk20-app.bin

Give execute permission to the image tool and make the image

$ chmod +x mkimg.sh
$ ./mkimg.sh

Now you will see sdk20-app.img in that directory

This video shows the process you just went through:

SB3_Compiling_M4_Image_pt2.mp4

Step 6: Set Up the Embeded Artist Dev Board for Flashing Images

First make sure the power switch is in the OFF position on the Embedded Artist Dev Board. Then short J2 on the board to place it into serial download mode. Make two USB connections. Use a micro-USB to USB-A to connect the micro-USB port on the dev boarcto your computer. For the wider USB micro B port, make sure to use the micro B OTG cable and connect to your computer.

Plug in the power supply for the dev board into a wall outlet and then flip the power switch to ON. You will see a few green status lights turn on. See the above image as an example with a black micro B USB OTG and white micro USB cable.

Step 7: Flash the M4 Image

Now it's time for the momment you've been waiting for: flashing images. Go ahead and move your created image into the files subfolder of the downloaded folder containing the UUU tool, Kernel and bootloader you just downloaded earlier. (which is in the Windows file system if using Windows or Linux file systems if using Linux) In my case, I have the files in the Windows side so I did this.

$cp sdk20-app.img C:\Users\loren\Downloads\uuu_imx7ulp_4.14.98\uuu_imx7ulp_ucom_4.14.98\files

Go to the 'uuu_imx7ulp_4.14.98\uuu_imx7ulp_ucom_4.14.98' folder

Ensure m4app.uuu refers to the image file we made (so make sure line 11 looks like this)

FB: download -f files/sdk20-app_flash.img

Now flash, (I am using an admin PowerShell as I am on Windows)

PS: uuu m4app.uuu

You will see a progres bar. It will display 'Done' when the flashing succesfully completes.

This video shows this process:

SB3_Flashing_M4_Image.mp4

Step 8: Flash the Premade Linux Image

Now to put the prebuilt Kernel 4.14.98 Yocto Linux images onto the A7 core use this command

PS: uuu full_tar.uuu

Wait for the flashing to complete as before.

Step 9: Open the Serial Consoles for Each Core

Now let's setup the serial interfaces to the dev board.

Unplug the USB cables and set the dev board switch to off.

Plug in the mico USB cable (the white on in the earlier photo example) and flip the switch on.

In Linux, look in /dev/ to find two new devices listed. These devices are the serial interfaces for the A7 and M4 core. In Windows use device manager thse appear as two COM ports.

Once you identify them, open two serial console sessiions on the software of your choice. Ensure the following settings are specified for your serial console if using minicom on Linux. Note that the USB number may vary and may not be ttyUSB1 for instance.

  • Serial Device: /dev/ttyUSB0 (application core) OR /dev/ttyUSB1 (real-time core)
  • Lockfile Location: /var/lock
  • Bps/Par/Bits: 115200 8N1
  • Hardware Flow Control: No
  • Software Flow Control: No  
  • RS485 Enable: No
  • RS485 Rts on Send: No
  • RS485 Rts After Send: No RS485 Rx During Tx: No
  • RS485 Terminate Bus: No
  • RS485 Delay Rts Before: 0
  • RS485 Delay Rts After: 0

Open a minicom session for each core

If you are using PuTTY, likely all you need to do is specify the serial line and a baud rate of 115200.

And voila! You are ready to try the demo

Step 10: Ping Pong!

Switch off and on the power switch. You will obseve the M4 core start and display it is waiting for a message and you will see the A7 core boot Linux! Congrats, your dev board is now alive!

To start the Ping Pong demo, you will need to install the Ping Pong kernel module on the A7 core. On the A7 core,navigate to the directory containing the kernel modules:

$ cd  /lib/modules/<kernel_version>+<commit#>/kernel/drivers/rpmsg

Note that for the most part, you can hit tab to auto-complete the <kernel_version>+<commit#> folder name since there is only one of those folders.

Now install tthe kernal module

$insmod imx_rpmsg_pingpong.ko

Immediately, you will see both processors display pings and pongs 100 times!

Congrats! You have achieved interprocessor communications!

Here is a video of the demo for reference.

SB3_PingPong_Demo.mp4

Step 11: (optional) Another Example of Power Saving

If you are curious, you can repeat steps 4-7, and 9 and use the directory shown in the Step 4 video to build and load the M4 demo controlling the power states. This is a video showing that demo:

SB3_Power_Modes_Switch_Demo.mp4

Step 12: Your Turn! Build Your Own Kernal and Add Your Own Kernal Module

Yocto is a build tool that allows you to make custom Linux distributions and can be used to remake the Linux distribution we used earlier. Even though these prebuilt kernels exist, making the kernel ourself lets us further customize our solution to exactly meet our own needs. We can even add layers and add or remove recipes.

A layer in Yocto is a collection of related repositories. Layers can 'build off of each other' and add modularity to a build system. A recipe tells the Yocto build system how to build one piece of the kernal software. It list out where to fetch source code, any other recipes/code it depends on, and any other recipe templates it might want to refer to when building out the component.

Thus, now I will show how to build the Kernel yourself and add your own Kernel modules! For the sake of demonstration we will ultimately build the Ping Pong demo again, but go through the full process oursevles and learn how to make our own Yocto layer!

For this section, it is again recommeneded you use Ubuntu 2018. Newer releases of Ubuntu will likely not be able to compile the version of the repository we will use.


Step 13: Install Yocto and Download the Repositories Needed to Build Linux

Navigate to your Ubuntu 2018 repository:

Install the tools for Yocto as shown below

$ sudo apt-get update

$ sudo apt-get install gawk wget git-core diffstat unzip texinfo \
gcc-multilib build-essential chrpath socat \
libsdl1.2-dev xterm sed cvs subversion \
coreutils texi2html docbook-utils python-pysqlite2 help2man make \
gcc g++ desktop-file-utils libgl1-mesa-dev libglu1-mesa-dev \
mercurial autoconf automake groff curl lzop asciidoc u-boot-tools

Install the Google Repo tool as shown below (https://gerrit.googlesource.com/git-repo)

$ mkdir -p ~/.bin
$ PATH="${HOME}/.bin:${PATH}"
$ curl https://storage.googleapis.com/git-repo-downloads/repo > ~/.bin/repo
$ chmod a+rx ~/.bin/repo

Make a directory to download the repo

$ mkdir ea-bsp
$ cd ea-bsp

Configure your git username and email if not already done

$ git config --global user.name "Your name"
$ git config --global user.email "Your e-mail"

Initialize the repo for Kernel 4.14.98

$repo init -u https://github.com/embeddedartists/ea-yocto-base -b ea-4.14.98

In ea-bsp/.repo/manifests/default.xml, change any source.codeaurora.org links to the the NXP GitHub links. Usually a Google search for the repository named after the 'source.codeaurora.org' section of the URL will give the equivalent official NXP repository on GitHub (e.g. https://github.com/nxp-imx).

After do a repo sync to download the repositories needed for the build

$ repo sync

Now you have everything needed to build an image.

Step 14: Build the Linux Kernel!

Firstly go ahead and initalize the build from the ea-bsp directory

$DISTRO=fsl-imx-fb MACHINE=imx7ulpea-ucom source ea-setup-release.sh -b build_dir

This will place you in the build_dir subdirectory Now try doing the initial build.

$bitbake ea-image-base

You'll likely encounter do-fetch errors. If those occurs, it's likely a 'source.codeaurora.org' deadlink. Edit the recipe ( (.bb) file listed in the erorr message) to replace the source.codeaurora.org deadlinks with the the equivalent NXP GitHub links. Note the build process takes a WHILE the first time (around about an hour on my machine with 32 GB of ram and an i7-13700H @ 2.90 GHz). Grabbing a tea or coffee is not a bad idea.Fortunately on later builds, built components are cached, thus the build time is shorter.

Once all deadlinks are resolved and the image builds, you'll find the build here:

~/ea-bsp/build_dir/tmp/deploy/images/imx7ulpea-ucom

As we are not changing any other details like device tree attributes (the device tree specifies to the kernel where and how devices are connected to the board), one can just overwrite the current Linux image in the uuu_imx7ulp_ucom_4.14.98 directory. Or if it is deserved to preserve the original image, make a differently named copy of this folder (with the same contents) and overwrite the ea-image-base-imx7ulpea-ucom.tar.bz2 image file as shown below in there. For instance I would do:

$ mv  ~/ea-bsp/build_dir/tmp/deploy/images/imx7ulpea-ucom/ea-image-base-imx7ulpea-ucom-<buildate>.rootfs.tar.bz2 \
C:\Users\loren\Downloads\uuu_imx7ulp_4.14.98\uuu_imx7ulp_ucom_4.14.98\files\ea-image-base-imx7ulpea-ucom.tar.bz2

Also for future note, if you need to restart a build after calling the ea-setup-releas.sh script, simply run this command

$ source setup-environment build_dir

Step 15: Start Our Own Layer

Now let's add our own layer to the build that makes our ping pong module. Let's pretend we are in a CubeSat lab and this will be some custom lab software. Run this command in the ea-bsp build_dir folder to initalize a layer I will call 'my-polysat-layer'.

$ bitbake-layers create-layer ../sources/my-polysat-layer

This registers our layer in the bblayers.conf file as well.

Go ahead and add a recipies-kernal folder. This folder will contain our recipie to make the kernel modules

$ mkdir ~/ea-bsp/sources/my-polysat-layer/recipes-kernal/


Step 16: Lay Out the Ingredients for Our Kernel Module

Now we can lay out the ingredients for our kernel module. To do so, first make a directory for the module under recipes-kernel which we will name poly-ping-pong-mod, as the name of our module will be poly-ping-pong. Then under that directory make a files directory for the 'ingredients' of our recipe to continue our baking analogy.

$  mkdir ~/ea-bsp/sources/my-polysat-layer/recipes-kernal/poly-ping-pong-mod
$ mkdir ~/ea-bsp/sources/my-polysat-layer/recipes-kernal/poly-ping-pong-mod/files
$ cd ~/ea-bsp/sources/my-polysat-layer/recipes-kernal/poly-ping-pong-mod/files

The first thing needed, is a source file. Again, we are just going to use the Ping Pong source code used in the A7, which is listed here and renamed poly_ping_pong.c

poly_ping_pong.c

/******************************************************************************
 *
 *   Copyright (C) 2011  Intel Corporation. All rights reserved.
 *
 *   This program is free software;  you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; version 2 of the License.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY;  without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 *   the GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program;  if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 *****************************************************************************/


#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/virtio.h>
#include <linux/rpmsg.h>


#define MSG     "hello world!"
static unsigned int rpmsg_pingpong;


// int init_module(void)
// {
//  printk("Hello World!\n");
//  return 0;
// }


// void cleanup_module(void)
// {
//  printk("Goodbye Cruel World!\n");
// }


static int rpmsg_pingpong_cb(struct rpmsg_device *rpdev, void *data, int len,
                        void *priv, u32 src)
{
    int err;


    /* reply */
    rpmsg_pingpong = *(unsigned int *)data;
    pr_info("get %d (src: 0x%x)\n", rpmsg_pingpong, src);


    /* pingpongs should not live forever */
    if (rpmsg_pingpong > 100) {
        dev_info(&rpdev->dev, "goodbye!\n");
        return 0;
    }
    rpmsg_pingpong++;
    err = rpmsg_sendto(rpdev->ept, (void *)(&rpmsg_pingpong), 4, src);


    if (err)
        dev_err(&rpdev->dev, "rpmsg_send failed: %d\n", err);


    return err;
}


static int rpmsg_pingpong_probe(struct rpmsg_device *rpdev)
{
    int err;


    dev_info(&rpdev->dev, "new channel: 0x%x -> 0x%x!\n",
            rpdev->src, rpdev->dst);


    /*
     * send a message to our remote processor, and tell remote
     * processor about this channel
     */
    err = rpmsg_send(rpdev->ept, MSG, strlen(MSG));
    if (err) {
        dev_err(&rpdev->dev, "rpmsg_send failed: %d\n", err);
        return err;
    }


    rpmsg_pingpong = 0;
    err = rpmsg_sendto(rpdev->ept, (void *)(&rpmsg_pingpong), 4, rpdev->dst);
    if (err) {
        dev_err(&rpdev->dev, "rpmsg_send failed: %d\n", err);
        return err;
    }


    return 0;
}


static void rpmsg_pingpong_remove(struct rpmsg_device *rpdev)
{
    dev_info(&rpdev->dev, "rpmsg pingpong driver is removed\n");
}


static struct rpmsg_device_id rpmsg_driver_pingpong_id_table[] = {
    { .name = "rpmsg-openamp-demo-channel" },
    { .name = "rpmsg-openamp-demo-channel-1" },
    { },
};
MODULE_DEVICE_TABLE(rpmsg, rpmsg_driver_pingpong_id_table);


static struct rpmsg_driver rpmsg_pingpong_driver = {
    .drv.name   = KBUILD_MODNAME,
    .drv.owner  = THIS_MODULE,
    .id_table   = rpmsg_driver_pingpong_id_table,
    .probe      = rpmsg_pingpong_probe,
    .callback   = rpmsg_pingpong_cb,
    .remove     = rpmsg_pingpong_remove,
};


static int __init init(void)
{
    return register_rpmsg_driver(&rpmsg_pingpong_driver);
}


static void __exit fini(void)
{
    unregister_rpmsg_driver(&rpmsg_pingpong_driver);
}


module_init(init);
module_exit(fini);


MODULE_AUTHOR("NXP-IMX");
MODULE_DESCRIPTION("PingPong test");
MODULE_LICENSE("GPL");

Add this to the files directory

Additionally, it is necessary to add a Makefile to the files directory. There is an example Kernel module boilerplate that the video which is linked at the start of this tutorial references. For conveneince, I provide the Makefile boilerplate cutomized for our source file here. The main change, was making the .o file match the name of our sourcefile (poly-ping-pong)


Makefile

obj-m := poly_ping_pong.o


SRC := $(shell pwd)


all:
    $(MAKE) -C $(KERNEL_SRC) M=$(SRC)


modules_install:
    $(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install


clean:
    rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
    rm -f Module.markers Module.symvers modules.order
    rm -rf .tmp_versions Modules.symvers

Step 17: Put It All Together in a Recipe

Make a recipe called poly-ping-pong-mod_0.1.bb in the above poly-ping-pong-mod directory. Again this is adapted from the boilerplate referenced in the video example. The key things to note, is inherit module and RPROVIDES_${PN} += "kernel-module-poly-ping-pong"

inherit means this modules inherits from the 'module' class, a recpie class for Kernel Modules. RPROVIDES tells the build system the name of the kernel module: poly-ping-pong


poly-ping-pong-mod_0.1.bb

SUMMARY = "Example of how to build an external Linux kernel module"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://COPYING;md5=12f884d2ae1ff87c09e5b7ccc2c4ca7e"


inherit module


SRC_URI = "file://Makefile \
           file://poly_ping_pong.c \
          "


S = "${WORKDIR}"


# The inherit of module.bbclass will automatically name module packages with
# "kernel-module-" prefix as required by the oe-core build environment.


RPROVIDES_${PN} += "kernel-module-poly-ping-pong"


Step 18: Tell Yocto to Build Our Layer

Now we need to build the recipe. Go back to the build_dir and type.

$bitbake poly-ping-pong-mod

Now add the module to our image by editing our ea-bsp/conf/local.conf file with:

$IMAGE_INSTALL_append = " poly-ping-pong-mod"

Finally build the image witth our kenerl module from the build directory

$bitbake ea-image-base

Step 19: Flash the Custom Image

Voila, now we have our own custom image with a Linux kernel module. Go ahead and follow step 14 to copy the image to the uuu directory and step 5,6 and 8 to flash it and the M4 side Ping Pong demo onto the dev board.

Step 20: Run the Kernel Module We Built

Open a serial console and log into the A7 core. Kernel moduels built in the way that was shown will appear in the directory shown below.

$ cd /lib/modules/<kernel-version>+<commit>/extra

Now you will see our Kernel module is there! Install it and watch the Ping Pong demo work!

$insmod poly_ping_pong.ko

Here is a video demo of this: SB3_PingPong_Demo_My_Module.mp4

Congrats! You've succesfully added another module to our custom Linux distro! Now go and build some cool heterogenous system software!