Starting PYNQ Programs At Boot

This post is designed to give an overview of how to get your own program to start when the PYNQ board boots while also giving an introduction to systemd. If you are only interested in the recipe feel free to skip to the end of the post where I give two ways of getting your script booting.

Systemd

Systemd is now the default init system for most Linux distributions including Ubuntu 2018.04 on which PYNQ Linux is based. Systemd uses a configuration file based approach for defining services which can be a bit daunting if you’ve not used it before.

Anatomy of a systemd service configuration

A systemd configuration file is an INI-like format with a .service extension that lives in the /lib/systemd/system folder. For this guide we’re just going to look at three core sections of a configuration file necessary for starting a PYNQ program at boot. For more details you’ll need to consult the systemd unit reference.

[Unit] section

This section describes basic metadata for our service - name it’s Description, boot timing using After/Before, and dependencies using Requires and Wants.

Requires and Wants both define dependencies but differ in what happens when the dependency fails to start. With Requires a failing dependency results in the service also failing or not being started at all. Wants is less strict and lets the service continue starting even if dependencies have failed.

After and Before determine what order services are started if multiple services are started at the same time - normally through dependencies or a common boot target. After and Before don’t define dependencies though. If a service specified in After isn’t being started for another reason it won’t be started. As a result After is generally used together with a Requires or Wants dependency to make sure that services are triggered at the same time.

We’ll use the service we created to flash the LEDs at boot as an example. Here we only want to flash the LEDs after both the PL server and Jupyter have successfully started so we use an After property to make sure the script doesn’t start until after Jupyter is running and Requires so that if either Jupyter or the PL server fail the LEDs won’t flash.

[Unit]
Description=Boot LED flashing
Requires=pl_server.service jupyter.service
After=pl_server.service jupyter.service

[Service] section

The Service section defines the commands that execute the service and how systemd should track the service’s execution. The two properties we’ll look at here are ExecStart and Type. ExecStart is simply the command to run that starts the service. This command should always be specified as an absolute path and you can’t make any assumptions about the environment the command will be executed in. In particular a shell may not have been created so any environment setup in your bash configuration files will not have been run. If you need the environment configured before running your Python program you can create a wrapper bash script which handles the environment setup before calling Python.

Depending on how the command runs we need to set the Type appropriately. There are many different types of service but the three most applicable for the use cases we are considering in this example are:

  • simple - The ExecStart command runs for as long as the service is active. If the command dies the server has had an error.
  • oneshot - The ExecStart command runs for a short period of time after which the service is active even though no process is running.
  • forking - The ExecStart command runs for a short period of time during which it forks the main service process. The service is considered started when ExecStart finishes and is running for as long as the forked process is alive.

Our boot_leds script is an example of a oneshot process. It only runs for a short amount of time and we don’t think of the service of having “failed” when it returns.

[Service]
Type=oneshot
ExecStart=/usr/local/bin/boot_leds.sh

{Install] section

Finally we have the Install section which specifies what happens when the service is enabled and disabled. The only property we use here is WantedBy which specifies which target will result in the service being started. WantedBy is the inverse to Wants in the [Unit] section and has the same semantics. You can also specify RequiredBy but this is strongly discouraged as it’s easy to end up in a situation where if your service fails the entire boot process fails.

systemd has a number of special targets that corresponded to different points in the boot process. As we always want our service to start we’ll attach it to basic.target which happens early in the boot process.

[Install]
WantedBy=basic.target

Now when we run the systemctl enable command systemd will add a link in the basic.target folder to our service.

PYNQ services

A standard PYNQ image on a PYNQ-Z1, PYNQ-Z2 or ZCU104 has three services that start at boot time

  1. The PL server which co-ordinates access to the hardware (pl_server.service)
  2. The Jupyter server (jupyter.service)
  3. Flashing the LEDs to signal a successful boot (boot_leds.service)

The boot_leds service depends on the other two so that by the time the LEDs are flashing Jupyter is ready to be accessed.

Running your own program

There are two ways of running your own program at boot: piggy-backing off one of the existing services; or creating a new service from scratch. Each approach as its own pros and cons

Modifying boot_leds

This is the quick and somewhat dirty way of getting a script to run at startup in place of flashing the LEDs. We can start by looking at the existing service:

vim /lib/systemd/system/boot_leds.service
[Unit]
Description=Boot LED flashing
Requires=pl_server.service jupyter.service
After=pl_server.service jupyter.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/boot_leds.sh

[Install]
WantedBy=basic.target

