Nov 6, 2022

Qt6 with Yocto for Vexpress A9 in QEMU

 Adding meta-qt5 layer and building a simple application that is run on boot.

First the examples, then a simple application

Switch to systemd?

This is part 5 of the QEMU Board Emulation post series.

In the previous post I showed how to use Yocto to build an image for Vexpress-A9 board. Yocto allows us to build a custom distribution and to streamline bootloader and kernel cross-compilation and handling.

In the Linux device driver development post series I showed custom memory-mapped and I2C devices for QEMU, and device driver and userspace application development for those devices. The userspace applications that were presented are usable from command line only, to demonstrate the desired functionality.


In real embedded systems, the use of non-GUI (command line only) applications is enough if the unit has no display. In that case data can be published to the outside world over some interface (usually network interface). We will cover this in some of the future posts.

However, there are a lot of cases where the embedded system has a display, and in that case the userspace application should provide graphical interface. There are different libraries that enable graphical interface to be shown, but in this post we will use Qt, version 6. We will integrate it into Yocto development using the meta-qt6 layer.

Items that will be covered in this post are

Sources for the Yocto build environment for Vexpress-A9 can be found in following repositories:

Qt6 application development

The application is developed in Qt6, using combination of C++ and QML. Qt provides many classes which provide different functionality and simplify development.

One of the most interesting features are the signals and slots mechanism, where asynchronous communication between threads of execution is provided with loose coupling. One class can provide a slot function, essentially a callback, and another class can provide a signal function, and then signal and slot have to be connected. Once signal is emitted, the slot function is called.

We will go into more details soon, and the source code of the developed application can be found in the qt6-sens-app github repository.

Ideas

The goal of this application is to provide graphical interface towards the custom memory-mapped and I2C QEMU devices developed in the Linux device driver development post series.

The user interface should be simple, one part showing items related to the memory-mapped sensor and the other part showing items related to the I2C sensor, thus combining functionalities provided by mmsens-app and i2csens-app.

I will try to reuse most of the functionalities from the existing classes, but updating them to use Qt-specific types (like QString instead of std::string). The biggest change for the existing implemented classes will be that they will inherit from QObject in order to be able to use the signals and slots functionality.

Handling memory-mapped sensor

The mmsens-app has a separate thread where poll syscall is used to block thread execution until sysfs_notify is executed on the interrupt sysfs attribute.

In this case, Qt6 provides the QFileSystemWatcher class which provides similar functionality: the QFileSystemWatcher can be configured to track a state of a certain file until notify event is received, and then can emit the fileChanged signal, when read of data register can be performed.

This way we can just set up correct signals and slots and we don't need to create additional explicit thread.

As for the contol, we should be able to

  • enable memory-mappped sensor and interrupt generation
  • select sampling frequency
  • present last read value

Handling I2C sensor

The i2csens-app has a separate thread which blocks until timeout period expires, and then reads the TEMPERATURE register.

Using Qt we can use the QTimer class. In this case, when QTimer expires it emits the timeout signal, so we can use that to trigger read.

Again, we can only set up correct signals and slots and we don't need to create additional explicit thread.

As for the contol, we should be able to

  • enable I2C sensor
  • show last read I2C value

In this case, since the value is 'temperature', we will use line chart to show last 10 measured values.

QML interaction

QML is used for frontend and C++ for backend implementation. They need to interact and exchange data, and there are multiple ways to do it.

In this case I wanted to make a loose coupling between QML and C++, so I just used signals/slots mechanism for exchanging data between QML and C++.

This is a simple application, so using only signals/slots was possible. In more complex situations some other mechanism presented in the link above may have to be used in order to provide full functionality.

Implemented GUI is shown in the following picture

Application design

The class organization is presented in the following picture. Manager class represents glue between QML and C++ classes, routing signals in both directions. Poller class implements functionality related to the memory-mapped sensor, while I2CHandler class implements functionality related to the I2C sensor.

An example interaction for memory-mapped sensor and I2C sensor are shown in the following pictures.

Using meta-qt6 Yocto layer

In order to simplify cross-compilation we will use Yocto. The meta-qt6 layer provides all needed dependencies.

However, before the layer could be added to the bblayers.conf file, we need to update Yocto to kirkstone version to be compatible with the meta-qt6 layer version 6.4.

Upgrading Yocto to kirkstone

Considering we have only few recipes in the meta-vexpress and meta-vexpress-apps repositories, the upgrade process mostly consisted in updating the layer specification in layer.conf to add kirkstone as supported version, and in updating append/prepend/overrides to use : instead of _ as separator (change that happened in honister).

Adding new layer

After we have updated Yocto to kirkstone, we can add the layer just by updating bblayers.conf with the following line

${TOPDIR}/../sources/meta-qt6 \

Adding qt6-sens-app recipe

The recipe for the qt6-sens-app is similar to the ones used for other userspace applications.

Main difference are the Qt-related items

inherit qt6-cmake

DEPENDS += " qtbase qtdeclarative qtdeclarative-native "
RDEPENDS:${PN} += " qtcharts-qmlplugins ttf-dejavu-sans "

The qtbase and qtdeclarative provide base Qt C++ and QML support. Since we are using QtCharts (QML version) we also need to add qtcharts-qmlplugins.

Base image does not have fonts installed, so I added ttf-dejavu-sans to be able to render text.

Testing

Building and running application

The Yocto image is built using the following command

# framebuffer image build command
$ DISTRO=mistra-framebuffer MACHINE=vexpress-qemu bitbake mistra-image

After image is built, copied to the SD card and QEMU is run, the following output will be visible

# QEMU output
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio
[ ... ]
Mistra FrameBuffer 4.0 vexpress-qemu /dev/ttyAMA0
vexpress-qemu login:

The application can be started using

$ qt6-sens-app

The application UI will be shown and we can interact with it.

Simple testing scenario that demonstrates application functionality is shown in the following image

Summary

In this blog post the Yocto image is extended with Qt6 functionality. A new application for interaction with custom memory-mapped and I2C sensor is implemented and it's functionality demonstrated.

Oct 18, 2022

Custom I2C QEMU peripheral and userspace application

This is part 4 of the Linux device driver development post series.

After showing custom memory-mapped sensor and it's handing in previous posts, we will now take a look at I2C peripheral.

In this post we will cover the following things

Custom I2C QEMU peripheral

Emulating I2C peripheral under QEMU is not much harder then emulating a memory-mapped peripheral.

The peripheral needs to be connected to an existing I2C controller, assigned and I2C address and should provide responses when data is written to or read from I2C bus.

Peripheral description

In this example we will implement a simple I2C temperature sensor. Once enabled, it will return a random value in the range of 15.0 to 25.0 with 0.5 degrees (Celsius) step on every read.


The peripheral has following registers

offset name description
0x0 ID Unique ID register, always returning value 0x5A
0x1 CONFIG Configuration register, used to enable temperature temperature
0x2 TEMPERATURE Data register holding current temperature. The value is coded as Q5.1 to be able to hold 0.5 degree step

Bit values of CONFIG register are shown in the following table

name pos dflt description
Reserved 7:1 0 Reserved
EN 0 0 Enable device

Peripheral implementation

The implementation of the peripheral is available in github repository.

Functions of interest are the ones related to data handling. The global structure is defined in the following way

typedef struct I2CSensor {
  /*< private >*/
  I2CSlave i2c;
  /*< public >*/
  uint8_t regs[NR_REGS];  // peripheral registers
  uint8_t count;          // counter used for tx/rx
  uint8_t ptr;            // current register index
} I2CSensor;

Writing to the device

The only register that supports writing is the CTRL register.

