Tablet Hacking RSS

UBoot MMC speedup for A83T, H3, H5, H6

UBoot's driver for MMC controller only uses PIO and doesn't try to use DDR mode when available.

I've implemented DMA access and DDR52 modes. I've also fixed the bus-width for mmc2 in TBS A711's dts in UBoot, which was still set to 1, instead of 4. This was already fixed in Linux.

The latest UBoot also contains a regression, where the MMC clock is set incorrectly, to half the required value, leading to halving the mmc speed on most of my boards.

This means that my fixes resulted in these nice speedups across all my boards:

I've also decompiled UBoot and SPL binaries in IDA Pro and searched for all calls to udelay/mdelay. This way I've found one 500ms delay that was not needed at all and was only included because of incorrect #ifdef check for a feature that was disabled in the config file.

A small demo / boot times measurement with a camera

All this combined leads to a very nice speedup of boot times from eMMC and SD card on TSB A711 tablet. :) Previously with 10MiB/s MMC speed and a needless 500ms delay, UBoot took almost an extra 1s during the whole boot time, which was significant.

On my setup kernel takes 1s to boot and mount rootfs and my userspace starts init and UI in about 600ms.

So all this optimization reduced the boot time perceptibly.

There's an extra 1s delay after power on that is probably comming from BROM doing some initialization and loading UBoot SPL from eMMC.

This delay is shorter when booting from SD card, so one more optimization may be to have a bootloader on SD card and then load the rest from eMMC.

DRM cursor plane

In order for the X server modesetting driver to NOT use a software cursor, which slows down rendering, and to use a DRM plane, I've created a patch to mark one of the planes as a cursor plane.

This revealed some issues with converting DRM plane setup into setup of DE2 mixer/blender HW registers plane changes in the current mixer driver.

More details and the patch are in the mailing list.

The two drm patches can be found in my repository:

With these two patches, lima/panfrost drivers work much better on H5 (Orange Pi PC 2) and H6 (Orange Pi 3), without any stuttering and slowdowns while moving a crusor. Even without GPU acceleration, moving a mouse now consumes less CPU (about 10–20% reduction) and scrolling in Firefox is faster (actually smooth).

Implmeneting mailbox/SCPI on Linux and crust probing

This is an attempt to implement „First steps“ from the previous post.

To speed up testing, we can build SCPI protocol driver as a kernel module and do unload/load cycle to trigger probing and thus communication with crust.

Afterwards, we can just load crust from userspace into SRAM, do the SCPI driver module reload and see if scpi_init_versions succeeds to send CMD_SCPI_CAPABILITIES to crust and gets a response.

Making Linux talk to crust via SCPI

First, we modify sun8i-a83t.dtsi to enable SCPI protocol driver and hook it up to mailbox driver and configure it with shared memory in SRAM A2. We'll use last 0x200 bytes of SRAM A2 for this purpose.

Linux changes are fairly simple. SRAM A2 doesn't require any configuration/mapping, so we don't need to modify any C code. We can just add this to A83T's dtsi:

/ {
        scpi_protocol: scpi {
                compatible = "arm,scpi";
                mboxes = <&msgbox 0>;
                shmem = <&cpu_scp>;
        };

        soc {
                syscon: system-control@1c00000 {
                        compatible = "allwinner,sun8i-h3-system-control";
                        reg = <0x01c00000 0x1000>;
                        #address-cells = <1>;
                        #size-cells = <1>;
                        ranges;

                        sram_a2: sram@40000 {
                                compatible = "mmio-sram";
                                reg = <0x00040000 0x14000>;
                                #address-cells = <1>;
                                #size-cells = <1>;
                                ranges = <0 0x00040000 0x14000>;

                                cpu_scp: scp-shmem@13c00 {
                                        compatible = "allwinner,sun8i-a83t-scp-shmem";
                                        reg = <0x13e00 0x200>;
                                };
                        };
                };

                msgbox: mailbox@1c17000 {
                        compatible = "allwinner,sun8i-a83t-msgbox",
                                     "allwinner,sun6i-a31-msgbox";
                        reg = <0x01c17000 0x1000>;
                        clocks = <&ccu CLK_BUS_MSGBOX>;
                        resets = <&ccu RST_BUS_MSGBOX>;
                        interrupts = <GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH>;
                        #mbox-cells = <1>;
                };
        };
};

Note: It turns out, that crust supports two SCPI clients (client 0, uses message box channels 0 and 1, client 1 uses channels 2 and 3). Client 0 is meant to be ATF and client 1 should be Linux. We'll link the Linux to the channel meant for ATF, because it has more privileges. That channel uses shmem from 0x13e00 to 0x14000 and message box channel 0 to transmit messages from the client side (that is Linux).

Drivers that need to be enabled are:

CONFIG_ARM_SCPI_PROTOCOL=m
CONFIG_MODULE_UNLOAD=y
CONFIG_MAILBOX=y
CONFIG_SUN6I_MSGBOX=y

We can blacklist arm_scpi in /etc/modprobe.d/scpi.conf:

blacklist arm_scpi

Now we can boot and start testing communication with crust by running:

modprobe arm_scpi
rmmod arm_scpi

Without crust running, this will fail with this message in dmesg:

scpi_protocol scpi: incorrect or no SCP firmware found
scpi_protocol: probe of scpi failed with error -62

SCPI uses different locations inside the channel's shmem for RX and TX. RX location is at offset 0, TX location is at offset size(shmem) / 2.

Therefore if we clear SRAM A2 before probing arm_scpi, we should see some payload at 0x00053f00. And we do:

0x00053eec : 00000000
0x00053ef0 : 00000000
0x00053ef4 : 00000000
0x00053ef8 : 00000000
0x00053efc : 00000000
0x00053f00 : 00000102
0x00053f04 : 00000000
0x00053f08 : 00000000
0x00053f0c : 00000000
0x00053f10 : 00000000
0x00053f14 : 00000000

This verifies that SCPI protocol driver writes to the expected location in SRAM. Value 2 is a code for CMD_SCPI_CAPABILITIES, which is a command that SCPI driver uses to probe the SCP initially.

Making crust listen to Linux over SCPI

To build crust we can use gcc 9.2 cross-compiler built for or1k target.

Crust doesn't support A83T at the moment, so we need to add some basic support for this SoC. We'll put our platform's files under platform/sun8i.

We will be loading crust to SRAM A2 from Linux's userspace. This way, we'll be able to avoid reboot cycles during testing. Crust has a tool at tools/load.c that can be used to load the firmware and enable the SCP. This tool expects:

Crust firmware binary doesn't include exception vectors. tools/load.c program calculates and writes exception vectors dynamically.