From this point we can change either the ExecStart entry to point to our own script or edit the /usr/local/bin/boot_leds.sh script to change what it does.

There are a couple of other subtleties we need to think about as well. One is Type of the service. The script is a oneshot service which means that systemd expects both the ExecStart script to finish running and to leave no processes left when it does so. This is incompatible with a continuously running program so we need to change the type to simple or forking depending on what our script does.

Secondly we may no longer want the Jupyter dependency. If we are not planning to have the system be interacted with via Jupyter then we need to remove the jupyter.service dependencies from the Requires and After section. If we don’t, systemd will always start Jupyter before running our script, even if we try and disable the jupyter service explicitly.

Creating a new service

Creating a new service is the cleaner approach but you need to make sure that the boot_leds script is disabled if you are planning to use anything other than the Base overlay. Otherwise, as systemd will parallelise starting of services, a race condition can occur where both services are trying to load different designs into the bitstream simultaneously.

systemctl disable boot_leds

If we’re using the board completely standalone we can disable Jupyter as well to reduce the boot time of the system

systemctl disable jupyter

We can now start writing our service file. For this example we’ll assume that our script acts like a simple service, i.e. it starts and continues running unless an error occurs.

vim /lib/systemd/system/my_script.service
[Unit]
Description=My Startup Script
Requires=pl_server.service
After=pl_server.service

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/xilinx/my_script.py

[Install]
WantedBy=basic.target

With the service file created we can now enable the service so it will start at boot

systemctl enable my_script

system command reference

All systemd control functions are handled through the systemctl command with various subcommands. The general command format is

systemctl <command> <service>

Where service is the name of your .service file. Some of the more common commands are

  • enable: enable a service at boot time.
  • disable: disable a service at boot time - note it may still get started if another enable service depends on it.
  • status: the current status of the service and recent log messages
  • start: start the service immediately - does not impact boot-time behaviour
  • stop: stop the service immediately - does not impact boot-time behaviour
  • restart: restart the service immediately

For more detailed logging information you can use the journalctl command to interact with journald - the logging counterpart to systemd.

journalctl -u <service>.service
6 Likes

Peter,

great explanation: it solved all my boot issues.

Many thanks

3 Likes

Peter,

This just what I need if I can get it to work. I am trying to run an Intel NCS 2 inside a Jupyter notebook and I need to configure the environment before Jupyter starts.

I tried adding a service before the jupyter.service starts.

[Unit]
Description=OpenVino config
Before=jupyter.service

[Service]
Type=oneshot
ExecStart=/opt/intel/openvino/bin/setupvars.sh

[Install]
WantedBy=basic.target

I can see the service runs before jupyter.service but I don’t see any effect.

image

I can successfully run the setupvars.sh in a terminal after Jupyter starts but I don’t think that I can alter the Notebook environment after the fact.

Do you know of a way that I can set environment variables for the Notebooks?

Thanks,

Ralph

1 Like

The issue you’re running into is that environment variables are local to the process that started them so your startup script is not having any effect on Jupyter. Try adding a simlink to the setup script in /etc/profile.d. That should ensure the variables are set up whenever a terminal is launched. Our Jupyter startup script will also source files from this directory.

Peter

Thanks Peter,

That’s what I needed and it makes sense.

Of course, when I put in the simlink and tried it - it didn’t work. It turned out that the setup script contained some code that that used the directory path that it was invoked from as a variable. When I fixed that it works great.

Ralph

Hi Peter, I tried following this tutorial, but I ran into a problem

failed to enable unit: invalid argument

I’ve created a Jupyter notebook running microblaze i2c code to monitor temperature sensors through pmoda. I downloaded it as python code and then created a file /home/xilinx/jupyter_notebooks/temperature-sensors.py

I then created a file temperature-sensors.service with the following contents. Running

sudo systemctl enable temperature-sensors

then gave me that error. Any help would be greatly appreciated.

Thanks

[Unit]
Description=Temperature sensors
Requires=pl_server.service jupyter.service
After=pl_server.service jupyter.service

[Service]
Type=simple
ExecStart=/usr/bin/python3 /home/xilinx/jupyter_notebooks/temperature-sensors.py

[Install]
WantedBy=basic.target

Hi @Ren27,
I am just sharing my experience with service. I had the same problem as you, because of some white spaces in the service file. The only difference is my program didn’t require to enable the jupyter.service. I don’t know if you need it or not. Also, change the service name to a simpler one.

Now you can use boot.py in boot directory to load your desired program at startup as well.

1 Like

Hi @mizan,
Thanks very much, that worked just fine. I appreciate your help.