In order to set the register that will be written, first I2C write must be the register address (that will update the ptr field), and second write must be the value that should be written.

static int i2c_sens_tx(I2CSlave *i2c, uint8_t data)
{
  I2CSensor *s = I2C_SENS(i2c);

  if (s->count == 0) {
    /* store register address */
    s->ptr = data;
    s->count++;
  } else {
    if (s->ptr == REG_CTRL_OFFSET) {
      s->regs[s->ptr++] = data;
    }
  }
  return 0;
}

Reading from device

Reading process is similar to writing: first one byte must be written to set the address of register to be read, and then the reading can proceed.

All registers support reading so it is just a matter of returning current register value.

/* Called when master requests read */
static uint8_t i2c_sens_rx(I2CSlave *i2c)
{
  I2CSensor *s = I2C_SENS(i2c);
  uint8_t ret = 0xff;

  if (s->ptr < NR_REGS) {
    ret = s->regs[s->ptr++];
  }

  return ret;
}

We also want each read to trigger loading of random value to the TEMPERATURE register. That is achieved by defining an event callback which modifies the s->regs[2] value when I2C_START_RECV event is received.

The register is modified only if device is currently enabled, otherwise a value of 0xff will be stored.

static int i2c_sens_event(I2CSlave *i2c, enum i2c_event event)
{
  I2CSensor *s = I2C_SENS(i2c);

  if (event == I2C_START_RECV) {
    if (s->ptr == REG_TEMPERATURE_OFFSET) {
      if (s->regs[REG_CTRL_OFFSET] & REG_CTRL_EN_MASK) {
        s->regs[REG_TEMPERATURE_OFFSET] = i2c_sens_get_temperature();
      } else {
        s->regs[REG_TEMPERATURE_OFFSET] = 0xff;
      }
    }
  }

  s->count = 0;

  return 0;
}

Integrating into QEMU

Similarly to the Memory-mapped peripheral, the new I2C peripheral must be integrated into the Versatile Express description. This time, instead of attaching to a place in memory map, it needs to be attached to an I2C controller at a selected I2C address.

We can choose either to use an already existing I2C controller, or to also add a new I2C controller in the memory map, and then attach this component to the I2C controller.

In this example we will add a new I2C controller at address 0x10010000 and attach our custom I2C component to that I2C controller. The I2C slave address is chosen as 0x36.

The excerpt from initialization of emulated Versatile Express board is

  dev = sysbus_create_simple(TYPE_VERSATILE_I2C, map[VE_I2C_SENSOR], NULL);
  i2c = (I2CBus *)qdev_get_child_bus(dev, "i2c");
  i2c_slave_create_simple(i2c, "mistra.i2csens", 0x36);

I2C Userspace handling

With the device created in QEMU we can turn to making a userspace application to access the I2C device.

The program flow will be similar to the one used for memory-mapped device

  1. Device should be initialized
  2. After device is initialized, we should periodically read the temperature data

The full userspace application can be found in github.

Initialization

The device initialization now involves setting up the I2C userspace communication. Everything is done using the dev file for the selected I2C controller. In our case it is /dev/i2c-2.

The process can be summarized as

  1. Opening the I2C controller device file
    fd = open("/dev/i2c-2", O_RDWR);
    
  2. Configuring I2C slave address
    ioctl(fd, I2C_SLAVE, 0x36);
    
  3. Enabling device by writing 1 to CTRL register. Since this write should be performed in two steps, an array consisting of CTRL register offset (0x01) and value to be written (1) is passed to the write function
    uint8_t buffer[2] = { 0x01, 0x01 };
    write(fd, buffer, 2);
    

Periodic execution

After the device is initialized, we can use a separate thread to periodically initiate reads from I2C device. Unlike the memory-mapped device, this peripheral has no interrupt generation possibilities, so we need to make periodic execution in some other way.

We will use std::this_thread::sleep_until to enable periodic readouts from the I2C peripheral.

The read process also consists of two steps: writing TEMPERATURE register offset and then reading the TEMPERATURE register value.

while (m_running)
{
  // Read current time so we know when to timeout
  current_time = std::chrono::system_clock::now();

  // read I2C device and print
  // first write address
  uint8_t buffer[1] = { 0x02 };
  write(fd, buffer, 1);
  // then read value
  read(fd, buffer, 1);

  std::cout << "Measured " << (buffer[0] / 2.) << std::endl;

  // sleep_until
  std::this_thread::sleep_until(current_time + std::chrono::seconds(1));
}

Testing in QEMU

i2c-tools

Before testing the userspace application, we need to test that device is integrated properly inside QEMU. For that we can use i2c-tools package (add it Yocto or install in Ubuntu rootfs).

Once i2c-tools are installed, we can use i2cdetect to verify that a peripheral at 0x36 address exists using

$ i2cdetect -y 2
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- 36 -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- -- 

The i2cget can be used to read a value and i2cset to set certain value.

We can check that ID register returns 0x5A

$ i2cget -y 2 0x36 0
0x5a

The last test would be to turn on the device and check that TEMPERATURE register returns different values every time

$ i2cset -y 2 0x36 1 1
$ i2cget -y 2 0x36 2
0x22
$ i2cget -y 2 0x36 2
0x25
$ i2cget -y 2 0x36 2
0x2c

Userspace application

Userspace application can be compiled in the same manner as the memory-mapped application, via external toolchain or using a Yocto SDK generated toolchain:

$ mkdir build && cd build
$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain.cmake ..
$ make -j

or

$ source /opt/mistra-framebuffer/3.1/environment-setup-armv7at2hf-neon-mistra-linux-gnueabi
$ mkdir build && cd build
$ cmake ..
$ make -j

Alternatively, application can be added to the Yocto build system with a recipe shown in github.


After the application has been built and copied to the rootfs, it can be started. The application will initialize the I2C peripheral and start printing read temperature value every second.

Summary

In this blog post userspace application development for custom QEMU I2C sensor is presented.

The application encapsulates the I2C userspace access and implements periodic reading of data.

It can be improved with unit tests, as well as more interactive behavior, but it is left for some other time.

Oct 8, 2022

Userspace application for custom QEMU memory-mapped peripheral

This is part 3 of the Linux device driver development post series.

In the previous post structure of a Linux device driver for the custom memory-mapped peripheral in QEMU was shown.

In this post we will cover the following things

Developing userspace application for designed memory-mapped device

The driver that was developed in the previous post provides interface to our custom memory-mapped peripheral. What remains is to make an application that will interact with the hardware using that driver.

The application should be able to initialize the device and then block until interrupts arrive, when it should capture the value from the data register and print it.

Device initialization

In order to initialize the device the application needs to:

  1. turn it on via EN bit;
  2. select desired frequency via FREQ bit;
  3. enable interrupt generation via IEN bit.

Since all of these bits are accessible via implemented sysfs attributes, the application should access this files and perform appropriate writes.

After device is initialized, the application will go into an infinite loop waiting for interrupt to happen.

Interrupt handling (poll)

The title of this subsection might be misleading, since interrupt handling is done in the kernel driver code. However, the device driver only acknowledges the interrupt and clears interrupt flag, so interrupt processing is left to the application code. The notification from device driver to user space is implemented via sysfs_notify function on interrupt sysfs attribute.

In order for user space application to be able to be woken up when interrupt happens we will use poll syscall. It will make application thread to block while waiting for notification from device driver. Once interrupt is received, the application will be woken up and continue processing.

As for the processing, the application should read the data register (the processed value provided by the sysfs attribute) and print the value that was read.

Coding ideas

In this case I opted for using C++/CMake implementation presented in github repository.

The low-level access to sysfs attribute files and poll syscall is implemented in the Poller class. This class handles initialization and interrupt notification so the higher level code can only worry about handling of read data.