For A83T we can use the whole SRAM A2 except the first 0x4000 bytes (16KiB) that are reserved for exception vectors. This means we will have 0x10000 bytes (64KiB) available for crust. Our platform/sun8i/include/platform/memory.h will contain:

/*
 * Copyright © 2019 The Crust Firmware Authors.
 * SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-only
 */

#ifndef PLATFORM_MEMORY_H
#define PLATFORM_MEMORY_H

#define FIRMWARE_BASE  0x00004000
#define FIRMWARE_LIMIT SCPI_MEM_BASE
#define FIRMWARE_SIZE  (FIRMWARE_LIMIT - FIRMWARE_BASE)

#define SCPI_MEM_BASE  0x00013c00
#define SCPI_MEM_LIMIT SRAM_A2_LIMIT
#define SCPI_MEM_SIZE  (SCPI_MEM_LIMIT - SCPI_MEM_BASE)

#define SRAM_A2_BASE   0x00000000
#define SRAM_A2_LIMIT  0x00014000
#define SRAM_A2_SIZE   (SRAM_A2_LIMIT - SRAM_A2_BASE)

#define STACK_SIZE     0x00000400

#endif /* PLATFORM_MEMORY_H */

We'll copy a bunch of files from platform/sun50i/include/platform, and inspect them and change the contents to match the A83T SoC.

Next we can compile crust with:

export PATH="/opt/toolchains/or1k-linux-musl/bin:$PATH"
export CROSS_COMPILE=or1k-linux-musl-

rm -rf .build-tbs-a711
make V=1 OBJ=.build-tbs-a711 tbs_a711_defconfig
make V=1 OBJ=.build-tbs-a711

To get load program built for ARM, we need to cross-compile it for ARM. That's a slightly trickier, since by default this program gets built for the host machine.

# for this to work, we need to comment the #include <config.h> line in
# tools/load.c first
arm-linux-musleabihf-gcc -static \
        -DCONFIG_PLATFORM='"sun8i"' \
        -Iinclude/{common,lib} -Iplatform/sun8i/include \
        -o load tools/load.c

Now we have enough to try to load crust on the tablet. We can copy .build-tbs-a711/scp/scp.bin and load to the tablet and run:

# reset/stop SCP
./load --reset
# load firmware and start SCP
./load scp.bin
# load arm_scpi module
rmmod arm_scpi
modprobe arm_scpi
# see the result
dmesg | tail

But first, let's check what crust does and how.

Crust's internals

When crust starts, it will decide if reset vector or exception vector was used to jump to start function and passes this information to the main function.

Crust doesn't use exceptions very much (aside from watchdog reset), and all exceptions are handled efectively by re-entering the main function again. Only BSS section is cleared in start code, but all statically initialized global data will keep values from the previous run of the main function. This may lead to interesting issues, and needs to be kept in mind while writing crust code.

Crust's stack size is by default 1KiB. We can increase this if necessary, because A83T is not as limited as other platforms in the usable SRAM space.

Crust communicates with two SCPI clients, and handles one SCPI message at a time per client. There's no queueing of messages.

The two clients are:

SCPI_CLIENT_EL3 = 0, /**< Client 0: Secure EL3 (ATF). */
SCPI_CLIENT_EL2 = 1, /**< Client 1: Nonsec EL2 (Linux). */

SCPI message size is fixed to 0x100 bytes. (see struct scpi_msg)

Each client has it's own area for incomming and outgoing messages defined as struct scpi_mem starting from SCPI_MEM_BASE.

struct scpi_mem {
        struct scpi_msg tx_msg; /**< Server to client message. */
        struct scpi_msg rx_msg; /**< Client to server message. */
};

The order of the areas is reversed! Client 0 has shmem at SCPI_MEM_BASE + 0x200, client 1 at SCPI_MEM_BASE.

To signal that messages are prepared in the shmem, crust uses different message box channels for each client and message direction.

#define TX_CHAN(client)  (2 * (client) + 1)
#define RX_CHAN(client)  (2 * (client))

// this translates to:
//
//   RX_CHAN(client 0) = 0
//   TX_CHAN(client 0) = 1
//   RX_CHAN(client 1) = 2
//   TX_CHAN(client 1) = 3

A message box message (which is a 32-bit number) that is sent and also expected is SCPI_VIRTUAL_CHANNEL, which equals to 1. Other message box messages are rejected.

Crust's main()

Crude overview of what main() function does:

From this, we can identify what SoC blocks will need to be verified for initial compatibility with A83T.

It's:

With this checked, all that's missing is testing out the connection.

Testing and outstanding issues

Sending messages works, and crust responds, but there's an issue where arm_scpi driver in Linux expects bi-directional message boxes, but sun6i-msgbox implements unidirectional ones, so arm_scpi will not see the response, and the probe times out.

One solution may be to switch sun6i-msgbox to use 2 HW channels and provide a bi-directional interface arm_scpi expects.

The other is to patch the arm_scpi driver to accept separate message boxes for tx and rx paths and share a single shmem. I've experimentally patched the arm_scpi driver.

The result is:

scpi_protocol scpi: SCP Protocol 1.2 Firmware 0.1.9000 version

Crust itself also prints some messages over UART

INFO:         Watchdog enabled
INFO:    SCPI: Initialization complete

So now we have Linux communicating with the SCP over SCPI.

:)

Current state and next steps

With SCPI interface working, we can try porting MCPM code for A83T to crust and start experimenting with suspending the CPU cores. Actual porting will probably not be hard, since the code already exists in css-a64.c. It will probably only need a few small tweaks.

Big hurdle here is that we will not be able to use PSCI interface and standard ARM implementation of it, so we'll have to write our own driver that will replace what it does.

Next step is to investigate what roles PSCI typically plays druring system power management and how to do without it, and minimally replicate it using direct communication with SCP from the kernel.

After we do this, we should have a working mechanism for disabling all CPU cores and wakeup.

Getting mailbox support up on A83T and testing crust

The main goal is to use crust to get to the lowest suspend state, we can. That is to turn of all CPUs at least, and power down CPU regulators. If possible, we can also put DRAM into self-refresh and turn off DRAM controller. We can potentially play with shutting down other parts of the SoC, like PLLs, other voltage regulators, etc.

Code locations

My crust tree is available at https://xff.cz/git/crust-firmware/ (this is also a clone URL).

I'll use my Linux tree's orange-pi-5.3 branch for testing, because it already includes everyhting necessary to comfortably run the TBS tablet.

My u-boot tree is available at https://megous.com/git/u-boot/ (opi-v2019.07 branch).

Prior art and docummentation

Samuel Holland's crust and Linux/U-Boot patches, and documentation:

My firmware and reverse engineering work for the ARISC firmware on H3/A83T:

