
Most of the things we abandon were once exciting—they began as play. Then, almost without noticing, we turned them into work. Track them, schedule them, tie them to progress and identity, and tell yourself you have to improve. The fun drained out; we change how we frame the activity. The same mind that resists ten forced minutes can happily work all day when something feels optional, rich with reward, and free of burden. In other words, we don’t lose interest, we just load weight onto it until we drown in it.
Some definitions:
Work is done when trying to rid of something (a nagging boss or a long to-do list) or to achieve money or power
Play is done in expectation of a reward: chemical (coffee, nicotine), sense (pleasant visuals or sound, good flavor or scent, touch), sexual (tension, release), novelty, conceptual elegance or power, expression (of self, ideas)
Meditation is when aware of the present activity more than the future relief or reward
The mistake is thinking these are fixed categories. They aren’t. We slide activities from one column to another without noticing. There can be substantial overlap between the three groups. To make an activity more pleasant, palatable, or likely to happen, one needs to add or increase the amount and/or variety of rewards. Conversely, to decrease or stop an activity, remove rewards and reframe it as the pursuit of money, power, or a mere checking off of boxes.
In other words, make it something you have to do, introduce an accountability partner, tie your survival and social status with it, and you will have succeeded in making it a thoroughly unpleasant activity that will happen only if there’s really no other option. But often our intentions or desires are quite the contrary: the activity is to be encouraged and made a habit.
Add rewards but reframe it as 100% optional. It never has to be done, there’s no time allocated for it, no one will get angry if you don’t do it. There’s a certain sense of “emptiness” associated with it: the activity doesn’t consume space in your mind, it’s just out there if you want to do it in a given moment. There’s no conscious tracking or keeping score (although an external reward mechanism could do the tracking, such as the elaborate programs that make social media so compelling). There’s no sense of sacrifice or dedication. With all these negatives, it may seem surprising anything would happen at all. But the underlying truth is that things arise out of nothingness: when the mind is empty, you can take on a new project, but not if you’re already juggling ten of them.
So what happened with me with a fun hobby activity: it started with the promise of novelty, as an escape from other work-like things I was doing at the time. When the novelty wore off, there was no other reward and I was left with a sense of “I have to do it”, as if I have to practice to get somewhere with it. But no one was measuring or noticing my progress, if there ever was any. Add to it the setup cost (having to clear the table to setup the tools for the activity every time) and now I don’t do it at all.
Or to take instrument practice: you may have to do it because there’s a lesson every two weeks or so, but the subconscious mind makes sure to do all in its power to absolutely minimize the time spent on the activity to exactly and only the amount you force yourself through various spreadsheets and plans and goals: about ten minutes a day, after which you’re convinced that you’ve totally spent your “practice credits” and that my brain would be unable to continue at all until they get refilled overnight. But that’s not how the brain works: one can sustain an intellectual activity for eight hours a day or even more, given sufficient reward quantity and, crucially, type.
To take another example, hobby programming. I started working on an app and there was freshness and quick progress, and then the thing became much more difficult, requiring to keep in mind way too much of the program structure. The initial excitement wore off and progress stopped, and the brain thought it cannot touch the code ever again. Recently I split it into tiny programs and it feels fun again, though time will tell whether that’s not just the thrill of something new again. Having said that, on hobby projects, if they’re short enough, it’s actually entirely okay to be motivated by curiosity and the excitement of exploration. Surely that’s a part of what hobbies are for: find out what’s worthwhile, and then choose one or two things and make it your primary work for a deep dive into the topic.
You can only keep exploring new things if they’re short enough so they can be completed before the sense of newness wears out. What instead happens to me is that initial enthusiasm introduces me to big projects which then get left in a half-finished (or barely-started) state when a new one comes along. Then before I know it there’s a list of 150 things to do in my pipeline, none of which give any satisfaction since they go nowhere, but they still exert a nagging sense of “I should do this”, “I should work on this”.
I have recently restricted myself in such a way that new novelty projects go at the bottom of the 150-item list, while I only work on the things on the top of this pipeline, so a queue data structure. The queue length is growing, but being just a text file on a 2TB SSD, it’ll be a little while before I max out the capacity. It’s been just a few days or weeks, but already I’m feeling the futility of starting new things to escape the present. The new things become the present! The hedonic wheel spins faster and faster, while everything else stays the same, until I fall over in exhaustion, stop the wheel, dump my to-do list, and start from scratch.
Much the same happens when shopping: an exciting new thing is bought, placed on shelf, integrated into the background noise, another new thing is bought, and the endless loop of wasting money is joined. I’d do well to append the things to buy as a list in a text file rather than stuff taking up shelf space in the apartment. In fact, so far this year I have dramatically cut down on online shopping, with happy effect on the bank balance but no observable downside. The thought I got in Sedona was the right one: when I get the urge to spend money, I can go spend it on a cause I believe in. Give it to Wikipedia, or my favorite religious organization (if I had any); donate to a school or university; plan a trip with my love.
Then there’s the futility of ownership: if I have something inside “my” apartment, does that mean I own it? Just because others are barred from using it, doesn’t really make it particularly “mine” in any significant sense. Ownership in this sense makes sense for things that I use all the time or would be unwise to share with others: my watch, some clothes, toothbrush. On the other hand, I don’t gain much by keeping my books to myself, or the GPU that I use once a month. If only there was a good way of sharing these things!
The internet is magical as a way of sharing things without yourself losing any of it. Quite the contrary: the people who are best at sharing their work openly gain tremendously: others contribute to the work and they grow in respect etc.