The Poller class will provide method to do the device initialization. It will also provide a method to start a thread where it uses poll syscall to wait for notification from sysfs attribute interrupt. Once notification is received, Poller will read the data sysfs attribute and print read value.

After Ctrl+c is pressed the application will call method from Poller to disable interrupt generation and turn off the device.

Building application manually

Using arm-none-linux-gnueabihf

CMake is chosen as the build system. Since we are cross-compiling for ARM architecture, we need to provide the toolchain.cmake file, which describes everything related to cross-compilation toolchain.

An example for arm-none-linux-gnueabihf toolchain is in github repository.

When setting up the build system, we will pass that toolchain file to cmake using -DCMAKE_TOOLCHAIN_PATH. The rest of the process is the standard CMake flow.

$ mkdir build && cd build
$ cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain.cmake ..
$ make -j

The resulting mmsens-app binary file will be in the build directory.

Using Yocto SDK

Yocto can be used to create and SDK which includes all required development libraries and build tools.

In order to build the SDK, process similar to building image (presented in part 4 of QEMU Board emulation) with additinal parameter of -c populate_sdk.

$ source setup-environment build_framebuffer
$ DISTRO=mistra-framebuffer MACHINE=vexpress-qemu bitbake core-image-test -c populate_sdk

The resulting toolchain file will be in build_framebuffer/tmp/deploy/sdk/, the mistra-framebuffer-glibc-x86_64-core-image-test-armv7at2hf-neon-vexpress-qemu-toolchain-3.1.sh.

Toolchain and SDK is installed using

$ ./mistra-framebuffer-glibc-x86_64-core-image-test-armv7at2hf-neon-vexpress-qemu-toolchain-3.1.sh
Mistra FrameBuffer SDK installer version 3.1
============================================
Enter target directory for SDK (default: /opt/mistra-framebuffer/3.1):
You are about to install the SDK to "/opt/mistra-framebuffer/3.1". Proceed [Y/n]?
Extracting SDK...........................................done
Setting it up...done
SDK has been successfully set up and is ready to be used.
Each time you wish to use the SDK in a new shell session, you need to source the environment setup script e.g.
 $ . /opt/mistra-framebuffer/3.1/environment-setup-armv7at2hf-neon-mistra-linux-gnueabi

From our mmsens-app we can initiate the build of the application in the following way

$ source /opt/mistra-framebuffer/3.1/environment-setup-armv7at2hf-neon-mistra-linux-gnueabi
$ mkdir build && cd build
$ cmake ..
$ make -j

There is no need to pass the toolchain file since sourcing environment script already does that for us.

Adding application to Yocto

Before we go to testing, we shall look into writing recipe for adding this application to the Yocto build.

The recipe should inherit cmake which will indicate to bitbake that CMake is used for building.

Summary of the recipe is presented here and full recipe can be found in meta-vexpress-apps.

DESCRIPTION = "Memory-mapped sensor userspace application"

SRC_URI = "git://github.com/straxy/mmsens-app.git;protocol=https;branch=main"

S = "${WORKDIR}/git"

inherit cmake

Only other thing needed is to add mmsens-app to IMAGE_INSTALL variable in the image recipe, and the application will be included in the final output image.

After recipe is added, the image can be built and loaded by following instructions from part 4 of QEMU Board emulation.

Testing application

If application is built manually, it needs to be copied to the rootfs of the SD card image following part 2 of QEMU Board emulation.

If application is build as part of the Yocto image, it will be included in the rootfs and located in /usr/bin.

Once application is in rootfs, it just needs to be executed using ./mmsens-app (if not in $PATH) or mmsens-app (if in $PATH).

The application will print the read value from the data register, and can be gracefully stopped by pressing Ctrl+c.

$ ./mmsens-app /sys/class/mmsens/mmsens0
Hello World!
[   51.670787] Interrupt received
Initial 0001
[   52.667802] Interrupt received
0002
[   53.667079] Interrupt received
0003
[   54.667250] Interrupt received
0004
[   55.666935] Interrupt received
0005
[   56.667187] Interrupt received
0006
^C[   57.666989] Interrupt received
0007
$

Running same application from Yocto image inside QEMU

Summary

In this blog post userspace application development for custom QEMU memory mapped sensor is presented.

The application encapsulates the sysfs files access and can block until interrupt is received so it can process it.

It can be improved with unit tests, as well as more interactive behavior, but it is left for some other time.

Aug 14, 2022

Developing Linux device driver for QEMU custom memory-mapped device

This is part 2 of the Linux device driver development post series.

In the previous post I presented the steps for creating a custom memory-mapped peripheral in QEMU.

In this post I will cover the following things

Developing character device driver for the designed memory-mapped device

The driver for the custom memory-mapped device should provide interface for user space applications to use the custom-memory mapped peripheral. This means that all bit-fields can be read and modified by a user-space application.

The device first needs to be 'recognized' by the system for which we will use Device-tree mapping and platform driver structures.

After device is recognized by the system, we need to have some methods to access and modify registers of our custom memory-mapped peripheral, and we will use character device driver structures with sysfs attributes.

Platform driver

Peripherals can be connected to the processor directly (via 'platform' bus, like our memory-mapped peripheral) and also via external busses: I2C, SPI, UART, USB, PCI, etc. Some of these busses support dynamic enumeration of devices (USB, PCI), but for others there needs to be a way to let the system know what is present in those busses.

In standard PCs BIOS is in charge of preparing and providing information about present devices to the operating system on boot.

ARM systems do not have BIOS, so the information must be provided in some other way. Initially, it was done by hardcoding details for each board in the Linux architecture specific code, so when board runs it has all of the information about peripherals that it needs.

However, that also meant that two boards with just minor differences could not use the same Linux kernel image.

To avoid that, Device Tree specification is now used. In Device Tree specifics of a device are described: memory regions, interrupt line numbers, dma channels, as well as key for binding the compatible driver with that device.

This way the device driver gets all relevant information about the way the device is integrated into the system from the Device Tree.

Device Tree description

Device Tree is a tree-like hierarchical description of system devices and busses they are connected to. It is used for describing peripherals that cannot be automatically enumerated.

Device Tree is written in textual form (.dts and .dtsi files, but also C header files can be preprocessed) and need to be translated into binary form (binary blob, .dtb) before they can be used on a board. At boot time Linux kernel (also recent versions of U-Boot) parse the Device Tree blob and try to match corresponding device drivers in order to initialize the system.

Without going into too much details (there are always good materials on Bootlin website), the Device Tree excerpt for our memory-mapped peripheral should look like

iofpga@7,00000000 {
...
    mmsens@18000 {
        compatible = "mistra,mmsens";
        reg = <0x18000 0x1000>;
        interrupts = <29>;
    };
...
};

The compatible string is used by the device driver when probing to match with this device.

Field reg is used to describe memory regions used by the device, in our case it is offset 0x18000 of the CS7 region. Memory range is 0x1000 long.

The interrupt line that is used is noted in the interrupts field.

Platform driver for memory-mapped peripheral

The platform driver provides callbacks that are called when Device Tree is parsed and device is probed, which are then used to get information about the device. The platform_driver structure has following important fields

  • .driver - initialized with driver name and table of compatible strings used for matching driver with the device from the Device Tree
  • .probe callback - called when Device Tree is parsed, to try to register device with the driver
  • .remove callback - called when device is removed (not particularly interesting for platform drivers) or when driver is removed from the system

In the case of our memory-mapped device, platform_driver structure should look like

