Tablet Hacking RSS

GC2145 camera support

I've been analyzing how the sensor works via the same workflow and tools I made for HM5065. GC2145 was even nicer in this regard, since it doesn't need stopping/restarting the stream when changing settings in the sensor's registers.

My setup is:

This is pretty neat, since I can both see the signalling:

And the effect of register value changes on the screen in real-time. This makes testing and understanding the registers very easy.


Powerup requires enabling power supplies in the following order:

Power down

Power down allows to disable power supplies at once. There's no „at once“ in reality, so we just use the reverse order of the powerup sequence.

GPIOs that control RESET and PWDN pins are put into hi-Z mode, to avoid any kind of leakage. There are pullup resistors that ensure the signals are in defined state when the power is still applied to the sensor.


Reset can be done via GPIO. There's no need to initialize the sensor register values manually to the reset values as the BSP driver does. The registers are initialized automatically.

Power save

Power save mode can be entered by pulling PWDN signal high. Register values are preserved in PWDN mode and the CSI signal drivers are put in hi-Z mode.


We need to be able to understand and control clocks in order to control the upper margin for framerate and lower margin for exposure. The sensor can use internal PLL to derive clock signals from externally supplied MCLK.

So far I have the following understanding of the clock tree, which looks like this:

2PCLK = MCLK / D * N / M where:

PCLK signal that can be measured at the output from the sensor is derived from 2PCLK clock under these circumstances:

It's possible to bypass the PLL by setting bit P0:0xf9[0].

DCLK has documented upper limit in the datasheet at 168MHz. Experimentally, I've found that DCLK is limited to around 200MHz (N=17 and MCLK=12MHzDCLK=204MHz). Althoug N=17 causes some color distortion that can be corrected by using N=16.

Parallel interface

When bypassing the PLL, enabling MCLK DIV2, and maxing out M, we can achieve 2PCLK of 750kHz. At this speed, it's possible to get unaliased captures of all CSI signals. This is useful for understanding effects of various horizontal/vertical timing options.

Parallel interface is configured in register P0:0x86.

There are a lot of moving bits here, so let us reference all signals to PCLK cycle. Non-inverted PCLK cycle is defined as a periodic HIGH LOW pattern and all signals change at the start of the cycle.

So for example when P0:0x86 is set to 0x04, the signals look like this (PCLK duty cycle is set to 1:8):

So with this configuration CSI controller in the SoC should sample signals at the falling edge of PCLK, when the signals are stable. (One exception is end of VSYNC pulse, which chnages in the second half of the PCLK cycle, for unknown reasons.)

The bits in P0:0x86 mean:

CIS – CMOS image sensor control / readout

CMOS image sensor circuitry works by reading out pixels from the sensor array row by row, passing voltage through an analog amplifier to A/D converter and then to ISP (digital post-processing of RAW sensor data).

GC2145 allows to control this process by:

The speed at which data are read from the sensor determines the speed at which it will be sent on the CSI bus. The way data are read out from the sensor, also determines the field of view. So there are several concerns here:

Alternatively, for better picture quality, we can use the scaler, and scan out the full image and scale it to half the size by averaging neighboring pixel values.

With row/column skipping, we should be able to comfortably achieve 30 FPS at 800×600 with full FOV.

Manipulating vertical blank time setting allows to fix framerate while allowing to change exposure without it affecting frame rate.

Sensor specification lists the framerate limit at 30 FPS at 1600×1200. A83T CSI limit is 30FPS at 1280×720 (translating to about 50–60MHz PCLK limit).

Strategy for format selection

Subdev API allows to select frame rate and resolution. Based on this information sensor driver will have to decide what settings to use for the sensor, to achieve the best image quality.

Following strategy will be used:

Shutting down the boot CPU and regulator powerdown

At this point, we have the Linux side of the suspend working. Now we need to focus on crust, to make it perform the actual power management tasks.

We'll implement a fixed function suspend process. This means, that we'll not be supporting individual control of the CPUs or clusters. We know that Linux prepares the system state so that only one CPU is running, and all other CPU cores are shut down, so SCP only has to:

Now it can wait for interrupts and resume by reversing the steps above.

Sending a suspend message to SCP from Linux

Now for the fun stuff. I've tried to add support for sending SCPI message from the suspend handler, and it doesn't work well.

Apparently, mailbox client used by SCPI protocol driver, uses some functionality that is already suspended at this stage (timekeeping). Bummer. We need to send message to SCP before syscore_suspend().

The last available hook that is called prior to syscore_suspend is prepare_late, but that's still called prior to disabling secondary CPUs. Tough luck.

The problem turns out to be just in mailbox core code, and it's failry simple to fix:

From f72801cbfe7fd53b59e4996f9948a6f2cc85977e Mon Sep 17 00:00:00 2001
From: Ondrej Jirman <>
Date: Sat, 2 Nov 2019 15:09:01 +0100
Subject: [PATCH 2/6] mailbox: Allow to run mailbox while timekeeping is

This makes it possible to send messages from CPU suspend finisher.

We simply implement cl->tx_block using a busywait loop when
timekeeping is suspended, instead of using hrtimer.

Signed-off-by: Ondrej Jirman <>
 drivers/mailbox/mailbox.c | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/drivers/mailbox/mailbox.c b/drivers/mailbox/mailbox.c
index 0b821a5b2db8..e5eb4bf447f8 100644
--- a/drivers/mailbox/mailbox.c
+++ b/drivers/mailbox/mailbox.c
@@ -82,9 +82,12 @@ static void msg_submit(struct mbox_chan *chan)
        spin_unlock_irqrestore(&chan->lock, flags);
-       if (!err && (chan->txdone_method & TXDONE_BY_POLL))
-               /* kick start the timer immediately to avoid delays */
-               hrtimer_start(&chan->mbox->poll_hrt, 0, HRTIMER_MODE_REL);
+       if (!err && (chan->txdone_method & TXDONE_BY_POLL)) {
+               if (!timekeeping_suspended) {
+                       /* kick start the timer immediately to avoid delays */
+                       hrtimer_start(&chan->mbox->poll_hrt, 0, HRTIMER_MODE_REL);
+               }
+       }
 static void tx_tick(struct mbox_chan *chan, int r)