Other useful documentation:

Glossary

Current state

For platforms that use ATF:

Currently, suspend works on supproted platforms (A64, H5, …) this way.

For A83T, we don't use either ATF, nor U-Boot to implement PSCI, so Linux kernel configures CPUs directly in kernel space via arch/arm/mach-sunxi/mc_smp.c.

Overview of how crust integration should work in the end

We can't use ATF on A83T, so Linux will need to talk directly to SCP using SCPI. To that end, Linux kernel already has a SCPI driver, that when enabled and configured to use A83T message box and shared memory in SRAM A2, will provide struct scpi_ops via get_scpi_ops() function to anything in the kernel.

By itself, this will not do anything, except provide a way to communicate with crust over the standard SCPI interface from any driver inside the kernel. Linux kernel will not automatically use SCPI interface to manage CPU hotplug/suspend.

We have to write code that will provide struct platform_suspend_ops for A83T, and that will actually notify SCP when the kernel wants the system to enter suspended state. For already supported SoCs (A64, H5), this is implemented by generic PSCI driver.

Commands required for CPU/system power management are not currently provided by struct scpi_ops. We will need to implement hooks for these commands:

SCPI_CMD_SET_CSS_PWR_STATE      = 0x03,
SCPI_CMD_GET_CSS_PWR_STATE      = 0x04,
SCPI_CMD_SET_SYS_PWR_STATE      = 0x05,

These are not provided yet, because no code in the kernel uses SCPI for CSS/SYS power state management. Everything is done either via PSCI, or via custom platform_suspend_ops defined by each platform. We will be the first to try that.

It's unclear if we should implement PM as A83T specific platform_suspend_ops or write a PSCI-like generic driver that would define platform_suspend_ops for all SoCs that include arm,scpi-pm compatible node in DT, for example. It's probably safer and easier to start with A83T specific driver.

An example how to use SCPI commands to suspend the system is provided in ATF firmware patch from Samuel.

SCP would normally also implement CPU/cluster power state changes that are used for CPU hotplug, but that is currently handled directly from kernel space. Clean solution would be to replace current MCPM A83T code in Linux with SCPI calls. Though this may not be necessary for initial experiments with suspend to RAM functionality, especially if CPU hotplug will not be used during suspend. Precise interactions between CPU hotplug code in kernel and SCP are not yet clear.

Boot sequence

To get crust running and communicating with the kernel, this needs to happen:

From now on, we can use get_scpi_ops() to communicate with crust from anywher inside the kernel.

First steps

Our first milestone is to have the above boot sequence working. This will require us to:

We'll achieve this milestone if scpi driver probes successfully.

Next steps

Touch control based tablet boot menu for u-boot

A711 tablet has a nice feature that it can boot either from internal eMMC or a microSD card, if the inserted card is found to be bootable.

When developing and testing either kernel or various root filesystem variants it is useful to be able to have multiple boot configurations and to be able to select one of those easily. That means not via UART based serial console and u-boot CLI. That's not the best UX for a tablet.

Ideally, I'd like to be able to have a bootable microSD card with u-boot configured such that:

Having a boot UI makes all this much less cubersome as it gets away with a need to switch sd cards, or with a need for an UART cable or other wired mechanism to control u-boot.

U-boot touch based UI

U-boot has support for framebuffer based video output on a LCD.

What's missing is a support for touch panel based input. That specifically means u-boot does not have any commands for getting list of touches from a touch panel controller, that could be used in a boot script to implement some kind of crude UI.

So in order to get a boot menu in u-boot, we need:

Touchpanel uclass

U-boot doesn't have interrupts enabled, so the interface to a touch panel can be as simple as a single function that returns a list of active touches and their X/Y positions registered by the touch panel controller.

#ifndef __TOUCHPANEL_H
#define __TOUCHPANEL_H

/**
 * struct touchpanel_priv - information about a touchpanel, for the uclass
 *
 * @sdev:       stdio device
 */
struct touchpanel_priv {
        int size_x;
        int size_y;
};

struct touchpanel_touch {
        int id;
        int x;
        int y;
};

/**
 * struct touchpanel_ops - touchpanel device operations
 */
struct touchpanel_ops {
        /**
         * start() - enable the touchpanel to be ready for use
         *
         * @dev:        Device to enable
         * @return 0 if OK, -ve on error
         */
        int (*start)(struct udevice *dev);

        /**
         * stop() - disable the touchpanel when no-longer needed
         *
         * @dev:        Device to disable
         * @return 0 if OK, -ve on error
         */
        int (*stop)(struct udevice *dev);

        /**
         * get_touches() - get list of active touches
         *
         * @dev:        Device to read from
         * @touches:    Array where to store touches. If NULL, the driver will
         *              only return number of touches available.
         * @max_touches:        Size of an touches array
         * @return -EAGAIN if no touch is available, otherwise number of touches
         * available.
         */
        int (*get_touches)(struct udevice *dev,
                           struct touchpanel_touch* touches, int max_touches);
};

#define touchpanel_get_ops(dev) ((struct touchpanel_ops *)(dev)->driver->ops)

int touchpanel_start(struct udevice *dev);
int touchpanel_stop(struct udevice *dev);
int touchpanel_get_touches(struct udevice *dev,
                           struct touchpanel_touch* touches, int max_touches);

#endif /* __TOUCHPANEL_H */

Implmentation of the uclass is therefore quite simple too:

// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (c) 2018 Ondrej Jirman <megous@megous.com>
 */

#include <common.h>
#include <errno.h>
#include <dm.h>
#include <dm/uclass-internal.h>
#include <touchpanel.h>

int touchpanel_start(struct udevice *dev)
{
        const struct touchpanel_ops *ops = touchpanel_get_ops(dev);

        if (!ops || !ops->start)
                return -ENOSYS;

        return ops->start(dev);
}

int touchpanel_stop(struct udevice *dev)
{
        const struct touchpanel_ops *ops = touchpanel_get_ops(dev);

        if (!ops || !ops->stop)
                return -ENOSYS;

        return ops->stop(dev);
}

int touchpanel_get_touches(struct udevice *dev,
                           struct touchpanel_touch* touches, int max_touches)
{
        const struct touchpanel_ops *ops = touchpanel_get_ops(dev);

        if (!ops || !ops->get_touches)
                return -ENOSYS;

        return ops->get_touches(dev, touches, max_touches);
}

static int touchpanel_pre_probe(struct udevice *dev)
{
        struct touchpanel_priv *uc_priv;

        uc_priv = dev_get_uclass_priv(dev);
        if (!uc_priv)
                return -ENXIO;

        uc_priv->size_x = dev_read_u32_default(dev, "touchscreen-size-x",
                                                -ENODATA);
        uc_priv->size_y = dev_read_u32_default(dev, "touchscreen-size-y",
                                                -ENODATA);

        if (uc_priv->size_x == -ENODATA || uc_priv->size_y == -ENODATA)
                uc_priv->size_x = uc_priv->size_y = -ENODATA;

        return 0;
}