static struct platform_driver mmsens_driver = {
    .driver = {
        .name = DRIVER_NAME,
        .of_match_table = mmsens_of_match,
    },
    .probe = mmsens_probe,
    .remove = mmsens_remove,
};

NOTE: The of_ prefix comes from 'Open Firmware', since full name of Device Tree is Open Firmware Device Tree.

Match table should contain the compatible string, which (from Device Tree excerpt above) is selected to be "mistra,mmsens"

static const struct of_device_id mmsens_of_match[] = {
    { .compatible = "mistra,mmsens", },
    { /* end of table */ }
};
MODULE_DEVICE_TABLE(of, mmsens_of_match);

The .probe callback should do the following things:

  1. Try to match device from Device Tree with the driver based on the 'compatible' string (of_match_node)
  2. Try to extract memory regions information from Device Tree (platform_get_resource) and remap memory so it is accessible by the driver (devm_ioremap_resource)
  3. Try to extract interrupt lines information from Device Tree (platform_get_irq) and register handler function for that interrupt (devm_request_irq)

Interrupt handler in our case should only clear the IFG flag

static irqreturn_t mmsens_isr(int irq, void *data)
{
    struct mmsens *dev = data;

    pr_info("Interrupt received\n");

    iowrite32(0, dev->base_addr + MMSENS_STATUS_OFFSET);

    return IRQ_HANDLED;
}

In the simplest scenario the .remove callback does not need to do anything, but once we add the character device operations it will change.

Chardev operations

So far, the platform driver only allows us to match the driver with the device when Device Tree description is parsed.

In order to be able to interact with the device and read/write some data to it, we need add another layer, which is character device. The use of character device allows us later on to add more operations, like IOCTL or sysfs attributes, to have even more ways to interact with the device.

Character device operations are executed when character device file (usually under /dev) is accessed, so we need to make sure that information obtained from platform driver framework (base address of the remapped region) can be used within the character device operations. For that purpose, we will create a custom structure which will be stored as private_data and shared between these two frameworks

/**
 * struct mmsens - mmsens device private data structure
 * @base_addr:    base address of the device
 * @irq:    interrupt for the device
 * @dev:    struct device pointer
 * @parent:    parent pointer
 * @cdev:    struct cdev
 * @devt:    dev_t member
 */
struct mmsens {
    void __iomem *base_addr;
    int irq;
    struct device *dev;
    struct device *parent;
    struct cdev cdev;
    dev_t devt;
};

The character device operations structure defines several callbacks

  • .open - used to prepare driver structures for accessing the device
  • .release - cleanup of operations done in .open
  • .read - read operation from the device, usually raw data that is copied to the user space
  • .write - write operation to the device, usually raw data that is copied from the user space

In the case of our memory-mapped device, it should look like

static struct file_operations mmsensdev_fops = {
    .owner = THIS_MODULE,
    .open = mmsens_open,
    .release = mmsens_release,
    .read = mmsens_read,
    .write = mmsens_write,
};

The details of the implementation are available in the github repository.

Sysfs attributes

The character device operations allow only reading or writing of raw data to the device. However, since our device has several registers and supports different operations, we need an additional interface to be able to control it.

That can be achieved using IOCTL callback, or by using sysfs attributes (we will use the latter).

The sysfs attributes are created in the /sys directory when the device is created as separate files. Each file can be used according to the way they are specified (read-only, write-only, read-write) and they can be used to access individual bits/registers, or perform specific operations.

In order to be able to use the sysfs attributes, the character device class must be created, and all devices of that class will appear under that directory.

The attributes can be read and/or written. The <operation>_show callback is used when attribute file is read, <operation>_store callback is used when attribute file is written, and attribute is initialized using static DEVICE_ATTR_RW(<operation>); (there are also the _RO and _WO variants).

In the case of our device, following attributes are supported

  • DEVICE_ATTR_RO(interrupt) - used to obtain interrupt status, and can be used to poll from user space application (more on that in next post)
  • DEVICE_ATTR_RW(enable_interrupt) - used to enable/disable interrupt generation
  • DEVICE_ATTR_RW(enable) - used to enable/disable device
  • DEVICE_ATTR_RW(frequency) - used to select desired sampling frequency
  • DEVICE_ATTR_RO(available_frequencies) - used to show available sampling frequencies
  • DEVICE_ATTR_RO(data) - used to show data in BCD format

If we take a look at the data attribute for instance, we can see that inside it reads the register value and returns string with formatted value

static ssize_t data_show(struct device *child, struct device_attribute *attr, char *buf)
{
    struct mmsens *dev = dev_get_drvdata(child);

    u32 data = ioread32(dev->base_addr + MMSENS_DATA_OFFSET);
    data &= DATA_MASK;

    return sprintf(buf, "%04X\n", data);
}

The details of the implementation are available in github repository.

Building driver out-of-tree

The kernel driver can be provided in two ways: as part of the kernel source code, or as an out-of-tree entity. In first case, using the kernel configuration (menuconfig for instance) it can be selected whether driver will be built into the kernel, or as a separate kernel module (.ko extension). In the out-of-tree build, kernel header files are needed and driver can be built as a kernel module file.

In this case, we will be using the out-of-tree approach. If we have the kernel source code in the $KERNEL_PATH, the Makefile for building the kernel module would look like

obj-m := mmsensdrv.o

SRC := $(shell pwd)

all:
    $(MAKE) ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- -C $(KERNEL_PATH) M=$(SRC)

After executing make command, the mmsensdrv.ko file would be available and can be transferred to the SD card and tested.

Adding driver to Yocto

If we want to add driver to Yocto a new recipe has to be created. The template (skeleton) exists at poky/meta-skeleton/recipes-kernel/hello-mod and we will reuse it.

The recipe for the module should look like

SUMMARY = "Memory-mapped QEMU sensor driver"
DESCRIPTION = "${SUMMARY}"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://COPYING;md5=12f884d2ae1ff87c09e5b7ccc2c4ca7e"

inherit module

SRC_URI = "git://github.com/straxy/mmsens-drv.git;protocol=https;branch=main"
SRCREV = "${AUTOREV}"

S = "${WORKDIR}/git"

# 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-mmsens-drv"

This module also needs to be added to the image recipe so it will be included in the output root filesystem image

IMAGE_INSTALL += "kernel-module-mmsens-drv"

After these changes have been added, the image can be rebuilt and run inside QEMU.

Testing driver

Once Linux kernel is started inside QEMU, the module can be loaded. If mmsensdrv.ko is copied to the SD card manually, it can me loaded into the kernel with

$ insmod mmsensdrv.ko

If driver was included in Yocto image, it will be loaded automatically on boot.

First we can check that mmsensX file exists in the /dev directory

$ ls /dev/mmsens*
/dev/mmsens0

Reading that file should return value 0 since device must be started manually, by setting the EN bit in CTRL register.

$ cat /dev/mmsens0 
mistra.mmsens:DATA: read of value 0x0
0

Next, we can check that appropriate entries exist in the sysfs

$ ls /sys/class/mmsens/mmsens0/
available_frequencies  device                 frequency              subsystem
data                   enable                 interrupt              uevent
dev                    enable_interrupt       power

Finally, we can do the proper testing.

Data incrementing

If we enable the device and try to read data attribute, as well as /dev/mmsens0, we should see that values are changing.

$ echo 1 > /sys/class/mmsens/mmsens0/enable
mistra.mmsens:CTRL: read of value 0x0
mistra.mmsens:CTRL: write of value 0x1
r_ctrl_post_write: Wrote 1 to CTRL
$ sleep 10
$ cat /sys/class/mmsens/mmsens0/data && cat /dev/mmsens0
mistra.mmsens:DATA: read of value 0x10
0010
mistra.mmsens:DATA: read of value 0x10
16