@@ -260,6 +263,24 @@ int mbox_send_message(struct mbox_chan *chan, void *mssg)
+       if (chan->cl->tx_block && timekeeping_suspended) {
+               int i = chan->cl->tx_tout * 10;
+               bool txdone;
+               while (i--) {
+                       txdone = chan->mbox->ops->last_tx_done(chan);
+                       if (txdone) {
+                               tx_tick(chan, 0);
+                               return 0;
+                       }
+                       udelay(100);
+               }
+               tx_tick(chan, -ETIME);
+               return -ETIME;
+       }
        if (chan->cl->tx_block) {
                unsigned long wait;
                int ret;

We just have to avoid using hrtimer when timekeeping_suspended is 1, and busywait instead.

Signalling SCP to suspend

I don't want to deal with complex power sequencing ATM, so I'll reuse existing SCPI_CMD_SET_SYS_PWR_STATE SCPI message to signal SCP to initiate system suspend. Normally this message is only used for reset/poweroff requests.

First, we need to extend scpi_ops to be able to send this message from CPU suspend finisher callback:

From 37f517b67a0267ce7c09ea8132ebba0966236fb6 Mon Sep 17 00:00:00 2001
From: Ondrej Jirman <>
Date: Sat, 2 Nov 2019 15:14:10 +0100
Subject: [PATCH 4/6] firmware: scpi: Add support for sending a

This is NOT the right message to signal SCP we want to suspend the
system, but we use it anyway, because we simply want to do a fixed
function suspend sequence, instead of more complicated and granular
cluster/cpu/system power management.

Normally we'd signal system suspend by sending a CSS power state
message. I guess that can be done later, if wanted.

For now, Linux will kill all secondary CPU cores via MCPM, and SCP
will kill the last CPU, and suspend the system.

Signed-off-by: Ondrej Jirman <>
 drivers/firmware/arm_scpi.c   | 10 ++++++++++
 include/linux/scpi_protocol.h |  1 +
 2 files changed, 11 insertions(+)

diff --git a/drivers/firmware/arm_scpi.c b/drivers/firmware/arm_scpi.c
index 02ca94faa162..16128306b23d 100644
--- a/drivers/firmware/arm_scpi.c
+++ b/drivers/firmware/arm_scpi.c
@@ -184,6 +184,7 @@ enum scpi_drv_cmds {
@@ -200,6 +201,7 @@ static int scpi_std_commands[CMD_MAX_COUNT] = {
 static int scpi_legacy_commands[CMD_MAX_COUNT] = {
@@ -215,6 +217,7 @@ static int scpi_legacy_commands[CMD_MAX_COUNT] = {
        -1, /* SET_DEVICE_PWR_STATE */
        -1, /* GET_DEVICE_PWR_STATE */
 struct scpi_xfer {
@@ -777,6 +780,12 @@ static int scpi_device_set_power_state(u16 dev_id, u8 pstate)
                                 sizeof(dev_set), &stat, sizeof(stat));
+static int scpi_sys_set_power_state(u8 pstate)
+       return scpi_send_message(CMD_SET_SYS_PWR_STATE, &pstate,
+                                sizeof(pstate), NULL, 0);
 static struct scpi_ops scpi_ops = {
        .get_version = scpi_get_version,
        .clk_get_range = scpi_clk_get_range,
@@ -793,6 +802,7 @@ static struct scpi_ops scpi_ops = {
        .sensor_get_value = scpi_sensor_get_value,
        .device_get_power_state = scpi_device_get_power_state,
        .device_set_power_state = scpi_device_set_power_state,
+       .sys_set_power_state = scpi_sys_set_power_state,
 struct scpi_ops *get_scpi_ops(void)
diff --git a/include/linux/scpi_protocol.h b/include/linux/scpi_protocol.h
index ecb004711acf..a695d43c91f9 100644
--- a/include/linux/scpi_protocol.h
+++ b/include/linux/scpi_protocol.h
@@ -64,6 +64,7 @@ struct scpi_ops {
        int (*sensor_get_value)(u16, u64 *);
        int (*device_get_power_state)(u16);
        int (*device_set_power_state)(u16, u8);
+       int (*sys_set_power_state)(u8);

And now we can call it from A83T specific PM code:

From 9e4886ecf37780ef65819f040c89a0a460f903a2 Mon Sep 17 00:00:00 2001
From: Ondrej Jirman <>
Date: Sat, 2 Nov 2019 15:21:04 +0100
Subject: [PATCH 6/6] ARM: sunxi: Use SCPI to send suspend message to SCP on

We use undefined value of 3, to mean SUSPEND_SYSTEM. SCP should:

- kill CPU0
- kill cluster 0
- shutdown power to both clusters (Linux MCPM doesn't do that)
- reverse all of the above on interrupt

Signed-off-by: Ondrej Jirman <>
 arch/arm/mach-sunxi/sunxi.c | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/arch/arm/mach-sunxi/sunxi.c b/arch/arm/mach-sunxi/sunxi.c
index 200f2c17fa17..64a70dccbf67 100644
--- a/arch/arm/mach-sunxi/sunxi.c
+++ b/arch/arm/mach-sunxi/sunxi.c
@@ -16,6 +16,7 @@
 #include <linux/platform_device.h>
 #include <linux/of_platform.h>
 #include <linux/reset/sunxi.h>
+#include <linux/scpi_protocol.h>
 #include <linux/suspend.h>
 #include <asm/mach/arch.h>
@@ -98,8 +99,18 @@ static int sun8i_a83t_pm_valid(suspend_state_t state)
 static int sun8i_a83t_suspend_finish(unsigned long val)
-       // don't do much
-       cpu_do_idle();
+       struct scpi_ops *scpi;
+       scpi = get_scpi_ops();
+       if (scpi && scpi->sys_set_power_state) {
+               //HACK: use invalid state to mean: suspend last CPU and the system
+               scpi->sys_set_power_state(3);
+               cpu_do_idle();
+       } else {
+               // don't do much if scpi is not available
+               cpu_do_idle();
+       }
        return 0;


I've extended crust SCP firmware to print a message to serial console, when it receives the above message from the kernel.

And it seems to work!

# ./load --reset

ARISC is already in reset

# ./load scp.bin

Asserting ARISC reset
Writing exception vectors
Writing firmware (8732/64512 bytes used)
Deasserting ARISC reset
INFO:    Watchdog enabled
INFO:    SCPI: Initialization complete

# echo mem > /sys/power/state

PM: suspend entry (deep)
Filesystems sync: 0.052 seconds
Freezing user space processes ... (elapsed 0.002 seconds) done.
OOM killer disabled.
Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done.
musb-sunxi 1c19000.usb: Error unknown readb offset 112
Disabling non-boot CPUs ...
calling scpi
INFO:    SCP got mesasge 5
INFO:    SCP got suspend mesasge 3

# [... sleeps here ...]

Enabling non-boot CPUs ...
CPU1 is up
CPU2 is up
CPU3 is up
CPU4 is up
CPU5 is up
CPU6 is up
CPU7 is up
musb-sunxi 1c19000.usb: Error unknown writeb offset 112
usb 1-1: reset high-speed USB device number 2 using ehci-platform
usb 1-1.3: reset full-speed USB device number 3 using ehci-platform
OOM killer enabled.
Restarting tasks ... done.
PM: suspend exit


So I've sent out a bunch of different patches upstream that should help with suspend/resume, fix CPU hotplug bug, and upstream the touch panel support in DTS:

(EDIT: looks like all the patches were accepted upstream)

I've also tested disabling MUSB in the DTS (and it saves about 40–60mW) of power, compared to using MUSB's suspend hook.

Crust and system sleep

My next steps are in adding some debugging functionality to crust, so that I can experiment with shutting down the last CPU from arisc core, instead of simply running the WFI on the last core, as I do now.

During sleep I want to be able to communicate with crust firmware over UART console and issue interactive commands to read/write SoC and PMIC registers.

With this I should be able to quickly experiment with this stage of the suspend process, like this:

Using this I should be able to quickly experiment with various suspend related tasks, without the need to re-compile crust/kernel, and I should be able to immediately see effects of individual shutdown steps on power consumption. All this with some simple scripting over serial port.

Baseline power consumption in suspend to idle and all my patches at the moment is 1.12W.

Current monitor

So I've made a power consumption monitoring tool for the tablet and calibrated it. It sends values periodically over USB-serial port. Now I can monitor and log power consumption without having to depend on the access to the PMIC.

Here's a boot/shutdown cycle log for example (the values are averages over 250ms, but INA226 can be set up to also provide fast non-averaged measurements at sub-millisecond intervals if necessary, since sliding window averaging can be misleading):

U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4307 mV I=182773 uA P=787 mW
U=4270 mV I=401916 uA P=1716 mW
U=4261 mV I=455855 uA P=1942 mW
U=4259 mV I=473732 uA P=2017 mW
U=4260 mV I=469108 uA P=1998 mW
U=4260 mV I=469108 uA P=1998 mW
U=4260 mV I=469725 uA P=2001 mW
U=4259 mV I=470958 uA P=2005 mW
U=4259 mV I=471882 uA P=2009 mW
U=4259 mV I=471882 uA P=2009 mW
U=4259 mV I=471882 uA P=2009 mW
U=4259 mV I=473115 uA P=2014 mW
U=4259 mV I=473732 uA P=2017 mW
U=4259 mV I=474040 uA P=2018 mW
U=4259 mV I=474040 uA P=2018 mW
U=4259 mV I=474656 uA P=2021 mW
U=4259 mV I=474656 uA P=2021 mW
U=4259 mV I=474348 uA P=2020 mW
U=4259 mV I=474656 uA P=2021 mW
U=4259 mV I=475889 uA P=2026 mW
U=4259 mV I=475581 uA P=2025 mW
U=4259 mV I=476197 uA P=2028 mW
U=4259 mV I=476505 uA P=2029 mW
U=4259 mV I=476197 uA P=2028 mW
U=4259 mV I=475889 uA P=2026 mW
U=4259 mV I=475889 uA P=2026 mW
U=4259 mV I=477122 uA P=2032 mW
U=4258 mV I=476505 uA P=2028 mW
U=4259 mV I=477430 uA P=2033 mW
U=4258 mV I=492841 uA P=2098 mW
U=4251 mV I=518115 uA P=2202 mW
U=4258 mV I=483903 uA P=2060 mW
U=4258 mV I=483595 uA P=2059 mW
U=4258 mV I=483595 uA P=2059 mW
U=4258 mV I=483595 uA P=2059 mW
U=4258 mV I=483286 uA P=2057 mW
U=4257 mV I=482670 uA P=2054 mW
U=4257 mV I=482978 uA P=2056 mW
U=4257 mV I=484211 uA P=2061 mW
U=4258 mV I=483903 uA P=2060 mW
U=4258 mV I=484519 uA P=2063 mW
U=4258 mV I=485136 uA P=2065 mW
U=4257 mV I=484827 uA P=2063 mW
U=4257 mV I=485444 uA P=2066 mW
U=4257 mV I=485444 uA P=2066 mW
U=4245 mV I=553560 uA P=2349 mW
U=4254 mV I=506403 uA P=2154 mW
U=4240 mV I=582841 uA P=2471 mW
U=4218 mV I=712910 uA P=3007 mW
U=4207 mV I=778252 uA P=3274 mW
U=4213 mV I=741882 uA P=3125 mW
U=4206 mV I=787807 uA P=3313 mW
U=4203 mV I=800136 uA P=3362 mW
U=4201 mV I=812464 uA P=3413 mW
U=4187 mV I=892601 uA P=3737 mW
U=4174 mV I=972738 uA P=4060 mW
U=4185 mV I=900615 uA P=3769 mW
U=4163 mV I=1023903 uA P=4262 mW
U=4147 mV I=1118218 uA P=4637 mW
U=4175 mV I=962875 uA P=4019 mW
U=4180 mV I=932670 uA P=3898 mW
U=4185 mV I=911711 uA P=3815 mW
U=4185 mV I=905547 uA P=3789 mW
U=4181 mV I=936985 uA P=3917 mW
U=4199 mV I=820478 uA P=3445 mW
U=4188 mV I=881505 uA P=3691 mW
U=4207 mV I=780410 uA P=3283 mW
U=4202 mV I=814314 uA P=3421 mW
U=4180 mV I=935444 uA P=3910 mW
U=4211 mV I=740958 uA P=3120 mW
U=4208 mV I=779177 uA P=3278 mW
U=4235 mV I=618595 uA P=2619 mW
U=4240 mV I=586848 uA P=2488 mW
U=4233 mV I=640170 uA P=2709 mW
U=4239 mV I=591779 uA P=2508 mW
U=4237 mV I=582533 uA P=2468 mW
U=4222 mV I=695341 uA P=2935 mW
U=4242 mV I=570821 uA P=2421 mW
U=4209 mV I=769622 uA P=3239 mW
U=4209 mV I=751129 uA P=3161 mW
U=4234 mV I=619519 uA P=2623 mW
U=4238 mV I=602875 uA P=2554 mW
U=4244 mV I=568355 uA P=2412 mW
U=4245 mV I=560033 uA P=2377 mW
U=4244 mV I=567430 uA P=2408 mW
U=4239 mV I=590547 uA P=2503 mW
U=4231 mV I=653423 uA P=2764 mW
U=4229 mV I=661437 uA P=2797 mW
U=4235 mV I=608115 uA P=2575 mW
U=4234 mV I=610889 uA P=2586 mW
U=4248 mV I=541540 uA P=2300 mW
U=4248 mV I=536608 uA P=2279 mW
U=4248 mV I=536300 uA P=2278 mW
U=4248 mV I=537841 uA P=2284 mW
U=4247 mV I=544930 uA P=2314 mW
U=4246 mV I=548012 uA P=2326 mW
U=4242 mV I=573595 uA P=2433 mW
U=4254 mV I=499314 uA P=2124 mW
U=4254 mV I=499314 uA P=2124 mW
U=4250 mV I=529211 uA P=2249 mW
U=4248 mV I=533834 uA P=2267 mW
U=4254 mV I=498697 uA P=2121 mW
U=4255 mV I=499622 uA P=2125 mW
U=4254 mV I=500238 uA P=2127 mW
U=4254 mV I=495923 uA P=2109 mW
U=4254 mV I=500547 uA P=2129 mW
U=4254 mV I=496848 uA P=2113 mW
U=4254 mV I=504862 uA P=2147 mW
U=4255 mV I=497773 uA P=2118 mW
U=4254 mV I=498081 uA P=2118 mW
U=4255 mV I=498389 uA P=2120 mW
U=4254 mV I=499005 uA P=2122 mW
U=4255 mV I=488834 uA P=2079 mW
U=4258 mV I=482053 uA P=2052 mW
U=4258 mV I=480821 uA P=2047 mW
U=4258 mV I=479896 uA P=2043 mW
U=4258 mV I=480204 uA P=2044 mW
U=4257 mV I=485752 uA P=2067 mW
U=4250 mV I=524588 uA P=2229 mW
U=4257 mV I=486985 uA P=2073 mW
U=4257 mV I=488218 uA P=2078 mW
U=4257 mV I=487293 uA P=2074 mW
U=4257 mV I=486985 uA P=2073 mW
U=4245 mV I=544622 uA P=2311 mW
U=4220 mV I=693492 uA P=2926 mW
U=4242 mV I=577293 uA P=2448 mW
U=4228 mV I=666677 uA P=2818 mW
U=4247 mV I=540615 uA P=2295 mW
U=4252 mV I=511026 uA P=2172 mW
U=4257 mV I=484519 uA P=2062 mW
U=4255 mV I=491916 uA P=2093 mW
U=4255 mV I=487601 uA P=2074 mW
U=4250 mV I=532601 uA P=2263 mW
U=4257 mV I=484519 uA P=2062 mW
U=4254 mV I=506403 uA P=2154 mW
U=4250 mV I=525512 uA P=2233 mW
U=4221 mV I=689177 uA P=2908 mW
U=4238 mV I=593629 uA P=2515 mW
U=4247 mV I=544314 uA P=2311 mW
U=4246 mV I=546779 uA P=2321 mW
U=4338 mV I=4622 uA P=20 mW
U=4338 mV I=4622 uA P=20 mW
U=4339 mV I=4622 uA P=20 mW
U=4338 mV I=4622 uA P=20 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW
U=4339 mV I=0 uA P=0 mW

Next steps for suspend on A83T, power management optimizations of TBS A711

Suspending devices and CPU cores

So I've moved to start suspending devices properly on s2idle. So far I've added suspend support to sun4i-drm driver (display is now properly powered down during suspend).

I've also looked at creating a simple implementation of suspend to mem that would just wait on WFI instruction. The difference against s2idle mode would be that Linux shuts down all secondary CPU cores in suspend to mem mode.

This should work, but does not, because MCPM is somehow broken on A83T. I can't even hotplug CPU cores using echo 0 > /sys/devices/system/cpu/cpu#/online anymore.

If I try to offline the cpu7, I get:

echo 0 > /sys/devices/system/cpu/cpu7/online
sunxi_mc_smp_cpu_die: cluster 1 cpu 3
sunxi_cpu_powerdown: cluster 1 cpu 3
sunxi_mc_smp_cpu_kill: cluster 1 cpu 3 powerdown: 0
#                 8<--- cut here ---
 Unable to handle kernel paging request at virtual address 616e6933
pgd = eca474fd
[616e6933] *pgd=00000000
Internal error: Oops: 5 [#1] PREEMPT SMP ARM
     odules linked in: bnep ac100_codec sun6i_csi nxp_nci_i2c nxp_nci nci videobuf2_dma_contig videobuf2_memops videobuf2_v4l2 nfc sun4i_i2s bma180 videobuf2_common industrialio_triggered_buffer snd_soc_core snd_pcm_dmaengine snd_pcm snd_timer hm5065 snd soundcore hci_uart v4l2_fwnode videodev btbcm bluetooth mc
4mtbs[~] # 8<--- cut here ---
CPU: 1 PID: 291 Comm: systemd-udevd Not tainted 5.4.0-rc4-00128-g6d68fc744b41-dirty #15
Hardware name: Allwinner A83t board
PC is at select_task_rq_fair+0x820/0xf6c
LR is at select_task_rq_fair+0x7ac/0xf6c
pc : [<c0156c5c>]    lr : [<c0156be8>]    psr: 60070093
sp : e8c61e80  ip : 00000007  fp : c0d0633c
r10: e8ff8d80  r9 : c0c50480  r8 : 000001a9
r7 : e83d87c0  r6 : e8ff8e80  r5 : 00000027  r4 : 000003fe
r3 : 616e692f  r2 : c0c50480  r1 : 00000008  r0 : 0006a400
Flags: nZCv  IRQs off  FIQs on  Mode SVC_32  ISA ARM  Segment none
Unable to handle kernel NULL pointer dereference at virtual address 00000004
Control: 10c5387d  Table: 68cfc06a  DAC: 00000051
Process systemd-udevd (pid: 291, stack limit = 0x0ad540ea)
Stack: (0xe8c61e80 to 0xe8c62000)
1e80: e83d87d4 c0d17828 e83d87c0 c0d5f458 00000001 c08ee044 c0d063dc 00000008
1ea0: e772db80 80070013 00000001 e8c60000 00000002 00000000 00000002 00000000
pgd = 1f7cb0a2
8<--- cut here ---
Unable to handle kernel NULL pointer dereference at virtual address 00000004
pgd = 1f7cb0a2
[00000004] *pgd=00000000
1ec0: 000003f5 e83d8600 00000100 00000005 00000400 00000017 0000000a e83d8600
1ee0: 00000070 00000008 51eb851f c0c50480 00000000 e8ff90b8 00000000 e8ff8d80
[00000004] *pgd=00000000
8<--- cut here ---
Unable to handle kernel NULL pointer dereference at virtual address 00000004
pgd = b9d75337
[00000004] *pgd=00000000
1f00: c015643c e8ff9238 01200000 00000000 00000000 000001f1 e8c61f3c c014f628
1f20: c0101204 60070013 00000000 e8ff8d80 e8c61f78 01200000 e97fe180 c0127fb4
1f40: 00000000 00000000 00000000 00000000 00000000 b6f45398 b6f457f0 bec509ec
1f60: 00000078 c0101204 e8c60000 00000078 00000123 c01283f8 01200000 00000000
1f80: 00000000 b6f45398 00000000 00000011 00000000 00000000 00000000 00000000
1fa0: c0101204 c01011e0 b6f45398 b6f457f0 01200011 00000000 00000000 00000000
1fc0: b6f45398 b6f457f0 bec509ec 00000078 b6c0ee34 00000000 00000004 00000123
1fe0: b6f45330 bec50818 b6b6a724 b6b6a758 60070010 01200011 00000000 00000000
[<c0156c5c>] (select_task_rq_fair) from [<c014f628>] (wake_up_new_task+0x5c/0x1d8)
[<c014f628>] (wake_up_new_task) from [<c0127fb4>] (_do_fork+0xbc/0x33c)
[<c0127fb4>] (_do_fork) from [<c01283f8>] (sys_clone+0x5c/0x64)
[<c01283f8>] (sys_clone) from [<c01011e0>] (__sys_trace_return+0x0/0x20)
Exception stack(0xe8c61fa8 to 0xe8c61ff0)
1fa0:                   b6f45398 b6f457f0 01200011 00000000 00000000 00000000
1fc0: b6f45398 b6f457f0 bec509ec 00000078 b6c0ee34 00000000 00000004 00000123
1fe0: b6f45330 bec50818 b6b6a724 b6b6a758
Code: eaffffde e59d7008 e1a00508 e597300c (e5936004)
---[ end trace 7dc21d288962c08c ]---
Internal error: Oops: 5 [#2] PREEMPT SMP ARM
Modules linked in: bnep ac100_codec sun6i_csi nxp_nci_i2c nxp_nci nci videobuf2_dma_contig videobuf2_memops videobuf2_v4l2 nfc sun4i_i2s bma180 videobuf2_common industrialio_triggered_buffer snd_soc_core snd_pcm_dmaengine snd_pcm snd_timer hm5065 snd soundcore hci_uart v4l2_fwnode videodev btbcm
note: systemd-udevd[291] exited with preempt_count 1
 bluetooth mc
CPU: 2 PID: 0 Comm: swapper/2 Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
Hardware name: Allwinner A83t board
PC is at update_sd_lb_stats+0x240/0x600
LR is at 0x20
pc : [<c0157bc0>]    lr : [<00000020>]    psr: 60030113
sp : eb08dc60  ip : 00000000  fp : 00000000
r10: e83d8654  r9 : c0d063dc  r8 : e83d8640
r7 : eb08dcf0  r6 : 00000008  r5 : eb08ddb0  r4 : eb08dc8c
r3 : 00000000  r2 : 00000008  r1 : eb8a936c  r0 : 00000008
Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment none
Control: 10c5387d  Table: 633d406a  DAC: 00000051
Process swapper/2 (pid: 0, stack limit = 0x62faa273)
Stack: (0xeb08dc60 to 0xeb08e000)
dc60: 00000009 00000000 c0c50480 00000000 00000003 c0d0633c c0c50480 00000001
dc80: eb08dd34 c0d03d00 00000000 00000000 00000000 00000000 00000000 00000000
dca0: 00000000 00000000 00000000 00000000 00000000 00000000 c0c50480 eb08ddb0
dcc0: 00000002 c0d0633c e772dc00 00000002 00000000 00000000 c0d063dc c0157fa8
dce0: eb8b0480 eb065100 eb08dcfc c014df54 00000000 e7401900 00000003 00000426
dd00: 00002000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
dd20: 00000000 00000000 00000000 00000000 00000000 00000097 00000426 00000162
dd40: 00001c00 000002f5 00000003 00000001 00000004 00000000 00000000 00000000
dd60: eb8b3200 e7401900 00000002 c0d0633c e772dc00 00000002 00000000 00000000
dd80: c0d063dc c01585e0 00000005 c0c50480 00000000 c0c50480 e772dc00 c0d0633c
dda0: 00000000 00000000 00000002 eb8a936c e772dc00 00000000 00000000 00000002
ddc0: eb8b0480 e7401914 00000000 00000000 00000000 eb8a936c 00000000 00000000
dde0: 00000020 00000000 00000002 00000000 eb08ddf0 eb08ddf0 eb8b0a50 00000000
de00: 00000000 00000001 00000000 c0d03d00 fffbb565 00000000 00000000 c0159094
de20: eb08de4c 00000004 00000001 00000000 eb8b0480 c0d062a8 00000002 00000000
de40: e772dc00 c0c493b8 80030113 00000001 eb8dd480 00000001 eb8dd480 fffbb56b
de60: 00000001 c0d0633c c0d61000 00000002 00000008 c015931c eb08c000 c0d03d00
de80: c0c50480 00000000 20030113 00000003 c0d61000 fffbb563 00000000 eb8b0480
dea0: 00000004 00000000 00000007 c0d0309c c0d03080 eb08c000 00000101 40000007
dec0: 00000008 c01022f0 c0d063dc eb08c000 eb08dec8 c0d03080 c0c49340 c0c50040
dee0: c0a8237c 0000000a c0c492cc fffbb565 c0d03d00 00200042 c0d063dc c0c50040
df00: 00000002 eb08df60 00000000 00000002 c0c50014 c0a8237c 00000000 c012e1a4
df20: c0c50014 c010d2c8 c0d066d8 eb08df60 f080200c f0802000 f0803000 eb08c000
df40: c0a8237c c046dd28 c0108524 60030013 ffffffff eb08df94 c0c4f770 c0101a8c
df60: 00000000 00014abc eb8b0c74 c0116ba0 00000002 eb08c000 c0d06264 c0d062a4
df80: c0c4f770 00000000 c0a8237c 00000000 00000000 eb08dfb0 c0108520 c0108524
dfa0: 60030013 ffffffff 00000051 00000000 00000002 c0152f78 eb08dfb8 c0d5ee80
dfc0: 4000406a 410fc075 00000000 00000089 00000051 10c0387d c0d5ee80 4000406a
dfe0: 410fc075 00000000 00000000 c01532b0 6b08006a 4010250c 00000000 00000000
[<c0157bc0>] (update_sd_lb_stats) from [<c0157fa8>] (find_busiest_group+0x28/0x500)
[<c0157fa8>] (find_busiest_group) from [<c01585e0>] (load_balance+0x160/0x988)
[<c01585e0>] (load_balance) from [<c0159094>] (rebalance_domains+0x28c/0x318)
[<c0159094>] (rebalance_domains) from [<c015931c>] (_nohz_idle_balance+0x1fc/0x200)
[<c015931c>] (_nohz_idle_balance) from [<c01022f0>] (__do_softirq+0x148/0x2d8)
[<c01022f0>] (__do_softirq) from [<c012e1a4>] (irq_exit+0xc8/0xe0)
[<c012e1a4>] (irq_exit) from [<c010d2c8>] (handle_IPI+0x118/0x1b8)
[<c010d2c8>] (handle_IPI) from [<c046dd28>] (gic_handle_irq+0x74/0x78)
[<c046dd28>] (gic_handle_irq) from [<c0101a8c>] (__irq_svc+0x6c/0xa8)
Exception stack(0xeb08df60 to 0xeb08dfa8)
df60: 00000000 00014abc eb8b0c74 c0116ba0 00000002 eb08c000 c0d06264 c0d062a4
df80: c0c4f770 00000000 c0a8237c 00000000 00000000 eb08dfb0 c0108520 c0108524
dfa0: 60030013 ffffffff
[<c0101a8c>] (__irq_svc) from [<c0108524>] (arch_cpu_idle+0x38/0x3c)
[<c0108524>] (arch_cpu_idle) from [<c0152f78>] (do_idle+0x20c/0x274)
[<c0152f78>] (do_idle) from [<c01532b0>] (cpu_startup_entry+0x18/0x1c)
[<c01532b0>] (cpu_startup_entry) from [<4010250c>] (0x4010250c)
Code: 358d3004 eaffffb7 e598300c e594b004 (e593a004)
Internal error: Oops: 5 [#3] PREEMPT SMP ARM
Modules linked in: bnep
---[ end trace 7dc21d288962c08d ]---
 ac100_codec sun6i_csi nxp_nci_i2c nxp_nci nci videobuf2_dma_contig videobuf2_memops videobuf2_v4l2
Kernel panic - not syncing: Fatal exception in interrupt
 nfc sun4i_i2s bma180 videobuf2_common industrialio_triggered_buffer snd_soc_core snd_pcm_dmaengine
CPU1: stopping
 snd_pcm snd_timer hm5065
CPU: 1 PID: 291 Comm: systemd-udevd Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
 snd soundcore
Hardware name: Allwinner A83t board
 hci_uart v4l2_fwnode videodev btbcm
[<c010e278>] (unwind_backtrace) from [<c010b00c>] (show_stack+0x10/0x14)
 bluetooth mc
[<c010b00c>] (show_stack) from [<c08d2f28>] (dump_stack+0x78/0x8c)
CPU: 3 PID: 498 Comm: journal-offline Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
Hardware name: Allwinner A83t board
[<c08d2f28>] (dump_stack) from [<c010d33c>] (handle_IPI+0x18c/0x1b8)
PC is at update_sd_lb_stats+0x240/0x600
[<c010d33c>] (handle_IPI) from [<c046dd28>] (gic_handle_irq+0x74/0x78)
LR is at 0x20
[<c046dd28>] (gic_handle_irq) from [<c0101a8c>] (__irq_svc+0x6c/0xa8)
pc : [<c0157bc0>]    lr : [<00000020>]    psr: 600f0093
Exception stack(0xe8c61bd8 to 0xe8c61c20)
sp : e332b830  ip : 00000000  fp : 00000000
1bc0:                                                       e8c8f548 00000040
r10: e83d8654  r9 : c0d063dc  r8 : e83d8640
r7 : e332b8c0  r6 : 00000008  r5 : e332b980  r4 : e332b85c
1be0: 00000000 00000000 e8c8f54c eba7fdb0 b6553000 e8c8f548 c0d0db84 e8c61d0c
r3 : 00000000  r2 : 00000008  r1 : eb8b836c  r0 : 00000008
Flags: nZCv  IRQs off  FIQs on  Mode SVC_32  ISA ARM  Segment none
1c00: b6552000 00000000 0006bfda e8c61c28 c022b46c c0116b68 40070113 ffffffff
Control: 10c5387d  Table: 6a2fc06a  DAC: 00000051
[<c0101a8c>] (__irq_svc) from [<c0116b68>] (cpu_ca15_set_pte_ext+0x4c/0x58)
Process journal-offline (pid: 498, stack limit = 0x2ed0d549)
Stack: (0xe332b830 to 0xe332c000)
CPU6: stopping
b820:                                     00000000 00000000 c0c50480 00000000
b840: 00000002 c0d0633c c0c50480 00000001 e332b904 c0d03d00 00000000 00000000
CPU: 6 PID: 0 Comm: swapper/6 Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
b860: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
Hardware name: Allwinner A83t board
b880: 00000000 00000000 00000000 e332b980 2ac6f000 eb8bf480 e772dc80 00000003
b8a0: 00000002 c0d062a8 c0d063dc c0157fa8 00000000 00009c00 00000002 00000000
[<c010e278>] (unwind_backtrace) from [<c010b00c>] (show_stack+0x10/0x14)
b8c0: 00000000 e7401900 00000002 00000028 00002000 00000000 00000000 00000000
[<c010b00c>] (show_stack) from [<c08d2f28>] (dump_stack+0x78/0x8c)
b8e0: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[<c08d2f28>] (dump_stack) from [<c010d33c>] (handle_IPI+0x18c/0x1b8)
b900: 00000000 00000005 00000028 00000014 00001c00 0000032a 00000002 00000001
[<c010d33c>] (handle_IPI) from [<c046dd28>] (gic_handle_irq+0x74/0x78)
b920: 00000004 00000000 00000000 00000000 00000006 e7401900 2ac6f000 eb8bf480
[<c046dd28>] (gic_handle_irq) from [<c0101a8c>] (__irq_svc+0x6c/0xa8)
b940: e772dc80 00000003 00000002 c0d062a8 c0d063dc c01585e0 eb8bfb50 c0c50480
Exception stack(0xeb095f60 to 0xeb095fa8)
b960: 00000000 c0c50480 e772dc80 c0d0633c 00000000 00000002 00000003 eb8b836c
b980: e772dc80 00000000 00000000 00000003 eb8bf480 e7401914 00000000 00000002
b9a0: 00000000 eb8b836c 00000000 00000000 00000020 00000000 00000002 00000000
5f60: 00000000 0001d4d8 eb8ecc74 c0116ba0 00000006 eb094000 c0d06264 c0d062a4
b9c0: e332b9c0 e332b9c0 00000004 eb8bf480 fffbb561 e772dc80 eb8bfb50 00000004
b9e0: 00000000 c0d062a8 5e7dfff3 c01598b0 e332ba24 0000b9e7 000029ab 00000000
ba00: 000029ab 00000000 000029ab 00000000 00000009 00000003 eb8bfb40 00000001
5f80: c0c4f770 00000000 c0a8237c 00000000 c0da5810 eb095fb0 c0108520 c0108524
ba20: 00000a71 00000001 00000000 e332ba8c e3330000 eb8bf480 e3330000 c0d063dc
5fa0: 60000013 ffffffff
ba40: e33303c8 c0901f8c e332bab4 c0159a24 eb8bf480 e3330000 00000000 e332ba8c
[<c0101a8c>] (__irq_svc) from [<c0108524>] (arch_cpu_idle+0x38/0x3c)
ba60: c0d063dc e33303c8 c0901f8c c08e94f4 eb293c00 c0d582a9 00000001 eb293cb0
[<c0108524>] (arch_cpu_idle) from [<c0152f78>] (do_idle+0x20c/0x274)
ba80: 00000000 c08e9984 00000000 eaa20a08 e332bae6 e3330000 e332a000 00000001
[<c0152f78>] (do_idle) from [<c01532b0>] (cpu_startup_entry+0x18/0x1c)
baa0: e332bb04 eaa20ab0 eaa19800 00000002 e332bac4 c08e9984 eaa20a08 eaa20a84
[<c01532b0>] (cpu_startup_entry) from [<4010250c>] (0x4010250c)
bac0: 00000000 c065159c eaa19800 00000000 e3330000 c016242c eaa20ab4 eaa20ab4
CPU4: stopping
bae0: eaa20a08 eb293c00 00000000 eb293cb0 eaa19800 c0652000 ea8f85b8 ea972000
CPU: 4 PID: 0 Comm: swapper/4 Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
bb00: fffca36f 00000000 eaa20a08 eaa19800 ea972000 eaa20a84 eaa20a10 eb293c00
Hardware name: Allwinner A83t board
bb20: 00000002 c0652ae8 00000908 00000002 ea8f85b8 eb293c00 eaa1bc00 00000001
bb40: 0000001e e332bb84 eb8c6d80 00000000 00000012 c03ef32c eb293c00 e96d4f01
[<c010e278>] (unwind_backtrace) from [<c010b00c>] (show_stack+0x10/0x14)
bb60: eb293c00 eaa1bc00 00000001 00000000 ea8f85b8 c03efb60 00000001 00000000
[<c010b00c>] (show_stack) from [<c08d2f28>] (dump_stack+0x78/0x8c)
bb80: 00000000 0000001d e332bbd0 eaa1bc00 eb293c00 c03efbc4 eaa1bc00 ea8f8a34
[<c08d2f28>] (dump_stack) from [<c010d33c>] (handle_IPI+0x18c/0x1b8)
bba0: e332bbd0 c03f3ecc eb293c30 e332bbd0 e332bbc8 00000000 e332bdfc e8f00c00
[<c010d33c>] (handle_IPI) from [<c046dd28>] (gic_handle_irq+0x74/0x78)
bbc0: 00000000 c03efb08 e332bbc8 e332bbc8 e332bbd0 e332bbd0 e332bbd8 e332bdfc
[<c046dd28>] (gic_handle_irq) from [<c0101a8c>] (__irq_svc+0x6c/0xa8)
bbe0: e332be04 e332bbf0 00000000 c03e5fcc e332bbf0 e332bbf0 eb293e00 eb293e00
Exception stack(0xeb091f60 to 0xeb091fa8)
bc00: ea8f85b8 00000000 00000001 e332bdfc e8f00c00 c03ef660 00000001 e96d4f00
bc20: ebe35208 eb293a00 00000014 0000001f ea8f85b8 00000000 00000000 00004801
bc40: eb8c6d80 eaa1bc00 0000001e e332bc60 e332a000 e332bc60 ea8f85b8 c03e4a80
1f60: 00000000 00027330 eb8cec74 c0116ba0 00000004 eb090000 c0d06264 c0d062a4
bc60: e8f00c00 e8f00c00 00000000 00000000 00000000 e8f00c00 00001d08 00000000
bc80: ea508000 00000001 00000000 c03e4d08 ea508000 00000000 00000000 e332bd7f
bca0: e69601a0 c0384dd4 ea2fed30 e620b330 e332bd80 00000001 003a2000 00000000
1f80: c0c4f770 00000000 c0a8237c 00000000 c0da5810 eb091fb0 c0108520 c0108524
bcc0: ea8ac288 e8f00c00 00000000 ea508000 00000001 00000000 00000000 c037dc74
1fa0: 600f0013 ffffffff
bce0: 00000000 ffffffff eb91a000 00040000 0000000f 00000088 00000000 ea508000
[<c0101a8c>] (__irq_svc) from [<c0108524>] (arch_cpu_idle+0x38/0x3c)
bd00: ea8ac2e8 00000001 e6960000 00000000 00000012 c037df38 e332bd18 00000000
[<c0108524>] (arch_cpu_idle) from [<c0152f78>] (do_idle+0x20c/0x274)
bd20: 00000000 00000048 ea508000 00000001 00000000 e332be60 ea508000 00000000
[<c0152f78>] (do_idle) from [<c01532b0>] (cpu_startup_entry+0x18/0x1c)
bd40: e332bd90 e6960100 000003a1 c038548c 00000000 00000000 00000000 00000004
[<c01532b0>] (cpu_startup_entry) from [<4010250c>] (0x4010250c)
bd60: 000003a1 ffffffff 00000002 00000001 00000000 00000001 00000000 01000000
CPU5: stopping
bd80: 00000000 ffffffff 00013c4e 00000000 00000100 ebe35010 ebe35034 ebe35058
bda0: ebe3507c ebe350a0 ebe350c4 ebe350e8 ebe3510c ebe35130 ebe35154 ebe35178
CPU: 5 PID: 0 Comm: swapper/5 Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
bdc0: ebe3519c ebe351c0 ebe351e4 ebe35208 00000000 e6960000 e6960100 e332be60
Hardware name: Allwinner A83t board
bde0: e6960100 ea508000 00000004 ea50802c 00000000 c0385758 00000000 e332bdfc
be00: e332bdfc e332be04 e332be04 00000000 e6960000 e6960100 e332be60 c0206738
[<c010e278>] (unwind_backtrace) from [<c010b00c>] (show_stack+0x10/0x14)
be20: 00000000 00000001 00000000 c0209a90 00000000 00000000 00000000 00000000
[<c010b00c>] (show_stack) from [<c08d2f28>] (dump_stack+0x78/0x8c)
be40: 00000000 e6960068 e6960100 e6960000 e6960100 ffffffff 7fffffff c0201d38
[<c08d2f28>] (dump_stack) from [<c010d33c>] (handle_IPI+0x18c/0x1b8)
be60: 7ffffc5d 00000000 00000000 00000000 ffffffff 7fffffff 00000001 00000000
[<c010d33c>] (handle_IPI) from [<c046dd28>] (gic_handle_irq+0x74/0x78)
be80: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[<c046dd28>] (gic_handle_irq) from [<c0101a8c>] (__irq_svc+0x6c/0xa8)
bea0: 00000000 e83ed000 ea508000 ffffffff 7fffffff 00000000 e6960100 c02030ec
Exception stack(0xeb093f60 to 0xeb093fa8)
bec0: ffffffff 7fffffff 00000001 00000000 e6960000 ea508000 000407dd 00000000
bee0: e83ed000 e696019c 00000000 c0361000 ffffffff 7fffffff 00000000 00000000
bf00: 7fffffff 00000000 00000000 00000000 00000000 00000000 00000001 00000000
3f60: 00000000 0004bc1c eb8ddc74 c0116ba0 00000005 eb092000 c0d06264 c0d062a4
bf20: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
bf40: 00000002 ffffffff 7fffffff e83ed000 00000076 c0101204 e332a000 00000076
bf60: b4929eac c036169c ffffffff 7fffffff 00000000 00000000 00000000 e83ed001
3f80: c0c4f770 00000000 c0a8237c 00000000 00000000 eb093fb0 c0108520 c0108524
bf80: 00000000 c029d248 ffffffff 7fffffff 00000000 00527088 0000001c 00527088
3fa0: 60000013 ffffffff
bfa0: 00000006 c01011e0 0000001c 00527088 0000001c 00000002 b492a3f4 00000000
[<c0101a8c>] (__irq_svc) from [<c0108524>] (arch_cpu_idle+0x38/0x3c)
bfc0: 0000001c 00527088 00000006 00000076 be8471b2 00000000 be8471b4 b4929eac
[<c0108524>] (arch_cpu_idle) from [<c0152f78>] (do_idle+0x20c/0x274)
bfe0: 00000000 b4929d40 b492a830 b6cbc870 800f0010 0000001c 00000000 00000000
[<c0157bc0>] (update_sd_lb_stats) from [<c0157fa8>] (find_busiest_group+0x28/0x500)
[<c0152f78>] (do_idle) from [<c01532b0>] (cpu_startup_entry+0x18/0x1c)
[<c0157fa8>] (find_busiest_group) from [<c01585e0>] (load_balance+0x160/0x988)
[<c01532b0>] (cpu_startup_entry) from [<4010250c>] (0x4010250c)
[<c01585e0>] (load_balance) from [<c01598b0>] (newidle_balance+0x2c4/0x418)
[<c01598b0>] (newidle_balance) from [<c0159a24>] (pick_next_task_fair+0x20/0x2e8)
[<c0159a24>] (pick_next_task_fair) from [<c08e94f4>] (__schedule+0xd4/0x50c)
[<c08e94f4>] (__schedule) from [<c08e9984>] (schedule+0x58/0xe8)
[<c08e9984>] (schedule) from [<c065159c>] (mmc_blk_rw_wait+0xf8/0xfc)
[<c065159c>] (mmc_blk_rw_wait) from [<c0652000>] (mmc_blk_mq_issue_rq+0x2a0/0x874)
[<c0652000>] (mmc_blk_mq_issue_rq) from [<c0652ae8>] (mmc_mq_queue_rq+0x11c/0x240)
[<c0652ae8>] (mmc_mq_queue_rq) from [<c03ef32c>] (__blk_mq_try_issue_directly+0x114/0x198)
[<c03ef32c>] (__blk_mq_try_issue_directly) from [<c03efb60>] (blk_mq_request_issue_directly+0x38/0x54)
[<c03efb60>] (blk_mq_request_issue_directly) from [<c03efbc4>] (blk_mq_try_issue_list_directly+0x48/0xbc)
[<c03efbc4>] (blk_mq_try_issue_list_directly) from [<c03f3ecc>] (blk_mq_sched_insert_requests+0x130/0x1b8)
[<c03f3ecc>] (blk_mq_sched_insert_requests) from [<c03efb08>] (blk_mq_flush_plug_list+0x120/0x140)
[<c03efb08>] (blk_mq_flush_plug_list) from [<c03e5fcc>] (blk_flush_plug_list+0xbc/0xc4)
[<c03e5fcc>] (blk_flush_plug_list) from [<c03ef660>] (blk_mq_make_request+0x230/0x3dc)
[<c03ef660>] (blk_mq_make_request) from [<c03e4a80>] (generic_make_request+0xa4/0x2f8)
[<c03e4a80>] (generic_make_request) from [<c03e4d08>] (submit_bio+0x34/0x188)
[<c03e4d08>] (submit_bio) from [<c037dc74>] (__submit_merged_bio+0x58/0x24c)
[<c037dc74>] (__submit_merged_bio) from [<c037df38>] (__submit_merged_write_cond+0xd0/0x144)
[<c037df38>] (__submit_merged_write_cond) from [<c038548c>] (f2fs_write_cache_pages+0x598/0x650)
[<c038548c>] (f2fs_write_cache_pages) from [<c0385758>] (f2fs_write_data_pages+0x214/0x254)
[<c0385758>] (f2fs_write_data_pages) from [<c0209a90>] (do_writepages+0x2c/0xc8)
[<c0209a90>] (do_writepages) from [<c0201d38>] (__filemap_fdatawrite_range+0xc4/0x100)
[<c0201d38>] (__filemap_fdatawrite_range) from [<c02030ec>] (file_write_and_wait_range+0x48/0x98)
[<c02030ec>] (file_write_and_wait_range) from [<c0361000>] (f2fs_do_sync_file+0xc8/0x728)
[<c0361000>] (f2fs_do_sync_file) from [<c036169c>] (f2fs_sync_file+0x3c/0x4c)
[<c036169c>] (f2fs_sync_file) from [<c029d248>] (do_fsync+0x3c/0x70)
[<c029d248>] (do_fsync) from [<c01011e0>] (__sys_trace_return+0x0/0x20)
Exception stack(0xe332bfa8 to 0xe332bff0)
bfa0:                   0000001c 00527088 0000001c 00000002 b492a3f4 00000000
bfc0: 0000001c 00527088 00000006 00000076 be8471b2 00000000 be8471b4 b4929eac
bfe0: 00000000 b4929d40 b492a830 b6cbc870
Code: 358d3004 eaffffb7 e598300c e594b004 (e593a004)
---[ end trace 7dc21d288962c08e ]---
CPU3: stopping
CPU: 3 PID: 498 Comm: journal-offline Tainted: G      D           5.4.0-rc4-00128-g6d68fc744b41-dirty #15
Hardware name: Allwinner A83t board
[<c010e278>] (unwind_backtrace) from [<c010b00c>] (show_stack+0x10/0x14)
[<c010b00c>] (show_stack) from [<c08d2f28>] (dump_stack+0x78/0x8c)
[<c08d2f28>] (dump_stack) from [<c010d33c>] (handle_IPI+0x18c/0x1b8)
[<c010d33c>] (handle_IPI) from [<c046dd28>] (gic_handle_irq+0x74/0x78)
[<c046dd28>] (gic_handle_irq) from [<c0101a8c>] (__irq_svc+0x6c/0xa8)
Exception stack(0xe332b6b0 to 0xe332b6f8)
b6a0:                                     ea1b1500 2ac6f000 00000002 0000004c
b6c0: e3330000 c0d180a4 e332a000 c0d0f1c8 bf000000 600f0193 e3330000 ffffe000
b6e0: 00000000 e332b700 c0137784 c08edfe8 600f0113 ffffffff
[<c0101a8c>] (__irq_svc) from [<c08edfe8>] (_raw_spin_unlock_irq+0x20/0x54)
[<c08edfe8>] (_raw_spin_unlock_irq) from [<c0137784>] (exit_signals+0x17c/0x274)
[<c0137784>] (exit_signals) from [<c012cd3c>] (do_exit+0xbc/0xafc)
[<c012cd3c>] (do_exit) from [<c010b330>] (die+0x320/0x33c)
[<c010b330>] (die) from [<c0112128>] (__do_kernel_fault.part.0+0x78/0x88)
[<c0112128>] (__do_kernel_fault.part.0) from [<c011248c>] (do_bad_area+0x0/0xac)
[<c011248c>] (do_bad_area) from [<00000000>] (0x0)
SMP: failed to stop secondary CPUs
Rebooting in 3 seconds..
SMP: failed to stop secondary CPUs

If I try to offline cpu1 first, I get some kind of soft lockup whenever I try to offline further cpus.

It's a real mess.

I've been eyeballing the mc_smp.c code for quite some time and haven't been able to figure out what might be wrong, yet.

At least I've figured out why disabling cpu4 before disabling the rest of the cluster's cores led to system lockup previously. A83T has CPU0 mapped to bit 4 and not bit 0 of PRCM_PWROFF_GATING_REG. So when disabling CPU4, parts of the support circuitry for the cluster were disabled instead, locking up the system.

Maybe it's just 5.4-rc kernel I'm using. I'll try again with 5.3, if CPU hotplug works there. (Edit: yes, on 5.3 CPU hotplug works well.)

So with Linux 5.3, my CPU hotplug and other fixes, I finally arrived at some success with suspend to ram:

PM: suspend entry (deep)
Filesystems sync: 0.505 seconds
Freezing user space processes ... (elapsed 0.001 seconds) done.
OOM killer disabled.
Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done.
musb-sunxi 1c19000.usb: Error unknown readb offset 112
Disabling non-boot CPUs ...

[ ... sleeps here ... ]
[ I presses the volume buttonses, aaaand... ]

Enabling non-boot CPUs ...
CPU1 is up
CPU2 is up
CPU3 is up
CPU4 is up
CPU5 is up
CPU6 is up
CPU7 is up
musb-sunxi 1c19000.usb: Error unknown writeb offset 112
usb 1-1: reset high-speed USB device number 2 using ehci-platform
usb 1-1.3: reset full-speed USB device number 3 using ehci-platform
OOM killer enabled.
Restarting tasks ... done.
PM: suspend exit

Now I have to figure out a way to measure the power savings while the system is suspended. :) My multimeter doesn't like continous current measurement, and gives out a burning smell after a while.

Patches are in the orange-pi-5.3 branch.

Next steps with s2idle

After we get maximum power savings from s2idle, it will be time to investigate deeper sleep states and CPU/cluster powerdown using crust, because at that point we'll have some baseline suspend state against which to measure further improvements in power consumption in deeper sleep states. Suspending all possible devices is necessary anyway.

Suspend to idle (s2idle) – part 2

Resume of WiFi fixed

So it turns out WiFi/Bluetooth issues were related to powering down the WiFi during suspend.

Adding keep-power-in-suspend to mmc1 fixed the issues and BT/WiFi and s2idle now successfully performs the entire suspend/resume cycle without knocking out any hardware.

Also, apparently, it should be possible to wake the tablet using WOL packet over WiFi.

Suspend/resume log follows. Now, it's time to analyze what sunxi devices fail to be suspended, because they're missing proper PM code in Linux. The log should help. I'll try to get better log in the future that will be focussed on devices that were skipped because they didn't have PM callbacks.

PM: suspend entry (s2idle)
Filesystems sync: 0.151 seconds
PM: Adding info for No Bus:vcs63
PM: Adding info for No Bus:vcsu63
PM: Adding info for No Bus:vcsa63
Freezing user space processes ... (elapsed 0.002 seconds) done.
OOM killer disabled.
Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done.
vc vcsa63: direct-complete suspend
vc vcsu63: direct-complete suspend
vc vcs63: direct-complete suspend
vc vcsa6: direct-complete suspend
vc vcsu6: direct-complete suspend
vc vcs6: direct-complete suspend
vc vcsa5: direct-complete suspend
vc vcsu5: direct-complete suspend
vc vcs5: direct-complete suspend
vc vcsa4: direct-complete suspend
vc vcsu4: direct-complete suspend
vc vcs4: direct-complete suspend
vc vcsa3: direct-complete suspend
vc vcsu3: direct-complete suspend
vc vcs3: direct-complete suspend
vc vcsa2: direct-complete suspend
vc vcsu2: direct-complete suspend
vc vcs2: direct-complete suspend
rfkill rfkill1: class suspend
media media0: direct-complete suspend
video4linux v4l-subdev0: direct-complete suspend
video4linux video0: direct-complete suspend
snd-soc-dummy snd-soc-dummy: bus suspend
hci_uart_bcm serial0-0: driver suspend, may wakeup
iio iio:device1: direct-complete suspend
iio trigger0: direct-complete suspend
sound timer: direct-complete suspend
hm5065 2-001f: direct-complete suspend
 ep_00: direct-complete suspend
 ep_83: direct-complete suspend
net usb0: direct-complete suspend
 ep_02: direct-complete suspend
g_ether direct-complete suspend
g_ether gadget: direct-complete suspend
 ep_81: direct-complete suspend
net wlan0: direct-complete suspend
 ep_00: direct-complete suspend
 ep_81: direct-complete suspend
 ep_00: direct-complete suspend
 ep_81: direct-complete suspend
usb usb3: type suspend
musb-hdrc bus suspend, may wakeup
musb-sunxi 1c19000.usb: Error unknown readb offset 112
usb_phy_generic bus suspend
musb-sunxi 1c19000.usb: bus suspend
 ep_00: direct-complete suspend
rfkill rfkill0: class suspend
 ep_81: direct-complete suspend
block mmcblk2boot0: direct-complete suspend
usb usb2: type suspend
ieee80211 phy0: class suspend
bdi 179:16: direct-complete suspend
block mmcblk2boot1: direct-complete suspend
bdi 179:24: direct-complete suspend
block mmcblk2p2: direct-complete suspend
block mmcblk2p1: direct-complete suspend
block mmcblk2: direct-complete suspend
bdi 179:8: direct-complete suspend
vtconsole vtcon1: direct-complete suspend
graphics fb0: direct-complete suspend
drm card0-LVDS-1: direct-complete suspend
drm card0: direct-complete suspend
input event2: direct-complete suspend
input input2: type suspend
sun4i-tcon 1c0c000.lcd-controller: bus suspend
phy phy-1c19400.phy.2: direct-complete suspend
phy phy-1c19400.phy.1: direct-complete suspend
phy phy-1c19400.phy.0: direct-complete suspend
extcon extcon0: direct-complete suspend
sun4i-usb-phy 1c19400.phy: bus suspend
i2c-dev i2c-2: direct-complete suspend
i2c i2c-2: direct-complete suspend
block mmcblk0p2: direct-complete suspend
block mmcblk0p1: direct-complete suspend
i2c-gpio i2c-gpio: bus suspend
leds flash: class suspend
block mmcblk0: direct-complete suspend
bdi 179:0: direct-complete suspend
leds-gpio leds: bus suspend
 ep_00: direct-complete suspend
 ep_81: direct-complete suspend
mmcblk mmc2:0001: bus suspend
ehci-platform 1c1b000.usb: bus suspend
brcmfmac mmc1:0001:2: bus suspend
usb 1-1.3:1.0: direct-complete suspend
mmcblk mmc0:aaaa: bus suspend
usb 1-1.3: type suspend
usb 1-1: type suspend
usb usb1: type suspend
ehci-platform 1c1a000.usb: bus suspend
sunxi-mmc 1c11000.mmc: bus suspend
brcmfmac mmc1:0001:1: bus suspend
mmc mmc1:0001: bus suspend
sunxi-mmc 1c10000.mmc: bus suspend
sunxi-mmc 1c0f000.mmc: bus suspend
pwrseq_simple wifi_pwrseq: bus suspend
thermal cooling_device1: direct-complete suspend
cpu cpu4: direct-complete suspend
thermal cooling_device0: direct-complete suspend
cpu cpu0: direct-complete suspend
cpufreq-dt cpufreq-dt: bus suspend
sun8i-thermal 1f04000.ths: bus suspend
i2c 1-0028: direct-complete suspend
bma180 1-0018: driver suspend
i2c-dev i2c-1: direct-complete suspend
edt_ft5x06 0-0038: driver suspend, may wakeup
mv64xxx_i2c 1c2b000.i2c: bus suspend
i2c-dev i2c-0: direct-complete suspend
mv64xxx_i2c 1c2ac00.i2c: bus suspend
input event1: direct-complete suspend
input input1: type suspend
sun4i-a10-lradc-keys 1f03c00.lradc: bus suspend, may wakeup
panel-lvds panel: bus suspend
gnss gnss0: direct-complete suspend
gnss-ubx serial1-0: direct-complete suspend
serial serial1: direct-complete suspend
dw-apb-uart 1c28800.serial: bus suspend
dw-apb-uart 1c28400.serial: bus suspend
tty ttyS0: direct-complete suspend
dw-apb-uart 1c28000.serial: bus suspend
backlight backlight: class suspend
pwm-backlight backlight: bus suspend
pwm pwmchip0: class suspend
gpio gpiochip0: direct-complete suspend
gpio gpiochip2: direct-complete suspend
sun8i-a83t-pinctrl 1c20800.pinctrl: bus suspend
sun4i-pwm 1c21400.pwm: bus suspend
rtc rtc0: class suspend
ac100-rtc ac100-rtc: bus suspend
ac100-codec ac100-codec: bus suspend
leds chgled: class suspend
axp20x-leds axp20x-leds: bus suspend
reg-userspace-consumer reg-userspace-consumer: bus suspend
axp20x-usb-power-supply axp20x-usb-power-supply: bus suspend
platform axp20x-ac-power-supply: bus suspend
axp20x-battery-power-supply axp20x-battery-power-supply: bus suspend
iio iio:device0: direct-complete suspend
axp20x-adc axp813-adc: bus suspend
gpio gpiochip414: direct-complete suspend
gpio gpiochip1: direct-complete suspend
axp20x-gpio axp20x-gpio: bus suspend
regulator regulator.27: class suspend
regulator regulator.26: class suspend
regulator regulator.25: class suspend
regulator regulator.24: class suspend
regulator regulator.23: class suspend
regulator regulator.22: class suspend
regulator regulator.21: class suspend
regulator regulator.20: class suspend
regulator regulator.19: class suspend
regulator regulator.18: class suspend
regulator regulator.17: class suspend
regulator regulator.16: class suspend
regulator regulator.15: class suspend
regulator regulator.14: class suspend
regulator regulator.13: class suspend
regulator regulator.12: class suspend
regulator regulator.11: class suspend
regulator regulator.10: class suspend
regulator regulator.9: class suspend
regulator regulator.8: class suspend
regulator regulator.7: class suspend
regulator regulator.6: class suspend
regulator regulator.5: class suspend
regulator regulator.4: class suspend
axp20x-regulator axp20x-regulator: bus suspend
input event0: direct-complete suspend
input input0: type suspend
axp20x-pek axp221-pek: bus suspend
sunxi-rsb 1f03400.rsb: bus suspend
regulator regulator.3: class suspend
reg-fixed-voltage reg-vmain: bus suspend
misc cpu_dma_latency: direct-complete suspend
nvmem sunxi-sid0: direct-complete suspend
event_source CCI_400_r1: direct-complete suspend
misc device-mapper: direct-complete suspend
watchdog watchdog0: direct-complete suspend
misc watchdog: direct-complete suspend
ppp ppp: direct-complete suspend
net bond0: direct-complete suspend
a711 pwr-modem: direct-complete suspend
gpio gpiochip352: direct-complete suspend
gpio gpiochip0: direct-complete suspend
sun8i-a83t-r-pinctrl 1f02c00.pinctrl: bus suspend
block zram0: direct-complete suspend
bdi 254:0: direct-complete suspend
misc apm_bios: direct-complete suspend
tty ttyS7: direct-complete suspend
tty ttyS6: direct-complete suspend
tty ttyS5: direct-complete suspend
tty ttyS4: direct-complete suspend
tty ttyS3: direct-complete suspend
serial8250 serial8250: bus suspend
tty ptmx: direct-complete suspend
dma dma0chan38: direct-complete suspend
dma dma0chan37: direct-complete suspend
dma dma0chan36: direct-complete suspend
dma dma0chan35: direct-complete suspend
dma dma0chan34: direct-complete suspend
dma dma0chan33: direct-complete suspend
dma dma0chan32: direct-complete suspend
dma dma0chan31: direct-complete suspend
dma dma0chan30: direct-complete suspend
dma dma0chan29: direct-complete suspend
dma dma0chan28: direct-complete suspend
dma dma0chan27: direct-complete suspend
dma dma0chan26: direct-complete suspend
dma dma0chan25: direct-complete suspend
dma dma0chan24: direct-complete suspend
dma dma0chan23: direct-complete suspend
dma dma0chan22: direct-complete suspend
dma dma0chan21: direct-complete suspend
dma dma0chan20: direct-complete suspend
dma dma0chan19: direct-complete suspend
dma dma0chan18: direct-complete suspend
dma dma0chan17: direct-complete suspend
dma dma0chan16: direct-complete suspend
dma dma0chan15: direct-complete suspend
dma dma0chan14: direct-complete suspend
dma dma0chan13: direct-complete suspend
dma dma0chan12: direct-complete suspend
dma dma0chan11: direct-complete suspend
dma dma0chan10: direct-complete suspend
dma dma0chan9: direct-complete suspend
dma dma0chan8: direct-complete suspend
dma dma0chan7: direct-complete suspend
dma dma0chan6: direct-complete suspend
dma dma0chan5: direct-complete suspend
dma dma0chan4: direct-complete suspend
dma dma0chan3: direct-complete suspend
dma dma0chan2: direct-complete suspend
dma dma0chan1: direct-complete suspend
dma dma0chan0: direct-complete suspend
misc autofs: direct-complete suspend
event_source software: direct-complete suspend
event_source breakpoint: direct-complete suspend
clockevents broadcast: direct-complete suspend
clockevents clockevent7: direct-complete suspend
clockevents clockevent6: direct-complete suspend
clockevents clockevent5: direct-complete suspend
clockevents clockevent4: direct-complete suspend
clockevents clockevent3: direct-complete suspend
clockevents clockevent2: direct-complete suspend
clockevents clockevent1: direct-complete suspend
clockevents clockevent0: direct-complete suspend
 clockevents: direct-complete suspend
alarmtimer alarmtimer: bus suspend
clocksource clocksource0: direct-complete suspend
 clocksource: direct-complete suspend
platform regulatory.0: bus suspend
thermal thermal_zone2: direct-complete suspend
thermal thermal_zone1: direct-complete suspend
thermal thermal_zone0: direct-complete suspend
tty tty63: direct-complete suspend
tty tty62: direct-complete suspend
tty tty61: direct-complete suspend
tty tty60: direct-complete suspend
tty tty59: direct-complete suspend
tty tty58: direct-complete suspend
tty tty57: direct-complete suspend
tty tty56: direct-complete suspend
tty tty55: direct-complete suspend
tty tty54: direct-complete suspend
tty tty53: direct-complete suspend
tty tty52: direct-complete suspend
tty tty51: direct-complete suspend
tty tty50: direct-complete suspend
tty tty49: direct-complete suspend
tty tty48: direct-complete suspend
tty tty47: direct-complete suspend
tty tty46: direct-complete suspend
tty tty45: direct-complete suspend
tty tty44: direct-complete suspend
tty tty43: direct-complete suspend
tty tty42: direct-complete suspend
tty tty41: direct-complete suspend
tty tty40: direct-complete suspend
tty tty39: direct-complete suspend
tty tty38: direct-complete suspend
tty tty37: direct-complete suspend
tty tty36: direct-complete suspend
tty tty35: direct-complete suspend
tty tty34: direct-complete suspend
tty tty33: direct-complete suspend
tty tty32: direct-complete suspend
tty tty31: direct-complete suspend
tty tty30: direct-complete suspend
tty tty29: direct-complete suspend
tty tty28: direct-complete suspend
tty tty27: direct-complete suspend
tty tty26: direct-complete suspend
tty tty25: direct-complete suspend
tty tty24: direct-complete suspend
tty tty23: direct-complete suspend
tty tty22: direct-complete suspend
tty tty21: direct-complete suspend
tty tty20: direct-complete suspend
tty tty19: direct-complete suspend
tty tty18: direct-complete suspend
tty tty17: direct-complete suspend
tty tty16: direct-complete suspend
tty tty15: direct-complete suspend
tty tty14: direct-complete suspend
tty tty13: direct-complete suspend
tty tty12: direct-complete suspend
tty tty11: direct-complete suspend
tty tty10: direct-complete suspend
tty tty9: direct-complete suspend
tty tty8: direct-complete suspend
tty tty7: direct-complete suspend
tty tty6: direct-complete suspend
tty tty5: direct-complete suspend
tty tty4: direct-complete suspend
tty tty3: direct-complete suspend
tty tty2: direct-complete suspend
tty tty1: direct-complete suspend
vc vcsa1: direct-complete suspend
vc vcsu1: direct-complete suspend
vc vcs1: direct-complete suspend
vc vcsa: direct-complete suspend
vc vcsu: direct-complete suspend
vc vcs: direct-complete suspend
tty tty0: direct-complete suspend
tty console: direct-complete suspend
tty tty: direct-complete suspend
mem kmsg: direct-complete suspend
mem urandom: direct-complete suspend
mem random: direct-complete suspend
mem full: direct-complete suspend
mem zero: direct-complete suspend
mem null: direct-complete suspend
mem kmem: direct-complete suspend
mem mem: direct-complete suspend
misc rfkill: direct-complete suspend
net lo: direct-complete suspend
regulator regulator.2: class suspend
regulator regulator.1: class suspend
graphics fbcon: direct-complete suspend
workqueue blkcg_punt_bio: direct-complete suspend
workqueue writeback: direct-complete suspend
cpu cpu7: direct-complete suspend
cpu cpu6: direct-complete suspend
cpu cpu5: direct-complete suspend
cpu cpu3: direct-complete suspend
cpu cpu2: direct-complete suspend
cpu cpu1: direct-complete suspend
tbs_a711 modem: bus suspend
reg-fixed-voltage reg-vbat: bus suspend
reg-fixed-voltage reg-gps: bus suspend
platform 1f01c00.r_cpucfg: bus suspend
platform 1ef0000.hdmi-phy: bus suspend
sun6i-csi bus suspend
sunxi-wdt 1c20ca0.watchdog: bus suspend
platform 1c20c00.timer: bus suspend
sun8i-a83t-ccu 1c20000.clock: bus suspend
sun6i-msgbox 1c17000.mailbox: bus suspend
eeprom-sunxi-sid 1c14000.eeprom: bus suspend
sun4i-tcon 1c0d000.lcd-controller: bus suspend
sun6i-dma 1c02000.dma-controller: bus suspend
platform bus suspend
platform 1c00000.syscon: bus suspend
ARM-CCI PMU 1799000.pmu: bus suspend
platform 1795000.slave-if: bus suspend
platform 1794000.slave-if: bus suspend
ARM-CCI 1790000.cci: bus suspend
platform 1700000.cpucfg: bus suspend
sun8i-mixer 1200000.mixer: bus suspend
sun8i-mixer 1100000.mixer: bus suspend
sunxi-de2-clks 1000000.clock: bus suspend
sun4i-i2s 1c23000.dai: bus suspend
platform soc: bus suspend
platform scpi: bus suspend
sun4i-drm display-engine: bus suspend
platform timer: bus suspend
vtconsole vtcon0: direct-complete suspend
regulator regulator.0: class suspend
reg-dummy reg-dummy: bus suspend
 workqueue: direct-complete suspend
 container: direct-complete suspend
 cpu: direct-complete suspend

[... here it sleeps ...]

axp20x-pek axp221-pek: noirq driver resume
eg-dummy reg-dummy: bus resume
regulator regulator.0: class resume
platform timer: bus resume
sun4i-drm display-engine: bus resume
platform scpi: bus resume
platform soc: bus resume
sun4i-i2s 1c23000.dai: bus resume
ehci-platform 1c1a000.usb: bus resume
ehci-platform 1c1b000.usb: bus resume
usb usb1: type resume
sunxi-de2-clks 1000000.clock: bus resume
usb usb2: type resume
sun8i-mixer 1100000.mixer: bus resume
sun8i-mixer 1200000.mixer: bus resume
platform 1700000.cpucfg: bus resume
ARM-CCI 1790000.cci: bus resume
platform 1794000.slave-if: bus resume
platform 1795000.slave-if: bus resume
ARM-CCI PMU 1799000.pmu: bus resume
platform 1c00000.syscon: bus resume
platform bus resume
sun6i-dma 1c02000.dma-controller: bus resume
sun4i-tcon 1c0d000.lcd-controller: bus resume
eeprom-sunxi-sid 1c14000.eeprom: bus resume
sun6i-msgbox 1c17000.mailbox: bus resume
sun8i-a83t-ccu 1c20000.clock: bus resume
platform 1c20c00.timer: bus resume
sunxi-wdt 1c20ca0.watchdog: bus resume
sun6i-csi bus resume
platform 1ef0000.hdmi-phy: bus resume
platform 1f01c00.r_cpucfg: bus resume
reg-fixed-voltage reg-gps: bus resume
reg-fixed-voltage reg-vbat: bus resume
tbs_a711 modem: bus resume
regulator regulator.1: class resume
regulator regulator.2: class resume
platform regulatory.0: bus resume
alarmtimer alarmtimer: bus resume
serial8250 serial8250: bus resume
sun8i-a83t-r-pinctrl 1f02c00.pinctrl: bus resume
reg-fixed-voltage reg-vmain: bus resume
regulator regulator.3: class resume
usb 1-1: type resume
sunxi-rsb 1f03400.rsb: bus resume
axp20x-pek axp221-pek: bus resume
input input0: type resume
axp20x-regulator axp20x-regulator: bus resume
regulator regulator.4: class resume
regulator regulator.5: class resume
regulator regulator.6: class resume
regulator regulator.7: class resume
regulator regulator.8: class resume
regulator regulator.9: class resume
regulator regulator.10: class resume
regulator regulator.11: class resume
regulator regulator.12: class resume
regulator regulator.13: class resume
regulator regulator.14: class resume
regulator regulator.15: class resume
regulator regulator.16: class resume
regulator regulator.17: class resume
regulator regulator.18: class resume
regulator regulator.19: class resume
regulator regulator.20: class resume
regulator regulator.21: class resume
regulator regulator.22: class resume
regulator regulator.23: class resume
regulator regulator.24: class resume
regulator regulator.25: class resume
regulator regulator.26: class resume
regulator regulator.27: class resume
usb 1-1: reset high-speed USB device number 2 using ehci-platform
axp20x-gpio axp20x-gpio: bus resume
axp20x-adc axp813-adc: bus resume
axp20x-battery-power-supply axp20x-battery-power-supply: bus resume
platform axp20x-ac-power-supply: bus resume
axp20x-usb-power-supply axp20x-usb-power-supply: bus resume
reg-userspace-consumer reg-userspace-consumer: bus resume
axp20x-leds axp20x-leds: bus resume
leds chgled: class resume
ac100-codec ac100-codec: bus resume
ac100-rtc ac100-rtc: bus resume
rtc rtc0: class resume
sun4i-pwm 1c21400.pwm: bus resume
sun8i-a83t-pinctrl 1c20800.pinctrl: bus resume
pwm pwmchip0: class resume
pwm-backlight backlight: bus resume
backlight backlight: class resume
dw-apb-uart 1c28000.serial: bus resume
dw-apb-uart 1c28400.serial: bus resume
dw-apb-uart 1c28800.serial: bus resume
panel-lvds panel: bus resume
sun4i-a10-lradc-keys 1f03c00.lradc: bus resume
input input1: type resume
mv64xxx_i2c 1c2ac00.i2c: bus resume
mv64xxx_i2c 1c2b000.i2c: bus resume
edt_ft5x06 0-0038: driver resume
bma180 1-0018: driver resume
sun8i-thermal 1f04000.ths: bus resume
cpufreq-dt cpufreq-dt: bus resume
pwrseq_simple wifi_pwrseq: bus resume
sunxi-mmc 1c0f000.mmc: bus resume
sunxi-mmc 1c10000.mmc: bus resume
mmcblk mmc0:aaaa: bus resume
sunxi-mmc 1c11000.mmc: bus resume
mmc mmc1:0001: bus resume
brcmfmac mmc1:0001:1: bus resume
brcmfmac mmc1:0001:2: bus resume
ieee80211 phy0: class resume
leds-gpio leds: bus resume
mmcblk mmc2:0001: bus resume
leds flash: class resume
i2c-gpio i2c-gpio: bus resume
sun4i-usb-phy 1c19400.phy: bus resume
sun4i-tcon 1c0c000.lcd-controller: bus resume
input input2: type resume
rfkill rfkill0: class resume
musb-sunxi 1c19000.usb: bus resume
usb_phy_generic bus resume
musb-hdrc bus resume
musb-sunxi 1c19000.usb: Error unknown writeb offset 112
usb usb3: type resume
hci_uart_bcm serial0-0: driver resume
snd-soc-dummy snd-soc-dummy: bus resume
rfkill rfkill1: class resume
usb 1-1.3: type resume
usb 1-1.3: reset full-speed USB device number 3 using ehci-platform
usb 1-1.3: completing type resume
usb 1-1: completing type resume
usb usb3: completing type resume
usb usb2: completing type resume
usb usb1: completing type resume
OOM killer enabled.
Restarting tasks ... done.

Suspend to idle (s2idle)

So I had an idea to try suspend to idle, since it's a mode where the kernel will freeze the userspace and turn off all the SoC blocks it can using the runtime PM hooks.

It requires to compile the kernel with PM_SLEEP and then running:

# either
echo s2idle > /sys/power/mem_sleep
echo mem > /sys/power/state
# or just
echo freeze > /sys/power/state

The system suspends:

PM: suspend entry (s2idle)
Filesystems sync: 0.218 seconds
Freezing user space processes ... (elapsed 0.001 seconds) done.
OOM killer disabled.
Freezing remaining freezable tasks ... (elapsed 0.001 seconds) done.

First hurdle was that there's no way to wake the system up, at this point. Sad that!

So I added support for wakeup to the sun4i-lradc-keys driver:

From 0e9ffdc0572446d35b06b440c6e9dddb13b779bc Mon Sep 17 00:00:00 2001
From: Ondrej Jirman <>
Date: Tue, 22 Oct 2019 00:15:41 +0200
Subject: [PATCH] input: sun4i-lradc-keys: Add wakup support

Allow the driver to wakeup the system on key press.

Signed-off-by: Ondrej Jirman <>
 drivers/input/keyboard/sun4i-lradc-keys.c | 49 ++++++++++++++++++++++-
 1 file changed, 48 insertions(+), 1 deletion(-)

diff --git a/drivers/input/keyboard/sun4i-lradc-keys.c b/drivers/input/keyboard/sun4i-lradc-keys.c
index 4a796bed48ac..0f413fb63d8a 100644
--- a/drivers/input/keyboard/sun4i-lradc-keys.c
+++ b/drivers/input/keyboard/sun4i-lradc-keys.c
@@ -22,6 +22,8 @@
 #include <linux/module.h>
 #include <linux/of_platform.h>
 #include <linux/platform_device.h>
+#include <linux/pm_wakeirq.h>
+#include <linux/pm_wakeup.h>
 #include <linux/regulator/consumer.h>
 #include <linux/slab.h>
@@ -89,6 +91,7 @@ struct sun4i_lradc_data {
        u32 chan0_map_count;
        u32 chan0_keycode;
        u32 vref;
+       int irq;
 static irqreturn_t sun4i_lradc_irq(int irq, void *dev_id)
@@ -233,6 +236,8 @@ static int sun4i_lradc_probe(struct platform_device *pdev)
        if (!lradc)
                return -ENOMEM;
+       dev_set_drvdata(dev, lradc);
        error = sun4i_lradc_load_dt_keymap(dev, lradc);
        if (error)
                return error;
@@ -272,7 +277,13 @@ static int sun4i_lradc_probe(struct platform_device *pdev)
        if (IS_ERR(lradc->base))
                return PTR_ERR(lradc->base);
-       error = devm_request_irq(dev, platform_get_irq(pdev, 0),
+       lradc->irq = platform_get_irq(pdev, 0);
+       if (lradc->irq < 0) {
+               dev_err(&pdev->dev, "Failed to get IRQ\n");
+               return lradc->irq;
+       }
+       error = devm_request_irq(dev, lradc->irq,
                                 sun4i_lradc_irq, 0,
                                 "sun4i-a10-lradc-keys", lradc);
        if (error)
@@ -282,8 +293,43 @@ static int sun4i_lradc_probe(struct platform_device *pdev)
        if (error)
                return error;
+       device_init_wakeup(dev, true);
+       error = dev_pm_set_wake_irq(dev, lradc->irq);
+       if (error) {
+               dev_err(dev, "Could not set wake IRQ\n");
+               return error;
+       }
+       return 0;
+/* Enable IRQ wake on suspend, to wake up from keypress. */
+static int sun4i_lradc_suspend(struct device *dev)
+       struct sun4i_lradc_data *lradc = dev_get_drvdata(dev);
+       if (device_may_wakeup(dev))
+               enable_irq_wake(lradc->irq);
+       return 0;
+/* Disable IRQ wake on resume. */
+static int sun4i_lradc_resume(struct device *dev)
+       struct sun4i_lradc_data *lradc = dev_get_drvdata(dev);
+       if (device_may_wakeup(dev))
+               disable_irq_wake(lradc->irq);
        return 0;
+static SIMPLE_DEV_PM_OPS(sun4i_lradc_pm_ops,
+                        sun4i_lradc_suspend, sun4i_lradc_resume);
 static const struct of_device_id sun4i_lradc_of_match[] = {
        { .compatible = "allwinner,sun4i-a10-lradc-keys",
@@ -298,6 +344,7 @@ static struct platform_driver sun4i_lradc_driver = {
        .driver = {
                .name   = "sun4i-a10-lradc-keys",
                .of_match_table = of_match_ptr(sun4i_lradc_of_match),
+               .pm = &sun4i_lradc_pm_ops,
        .probe  = sun4i_lradc_probe,

Now I can wakeup from idle by pressing the volume keys:

printk: Suspending console(s) (use no_console_suspend to debug)
** 3073 printk messages dropped **
sunxi-mmc 1c10000.mmc: send stop command failed
sunxi-mmc 1c10000.mmc: data error, sending stop command
sunxi-mmc 1c10000.mmc: send stop command failed
sunxi-mmc 1c10000.mmc: data error, sending stop command
sunxi-mmc 1c10000.mmc: send stop command failed
sunxi-mmc 1c10000.mmc: data error, sending stop command
sunxi-mmc 1c10000.mmc: send stop command failed
sunxi-mmc 1c10000.mmc: data error, sending stop command
sunxi-mmc 1c10000.mmc: send stop command failed
sunxi-mmc 1c10000.mmc: data error, sending stop command
sunxi-mmc 1c10000.mmc: send stop command failed
sunxi-mmc 1c10000.mmc: data error, sending stop command
sunxi-mmc 1c10000.mmc: send stop command failed


brcmfmac: brcmf_sdio_dpc: failed backplane access over SDIO, halting operation
brcmfmac: brcmf_sdio_htclk: HT Avail request error: -110
brcmfmac: brcmf_sdio_dpc: failed backplane access over SDIO, halting operation
brcmfmac: brcmf_sdio_htclk: HT Avail request error: -110


Though the wakeup is not pretty, and ends up with a huge flood of messages from 1c10000.mmc, which is mmc1 interface where the WiFi SDIO is connected to.

There's also a loss of communication with the BT chip. Error -110 means ETIMEDOUT.

I checked the power draw during suspend to idle with a multimeter, and while the power draw drops a bit, it's not significant and is probably just a result of shutting down a bunch of devices, that can also be shut down at runtime.

Also s2idle will seemingly not shut down some devices, because there's quite a large difference in current draw between doing:

echo 1 > /sys/class/graphics/fb0/blank

and not doing it before s2idle. The screen goes blank in both cases, but current draw is much higher during s2idle without blanking the fb0 first.

The reason for failure to communicate with WiFi/BT after wakeup is unclear. mmc0 works after wakeup, and system can be powered off. It's possible that what is done with devices during system freeze/sleep states needs to be configured somewhere first.

Potentially dangerous powerdown issue

Also I noticed one other strange thing. Shutting down the system from Linux with powerdown command leaves the tablet still drawing 6mA from the battery. That will easily kill the battery in around 40–50 days if left unchecked.

Interestingly powering up the tablet to my u-boot boot menu, and using u-boot based poweroff drops this current to ~600uA, which allows for more tablet storage time without use.

Something probably stays on after Linux issues poweroff, and u-boot clears that up. It can be anything. Linux doesn't really do any deinitialization of HW on poweroff. It just sends a poweroff command to AXP813.

Audio codec AC100 on A83T

I've been trying to implement some basic audio playback using this codec.

My general approach is to make the SoC generate all the clocks and be the clock master. This way, no PLL setup is needed on AC100 chip. That eliminated one source of complexity.

Other simplification is to keep all DAPs (digital audio processing units) turned off. They are not necessary for volume control or playback.

The next simplification I wanted to use was to just add the controls necessary just for the playback path and ignore the rest. It turns out, that there are like 7 elements in the path, so this approach doesn't save much, because most of the things in the audio signal path need to be configured properly anyway.

I find writing SoC audio codec drivers quite error prone. A lot depends on string matching that C compiler will not check for me, there's a lot of duplication of identifiers, references between structs with long names, register references. Writing driver for a chip that has more than a few multiplexers, mixers and amplifiers, became confusing rather quickly. There's an awful amount of duplication, variable and register names usually differ just in 1 character and mentally the code just doesn't map easilly to how the codec is wired up internally. Information for one widget is spread in multiple structs all over the file. Not pleasant to work with.

So I changed my approach, and described the codec widgets in my device-tree like configuration language, which turned out to be rather straightforward, and I plan to generate most of the hard parts of C code from this description, while filling the blanks manually.

I've also described AC100 registers in my register description language, and I generate C header file from that. This also serves as a nice register debugging tool, as seen in this README.

It turns out, that basic structure of the routes/widgets in the codec can be fit into less than 300 lines of description. Now I'll just need to add some metadata to the widget nodes and write a code generator.

// Headphones

hpoutl_mux: mux {
        sources = &dac_left_mux, &out_left_mix

hpoutr_mux: mux {
        sources = &dac_right_mux, &out_right_mix

hpoutl_amp: amp {
        sources = &hpoutl_mux

hpoutr_amp: amp {
        sources = &hpoutr_mux
hpoutl: output {
        sources = &hpoutl_amp
hpoutr: output {
        sources = &hpoutr_amp

// Speaker

spoutl: output {
        sources = &spoutl_amp
spoutr: output {
        sources = &spoutr_amp

spoutl_amp: amp {
        sources = &spoutl_mix

spoutr_amp: amp {
        sources = &spoutr_mix

spoutl_mix: mixer {
        sources = &out_left_mix, &out_right_mix

spoutr_mix: mixer {
        sources = &out_left_mix, &out_right_mix

// Earphone

epout: output {
        sources = &epout_amp

epout_amp: amp {
        sources = &epout_mux

epout_mux: mux {
        sources = &dac_right_mux, &out_right_mix, &dac_left_mux, &out_left_mix

// Lineout

lineout: output {
        sources = &lineout_amp

lineout_amp: amp {
        sources = &lineout_mix

lineout_mix: mixer {
        sources = &out_right_mix, &out_left_mix, &mic_bst1, &mic_bst2

// DAC output

dac_left_mux: mux {
        sources = &dacl, &dacr

dac_right_mux: mux {
        sources = &dacr, &dacl

out_left_mix: mixer {
        sources = &dac_left_mux, &mic_bst1, &mic_bst2, &linein_amp, &linein_left, &auxin_left_amp

out_right_mix: mixer {
        sources = &dac_left_mux, &mic_bst1, &mic_bst2, &linein_amp, &linein_right, &auxin_right_amp

dacl: dac {
        sources = &dac_left_input_mix

dacr: dac {
        sources = &dac_right_input_mix

dac_left_input_mix: mixer {
        sources = &i2s0_slot0_left_input, &i2s0_slot1_left_input, &i2s1_left_input, &adc_left_out_mux

dac_right_input_mix: mixer {
        sources = &i2s0_slot0_right_input, &i2s0_slot1_right_input, &i2s1_right_input, &adc_right_out_mux

// ADC input

adcl: adc {
        sources = &adc_left_input_mix_amp

adcr: adc {
        sources = &adc_right_input_mix_amp

adc_left_out_mux: mux {
        sources = &adcl, &dmic

adc_right_out_mux: mux {
        sources = &adcr, &dmic

dmic: input {

adc_left_input_mix_amp: amp {
        sources = &adc_left_input_mix

adc_right_input_mix_amp: amp {
        sources = &adc_right_input_mix

adc_left_input_mix: mixer {
        sources = &mic_bst1, &mic_bst2, &linein_amp, &linein_left, &auxin_left_amp, &out_left_mix, &out_right_mix

adc_right_input_mix: mixer {
        sources = &mic_bst1, &mic_bst2, &linein_amp, &linein_right, &auxin_right_amp, &out_left_mix, &out_right_mix

// Inputs

mic1: input {

mic2: input {

mic3: input {

linein_left: input {

linein_right: input {

auxin_left: input {

auxin_right: input {

mic_bst1: amp {
        sources = &mic1

mic2_mux: mux {
        sources = &mic2, &mic3

mic_bst2: amp {
        sources = &mic2_mux

linein_amp: amp {
        sources = &linein_left, &linein_right

auxin_left_amp: amp {
        sources = &auxin_left

auxin_right_amp: amp {
        sources = &auxin_right

// Digital paths

i2s0: aif {
        i2s0_slot0_left_input: slot {
        i2s0_slot0_right_input: slot {
        i2s0_slot1_left_input: slot {
        i2s0_slot1_right_input: slot {
        i2s0_slot0_left_output: slot {
        i2s0_slot0_right_output: slot {
        i2s0_slot1_left_output: slot {
        i2s0_slot1_right_output: slot {

i2s1: aif {
        i2s1_left_input: slot {
        i2s1_right_input: slot {
        i2s1_left_output: slot {
        i2s1_right_output: slot {

i2s2: aif {
        i2s2_output: slot {
        i2s2_input: slot {

// two output channels
i2s1_input_mux: mux {
        sources = &i2s2_input, &i2s1_left_input, &i2s1_right_input

i2s2_output_mux: mux {
        sources = &i2s1_left_output, &i2s1_right_output

i2s0_slot0_left_output_mix: mixer {
        sources = &adc_left_out_mux, &i2s0_slot0_left_input, &i2s1_left_input, &i2s1_right_input

i2s0_slot0_right_output_mix: mixer {
        sources = &adc_right_out_mux, &i2s0_slot0_right_input, &i2s1_left_input, &i2s1_right_input

i2s0_slot1_left_output_mix: mixer {
        sources = &adc_left_out_mux, &i2s1_left_input

i2s0_slot1_right_output_mix: mixer {
        sources = &adc_right_out_mux, &i2s1_right_input

i2s1_right_output_mix: mixer {
        sources = &adc_right_out_mux, &i2s0_slot0_right_input, &i2s0_slot1_right_input, &i2s1_right_input

i2s1_left_output_mix: mixer {
        sources = &adc_left_out_mux, &i2s0_slot0_left_input, &i2s0_slot1_left_input, &i2s1_left_input

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>;

                        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",
                        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:


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


#define FIRMWARE_BASE  0x00004000

#define SCPI_MEM_BASE  0x00013c00

#define SRAM_A2_BASE   0x00000000
#define SRAM_A2_LIMIT  0x00014000

#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 tbs_a711_defconfig
make V=1

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.


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 (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 (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:


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:


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 <>

#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",
        uc_priv->size_y = dev_read_u32_default(dev, "touchscreen-size-y",

        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 <>
 * Based on the Linux driver drivers/input/touchscreen/edt-ft5x06.c (v4.18):
 * Copyright (C) 2012 Simon Budig, <>
 * Daniel Wagener <> (M09 firmware support)
 * Lothar Waßmann <> (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>


#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 {

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;

        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;

        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);

                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]) {
                                "crc error: 0x%02x expected, got 0x%02x\n",
                                wrbuf[0] ^ wrbuf[1] ^ rdbuf[0],
                        return -EIO;

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

                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);
                case 0xa1:   /* EDT EP1010ML00 */
                        priv->version = EDT_M09;
                        snprintf(model_name, EDT_NAME_LEN, "EP%i%i0ML00",
                                rdbuf[0] >> 4, rdbuf[0] & 0x0F);
                case 0x5a:   /* Solomon Goldentek Display */
                        snprintf(model_name, EDT_NAME_LEN, "GKTW50SCED1R0");
                        snprintf(model_name, EDT_NAME_LEN,
                                 "generic ft5x06 (%02x)",

        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;

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,
        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;

        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;

        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;

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 */

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

                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)

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

                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)

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

        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;


        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,
        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 <>
#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,

        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 */

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

        if (c)
                return c->cmd(cmdtp, flag, argc, argv);
                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 <>
#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;

                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;

                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;

        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);
                                               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)

                        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;

        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).