UCLASS_DRIVER(touchpanel) = {
        .id             = UCLASS_TOUCHPANEL,
        .name           = "touchpanel",
        .pre_probe      = touchpanel_pre_probe,
        .per_device_auto_alloc_size = sizeof(struct touchpanel_priv),
};

Touchpanel driver for FT5×06

There's no need to reinvent the wheel. We can simply take the Linux driver and adapt it to u-boot. This means mostly:

// SPDX-License-Identifier: GPL-2.0
/*
 * (C) Copyright 2018 Ondrej Jirman <megous@megous.com>
 *
 * Based on the Linux driver drivers/input/touchscreen/edt-ft5x06.c (v4.18):
 *
 * Copyright (C) 2012 Simon Budig, <simon.budig@kernelconcepts.de>
 * Daniel Wagener <daniel.wagener@kernelconcepts.de> (M09 firmware support)
 * Lothar Waßmann <LW@KARO-electronics.de> (DT support)
 */

#include <common.h>
#include <dm.h>
#include <errno.h>
#include <input.h>
#include <asm/io.h>
#include <asm/gpio.h>
#include <command.h>
#include <i2c.h>
#include <touchpanel.h>
#include <power/regulator.h>

DECLARE_GLOBAL_DATA_PTR;

#define WORK_REGISTER_THRESHOLD         0x00
#define WORK_REGISTER_REPORT_RATE       0x08
#define WORK_REGISTER_GAIN              0x30
#define WORK_REGISTER_OFFSET            0x31
#define WORK_REGISTER_NUM_X             0x33
#define WORK_REGISTER_NUM_Y             0x34

#define M09_REGISTER_THRESHOLD          0x80
#define M09_REGISTER_GAIN               0x92
#define M09_REGISTER_OFFSET             0x93
#define M09_REGISTER_NUM_X              0x94
#define M09_REGISTER_NUM_Y              0x95

#define NO_REGISTER                     0xff

#define WORK_REGISTER_OPMODE            0x3c
#define FACTORY_REGISTER_OPMODE         0x01

#define TOUCH_EVENT_DOWN                0x00
#define TOUCH_EVENT_UP                  0x01
#define TOUCH_EVENT_ON                  0x02
#define TOUCH_EVENT_RESERVED            0x03

#define EDT_NAME_LEN                    23
#define EDT_SWITCH_MODE_RETRIES         10
#define EDT_SWITCH_MODE_DELAY           5 /* msec */
#define EDT_RAW_DATA_RETRIES            100
#define EDT_RAW_DATA_DELAY              1000 /* usec */

enum edt_ver {
        EDT_M06,
        EDT_M09,
        EDT_M12,
        GENERIC_FT,
};

struct edt_reg_addr {
        int reg_threshold;
        int reg_report_rate;
        int reg_gain;
        int reg_offset;
        int reg_num_x;
        int reg_num_y;
};

struct ft5x06_priv {
        struct udevice *reg;
        struct gpio_desc reset_gpio;

        u16 num_x;
        u16 num_y;

        int threshold;
        int gain;
        int offset;
        int report_rate;
        int max_support_points;

        char name[EDT_NAME_LEN];

        struct edt_reg_addr reg_addr;
        enum edt_ver version;
};

static int ft5x06_readwrite(struct udevice *dev,
                            u16 wr_len, void *wr_buf,
                            u16 rd_len, void *rd_buf)
{
        struct dm_i2c_chip *chip = dev_get_parent_platdata(dev);
        struct i2c_msg wrmsg[2];
        int ret, i = 0;

        if (wr_len) {
                wrmsg[i].addr  = chip->chip_addr;
                wrmsg[i].flags = 0;
                wrmsg[i].len = wr_len;
                wrmsg[i].buf = wr_buf;
                i++;
        }

        if (rd_len) {
                wrmsg[i].addr  = chip->chip_addr;
                wrmsg[i].flags = I2C_M_RD;
                wrmsg[i].len = rd_len;
                wrmsg[i].buf = rd_buf;
                i++;
        }

        ret = dm_i2c_xfer(dev, wrmsg, i);
        if (ret < 0)
                return ret;

        return 0;
}

static int ft5x06_register_write(struct udevice *dev, u8 addr, u8 value)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        u8 wrbuf[4];

        switch (priv->version) {
        case EDT_M06:
                wrbuf[0] = 0xfc;
                wrbuf[1] = addr & 0x3f;
                wrbuf[2] = value;
                wrbuf[3] = wrbuf[0] ^ wrbuf[1] ^ wrbuf[2];
                return ft5x06_readwrite(dev, 4, wrbuf, 0, NULL);
        case EDT_M09:
        case EDT_M12:
        case GENERIC_FT:
                wrbuf[0] = addr;
                wrbuf[1] = value;

                return ft5x06_readwrite(dev, 2, wrbuf, 0, NULL);

        default:
                return -EINVAL;
        }
}

static int ft5x06_register_read(struct udevice *dev, u8 addr)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        u8 wrbuf[2], rdbuf[2];
        int error;

        switch (priv->version) {
        case EDT_M06:
                wrbuf[0] = 0xfc;
                wrbuf[1] = addr & 0x3f;
                wrbuf[1] |= 0x40;

                error = ft5x06_readwrite(dev, 2, wrbuf, 2, rdbuf);
                if (error)
                        return error;

                if ((wrbuf[0] ^ wrbuf[1] ^ rdbuf[0]) != rdbuf[1]) {
                        dev_err(dev,
                                "crc error: 0x%02x expected, got 0x%02x\n",
                                wrbuf[0] ^ wrbuf[1] ^ rdbuf[0],
                                rdbuf[1]);
                        return -EIO;
                }
                break;

        case EDT_M09:
        case EDT_M12:
        case GENERIC_FT:
                wrbuf[0] = addr;
                error = ft5x06_readwrite(dev, 1, wrbuf, 1, rdbuf);
                if (error)
                        return error;
                break;

        default:
                return -EINVAL;
        }

        return rdbuf[0];
}