After enabling the device, the data attribute returns the BCD formatted value, while /dev/mmsens0 returns the raw (integer) value, as expected.

Frequency change

The list of available frequencies can be obrained from the available_frequencies attribute and current frequency selection from frequency attribute.

$ cat /sys/class/mmsens/mmsens0/available_frequencies
normal fast
$ cat /sys/class/mmsens/mmsens0/frequency 
mistra.mmsens:CTRL: read of value 0x0
normal

Per specification, normal frequency means that value changes once per second, while fast frequency means that value changes twice per second (every 0.5 seconds).

If we change sampling frequency from normal to fast, we should see that values are changing twice as often.

# Before
$ pushd /sys/class/mmsens/mmsens0
$ cat data && sleep 1 && cat data
0081
0082
# Change
$ echo fast > frequency
# After
$ cat data && sleep 1 && cat data
0085
0087
# Cleanup
$ popd

Interrupt generation

Finally, we should check that interrupts are generated properly. However, since we do not have userspace application that would do something useful with those interrupts, we can use output from /proc/interrupts

# Before enabling interrupt generation
$ cat /proc/interrupts | grep mmsens
 40:          0     GIC-0  61 Level     10018000.mmsens

NOTE: Where is value 29 from our description? First 32 interrupt lines are private interrupts per core, so numbering actually starts from 32. That means that actual interrupt number is 61 (32+29).

We can enable interrupt by writing 1 to enable_interrupt and number of occurences (column 1) increases. It is also visible from QEMU debug code that once interrupt is generated, interrupt handler in device driver clears STATUS register, thus acknowledging interrupt.

# After enabling interrupt generation
$ echo 1 > /sys/class/mmsens/mmsens0/enable_interrupt
mm_sens_update_irq: Interrupt generated
[ 8104.791017] Interrupt received
mistra.mmsens:STATUS: write of value 0x0
r_status_post_write: Wrote 0 to STATUS
mm_sens_update_irq: Interrupt none
$ cat /proc/interrupts | grep mmsens
 40:          1     GIC-0  61 Level     10018000.mmsens

Summary

In this blog post a simple character device platform driver is presented. The driver allows initialization and bit-field access of memory mapped peripheral.

Device Tree description for the memory-mapped device is also shown, which allows device drvier to obtain information about the device that is present.

The driver itself handles interrupt, but we have not gone into processing that event, and if someone would want to use it as it is (for instance with bash script), they could only do a polling approach to handle data.


Next step is to develop a user space application which will be able to initialize device using the driver, as well as receive information about received interrupt and process updated data. This will be done in next blog post in this series.

Jun 25, 2022

Developing custom memory-mapped peripheral for QEMU

This is part 1 of the Linux device driver development post series.

In the previous post I presented some of the goals for doing this work with QEMU.

In this post I will cover the following things

Designing custom memory-mapped device in QEMU

QEMU can emulate many different peripherals. More importantly, it is possible to create new peripherals that are emulated.

That is particularly useful when someone wants to learn device driver development, since that peripheral will be unique, and it would be impossible to find already existing driver.


In this post we will develop a new, custom peripheral for QEMU.

The peripheral will have several 32-bit registers and ability to generate interrupts. It will function as an 4-digit BCD counter. It can be enabled or disabled, generate interrupts on each count, and counting frequency can be selected.

In the next post we will develop Linux device driver that will enable access to this peripheral.

Register map

Register map for the custom peripheral is shown in the following table

offset name description
0x0 CONFIG Configuration register, used to enable component, interrupt generation and select frequency
0x4 STATUS Status register, used to read/clear interrupt flag
0x8 DATA Data register holding current counter value

Bit values of CONFIG register are shown in the following table

name pos dflt description
Reserved 31:3 0 Reserved
FREQ 2 0 Frequency setting:
  • 0 - normal frequency (1 Hz)
  • 1 - fast frequency (2 Hz)
IEN 1 0 Interrupt enable
EN 0 0 Enable device

Bit values of STATUS register are shown in the following table

name pos dflt description
Reserved 31:2 0 Reserved
IFG 1 0 Interrupt flag
Reserved 0 0 Reserved

Bit values of DATA register are shown in the following table

name pos dflt description
Reserved 31:16 0 Reserved
SAMPLE 15:0 0 Current counter value

Fitting into Vexpress-A9 memory map

In order to instantiate and use developed memory-mapped component, we need to integrate it into the Vexpress-A9 memory map and connect it to interrupt controller.

Looking at the memory map for Vexpress-A9, there are several regions that are unused, where we can place our custom component. In this example, I chose offset 0x00018000 in the motherboard peripheral memory map, region CS7. This means that the absolute address is 10018000.

Also, we need an interrupt line for the custom component. Again, looking at the documentation, there are several "reserved" interrupt lines which we can use. In this example, I chose interrupt line 29.

QEMU implementation

Details of implementation of a new memory-mapped device in QEMU will be shown in this section. Main points will be displayed, so someone can use it as instructions for creating a new device.

The device will be described by it's registers and added to the Versatile Express memory map.

Register mapping

QEMU has a very simple and verbose way of describing registers of a component.

For each register, it's offset from the base address is specified, as well as bit-fields that the register consists of. Additionally, for each bitfiled access permissions can be specified and callback functions that are executed before and/or after accessing the register.


Register description for our custom component can be described as

REG32(CTRL, 0x00)
    FIELD(CTRL,     EN,     0,  1)      /* component enable */
    FIELD(CTRL,     IEN,    1,  1)      /* interrupt enable */
    FIELD(CTRL,     FREQ,   2,  1)      /* sampling frequency setting */

REG32(STATUS, 0x04)
    FIELD(STATUS,   IFG,    1,  1)      /* interrupt flag */

REG32(DATA, 0x08)
    FIELD(DATA,     SAMPLE, 0,  16)     /* current value */

The previous code excerpt defines that we have three 32-bit registers called CTRL (offset 0x0 from base address), STATUS (offset 0x4) and DATA (offset 0x8). If we look at the the register CTRL, it has three bit-fields that are used: EN (bit position 0, size 1 bit), IEN (bit position 1, size 1 bit), FREQ (bit position 2, size 1 bit). Similar goes for other two registers.

NOTE: The details of the REG32 and FIELD macros can be found in hw/registerfields.h include file. Thing to keep in mind is that these macros create additional macros (mask, shift, etc.) which we can use later for accessing individual registers and bit-fields.


In order to specify actions that are performed when someone tries to access these registers (read or write), following data must be defined

static const RegisterAccessInfo mm_sens_regs_info[] = {
    {   .name = "CTRL",           .addr = A_CTRL,
        .reset = 0,
        .rsvd = ~(R_CTRL_EN_MASK | R_CTRL_IEN_MASK | R_CTRL_FREQ_MASK),
        .post_write = r_ctrl_post_write,
    },
    {   .name = "STATUS",           .addr = A_STATUS,
        .reset = 0,
        .rsvd = ~R_STATUS_IFG_MASK,
        .post_write = r_status_post_write,
    },
    {   .name = "DATA",         .addr = A_DATA,
        .reset = 0,
        .rsvd = ~R_DATA_SAMPLE_MASK,
        .ro = R_DATA_SAMPLE_MASK,
    },
};

static const MemoryRegionOps mm_sens_reg_ops = {
    .read = register_read_memory,
    .write = register_write_memory,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid = {
        .min_access_size = 4,
        .max_access_size = 4,
    }
};

Looking at the second structure, mm_sens_reg_ops, it defines that we are using register read and write functions when accessing this component.