This is Part 10 in the series: Linux on STM32MP135. See other articles.
The display backlight is controlled via a single GPIO:
panel_backlight: panel-backlight {
compatible = "gpio-backlight";
gpios = <&gpiob 15 GPIO_ACTIVE_HIGH>;
default-on;
default-brightness-level = <1>;
status = "okay";
};With that, the backlight can be turned on and off as follows:
# echo 1 > /sys/class/backlight/panel-backlight/brightness
# echo 0 > /sys/class/backlight/panel-backlight/brightnessThere’s no tricks to this, so long as we make sure that the GPIO mentioned in the DTS matches the one on the circuit schematic.
To control the brightness, we need to enable the PWM corresponding to this GPIO pin and make sure the panel backlight is controlled by this PWM.
We have already implemented this previously
in bare-metal using the HAL driver. We can check the TIM1, GPIOB, and
GPIOC registers to see what is the effect of our configuration. In the
following table, “Before” and “After” are on bare metal before and after
configuring the PWM output.
| Register | Address | Before | After |
|---|---|---|---|
TIM1_CR1 | 0x44000000 | 0x00000000 | 0x00000001 |
TIM1_SR | 0x44000010 | 0x00000000 | 0x0003001f |
TIM1_CCMR2 | 0x4400001c | 0x00000000 | 0x00000068 |
TIM1_CCER | 0x44000020 | 0x00000000 | 0x00000400 |
TIM1_CNT | 0x44000024 | 0x00000000 | 0x000001eb |
TIM1_PSC | 0x44000028 | 0x00000000 | 0x00000063 |
TIM1_ARR | 0x4400002c | 0x00000000 | 0x000003e7 |
TIM1_CCR3 | 0x4400003c | 0x00000000 | 0x000001f4 |
TIM1_BDTR | 0x44000044 | 0x00000000 | 0x00008000 |
TIM1_DMAR | 0x4400004c | 0x00000000 | 0x00000001 |
TIM1_AF1 | 0x44000060 | 0x00000000 | 0x00000001 |
TIM1_AF2 | 0x44000064 | 0x00000000 | 0x00000001 |
TIM1_VERR | 0x440003f4 | 0x00000000 | 0x00000035 |
TIM1_IPIDR | 0x440003f8 | 0x00000000 | 0x00120002 |
TIM1_SIDR | 0x440003fc | 0x00000000 | 0xa3c5dd01 |
GPIOB_MODER | 0x50003000 | 0xffbfffff | 0xbfbfffff |
GPIOB_AFR[1] | 0x50003024 | 0x0000b000 | 0x1000b000 |
GPIOC_IDR | 0x50004010 | 0x00001f00 | 0x00000f00 |
RCC_MP_APB2ENSETR | 0x50000708 | 0x00000000 | 0x00000001 |
RCC_MP_APB2ENCLRR | 0x5000070c | 0x00000000 | 0x00000001 |
The bare-metal bootloader configures the PWM for 3.0489 kHz operating with 50% duty cycle.
Now let’s configure the PWM in the DTS as follows:
timers1: timer@44000000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32-timers";
reg = <0x44000000 0x400>;
interrupts = <GIC_SPI 25 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 28 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "brk", "up", "trg-com", "cc";
clocks = <&rcc TIM1_K>;
clock-names = "int";
status = "okay";
pwm1: pwm {
compatible = "st,stm32-pwm";
#pwm-cells = <3>;
status = "okay";
pinctrl-0 = <&pwm1_pins>;
pinctrl-names = "default";
};When the PWM driver module is inserted, we can control the screen brightness as follows:
modprobe pwm-stm32
cd /sys/class/pwm/pwmchip0
echo 2 > export
echo 1000000 > pwm2/period
echo 500000 > pwm2/duty_cycle
echo 1 > pwm2/enableAfter that, the registers read:
| Register | Value |
|---|---|
TIM1_CR1 | 0x00000081 |
TIM1_SR | 0x0003001f |
TIM1_CCMR2 | 0x00000068 |
TIM1_CCER | 0x00000500 |
TIM1_CNT | 0x00008a35 |
TIM1_PSC | 0x00000004 |
TIM1_ARR | 0x0000ee33 |
TIM1_CCR3 | 0x0000771a |
TIM1_BDTR | 0x00008000 |
TIM1_DMAR | 0x00000081 |
TIM1_AF1 | 0x00000001 |
TIM1_AF2 | 0x00000001 |
TIM1_VERR | 0x00000035 |
TIM1_IPIDR | 0x00120002 |
TIM1_SIDR | 0xa3c5dd01 |
GPIOB_MODER | 0xbfbf7fff |
GPIOB_AFR[1] | 0x1000b000 |
GPIOC_IDR | 0x00001f00 |
RCC_MP_APB2ENSETR | 0x00000001 |
RCC_MP_APB2ENCLRR | 0x00000001 |
Finally, we need to make the panel backlight driver automatically talk to the PWM driver so that the backlight can be adjusted as brightness and not as PWM. We add the following to the DTS:
panel_backlight: panel-backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 2 1000000 0>;
brightness-levels = <0 16 32 64 128 255>;
default-brightness-level = <4>;
power-supply = <&v3v3_ao>;
default-on;
status = "okay";
};The panel_backlight is referenced from panel_rgb, but still does not turn on
automatically. Apparently the pwm-backlight driver does not have a
DTS-controlled “power on” option, so we have to turn it on manually:
echo 0 > /sys/class/backlight/panel-backlight/bl_powerWith that, the backlight brightness and power control work as they should via
the /sys/class/backlight/ controls.
Let’s add two lines to the Buildroot configuration to make the modetest
command available:
BR2_PACKAGE_LIBDRM=y
BR2_PACKAGE_LIBDRM_INSTALL_TESTS=yI am using the Rocktech RK050HR01-CT LCD display and the following DTS
fragment:
panel_rgb: panel-rgb {
compatible = "rocktech,rk050hr18-ctg", "panel-dpi";
enable-gpios = <&gpiog 7 GPIO_ACTIVE_HIGH>;
backlight = <&panel_backlight>;
power-supply = <&v3v3_ao>;
data-mapping = "rgb565";
status = "okay";
width-mm = <108>;
height-mm = <65>;
port {
panel_in_rgb: endpoint {
remote-endpoint = <<dc_out_rgb>;
};
};
panel-timing {
clock-frequency = <24000000>;
hactive = <800>;
vactive = <480>;
hsync-len = <4>;
hfront-porch = <8>;
hback-porch = <8>;
vsync-len = <4>;
vfront-porch = <16>;
vback-porch = <16>;
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <1>;
};
};Run just modetest to determine the numbers (CRTC 41, connector 32) of the
display:
# modetest | head
trying to open device '/dev/dri/card0'... done
opened device `STMicroelectronics SoC DRM` on driver `stm` (version 1.0.0 at 20170330)
Encoders:
id crtc type possible crtcs possible clones
31 41 DPI 0x00000001 0x00000001
Connectors:
id encoder status name size (mm) modes encoders
32 31 connected DPI-1 108x65 1 31Modetest is now able to display a test pattern on the LCD with the following command:
# modetest -M stm -s 32@41:800x480
opened device `STMicroelectronics SoC DRM` on driver `stm` (version 1.0.0 at 20170330)
setting mode 800x480-56.72Hz on connectors 32, crtc 41Adjust the pin configuration in the DTS according to the way the PCB is wired. For completeness, here’s mine:
i2c5: i2c@4c006000 {
compatible = "st,stm32mp13-i2c";
reg = <0x4c006000 0x400>;
interrupt-names = "event", "error";
interrupts = <GIC_SPI 114 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 115 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc I2C5_K>;
resets = <&rcc I2C5_R>;
#address-cells = <1>;
#size-cells = <0>;
st,syscfg-fmp = <&syscfg 0x4 0x10>;
i2c-analog-filter;
feature-domains = <&etzpc STM32MP1_ETZPC_I2C5_ID>;
pinctrl-names = "default", "sleep";
pinctrl-0 = <&i2c5_pins_a>;
pinctrl-1 = <&i2c5_sleep_pins_a>;
i2c-scl-rising-time-ns = <170>;
i2c-scl-falling-time-ns = <5>;
clock-frequency = <400000>;
status = "okay";
goodix: goodix-ts@5d {
compatible = "goodix,gt911";
reg = <0x5d>;
pinctrl-names = "default";
pinctrl-0 = <&goodix_pins_a>;
interrupt-parent = <&gpioh>;
interrupts = <12 IRQ_TYPE_EDGE_FALLING>;
reset-gpios = <&gpiob 7 GPIO_ACTIVE_LOW>;
AVDD28-supply = <&v3v3_ao>;
VDDIO-supply = <&v3v3_ao>;
touchscreen-size-x = <800>;
touchscreen-size-y = <480>;
status = "okay" ;
};
};The I2C5 pins are set as follows:
i2c5_pins_a: i2c5-0 {
pins {
pinmux = <STM32_PINMUX('H', 13, AF4)>, /* I2C5_SCL */
<STM32_PINMUX('F', 3, AF4)>; /* I2C5_SDA */
bias-disable;
drive-open-drain;
slew-rate = <0>;
};
};Now verify that the CTP driver has been initialized correctly:
# dmesg | grep -i good
[ 1.071181] Goodix-TS 0-005d: ID 911, version: 1060
[ 1.074418] input: Goodix Capacitive TouchScreen as /devices/platform/soc/5c007000.etzpc/4c006000.i2c/i2c-0/0-005d/input/input0Let’s use evtest (add BR2_PACKAGE_EVTEST=y in Buildroot) to verify that
we can receive touchpad events. With evtest running, we can touch the
touchpad and see that the events are streaming in:
# evtest
Event: time 54.972748, -------------- SYN_REPORT ------------
Event: time 54.976560, type 3 (EV_ABS), code 53 (ABS_MT_POSITION_X), value 575
Event: time 54.976560, type 3 (EV_ABS), code 54 (ABS_MT_POSITION_Y), value 236
Event: time 54.976560, type 3 (EV_ABS), code 0 (ABS_X), value 575
Event: time 54.976560, type 3 (EV_ABS), code 1 (ABS_Y), value 236
This is Part 9 in the series: Linux on STM32MP135. See other articles.
In this article we will take the custom STM32MP135 board through the end of the Linux boot process. In a previous article we saw the “Booting Linux” message for the first time; now it’s time to run our own programs under the OS and configure some of the devices.
Having fixed the DDR issues (swizzling gone wrong) in a new revision of the board, wrote the 50 lines of Makefile needed to build the bootloader and kernel and the root file system, it only remains to fix up the device tree source.
The STMicroelectronics kernel repository contains a comprehensive DTS that works on the evaluation board, together with the pin assignment file. It’s an excellent starting point since only a few lines need to be adjusted to accommodate the slightly different pinout used on the larger BGA package (0.8mm) used on my custom board. See the DTS files here for the final result.
The board started to boot but did not find the root filesystem on the SD card, even though the bootloader successfully copied the kernel from the card into DDR. As it turns out, the board miswired the “card detect” pin, so we have to ignore it in the DTS:
&sdmmc1 {
...
broken-cd;
...
}With the fixed DTS, the boot process finishes with this:
[ 1.332380] Run /sbin/init as init process
Hello, world!
$ hello
hello: command not foundNaturally the hello command does not exist, nor any other. Mind you, my
init
(PID = 1) program is essentially just “Hello, world!” and entirely vibecoded at
that.
The next step is to have a more useful set of tools installed on the target, such as BusyBox. We can easily enough build it as follows:
git clone git://busybox.net/busybox.git
cd busybox
make menuconfig # select static build
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
But when we copy the resulting binary as init, we discover that it would
really like to have a proper inittab, and a couple device files. With the
“Hello, world!” init, we created the root filesystem as follows:
root:
mkdir -p build/rootfs.dir/sbin
cp build/init build/rootfs.dir/sbin/init
dd if=/dev/zero of=build/rootfs bs=1M count=10
mke2fs -t ext4 -F -d build/rootfs.dir build/rootfsBut to add the device files we need to work as root or do something more clever.
It’s reasonable to yield to the temptation of build automation. (Why the hesitation? Package management and build systems inevitably result in a massive increase in complexity of a system, because it becomes easy to add more stuff into a build.)
Obtain Buildroot:
git clone https://gitlab.com/buildroot.org/buildroot.git
Create a very minimal buildroot/.config such as the following:
BR2_arm=y
BR2_cortex_a7=y
BR2_TOOLCHAIN_BUILDROOT_UCLIBC=y
BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_6_1=y
BR2_CCACHE=y
BR2_CCACHE_DIR="/tmp/buildroot-ccache"
BR2_ENABLE_DEBUG=y
BR2_SHARED_STATIC_LIBS=y
BR2_ROOTFS_DEVICE_CREATION_STATIC=y
BR2_TARGET_GENERIC_ROOT_PASSWD="root"
BR2_ROOTFS_OVERLAY="board/stmicroelectronics/stm32mp135-test/overlay"
BR2_PACKAGE_DROPBEAR=y
BR2_PACKAGE_IPERF3=y
BR2_TARGET_ROOTFS_EXT2=y
BR2_TARGET_ROOTFS_EXT2_4=y
BR2_TARGET_ROOTFS_EXT2_SIZE="5M"
Note the overlay directory: it can be empty, or you can use it to copy any
additional files onto the target filesystem. Run make and a long time later
(enough to get and build the toolchain and the two selected packages) the root
file system image will appear under buildroot/output/images.
The advantage of this approach is that we can quickly and directly rebuild the kernel and the DTB without invoking Buildroot, while being able to quickly get all the packages compiled from Buildroot and included in the target system.
So far, installing a new DTB required rebuilding the SD image and writing it to the target using our bootloader. Since DTB is a tiny file, it’s much faster if we could just change the DTB and not the whole SD card image. Let’s do it over ssh so as to easily automate it from the “50 line Makefile”.
First we need to make sure the target has an SSH server. This is provided by the Dropbear package that we have added to the Buildroot configuration. By default, ssh will ask for a password each time which is tedious and insecure. Instead, let’s copy our public key to the overlay directory:
cp ~/.ssh/id_ed25519.pub overlay/root/.ssh/authorized_keysMoreover, each time Dropbear is started from a “clean” Buildroot-generated rootfs, it will generate its own host key which we’ll have to manually accept. Instead, let’s pre-seed the target key by copying the generated key from the target one time (replace the IP address with that of your target):
mkdir -p overlay/etc/dropbear
scp root@172.25.0.132:/etc/dropbear/dropbear_ed25519_host_key overlay/etc/dropbearFinally, to install the new DTB over ssh, copy it over and then write it to the second SD card partition:
ssh root@172.25.0.132 "dd of=/dev/mmcblk0p2" < linux/arch/arm/boot/dts/custom.dtbAfter installing the new DTB we need to reboot the system. This can be done by power cycling the board, but that quickly get tiresome. In the default configuration, the PSCI interface communicates with the “secure” OS or bootloader (TF-A), but to my mind that’s complexity we can do without.
Thus, we need to implement a “restart handler” so that the reboot command will
be able to reboot the system. It will be very simple: it needs to flip one bit
in the GRSTCSETR register. As we can read in the RM0475 reference manual for
this SoC, that register has only one bit, called MPSYSRST, where writing ‘1’
generates a system reset.
Notice that the device tree already defines a driver to talk to the RCC unit:
rcc: rcc@50000000 {
compatible = "st,stm32mp13-rcc", "syscon";
reg = <0x50000000 0x1000>;
#clock-cells = <1>;
#reset-cells = <1>;
clock-names = "hse", "hsi", "csi", "ck_lse", "lsi";
clocks = <&clk_hse>,
<&clk_hsi>,
<&clk_csi>,
<&clk_lse>,
<&clk_lsi>;
};This st,stm32mp13-rcc driver is to be found under
drivers/clk/stm32/clk-stm32mp13.c. First we define the register and bit needed
to execute the reset:
#define RCC_MP_GRSTCSETR 0x114
#define RCC_MP_GRSTCSETR_MPSYSRST BIT(0)Then the reset handler is very simple:
static int stm32mp1_restart(struct sys_off_data *data)
{
void __iomem *rcc_base = data->cb_data;
pr_info("System reset requested...\n");
dsb(sy);
writel(RCC_MP_GRSTCSETR_MPSYSRST, rcc_base + RCC_MP_GRSTCSETR);
while (1)
wfe();
return NOTIFY_DONE;
}Finally, we need to register this reset handler. A good place is at the bottom
of stm32mp1_rcc_init():
devm_register_sys_off_handler(dev, SYS_OFF_MODE_RESTART,
SYS_OFF_PRIO_HIGH, stm32mp1_restart, rcc_base);
Complex problems can be broken down into steps that are small and easy, or, at worst, small and tedious. A big chess game unfolds move by move. In life too, you can only make one move at a time, and then it’s the universe’s turn.
The only question is, what is my move right now? But few ask it. Be aware!