static int ft5x06_identify(struct udevice *dev, char *fw_version)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        u8 rdbuf[EDT_NAME_LEN];
        char *p;
        int error;
        char *model_name = priv->name;

        /* see what we find if we assume it is a M06 *
         * if we get less than EDT_NAME_LEN, we don't want
         * to have garbage in there
         */
        memset(rdbuf, 0, sizeof(rdbuf));
        error = ft5x06_readwrite(dev, 1, "\xBB", EDT_NAME_LEN - 1, rdbuf);
        if (error)
                return error;

        /* Probe content for something consistent.
         * M06 starts with a response byte, M12 gives the data directly.
         * M09/Generic does not provide model number information.
         */
        if (!strncasecmp(rdbuf + 1, "EP0", 3)) {
                priv->version = EDT_M06;

                /* remove last '$' end marker */
                rdbuf[EDT_NAME_LEN - 1] = '\0';
                if (rdbuf[EDT_NAME_LEN - 2] == '$')
                        rdbuf[EDT_NAME_LEN - 2] = '\0';

                /* look for Model/Version separator */
                p = strchr(rdbuf, '*');
                if (p)
                        *p++ = '\0';
                strlcpy(model_name, rdbuf + 1, EDT_NAME_LEN);
                strlcpy(fw_version, p ? p : "", EDT_NAME_LEN);
        } else if (!strncasecmp(rdbuf, "EP0", 3)) {
                priv->version = EDT_M12;

                /* remove last '$' end marker */
                rdbuf[EDT_NAME_LEN - 2] = '\0';
                if (rdbuf[EDT_NAME_LEN - 3] == '$')
                        rdbuf[EDT_NAME_LEN - 3] = '\0';

                /* look for Model/Version separator */
                p = strchr(rdbuf, '*');
                if (p)
                        *p++ = '\0';
                strlcpy(model_name, rdbuf, EDT_NAME_LEN);
                strlcpy(fw_version, p ? p : "", EDT_NAME_LEN);
        } else {
                /* If it is not an EDT M06/M12 touchscreen, then the model
                 * detection is a bit hairy. The different ft5x06
                 * firmares around don't reliably implement the
                 * identification registers. Well, we'll take a shot.
                 *
                 * The main difference between generic focaltec based
                 * touches and EDT M09 is that we know how to retrieve
                 * the max coordinates for the latter.
                 */
                priv->version = GENERIC_FT;

                error = ft5x06_readwrite(dev, 1, "\xA6", 2, rdbuf);
                if (error)
                        return error;

                strlcpy(fw_version, rdbuf, 2);

                error = ft5x06_readwrite(dev, 1, "\xA8", 1, rdbuf);
                if (error)
                        return error;

                /* This "model identification" is not exact. Unfortunately
                 * not all firmwares for the ft5x06 put useful values in
                 * the identification registers.
                 */
                switch (rdbuf[0]) {
                case 0x35:   /* EDT EP0350M09 */
                case 0x43:   /* EDT EP0430M09 */
                case 0x50:   /* EDT EP0500M09 */
                case 0x57:   /* EDT EP0570M09 */
                case 0x70:   /* EDT EP0700M09 */
                        priv->version = EDT_M09;
                        snprintf(model_name, EDT_NAME_LEN, "EP0%i%i0M09",
                                rdbuf[0] >> 4, rdbuf[0] & 0x0F);
                        break;
                case 0xa1:   /* EDT EP1010ML00 */
                        priv->version = EDT_M09;
                        snprintf(model_name, EDT_NAME_LEN, "EP%i%i0ML00",
                                rdbuf[0] >> 4, rdbuf[0] & 0x0F);
                        break;
                case 0x5a:   /* Solomon Goldentek Display */
                        snprintf(model_name, EDT_NAME_LEN, "GKTW50SCED1R0");
                        break;
                default:
                        snprintf(model_name, EDT_NAME_LEN,
                                 "generic ft5x06 (%02x)",
                                 rdbuf[0]);
                        break;
                }
        }

        return 0;
}

static void ft5x06_get_defaults(struct udevice *dev)
{
//XXX: not supported yet, should be read from DT
#if 0
        struct ft5x06_priv *priv = dev_get_priv(dev);
        struct edt_reg_addr *reg_addr = &priv->reg_addr;
        u32 val;
        int error;

        error = device_property_read_u32(dev, "threshold", &val);
        if (!error) {
                ft5x06_register_write(dev, reg_addr->reg_threshold, val);
                priv->threshold = val;
        }

        error = device_property_read_u32(dev, "gain", &val);
        if (!error) {
                ft5x06_register_write(dev, reg_addr->reg_gain, val);
                priv->gain = val;
        }

        error = device_property_read_u32(dev, "offset", &val);
        if (!error) {
                ft5x06_register_write(dev, reg_addr->reg_offset, val);
                priv->offset = val;
        }
#endif
}

static void ft5x06_get_parameters(struct udevice *dev)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        struct edt_reg_addr *reg_addr = &priv->reg_addr;

        priv->threshold = ft5x06_register_read(dev, reg_addr->reg_threshold);
        priv->gain = ft5x06_register_read(dev, reg_addr->reg_gain);
        priv->offset = ft5x06_register_read(dev, reg_addr->reg_offset);
        if (reg_addr->reg_report_rate != NO_REGISTER)
                priv->report_rate = ft5x06_register_read(dev,
                                                reg_addr->reg_report_rate);
        if (priv->version == EDT_M06 ||
            priv->version == EDT_M09 ||
            priv->version == EDT_M12) {
                priv->num_x = ft5x06_register_read(dev, reg_addr->reg_num_x);
                priv->num_y = ft5x06_register_read(dev, reg_addr->reg_num_y);
        } else {
                priv->num_x = -1;
                priv->num_y = -1;
        }
}

static void ft5x06_set_regs(struct udevice *dev)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        struct edt_reg_addr *reg_addr = &priv->reg_addr;

        switch (priv->version) {
        case EDT_M06:
                reg_addr->reg_threshold = WORK_REGISTER_THRESHOLD;
                reg_addr->reg_report_rate = WORK_REGISTER_REPORT_RATE;
                reg_addr->reg_gain = WORK_REGISTER_GAIN;
                reg_addr->reg_offset = WORK_REGISTER_OFFSET;
                reg_addr->reg_num_x = WORK_REGISTER_NUM_X;
                reg_addr->reg_num_y = WORK_REGISTER_NUM_Y;
                break;

        case EDT_M09:
        case EDT_M12:
                reg_addr->reg_threshold = M09_REGISTER_THRESHOLD;
                reg_addr->reg_report_rate = NO_REGISTER;
                reg_addr->reg_gain = M09_REGISTER_GAIN;
                reg_addr->reg_offset = M09_REGISTER_OFFSET;
                reg_addr->reg_num_x = M09_REGISTER_NUM_X;
                reg_addr->reg_num_y = M09_REGISTER_NUM_Y;
                break;

        case GENERIC_FT:
                /* this is a guesswork */
                reg_addr->reg_threshold = M09_REGISTER_THRESHOLD;
                reg_addr->reg_gain = M09_REGISTER_GAIN;
                reg_addr->reg_offset = M09_REGISTER_OFFSET;
                break;
        }
}