Register functions use the mm_sens_regs_info array defined above, where RegisterAccessInfo structure has several useful fields:

  • name - register name, used for debugging
  • addr - register offset
  • reset - reset value of register
  • ro - read-only bitmask
  • w1c - write one to clear bitmask
  • cor - clear one read bitmask
  • rsvd - reserved bits bitmask
  • pre_write - callback executed before write command
  • post_write - callback executed after write command
  • post_read - callback executed after read command

The post_write functions in the previous code block need to perform certain actions based on the values that are written to the registers.

For instance, after CTRL register bit EN bit is set to 1, the DATA values should start incrementing. Or, after bit FREQ has changed, the frequency of incrementing DATA register should change.

Before we go into details of post_write functions, it would be useful to first explain how is DATA register periodically incremented, as well as how are interrupts implemented in QEMU code.

QEMU timers

QEMU uses timers to enable periodical execution. Timers have a simple API (described in hw/ptimer.h) which we will go over in this section.

Our plan is to increment value of DATA register at two different frequencies: normal (1 Hz) and fast (2 Hz). The value should be incremented only when bit EN in CTRL register is set to 1. The value should also be incremented as BCD value, so each nibble should have values only in the range of 0-9.


Having analyzed our needs, following ptimer functions are of interest

  • ptimer_init - create timer object and define callback that is executed when timer period expires
  • ptimer_set_freq - set timer reload frequency in Hz
  • ptimer_run - start timer and select whether continuous or oneshot mode is used
  • ptimer_stop - stop timer

Based on this, the post_write function for CTRL has two parts.

In the first part FREQ is handled so after every write a check is made if value of FREQ bit has changed, and if it has, timer frequency is updated.

// first part, FREQ handling
...
    new_sfreq = (s->regs[R_CTRL] & R_CTRL_FREQ_MASK) >> R_CTRL_FREQ_SHIFT;

    if (new_sfreq != s->sampling_frequency) {
        s->sampling_frequency = new_sfreq;
        switch (s->sampling_frequency) {
            case FREQ_NORMAL:
                ptimer_set_freq(s->timer, DATA_UPDATE_NORMAL_FREQ);
                break;
            case FREQ_FAST:
                ptimer_set_freq(s->timer, DATA_UPDATE_FAST_FREQ);
                break;
            default:
                DB_PRINT("Unknown frequency %u\n", s->sampling_frequency);
                break;
        }
    }
...

In the second part EN is handled and timer is started/stopped if EN bit is set/reset.

Additionally, if timer is enabled and IEN bit is also set, then evaluation of interrupt generation condition must be made (more on interrupts in next subsection).

// second part, EN/IEN handling
...
    if (s->regs[R_CTRL] & R_CTRL_EN_MASK) {
        /* start timer if not started*/
        ptimer_run(s->timer, 0);

        if (s->regs[R_CTRL] & R_CTRL_IEN_MASK) {
            /* check if alarm should be triggered */
            mm_sens_update_irq(s);
        }
    } else {
        /* stop timer */
        ptimer_stop(s->timer);
    }
...

Increments of DATA register are implemented in the timer callback in the following manner

// DATA incrementing
static void mm_sens_update_data(void *opaque)
{
    MMSensor *s = MM_SENS(opaque);

    s->regs[R_DATA] = s->regs[R_DATA] + 1;
    if ((s->regs[R_DATA] & 0x000fu) > 0x0009u) {
        s->regs[R_DATA] += 0x0006u;
        if ((s->regs[R_DATA] & 0x00f0u) > 0x0090u) {
            s->regs[R_DATA] += 0x0060u;
            if ((s->regs[R_DATA] & 0x0f00u) > 0x0900u) {
                s->regs[R_DATA] += 0x0600u;
                if ((s->regs[R_DATA] & 0xf000u) > 0x9000u) {
                    s->regs[R_DATA] += 0x6000u;
                }
            }
        }
    }

    s->regs[R_STATUS] |= R_STATUS_IFG_MASK;

    mm_sens_update_irq(s);
}

This way the BCD requirement is met and counting will look like in the following diagram

QEMU interrupt handling

Interrupt handling in peripheral in QEMU is performed using the qemu_set_irq function. The function receives an additional parameter which indicates whether interrupt is pending or not. If interrupt is pending (and is not masked in the interrupt controller) it will be raised to the CPU.

In the case of our peripheral, interrupt is pending if both bit IFG in STATUS register and bit IEN in CTRL register are set. This condition has to be checked every time a change happens to any of these two registers, so there is a function that can be reused.

// IRQ handling
static void mm_sens_update_irq(MMSensor *s)
{
    bool pending = s->regs[R_CTRL] & s->regs[R_STATUS] & R_CTRL_IEN_MASK;

    qemu_set_irq(s->irq, pending);
}

Integrating with board file and build system

In order to use the custom memory mapped peripheral, it must be 'placed' in the memory map of the emulated board. Since we are using Versatile Express A9, then it's description must be updated.

Luckily, this is done with one simple command, where we can see chosen base address (0x10018000) and interrupt number (29).

sysbus_create_simple("mistra.mmsens", 0x10018000, pic[29]);

Since build system uses meson and ninja, the new component file is added to the build system in the following manner

softmmu_ss.add(files('mmsens.c'))

The patch file with implementation of the custom component is available in github. The main details of our custom component were explained in previous sections. However, there are standard QEMU structures that also must be used in order to describe VMState, as well as initialization and those can be reused from the patch.

Patch is applied to the QEMU source tree with following command

$ cd $QEMU_SRC
$ patch -p1 < BCD-memory-mapped.patch

After the patch is applied, QEMU must be rebuilt.

Testing developed peripheral

Testing peripheral without appropriate Linux device driver is a bit harder, but not impossible.

We can use the embedded debug prints from the memory-mapped component. Before they can be used, the MM_SENS_ERR_DEBUG definition must be changed from 0 to 1. This way all debug prints from the component will be visible.


U-Boot has integrated commands for reading and writing to memory addresses, so we can use it to try to enable the component, interrupt generation and read current data value.

After QEMU is started with

# Run QEMU with SD card and networking
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel $UBOOT \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio

U-Boot prompt should be reached by pressing a key.

Following commands are available

  • md <addr> - read data from address addr
  • mw <addr> <val> - write val to address addr

We can first try reading the CTRL and DATA registers

# Run QEMU with SD card and networking
U-Boot> md 0x10018000 1
10018000:mistra.mmsens:CTRL: read of value 0x0
 00000000                               ....
U-Boot> md 0x10018008 1
10018008:mistra.mmsens:DATA: read of value 0x0
 00000000                               ....

The lines starting with 10018000:mistra.mmsens: at debug prints from QEMU, while second lines are written by U-Boot.

In order to enable peripheral, so DATA value starts incrementing, we can write 1 to EN bit in CTRL register. If we read DATA register afterwards, we can see that the values are changing.

# Run QEMU with SD card and networking
U-Boot> mw 0x10018000 1
mistra.mmsens:CTRL: write of value 0x1
r_ctrl_post_write: Wrote 1 to CTRL
U-Boot> md 0x10018008 1
10018008:mistra.mmsens:DATA: read of value 0x1
 00000001                               ....
U-Boot> md 0x10018008 1
10018008:mistra.mmsens:DATA: read of value 0x2
 00000002                               ....

We can also check that the IFG in STATUS register is set. However, interrupt is not triggered since handling of this interrupt is not implemented in U-Boot, which is expected. We will implement interrupt handling in the next blog post, when we develop the Linux device driver for this component.

Summary

In this blog post the process of developing a custom memory-mapped peripheral for QEMU is shown. Main details are described and the complete patch is available with full details of the component.

Using this process many different components can be implemented.


