Using QMP to interact with QEMU Board

Using QMP to interact with QEMU Board

QEMU Machine Protocol (QMP)

QEMU is a very useful tool for learning embedded system operation since different processors, configurations and peripherals using different protocols can be emulated.

For this blog I’ve made several custom peripherals and it is not always easy to make a demo, since some meaningful data has to be provided. For instance, in the custom peripherals examples for I2C and SPI, the temperature sensors generate random data in certain range and that data can be read. It was a way to make some data which is not static, so the example can be interactive, but the downside is that the values cannot be reproduced and tested due to randomness.

QEMU has another way of providing that data from outside of the running instance. It is via device properties and QEMU Machine Protocol (QMP).

In order to use QMP the QEMU instance has to be started with the following argument

-qmp <socket>

where <socket> can be a tcp or unix domain socket. In this example we will be using a unix domain socket, so the argument would be

-qmp unix:/tmp/qmp.sock,server,nowait

This will start a QMP server in the QEMU instance.

A client can interact via QMP using scripts available in the scripts/ directory in QEMU source code, or via Python qemu.qmp module.

TMP105 QEMU Model

Before we go into details on the QMP we will take a look at the TMP105 peripheral that can be added to the emulated QEMU board. This peripheral is interesting since it already has the temperature property exported.

TMP105 is a I2C temperature sensor which can provide temperature in the range from -128 to 128 degrees in 9-12 bit resolution. For the purpose of this test, we will use only top 8 bits since that will give temperature with resolution of 1 degree.

The temperature can be read from the register at offset 0x0, so i2c-tools command to read the current temperature would be

i2cget -y <bus> <address> 0x0

The TMP105 can be added in the source code of the emulated board (same way as it was done in the Custom I2C peripheral example), but it can also be added when invoking QEMU by passing the following argument

-device tmp105,id=sensor,address=0x50

The ID of the peripheral is passed so we can easily identify it from QMP. This command will make the device attached to the I2C bus 1.

For reading the values from the emulated TMP105 peripheral in the QEMU we can use i2c-tools, so a read of temperature is performed using i2cget -y 1 0x50.

The value that is read will be a hex representation of the temperature in degrees.

QMP scripts

QEMU has several scripts in the scripts/ directory that can be used to interact over QMP

  • qmp-shell - universal one, can work with all of the commands; we are interested with ones working with the QEMU Object Model (QOM)
  • qom-list - list all objects that are available
  • qom-get - get a property of a object
  • qom-set - set a value of the property of an object

In this example we will use the qmp-shell for issuing commands. It is started using

qmp-shell -p /tmp/qmp.sock

(-p in the command above is for pretty print of JSON return values).

Once QMP shell is started we can issue commands.

We can list all the properties of the sensor node using

qom-list path=sensor
{
    "return": [
        {
            "name": "type",
            "type": "string"
        },
        {
            "name": "parent_bus",
            "type": "link<bus>"
        },
        {
            "name": "realized",
            "type": "bool"
        },
        {
            "name": "hotplugged",
            "type": "bool"
        },
        {
            "name": "hotpluggable",
            "type": "bool"
        },
        {
            "name": "address",
            "type": "uint8"
        },
        {
            "name": "unnamed-gpio-out[0]",
            "type": "link<irq>"
        },
        {
            "name": "temperature",
            "type": "int"
        }
    ]
}

The temperature property is at the bottom. In order to read the current value of the temperature property, following command can be used

qom-get path=sensor property=temperature

{
    "return": 0
}

The value of the temperature property can be changed using the following command


qom-set path=sensor property=temperature value=30000
{
    "return": {}
}

This will set the value to 30 degrees (temperature is written in milicelsius).

Python QMP access

QEMU has several Python modules available in the source code, one of them is the QMP module.

Assuming QEMU_SRC_PATH holds path to the QEMU source code, Python QEMU module can be installed in the virtual environment using pip

python3 -m venv venv
. ./venv/bin/activate
pip3 install ${QEMU_SRC_PATH}/python/

Once installed, the QMP module can be imported using import qemu.qmp. It provides functions for creating and connecting a client, as well as executing the QMP JSON commands.

In this example, a simple interactive python script is created. The script will initialize a curses screen and a QMP client and then read the initial temperature value.

Once initialized, the script will wait for key press and following characters will be accepted:

  • + to increase temperature value by 1 degree
  • - to decrease temperature value by 1 degree
  • q to exit the program

The source code for the script follows

import asyncio
from qemu.qmp import QMPClient
from curses import ERR, wrapper


async def update_temperature(client, value):
    await client.execute(
        "qom-set",
        {
            "path": "/machine/peripheral/sensor",
            "property": "temperature",
            "value": value,
        },
    )


def temperature_to_tmp105_hex(value):
    return hex(value // 1000)


def log_current_temperature(win, temperature):
    win.clear()
    win.addstr(
        f"Current temperature {temperature // 1000} ({temperature_to_tmp105_hex(temperature)})"
    )


async def display_main(win):
    qmp = QMPClient("cubieboard-ng vm")
    await qmp.connect("/tmp/qmp.sock")

    win.nodelay(True)

    temperature = await qmp.execute(
        "qom-get", {"path": "/machine/peripheral/sensor", "property": "temperature"}
    )
    log_current_temperature(win, temperature)

    while True:
        char = win.getch()
        if char == ERR:
            await asyncio.sleep(0.1)
        elif char in (ord("q"), ord("Q")):
            break
        elif char == ord("+"):
            temperature += 1000
            if temperature > 127000:
                temperature = 127000
            log_current_temperature(win, temperature)
            await update_temperature(qmp, temperature)
        elif char == ord("-"):
            temperature -= 1000
            if temperature < 0:
                temperature = 0
            log_current_temperature(win, temperature)
            await update_temperature(qmp, temperature)

    await qmp.disconnect()


def main(stdscr) -> None:
    return asyncio.run(display_main(stdscr))


if __name__ == "__main__":
    wrapper(main)

In order to test that the value changes are propagated to QEMU we can use watch to read the temperature value periodically with

watch i2cget -y 1 0x50

A short recording of how python script is used to modify the value of temperature property follows. The left screen is the QEMU instance with the watch command and on the right side is the python script showing current temperature and waiting for key press.

Summary

This post demonstrates use of QMP to change a value of property of a peripheral.

The approach can be used for other custom components to make them more interactive instead of relying on random data.

Subscribe
If you would like to get information as soon as new content is published, please subscribe to the "MistraSolutions newsletter".