static bool ft5x06_check_crc(struct udevice *dev, u8 *buf, int buflen)
{
        int i;
        u8 crc = 0;

        for (i = 0; i < buflen - 1; i++)
                crc ^= buf[i];

        if (crc != buf[buflen-1]) {
                dev_err(dev, "crc error: 0x%02x expected, got 0x%02x\n",
                        crc, buf[buflen-1]);
                return false;
        }

        return true;
}

static int ft5x06_get_touches(struct udevice* dev,
                              struct touchpanel_touch* touches, int max_touches)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        u8 cmd;
        u8 rdbuf[63];
        int i, type, x, y, id;
        int offset, tplen, datalen, crclen;
        int error;
        int touches_count = 0;

        switch (priv->version) {
        case EDT_M06:
                cmd = 0xf9; /* tell the controller to send touch data */
                offset = 5; /* where the actual touch data starts */
                tplen = 4;  /* data comes in so called frames */
                crclen = 1; /* length of the crc data */
                break;

        case EDT_M09:
        case EDT_M12:
        case GENERIC_FT:
                cmd = 0x0;
                offset = 3;
                tplen = 6;
                crclen = 0;
                break;

        default:
                goto out;
        }

        memset(rdbuf, 0, sizeof(rdbuf));
        datalen = tplen * priv->max_support_points + offset + crclen;

        error = ft5x06_readwrite(dev, sizeof(cmd), &cmd, datalen, rdbuf);
        if (error) {
                dev_err(dev, "Unable to fetch data, error: %d\n", error);
                goto out;
        }

        /* M09/M12 does not send header or CRC */
        if (priv->version == EDT_M06) {
                if (rdbuf[0] != 0xaa || rdbuf[1] != 0xaa ||
                        rdbuf[2] != datalen) {
                        dev_err(dev, "Unexpected header: %02x%02x%02x!\n",
                                rdbuf[0], rdbuf[1], rdbuf[2]);
                        goto out;
                }

                if (!ft5x06_check_crc(dev, rdbuf, datalen))
                        goto out;
        }

        for (i = 0; i < priv->max_support_points; i++) {
                u8 *buf = &rdbuf[i * tplen + offset];
                bool down;

                type = buf[0] >> 6;
                /* ignore Reserved events */
                if (type == TOUCH_EVENT_RESERVED)
                        continue;

                /* M06 sometimes sends bogus coordinates in TOUCH_DOWN */
                if (priv->version == EDT_M06 && type == TOUCH_EVENT_DOWN)
                        continue;

                x = ((buf[0] << 8) | buf[1]) & 0x0fff;
                y = ((buf[2] << 8) | buf[3]) & 0x0fff;
                id = (buf[2] >> 4) & 0x0f;
                down = type != TOUCH_EVENT_UP;

                if (!down)
                        continue;

                if (max_touches > touches_count) {
                        touches[touches_count].x = x;
                        touches[touches_count].y = y;
                        touches[touches_count].id = id;
                        touches_count++;
                }
        }

out:
        return touches_count;
}

static int ft5x06_start(struct udevice *dev)
{
        debug("%s: started\n", __func__);
        return 0;
}

static int ft5x06_stop(struct udevice *dev)
{
        debug("%s: stopped\n", __func__);
        return 0;
}

/**
 * Set up the touch panel.
 *
 * @return 0 if ok, -ERRNO on error
 */
static int ft5x06_probe(struct udevice *dev)
{
        struct touchpanel_priv *uc_priv = dev_get_uclass_priv(dev);
        struct ft5x06_priv *priv = dev_get_priv(dev);
        int ret;

        priv->max_support_points = 5;

        if (priv->reg && CONFIG_IS_ENABLED(DM_REGULATOR)) {
                ret = regulator_set_enable(priv->reg, true);
                if (ret) {
                        debug("%s: Cannot enable regulator for touchpanel '%s'\n",
                              __func__, dev->name);
                        return ret;
                }

                udelay(20 * 1000);
        }

        if (dm_gpio_is_valid(&priv->reset_gpio)) {
                ret = dm_gpio_set_value(&priv->reset_gpio, 0);
                if (ret)
                        return ret;
        }
        udelay(300 * 1000);

        char fw_version[EDT_NAME_LEN];
        ret = ft5x06_identify(dev, fw_version);
        if (ret) {
                dev_err(dev, "touchscreen probe failed %d\n", ret);
                return ret;
        }

        ft5x06_set_regs(dev);
        ft5x06_get_defaults(dev);
        ft5x06_get_parameters(dev);

        debug("Model \"%s\", Rev. \"%s\", %dx%d sensors\n",
                priv->name, fw_version, priv->num_x, priv->num_y);

        if (priv->version == EDT_M06 ||
            priv->version == EDT_M09 ||
            priv->version == EDT_M12) {
                uc_priv->size_x = priv->num_x * 64;
                uc_priv->size_y = priv->num_y * 64;
        } else {
                //XXX: perhaps check that the user set the values in DT
        }

        debug("%s: ready\n", __func__);
        return 0;
}

static int ft5x06_ofdata_to_platdata(struct udevice *dev)
{
        struct ft5x06_priv *priv = dev_get_priv(dev);
        int ret;

        debug("%s: start\n", __func__);

        ret = uclass_get_device_by_phandle(UCLASS_REGULATOR, dev,
                                           "power-supply", &priv->reg);
        if (ret) {
                debug("%s: Cannot get power supply: ret=%d\n", __func__, ret);
                if (ret != -ENOENT)
                        return ret;
        }

        ret = gpio_request_by_name(dev, "reset-gpios", 0, &priv->reset_gpio,
                                   GPIOD_IS_OUT);
        if (ret) {
                debug("%s: Warning: cannot get enable GPIO: ret=%d\n",
                      __func__, ret);
                if (ret != -ENOENT)
                        return ret;
        }

        debug("%s: done\n", __func__);
        return 0;
}

static const struct touchpanel_ops ft5x06_ops = {
        .start = ft5x06_start,
        .stop = ft5x06_stop,
        .get_touches = ft5x06_get_touches,
};

static const struct udevice_id ft5x06_ids[] = {
        { .compatible = "edt,edt-ft5x06" },
        { }
};

U_BOOT_DRIVER(ft5x06) = {
        .name = "touchpanel-ft5x06",
        .id = UCLASS_TOUCHPANEL,
        .of_match = ft5x06_ids,
        .probe = ft5x06_probe,
        .ops = &ft5x06_ops,
        .ofdata_to_platdata = ft5x06_ofdata_to_platdata,
        .priv_auto_alloc_size = sizeof(struct ft5x06_priv),
};