In the next blog post I will show process of development of Linux device driver for this component.

May 19, 2022

Learning Linux device driver development using QEMU - Introduction

In the previous blog series QEMU board emulation I introduced simple methods to prepare and run a QEMU image for board emulation. Target board was Versatile express and mainline U-Boot and Linux kernel were used for testing. The goal of that blog series is to show how to use QEMU for board emulation.


In this blog post series I will go deeper into the topics of device driver and userspace application development for embedded Linux systems.

We will continue using QEMU for testing. There are multiple benefits from using QEMU over some standard COTS board:

  • it is cheaper, there is no need to buy additional hardware, also making it easier to try and follow
  • 'new' hardware can be designed in QEMU, compared to COTS board where hardware cannot change and drivers are already available
    • even if we consider connecting peripherals over external parallel or serial busses as 'changing the COTS board', drivers already exist even for those peripherals
  • it is easier to debug during development in QEMU

There is also a possibility to use FPGA-SoC boards, where custom peripherals can be created in the FPGA part, but it is much more complex and also requires the board to be available.


The goals of this post series would be to show development of

  • character device driver for a custom memory-mapped hardware emulated in QEMU
  • userspace application that interacts with character device driver for memory-mapped peripheral
  • userspace I2C application/driver used to interact with custom I2C peripheral

In both cases I will also cover details on how to write a simple memory-mapped and I2C peripherals in QEMU.


Quick links to other posts in the series:

Apr 23, 2022

Yocto for Vexpress A9 in QEMU

This is part 4 of the QEMU Board Emulation post series.

In parts 2 and 3 of this post series the complete boot procedure from SD card has been presented, as well as how to configure kernel support required to enable graphical display for Vexpress-A9 board.

In both posts Ubuntu was used as root filesystem. Using Ubuntu as root filesystem is simple and fast to use, but it also has a lot of packages which are not necessary.

In this post I will cover the Yocto setup for Vexpress-A9 board. Using Yocto we will be able to build a custom distribution which will allow us to run Linux with or without GUI on QEMU Vexpress-A9.

Items that will be covered are

Sources for the Yocto build environment for Vexpress-A9 can be found in following repositories:

Yocto introduction

Yocto is tool used to build Board Support Package (BSP) and Linux distributions, especially for embedded targets. It is very configurable and provides fine grained control of output images.

The project is organized into layers and applications that can be built are described in recipes. Configuration for a build depends on selected image, machine and distribution, as well as local configuration parameters.

For more details about Yocto Bootling Yocto training slides can be used.

I tried to use the Freescale/NXP Yocto organization as a reference when creating these layers, so method of use is very similar to the method when working with iMX SoCs.

Base Yocto setup

As stated previously, the base repository holds the manifest file and base configuration.

The manifest file describes all the layers that are used in the project. In this case only 'dunfell' branch is selected, but selection can be made on a specific commit.

<?xml version="1.0" encoding="UTF-8" ?>
  <manifest>
  <default revision="dunfell" sync-j="4"/>

  <remote fetch="git://git.yoctoproject.org" name="yocto"/>
  <remote fetch="https://github.com/straxy" name="vexpress"/>
  <remote fetch="https://github.com/openembedded" name="oe"/>

  <project name="poky" path="sources/poky" remote="yocto" revision="dunfell"/>
  <project name="meta-openembedded" path="sources/meta-openembedded" remote="oe" revision="dunfell"/>
  <project name="qemu-vexpress-yocto" path="sources/base" remote="vexpress" revision="main">
    <copyfile dest="setup-environment" src="scripts/setup-environment"/>
  </project>
  <project name="meta-vexpress" path="sources/meta-vexpress" remote="vexpress" revision="main"/>

  </manifest>

The poky and meta-oe layers provide base applications and images, which can be extended by the higher-level layers, like meta-vexpress.

The setup-environment script is used to initialize a build environment. Part of it initializes the bblayers.conf and local.conf based on the input files in the templates directory.

Distribution configuration

Distribution configuration is in the meta-vexpress layer, in the conf/distro directory.

Two distributions are configured:

  • The framebuffer distribution which disables all graphical backends, like X11, Wayland, Vulkan. This way the output image size is reduced.
  • The X11 distribution which uses the X11 server.

Selection of distribution is made at compile time by passing either mistra-framebuffer or mistra-x11 to the DISTRO variable.

Machine configuration

Machine configuration is in the meta-vexpress layer, in the conf/machine directory.

Machine has definitions output images that should be built, as well as parameters for U-Boot and Linux kernel. This way, differences between machines can be kept in separate files and same distribution can be used for different machines.

U-boot recipe

The goal with this exercise is to use the same software versions as in part 2 of the blog post series.

The u-boot related recipes are in recipes-bsp directory.

The u-boot directory holds the main u-boot recipe, which targets a specific git commit in order to use the same version as in part 2 of the blog post series.

In the u-boot-scr directory is recipe used to build a u-boot script. The u-boot script is a special script run by U-Boot at boot. This way, all of the commands that were entered manually in part 2 of the blog post series will be automatically executed.

The commands are in boot.cmd file, and instructions on how this script can be manually built are in the do_compile step of the u-boot-scr.bb recipe.

Linux kernel recipe

Linux recipe is stored in the recipes-kernel. It selects the appropriate git commit in order to have the same version as in part 2 of the blog post series.

Image recipe

Image recipe is stored in the recipes-extended/images directory.

This is a copy of the core-image-minimal image supplied from Yocto, but can be extended if needed. This is an image that does not use graphics.

Another image that will be used is core-image-sato, which uses graphical environment.

Building and running images in QEMU

Prerequisites

Before Yocto image can be built, several packages must be installed:

$ sudo apt-get install gawk wget git-core diffstat unzip texinfo \
     gcc-multilib build-essential chrpath socat cpio python3 \
     python3-pip python3-pexpect xz-utils debianutils iputils-ping \
     python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev pylint3 \
     xterm

NOTE: For details look at Yocto reference manual

Preparing environment

In order to prepare for a build, the manifest repository must be used with repo tool.

$ repo init -u https://github.com/straxy/qemu-vexpress-yocto -m default.xml
$ repo sync

After the repo is initialized and synced, all recipe sources will be in the sources directory. Two additional directories will be created, downloads, where archives and git repositores with sources for packages are downloaded and kept, and sstate-cache intermediate build products for reuse will be stored during the build process. The sstate-cache directory can speed up rebuilds, or different flavor builds, since packages that are not changed will be reused.

The next step is to initialize the build environment

$ source setup-environment <build_dir>

where <build_dir> is custom directory where build will be performed and output files stored. After this command is executed current directory is automatically changed to build_dir.

Building and running images

The build is started using the following command

# build command
$ DISTRO=<selected_distribution> MACHINE=<selected_machine> bitbake <selected_image>

Once build is completed, output image will be placed in <build_dir>/tmp/deploy/images/<selected_machine>/<selected_image>-<selected_machine>.wic. There will be also other build products, like u-boot binary (u-boot.elf will be used for running QEMU), linux kernel binary, etc.

Image is in wic format and can be copied to the SD card image using following commands

# create SD card image
$ qemu-img create sd.img 4G
# 'insert' SD card
$ sudo kpartx -av ./sd.img
# note the loopXX that is used and use dd to copy wic image
# if there is no output, look at $ losetup -a to find the loop device
$ sudo dd if=<selected_image>-<selected_machine>.wic of=/dev/loopXX bs=1M iflag=fullblock oflag=direct conv=fsync
# after copying is done 'remove' the SD card
$ sudo kpartx -d ./sd.img

Once SD card is ready, QEMU can be started using the following command

# Run QEMU with SD card and networking
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio

Framebuffer image

Distro that is used is mistra-framebuffer and image is core-image-test, so complete build command is

# framebuffer image build command
$ DISTRO=mistra-framebuffer MACHINE=vexpress-qemu bitbake core-image-test

After image is built, copied to the SD card and QEMU is run, the following output will be visible

# QEMU output
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio
[ ... ]
Mistra FrameBuffer 3.1 vexpress-qemu /dev/ttyAMA0
vexpress-qemu login:

X11 image and Sato

Distro that is used is mistra-x11 and image is core-image-sato, so complete build command is

# framebuffer image build command
$ DISTRO=mistra-x11 MACHINE=vexpress-qemu bitbake core-image-sato

After image is built, copied to the SD card and QEMU is run, the following output will be visible

# QEMU output
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel u-boot.elf \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio
[ ... ]
Mistra X11 3.1 vexpress-qemu /dev/ttyAMA0
vexpress-qemu login:

During the boot process following image is visible

After the boot process is complete the environment is ready

Summary

In this blog post the procedure for using Yocto to build the distribution is shown. It can be further extended with other layers (like meta-qt5).

Feb 6, 2022

Emulating Ubuntu GUI on Vexpress in QEMU

This is part 3 of the QEMU Board Emulation post series.

In the previous post the complete boot procedure from SD card or network boot has been presented.

In this post I will cover the following things

The goal is to use the emulated Versatile express board to display some simple graphics. This is also a very good feature of QEMU, where graphical applications can be run for some of the emulated boards.

Modifying Linux kernel configuration

Versatile Express V2P-CA9 has the PL111 LCD display controller which is emulated in QEMU. The LCD controller is connected to a SiI9022 display bridge controlled over an I2C bus, and a Versatile display panel. However, none of it is not enabled by default in the Linux configuration.

In order to enable it, several configuration options in the linux kernel should be enabled

  • CONFIG_DRM_PL111 - enable LCD display controller
  • CONFIG_I2C_VERSATILE - enable I2C bus support; the display bridge is on the I2C bus
  • CONFIG_DRM_SII902X - enable display bridge
  • CONFIG_DRM_PANEL_ARM_VERSATILE - enable ARM versatile display panel

Besides the hardware dependencies, in order to enable framebuffer and display image on the display, several additional options must be enabled

  • CONFIG_FB - enable framebuffer support
  • CONFIG_FB_ARMCLCD - enable ARM LCD framebuffer support
  • CONFIG_FRAMEBUFFER_CONSOLE - enable console to be shown on the framebuffer
  • CONFIG_LOGO - show tux logo when booting up

Enabling these configuration options is done using menuconfig. First the environment script env.sh should be sourced and then the menuconfig interface can be entered using

# Enter menuconfig
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress menuconfig

This will show an interface like

The menuconfig can be traversed using arrow keys. Options are selected using the space key.

There are two types of options that can be selected

  • boolean
    • [ ] - option is not enabled
    • [*] - option is enabled
  • tristate
    • < > - option is not enabled
    • <M> - functionality will be compiled as a kernel module
    • <*> - functionality will be compiled in the linux kernel image

There are other filed types that are available, but they are not important for our analysis.

If we want to search for a keyword in menuconfig, we can press / and then type the keyword. This will open a window where selection can be made using a number on the keyboard.


Since we know which configuration options we want, we can search for them. Otherwise, they can be found in the following paths

CONFIG_DRM_PL111
-> Device Drivers
  -> Graphics support
    <*> DRM Support for PL111 CLCD Controller
CONFIG_I2C_VERSATILE
-> Device Drivers
  -> I2C support
    -> I2C Hardware Bus support
      <*> ARM Versatile/Realview I2C bus support
CONFIG_DRM_SII902X
-> Device Drivers
  -> Graphics support
    -> Display Interface Bridges
      <*> Silicon Image sii902x RGB/HDMI bridge
CONFIG_DRM_PANEL_ARM_VERSATILE
-> Device Drivers
  -> Graphics support
    -> Display Panels
      <*> ARM Versatile panel driver
CONFIG_FB
-> Device Drivers
  -> Graphics support
    -> Frame buffer Devices
      <*> Support for frame buffer devices  --->
CONFIG_FB_ARMCLCD
-> Device Drivers
  -> Graphics support
    -> Frame buffer Devices
      -> Support for frame buffer devices
        <*> ARM PrimeCell PL110 support
CONFIG_FRAMEBUFFER_CONSOLE
-> Device Drivers
  -> Graphics support
    -> Console display driver support
      [*] Framebuffer Console support
CONFIG_LOGO
-> Device Drivers
  -> Graphics support
    [*] Bootup logo

After the kernel configuration is modified, kernel has to be rebuilt again and all related files copied to the SD card (kernel image and kernel modules). For detailed instructions consult part 1 and part 2 of this series. If scripts from github are used, then prepare-qemu.bash script has to be executed after the Linux kernel is rebuilt in order to pull new image and kernel modules to the SD card.

Updating kernel config manually

If scritps from github are used for testing, the supplied defconfig file can be used to update the kernel configuration to support display.

The defconfig needs to be copied into the $PROJ_DIR/linux/build_vexpress/ directory and renamed to .config. After that the olddefconfig command has to be executed.

# Using defconfig
$ cp defconfig $PROJ_DIR/linux/build_vexpress/.config
$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- O=build_vexpress olddefconfig

After this the build can be started and prepare-qemu.bash script can be used to prepare the new SD card.

Enabling graphics in Ubuntu minimal

In order to enable graphics in the Ubuntu minimal image, we need to boot the image and install apropriate packages.

QEMU can be started in the following manner in order to use image from SD card and allow network connection and use graphics for display

# Run QEMU with SD card and networking
$ qemu-system-arm -M vexpress-a9 -m 1G -kernel $UBOOT \
                  -drive file=sd.img,format=raw,if=sd \
                  -net nic -net tap,ifname=qemu-tap0,script=no \
                  -serial mon:stdio

NOTE: It is assumed that network is enabled in the host according to previous instructions.

Please note that the command for starting QEMU has a significant difference compared to instructions from part 1 and part 2 of blog series: instead of passing parameter -nographics the parameter -serial mon:stdio is used.

This will enable graphical window to appear and serial output for U-Boot will still go to terminal window from where QEMU is started. Serial output direction for Linux kernel can be controlled by specifying console parameter in the bootargs U-Boot variable:

  • if console=ttyAMA0 linux console output will go to the same terminal as U-boot output,
  • if console=tty0 linux console output will go to the graphical window.

First check if everything is working ok can be done by looking at the display while the system is booting, it should show a tux logo

An addition check if framebuffer support is working can be done by writing random data to framebuffer, which should show a 'noisy' image.

# Write random data to framebuffer
$ sudo cat /dev/urandom > /dev/fb0

Following image is the result


After network is enabled in the emulated board then apt-get can be used to update and download necessary applications (this post from askubuntu.com has been used as a reference for installation of packages).

# Install xserver apps
$ sudo apt-get update
$ sudo apt-get install xserver-xorg-core --no-install-recommends --no-install-suggests
$ sudo apt-get install openbox --no-install-recommends --no-install-suggests
$ sudo apt-get install xinit
$ sudo apt-get install slim

In order to display graphics on the graphics window following command can be used

# Start graphics
$ sudo service slim start

This will show following login screen and, after logging in, an application running (xterm). Please note that this is bare minimum, so not much can be done without installing additional applications.

Summary

In this post a way to configure Linux kernel to enable graphics support for Versatile Express board is shown. Enabling on the root filesystem side can be optimized by using a root filesystem image built by Yocto or Buildroot.