In the previous article, we compiled a “blink” program and sent it to the ADSP-2156 chip via UART. Now we can try to do the same over a faster interface: QSPI.
The quad SPI2 interface on the evaluation board
EV-SOMCRR-EZLITE
is connected to the FT4222HQ-D-T chip that connects to one of the USB-C ports
on the carrier boards.
FTDI provides software examples for the FT4222HQ on various operating systems. There is also a Python library, ft4222, which we’ll use for this test.
First we make sure the computer sees the FTDI chip:
import ft4222
for i in range(ft4222.createDeviceInfoList()):
print(ft4222.getDeviceInfoDetail(i, False))In the printout, we see FT4222 A and FT4222 B. The first one is used for I2C
and SPI, and the second for GPIO.
The same FTDI part also controls an I2C GPIO expander, ADP5587ACPZ-1, which is
in charge of the LEDs on the carrier boards as well as the QSPI interface. We’ll
be interested in the following pins at U22:
C0 = USBI_SPI0_EN*
C1 = USBI_SPI1_EN*
C2 = USB_QSPI_EN*
C3 = USB_QSPI_RESET*
C4 = ETH0_RESET
C5 = ADAU1372_PWRDWN*
C6 = PUSHBUTTON_EN
C7 = DS8 (green LED on SOMCRR)
C8 = DS7 (green LED on SOMCRR)
C9 = DS6 (green LED on SOMCRR)
Let’s open the I2C device:
dev = ft4222.openByDescription('FT4222 A')
dev.i2cMaster_Init(100)We can write to it by means of this helper function:
def wr(d):
for a, b, c in d:
dev.i2cMaster_WriteEx(
a, ft4222.I2CMaster.Flag.START_AND_STOP, bytes([b, c]))First, we’ll make sure that all pins are set as GPIO (as opposed to “keypad” mode):
wr([(0x30, 0x1D, 0x00)]) # R7:0 -> GPIO
wr([(0x30, 0x1E, 0x00)]) # C7:0 -> GPIO
wr([(0x30, 0x1F, 0x00)]) # C9:8 -> GPIONext, we would like to set the pin output states to USBI_SPI0,1 disabled,
USB_QSPI neither enabled nor under reset, ETH0 reset asserted, ADAU1372
powered down, pushbutton disabled, LEDs off:
# default output config
wr([(0x30, 0x18, 0b1000_1111)])
wr([(0x30, 0x19, 0b0000_0011)])Finally, to make this configuration active, we enable the outputs on all port-C
pins. Make sure to do this after configuring the pin output states, otherwise
you can put the FT4222 into reset (pins default to 0 output states, and pulling
USB_QSPI_RESET* low puts the FT4222 into reset).
wr([(0x30, 0x24, 0xff)]) # C7:0 -> output
wr([(0x30, 0x25, 0xff)]) # C9:8 -> outputTo test that this all works, we can enable the three green LEDs on the SOMCRR one by one:
wr([(0x30, 0x18, 0b0001_1111)]) # enable LED DS8
wr([(0x30, 0x19, 0b0000_0010)]) # enable LED DS7
wr([(0x30, 0x19, 0b0000_0000)]) # enable LED DS6These are the green LEDs on the SOMCRR board, not the yellow ones on the SOM board.
The FT4222 can act either as an SPI master or a slave, and so can the SHARC. Both modes are of interest to us:
| FT4222 | SHARC | S5 Set | Application |
|---|---|---|---|
| Master | Slave | 2-3 / Dn | Boot SHARC from USB |
| Slave | Master | 1-2 / Up | Transmit data from SHARC to USB |
S5 is the slide switch located on the SOMCRR board next to the USB-C QSPI
connector. With the SOMCRR board oriented so the USB-C connectors are on the
top, the S5 slider is in the 1-2 position when it is pushed up (towards the
USB-C connectors), and 2-3 when it is down (away from USB-C connectors).
To enable the SPI boot, we need to make three hardware selections:
Enable the SPI interface by pulling USB_QSPI_EN* low:
wr([(0x30, 0x18, 0b1001_1011)])Make sure to follow the steps from the previous section to set the GPIO expander into GPIO mode, set to correct output defaults, etc., or else it will not work!
Flip the S5 switch up so that the 2-3 position is selected, making the
FT4222 the SPI master. The switch slider must face away from the USB-C
connectors.
Set the rotary boot mode selector switch to mode 2 decimal (010 binary, “External SPI2 host”).
While the elfloader utility supports both -b UARTHOST and -b SPIHOST
flags, it appears to generate the same bootstream with either flag. So, let’s
try to use the same bootstream as we had used in the Blink example from the
previous article, except to make sure to send 0x03
as the first byte as instructed by the SHARC manual:
boot_buffer = bytearray([0x03]) # SPICMD = 0x3: Keep single-bit mode
with open('blink.ldr', 'r') as f:
for line in f:
boot_buffer.append(int(line.strip(), 16))The SHARC hardware reference manual describes SPI mode 1 where the clock idles low and data is sampled on falling edge and shifted out on rising edge:
In SPI slave boot mode, the boot kernel sets the
SPI_CTL.CPHAbit and clears theSPI_CTL.CPOLbit. Therefore theSPI_MISOpin is latched on the falling edge of theSPI_MOSIpin.
With that in mind, we can initialize the FT4222 device for SPI as follows, send the bootstream, and close the device:
dev = ft4222.openByDescription('FT4222 A')
dev.spiMaster_Init(
ft4222.SPIMaster.Mode.SINGLE,
ft4222.SPIMaster.Clock.DIV_8,
ft4222.SPI.Cpol.IDLE_LOW,
ft4222.SPI.Cpha.CLK_TRAILING,
ft4222.SPIMaster.SlaveSelect.SS0)
dev.spiMaster_SingleWrite(boot_buffer, True)
dev.close()On my first attempt, this did not work since I had the S5 switch set in the
wrong polarity (the slider should face away from the USB-C connectors!).
Fixing that, the code boots in an instant—much, much faster than the couple
seconds it took over UART.
With a bootstream of only about 62 kB, the boot time is not a significant consideration. For larger binaries, ADI offers estimates[1] of how long the boot will take as a function of the binary size. Here’s some representative numbers from the SPI2 figure:
| Size / kB | Single | Dual | Quad |
|---|---|---|---|
| 2000 | 0.3 s | 0.15 s | 0.08 s |
| 4000 | 0.6 s | 0.3 s | 0.15 s |
| 8000 | 1.1 s | 0.6 s | 0.3 s |
Chances are that your bootstream is much, much smaller than these figures, so it would in general add a negligible duration to the boot process.