Touch utility command

U-boot has a CLI and scripting interface that can run commands. These commands are C functions that can be registered with u-boot and utilize u-boot's driver model (via uclass interfaces) to perform their, well… function.

So here we create a command that can probe the touch panel device and query the basic information about it (touchpanel size and the list of touches).

// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2018 BayLibre, SAS
 * Author: Neil Armstrong <narmstrong@baylibre.com>
 */
#include <common.h>
#include <command.h>
#include <dm.h>
#include <touchpanel.h>

static int do_touch_list(cmd_tbl_t *cmdtp, int flag, int argc,
                       char *const argv[])
{
        struct udevice *dev;
        int ret;

        ret = uclass_first_device_err(UCLASS_TOUCHPANEL, &dev);
        if (ret) {
                printf("No available touchpanel device\n");
                return CMD_RET_FAILURE;
        }

        do {
                printf("- %s\n", dev->name);

                ret = uclass_next_device(&dev);
                if (ret)
                        return CMD_RET_FAILURE;
        } while (dev);

        return CMD_RET_SUCCESS;
}

static int do_touch_info(cmd_tbl_t *cmdtp, int flag, int argc,
                       char *const argv[])
{
        struct touchpanel_priv *uc_priv;
        struct udevice *dev;
        int ret;

        if (argc < 2)
                return CMD_RET_USAGE;

        ret = uclass_get_device_by_name(UCLASS_TOUCHPANEL, argv[1], &dev);
        if (ret) {
                printf("Unknown touchpanel device %s\n", argv[1]);
                return CMD_RET_FAILURE;
        }

        uc_priv = dev_get_uclass_priv(dev);

        printf("Touchpanel Device '%s' :\n", argv[1]);
        printf("size_x: %d\n", uc_priv->size_x);
        printf("size_y: %d\n", uc_priv->size_y);

        return CMD_RET_SUCCESS;
}

static int do_touch_get(cmd_tbl_t *cmdtp, int flag, int argc,
                        char *const argv[])
{
        //struct touchpanel_priv *uc_priv;
        struct touchpanel_touch touches[10];
        struct udevice *dev;
        int ret, i;

        if (argc < 2)
                return CMD_RET_USAGE;

        ret = uclass_get_device_by_name(UCLASS_TOUCHPANEL, argv[1], &dev);
        if (ret) {
                printf("Unknown touchpanel device %s\n", argv[1]);
                return CMD_RET_FAILURE;
        }

        //uc_priv = dev_get_uclass_priv(dev);

        printf("Touchpanel Device '%s' :\n", argv[1]);

        ret = touchpanel_start(dev);
        if (ret < 0) {
                printf("Failed to start %s, err=%d\n", argv[1], ret);
                return CMD_RET_FAILURE;
        }

        ret = touchpanel_get_touches(dev, touches, ARRAY_SIZE(touches));
        if (ret < 0) {
                printf("Failed to get touches from %s, err=%d\n", argv[1], ret);
                return CMD_RET_FAILURE;
        }

        for (i = 0; i < ret; i++) {
                printf("touch: id=%d x=%d y=%d\n", touches[i].id, touches[i].x,
                       touches[i].y);
        }

        ret = touchpanel_stop(dev);
        if (ret < 0) {
                printf("Failed to stop %s, err=%d\n", argv[1], ret);
                return CMD_RET_FAILURE;
        }

        return CMD_RET_SUCCESS;
}

static cmd_tbl_t cmd_touch_sub[] = {
        U_BOOT_CMD_MKENT(list, 1, 1, do_touch_list, "", ""),
        U_BOOT_CMD_MKENT(info, 2, 1, do_touch_info, "", ""),
        U_BOOT_CMD_MKENT(get, 2, 1, do_touch_get, "", ""),
};

static int do_touch(cmd_tbl_t *cmdtp, int flag, int argc,
                  char *const argv[])
{
        cmd_tbl_t *c;

        if (argc < 2)
                return CMD_RET_USAGE;

        /* Strip off leading 'touch' command argument */
        argc--;
        argv++;

        c = find_cmd_tbl(argv[0], &cmd_touch_sub[0], ARRAY_SIZE(cmd_touch_sub));

        if (c)
                return c->cmd(cmdtp, flag, argc, argv);
        else
                return CMD_RET_USAGE;
}

static char touch_help_text[] =
        "list - list touchpanel devices\n"
        "touch info <name> - Get touchpanel device info\n"
        "touch get <name> - Get touches";

U_BOOT_CMD(touch, 4, 1, do_touch, "Touchpanel sub-system", touch_help_text);

This code also doubles as a compact example of the use of touchpanel uclass itnerface.

Boot menu UI drawing and touch handling

The last step is to create a command that can draw the boot menu UI over the framebuffer and waits for the touch over the active areas (buttons).

To be somewhat configurable without the need to recompile u-boot, the command takes as parameters the list of boot menu options. On exit it returns the selected option as a exit status code.

// SPDX-License-Identifier: GPL-2.0+
/*
 * Copyright (C) 2018 Ondrej Jirman <megous@megous.com>
 */
#include <common.h>
#include <command.h>
#include <cli_hush.h>
#include <video.h>
#include <video_font.h>
#include <dm/uclass.h>
#include <dm/device.h>
#include <touchpanel.h>

// first some generic drawing primitives

struct painter {
        u8* fb;
        u8* fb_end;
        u8* cur;
        u32 line_length;
        u32 bpp;
        u32 rows;
        u32 cols;
};

static void painter_set_xy(struct painter* p, uint x, uint y)
{
        p->cur = p->fb + min(y, p->rows - 1) * p->line_length + min(x, p->cols - 1) * p->bpp;
}

static void painter_move_dxy(struct painter* p, int dx, int dy)
{
        p->cur += dy * p->line_length + dx * p->bpp;

        if (p->cur >= p->fb_end)
                p->cur = p->fb_end - 1;

        if (p->cur < p->fb)
                p->cur = p->fb;
}

static void painter_rect_fill(struct painter* p, uint w, uint h, u32 color)
{
        int x, y;
        u32* cur;

        for (y = 0; y < h; y++) {
                cur = (u32*)(p->cur + p->line_length * y);

                for (x = 0; x < w; x++)
                        *(cur++) = color;
        }
}

static void painter_line_h(struct painter* p, int dx, u32 color)
{
        if (dx < 0) {
                painter_move_dxy(p, 0, dx);
                painter_rect_fill(p, 1, -dx, color);
        } else {
                painter_rect_fill(p, 1, dx, color);
                painter_move_dxy(p, 0, dx);
        }
}

static void painter_line_v(struct painter* p, int dy, u32 color)
{
        if (dy < 0) {
                painter_move_dxy(p, 0, dy);
                painter_rect_fill(p, 1, -dy, color);
        } else {
                painter_rect_fill(p, 1, dy, color);
                painter_move_dxy(p, 0, dy);
        }
}

static void painter_bigchar(struct painter* p, char ch, u32 color)
{
        int i, row;
        void *line = p->cur;

        for (row = 0; row < VIDEO_FONT_HEIGHT * 2; row++) {
                uchar bits = video_fontdata[ch * VIDEO_FONT_HEIGHT + row / 2];
                uint32_t *dst = line;

                for (i = 0; i < VIDEO_FONT_WIDTH; i++) {
                        if (bits & 0x80) {
                                *dst = color;
                                *(dst+1) = color;
                        }

                        bits <<= 1;
                        dst+=2;
                }

                line += p->line_length;
        }

        painter_move_dxy(p, VIDEO_FONT_WIDTH * 2, 0);
}

static void painter_char(struct painter* p, char ch, u32 color)
{
        int i, row;
        void *line = p->cur;

        for (row = 0; row < VIDEO_FONT_HEIGHT; row++) {
                uchar bits = video_fontdata[ch * VIDEO_FONT_HEIGHT + row];
                uint32_t *dst = line;

                for (i = 0; i < VIDEO_FONT_WIDTH; i++) {
                        if (bits & 0x80)
                                *dst = color;

                        bits <<= 1;
                        dst++;
                }

                line += p->line_length;
        }

        painter_move_dxy(p, VIDEO_FONT_WIDTH, 0);
}

// menu command

static int handle_tmenu(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[], int no_touch)
{
        struct udevice *vdev, *tdev;
        struct video_priv *vpriv;
        struct touchpanel_touch touches[10];
        int ret, i, row, j;

        if (argc < 2) {
                return CMD_RET_USAGE;
        }

        // set some params: (parse from argv in the future)
        char* const* items = argv + 1;
        int items_count = argc - 1;
        int w = 350, h = 120, x = 30, y = (600 - (h + 10) * items_count - 10) / 2;

        ret = uclass_first_device_err(UCLASS_VIDEO, &vdev);
        if (ret)
                return CMD_RET_FAILURE;

        if (!no_touch) {
                ret = uclass_first_device_err(UCLASS_TOUCHPANEL, &tdev);
                if (ret)
                        return CMD_RET_FAILURE;
        }

        vpriv = dev_get_uclass_priv(vdev);

        if (vpriv->bpix != VIDEO_BPP32) {
                printf("tmenu requires 32BPP video device\n");
                return CMD_RET_FAILURE;
        }

        struct painter p = {
                .fb = vpriv->fb,
                .fb_end = vpriv->fb + vpriv->fb_size,
                .cur = vpriv->fb,
                .line_length = vpriv->line_length,
                .bpp = VNBYTES(vpriv->bpix),
                .cols = vpriv->xsize,
                .rows = vpriv->ysize,
        };

        int selected = -1;
        int redraw = 1;

        if (!no_touch) {
                ret = touchpanel_start(tdev);
                if (ret < 0) {
                        printf("Failed to start %s, err=%d\n", tdev->name, ret);
                        return CMD_RET_FAILURE;
                }
        }

next:
        while (1) {
                if (redraw) {
                        // redraw output
                        row = y;
                        for (i = 0; i < items_count; i++) {
                                painter_set_xy(&p, x, row);
                                painter_rect_fill(&p, w, h, i == selected ? 0xff555500 : 0xff005500);
                                painter_set_xy(&p,
                                               x + (w - strlen(items[i]) * VIDEO_FONT_WIDTH * 2) / 2,
                                               row + (h - VIDEO_FONT_HEIGHT * 2) / 2);

                                for (j = 0; items[i][j]; j++)
                                        painter_bigchar(&p, items[i][j], 0xffffffff);

                                row += h + 10;
                        }

                        video_sync(vdev, true);
                        redraw = 0;
                }

                if (no_touch)
                        return CMD_RET_SUCCESS;

                // don't be too busy reading i2c
                udelay(50 * 1000);

                // handle input
                ret = touchpanel_get_touches(tdev, touches, ARRAY_SIZE(touches));
                if (ret < 0) {
                        printf("Failed to get touches from %s, err=%d\n", tdev->name, ret);
                        return CMD_RET_FAILURE;
                }

                for (i = 0; i < ret; i++) {
                        int tx = touches[i].x;
                        int ty = touches[i].y;

                        if (tx < x || tx > x + w)
                                continue;

                        row = y;
                        for (j = 0; j < items_count; j++) {
                                if (ty > row && ty < row + h) {
                                        // got match
                                        if (selected != j) {
                                                selected = j;
                                                redraw = 1;
                                        }
                                        goto next;
                                }

                                row += h + 10;
                        }
                }

                if (selected != -1) {
                        // we are done
                        char buf[16];
                        snprintf(buf, sizeof buf, "ret=%d", selected);
                        set_local_var(buf, 1);
                        selected = -1;
                        redraw = 1;
                        break;
                }
        }

        ret = touchpanel_stop(tdev);
        if (ret < 0) {
                printf("Failed to stop %s, err=%d\n", tdev->name, ret);
                return CMD_RET_FAILURE;
        }

        return CMD_RET_SUCCESS;
}

static int do_tmenu_render(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
        return handle_tmenu(cmdtp, flag, argc, argv, 1);
}

static int do_tmenu_input(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
        return handle_tmenu(cmdtp, flag, argc, argv, 0);
}

static int do_tmenu(cmd_tbl_t *cmdtp, int flag, int argc, char *const argv[])
{
        int ret;
        
        ret = do_tmenu_render(cmdtp, flag, argc, argv);
        if (ret == CMD_RET_SUCCESS)
                ret = do_tmenu_input(cmdtp, flag, argc, argv);
        
        return ret;
}

U_BOOT_CMD(tmenu, 8, 1, do_tmenu, "tmenu", "tmenu item1 [item2...] - show touch menu and wait for input");
U_BOOT_CMD(tmenu_render, 8, 1, do_tmenu_render, "tmenu_render", "tmenu_render item1 [item2...] - show touch menu");
U_BOOT_CMD(tmenu_input, 8, 1, do_tmenu_input, "tmenu_input", "tmenu_input item1 [item2...] - wait for touch menu input");

Combining it all

What's this about?

This is a log of my FOSS hobby (and some non-hobby) work on u-boot/Linux kernel with relation to Allwinner SoC based SBCs and tablets. Mostly H3 based Orange Pi PC and PC2 and A83T based A711 tablet I got from TBS (Touchless Biometric Systems AG).