
All of the quotes below are taken from the interview with Peter J. Weinberger (Murray Hill, 8 September 1989), with Michael S. Mahoney interviewing. This expands on the Unix values captured in the previous article.
What you tell the machine to do, it’s not doing it on the model, it’s doing it on the mathematical reality. There’s no gap. Which, I think, makes it very appealing. There’s a difference between a computer program and a theorem, which is that when you prove a theorem you now sort of know something you didn’t know before. But when you write a program, the universe has changed. You could do something you couldn’t do before.
I also have this feeling that you never want to have to touch the program. So it’s important to do it right early and that it always be okay. It’s not just a problem of the minute, although one writes a lot of code that’s got to do the problem of the minute, it’s got to fill the niche permanently—which is completely unrealistic but it’s certainly an attitude. And I think that matches this other. If it’s just going to be a slipshod temporary hacked up way of doing it it’s just not going to work long enough. And you’re going to just have to come back and do it again and it’s just too much like work. Not that reality actually matches this in any way but I think that’s the attitude.
If you have a theory based program you can believe you got the last bug out. If you have a hacked up program, all it is, is bugs. Surrounded by, you know, something that does something. You never get the last bug out. So we’ve got both kinds. And it’s partly the style of the programmer. Both kinds are useful. But one kind’s a lot easier to explain and understand even if it’s not more useful.
A program that’s sufficiently useful can have any number of bad effects or properties. But people prefer small, clean, easy-to-understand programs. But they will use big, horrible, grotesque, disgusting, buggy programs if they’re sufficiently useful. And some will complain louder than others, but it’s a rare few who will say “this is just so awful I won’t use it.”
My guess is that there is a modest amount to learn and you can use it. And the truth is our secretaries use it. We don’t have a special system for secretaries. They just use it. Now, when you watch them use it you say “oh, but there’s so many easier ways of doing it, there this and this”, but it doesn’t really matter. They don’t have to use it perfectly.
The story is if you write the documentation early, it’s likely it’ll be possible to explain what your program does, whereas if you wait until your program is completely finished, you may discover that however coherent it looked while you writing the various pieces, it’s impossible to explain it.

In this writeup we’ll go through the steps needed to bring up the Ethernet
peripheral (ETH1) on the STM32MP135 eval
board as well as a
custom board.
The evaluation board uses the LAN8742A-CZ-TR Ethernet PHY chip, connected to
the SoC as follows:
| PHY pin | PHY signal | SoC signal | SoC pin | Alt. Fn. | Notes |
|---|---|---|---|---|---|
| 16 | TXEN | PB11/ETH1_TX_EN | AA2 | AF11 | |
| 17 | TXD0 | PG13/ETH1_TXD0 | AA9 | AF11 | |
| 18 | TXD1 | PG14/ETH1_TXD1 | Y10 | AF11 | |
| 8 | RXD0/MODE0 | PC4/ETH1_RXD0 | Y7 | AF11 | 10k PU |
| 7 | RXD1/MODE1 | PC5/ETH1_RXD1 | AA7 | AF11 | 10k PU |
| 11 | CRS_DV/MODE2 | PC1/ETH1_CRS_DV | Y9 | AF10 | 10k PU |
| 13 | MDC | PG2/ETH1_MDC | V3 | AF11 | |
| 12 | MDIO | PA2/ETH1_MDIO | Y4 | AF11 | 1k5 PU |
| 15 | nRST | ETH1_NRST | IO9 | MPC IO | |
| 14 | nINT/RECLKO | PA1/ETH1_RX_CLK | AA3 | AF11 |
In this design, the Ethernet PHY connected to ETH1 has its own 25MHz crystal.
Note the ETH1_RX_CLK connection, which uses the MCP23017T-E/ML I2C I/O
expander.
One wonders if it was really necessary to complicate Ethernet bringup by
requiring this extra step (I2C + IO config) on an SoC that has 320 pins. True to
form, the simple IO expander needs more than 1,300 lines of ST driver code plus
lots more in the pointless BSP abstraction layer wrapper.
With a driver that complicated, it’s easier to start from scratch. As it happens, writing these GPIO pins involves just two I2C transactions. The I2C code is trivial, find it here.
Again ST code examples are very complex, but it takes just over 300 lines of code to send an Ethernet frame, by way of verifying that data can be transmitted over this interface. I asked ChatGPT to summarize what happens in the code:
Configure the pins for Ethernet First, all the GPIO pins required by the RMII interface are set up. Each pin is switched to its Ethernet alternate function, configured for push-pull output, and set to a high speed. This ensures the STM32’s MAC can physically drive the Ethernet lines correctly. If you’re using an external GPIO expander like the MCP23x17, it is also initialized here, and relevant pins are set high to enable the PHY or other control signals.
Enable the Ethernet clocks Before the MAC can operate, the clocks for the Ethernet peripheral—MAC, TX, RX, and the reference clock—are enabled in the RCC. This powers the Ethernet block inside the STM32 and allows it to communicate with the PHY.
Initialize descriptors and buffers DMA descriptors for transmit (TX) and receive (RX) are allocated and zeroed. The transmit buffer is allocated and aligned to 32 bytes, as required by the DMA. A TX buffer descriptor is created, pointing to the transmit buffer. This descriptor tells the HAL exactly where the frame data is and how long it is.
Configure the Ethernet peripheral structure The ETH_HandleTypeDef is
populated with the MAC address, RMII mode, pointers to the TX and RX
descriptors, and the RX buffer size. The clock source for the peripheral is
selected. At this stage, the HAL has all the information needed to manage the
hardware.
Initialize the MAC and PHY Calling HAL_ETH_Init() programs the MAC with
the descriptor addresses, frame length settings, and other features like
checksum offload. The PHY is reset and auto-negotiation is enabled via MDIO.
Reading the PHY ID verifies that the PHY is responding correctly.
Start the MAC With HAL_ETH_Start(), the MAC begins normal operation,
monitoring the RMII interface for frames to transmit or receive.
Build the Ethernet frame A frame is constructed in memory. The first 6 bytes are the destination MAC (broadcast in this case), the next 6 bytes are the source MAC (the STM32’s MAC), followed by a 2-byte EtherType. The payload is copied into the frame (e.g., a short test string), and the frame is padded to at least 60 bytes to satisfy Ethernet minimum length requirements.
Transmit the frame The TX buffer descriptor is updated with the frame
length and pointer to the buffer. HAL_ETH_Transmit() is called, which
programs the DMA to fetch the frame from memory and put it onto the Ethernet
wire. After this call completes successfully, the frame is sent, and you can
see it in Wireshark on the network.
For the record, when a cable is connected, the PHY sees the link is up:
> eth_status
Ethernet link is up
Speed: 100 Mbps
Duplex: full
BSR = 0x782D, PHYSCSR = 0x1058
The custom board (Rev A) also uses the LAN8742A-CZ-TR Ethernet PHY chip,
connected to the SoC as follows:
| PHY pin | PHY signal | SoC signal | SoC pin | Alt. Fn. | Notes |
|---|---|---|---|---|---|
| 16 | TXEN | PB11/ETH1_TX_EN | N5 | AF11 | |
| 17 | TXD0 | PG13/ETH1_TXD0 | P8 | AF11 | |
| 18 | TXD1 | PG14/ETH1_TXD1 | P9 | AF11 | |
| 8 | RXD0/MODE0 | PC4/ETH1_RXD0 | U6 | AF11 | 10k PU |
| 7 | RXD1/MODE1 | PC5/ETH1_RXD1 | R7 | AF11 | 10k PU |
| 11 | CRS_DV/MODE2 | PA7/ETH1_CRS_DV | U2 | AF11 | 10k PU |
| 13 | MDC | PG2/ETH1_MDC | R1 | AF11 | |
| 12 | MDIO | PG3/ETH1_MDIO | L5 | AF11 | 1k5 PU |
| 15 | nRST | PG11 | M3 | 10k PD | |
| 14 | nINT/RECLKO | PG12/ETH1_PHY_INTN | T1 | AF11 | 10k PU |
| 5 | XTAL1/CLKIN | PA11/ETH1_CLK | T2 | AF11 |
The differences with respect to eval board are:
| Signal | Eval board | Custom board |
|---|---|---|
ETH1_CRS_DV | PC1/ETH1_CRS_DV | PA7/ETH1_CRS_DV |
ETH1_MDIO | PA2/ETH1_MDIO | PG3/ETH1_MDIO |
nRST | GPIO expander | PG11, 10k pulldown |
nINT/REFCLKO | PA1/ETH1_RX_CLK | PG12/ETH1_PHY_INTN |
XTAL1/CLKIN | 25 MHz XTAL | PA11/ETH1_CLK |
That is, two different port assignments, direct GPIO for reset instead of
expander, clock to be output from the SoC to the PHY, and using INTN signal
instead of RX_CLK. All alternate functions are 11, while on the eval board one
of them (CRS_DV) was 10.
First, we need to set the clock correctly. Since Ethernet does not have a
dedicated crystal on the custom board, we need to source it from a PLL. In
particular, we can set PLL3Q to output 24/2*50/24=25 MHz, and select the
ETH1 clock source:
pclk.PeriphClockSelection = RCC_PERIPHCLK_ETH1;
pclk.Eth1ClockSelection = RCC_ETH1CLKSOURCE_PLL3;
if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK)
ERROR("ETH1");With the scope, I can see a 25 MHz clock on the ETH_CLK trace and the nRST
pin is driven high (3.3V). Nonetheless, HAL_ETH_Init() returns with an error.
Of course, we forgot to tell the HAL what the Ethernet clock source is. On the eval board, we had
eth_handle.Init.ClockSelection = HAL_ETH1_REF_CLK_RX_CLK_PIN;But on the custom board, the SoC provides the clock to the PHY:
eth_handle.Init.ClockSelection = HAL_ETH1_REF_CLK_RCC;With the RCC clock selected for Ethernet, yet again HAL_ETH_Init() fails. This
time, it tries to select the RCC clock source:
if (heth->Init.ClockSelection == HAL_ETH1_REF_CLK_RCC)
{
syscfg_config |= SYSCFG_PMCSETR_ETH1_REF_CLK_SEL;
}
HAL_SYSCFG_ETHInterfaceSelect(syscfg_config);The Ethernet interface and clocking setup is done in the PMCSETR register,
together with some other configuration.
void HAL_SYSCFG_ETHInterfaceSelect(uint32_t SYSCFG_ETHInterface)
{
assert_param(IS_SYSCFG_ETHERNET_CONFIG(SYSCFG_ETHInterface));
SYSCFG->PMCSETR = (uint32_t)(SYSCFG_ETHInterface);
}Now the driver trips over the assertion. The assertion macro expects the
config word to pure interface selection, forgetting that the same register also
carries the ETH1_REF_CLK_SEL field (amongst others!):
#define IS_SYSCFG_ETHERNET_CONFIG(CONFIG) \
(((CONFIG) == SYSCFG_ETH1_MII) || ((CONFIG) == SYSCFG_ETH1_RMII) || \
((CONFIG) == SYSCFG_ETH1_RGMII) || ((CONFIG) == SYSCFG_ETH2_MII) || \
((CONFIG) == SYSCFG_ETH2_RMII) || ((CONFIG) == SYSCFG_ETH2_RGMII))
#endif /* SYSCFG_DUAL_ETH_SUPPORT */If we comment out this assertion, the initialization proceeds without further errors. However, link is still down.
Even with an Ethernet cable plugged in, link is down:
// Read basic status register
if (HAL_ETH_ReadPHYRegister(ð_handle, LAN8742_ADDR,
LAN8742_BSR, &v) != HAL_OK) {
my_printf("PHY BSR read failed\r\n");
return;
}
if ((v & LAN8742_BSR_LINK_STATUS) == 0u) {
my_printf("Link is down (no cable or remote inactive)\r\n");
return;
}On the schematic diagram of the custom board, we notice that the RJ-45
transformer center taps (TXCT, RXCT on the J1011F21PNL connector) are
decoupled to ground, but are not connected to 3.3V unlike on the eval board. The
LAN8742A datasheet does not talk about it explicitly, but instead shows a
schematic diagram (Figure 3-23) where the two center taps are tied together and
pulled up to 3.3V via a ferrite bead.
Tying the center taps to 3.3V, we still get no link. Printing the PHY Basic Status Register, we see:
Link is down (no cable or remote inactive)
BSR = 0x7809
This means: link down, auto-negotiation not complete.
REF_CLK pin is not outputting a 50 MHz clock but instead sits at about 3.3V.
The PHY chip shares LED pins with straps.
LED1 is shared with REGOFF and is tied to the anode of the LED, which pulls
down the pin such that REGOFF=0 and the regulator is enabled. We measure that
VDDCR is at 1.25V, which indicates that the internal regulator started
successfully. During board operation, this pin is low (close to 0V).
LED2 is shared with the nINTSEL pin, and is connected to the LED cathode.
During board operation, this pin is high (close to 3.3V). Selecting nINTSEL=1
means REF_CLK In Mode, as is explained in Table 3-6: “nINT/REFCLKO is an
active low interrupt output. The REF_CLK is sourced externally and must be
driven on the XTAL1/CLKIN pin.”
Section 3.7.4 explains further regarding the “Clock In” mode:
In
REF_CLKIn Mode, the 50 MHzREF_CLKis driven on theXTAL1/CLKINpin. This is the traditional system configuration when using RMII [...]In
REF_CLKIn Mode, the 50 MHzREF_CLKis driven on theXTAL1/CLKINpin. A 50 MHz source forREF_CLKmust be available external to the device when using this mode. The clock is driven to both the MAC and PHY as shown in Figure 3-7.
Furthermore, according to Section 3.8.1.6 of the PHY datasheet, the absence of a
pulldown resistor on LED2/nINTSEL pin means that LED2 output is active low.
That means that the anode of LED2 should have been tied to VDD2A according
to Fig. 3-15, rather than ground as is currently the case.
This means we have two alternatives:
Add a 10k pulldown from LED2/nINTSEL to ground, and flip the polarity of the
LED (connect the PHY to anode, or pin 9 of the connector). This would select
nINTSEL=0. In that case, a 25 MHz clock is to be provided to XTAL1/CLKIN.
Keep LED2/nINTSEL connected to the LED cathode, without any pulldown
resistor. This selects nINTSEL=1. However, make sure to connect the LED
anode (pin 9, +LEDR, of connector) to VDD2A instead of GND. In this
case, a 50 MHz clock is to be provided to XTAL1/CLKIN.
In this instance I chose the latter option and ordered PLL3Q to output
24/2*50/12=50 MHz. The link is briefly up and the green LED2 blinks:
> eth_status
Ethernet link is up
Speed: 100 Mbps
Duplex: full
BSR = 0x782D, PHYSCSR = 0x1058
But strange enough, when I check the status just a moment later, the link is down again:
> eth_status
Link is down (no cable or remote inactive)
BSR = 0x7809
Checking repeatedly, sometimes it’s up, and sometimes it’s down.
I see that the current drawn from the 3.3V supply switches between 0.08A and 0.13A continuously, every second or two.
Printing out some more info in both situations:
Link is down (no cable or remote inactive)
BSR = 0x7809, PHYSCSR = 0x0040, ISFR = 0x0098, SMR = 0x60E0, SCSIR = 0x0040
SYSCFG_PMCSETR = 0x820000
> e
Ethernet link is up
Speed: 100 Mbps
Duplex: full
BSR = 0x782D, PHYSCSR = 0x1058, ISFR = 0x00CA, SMR = 0x60E0, SCSIR = 0x1058
SYSCFG_PMCSETR = 0x820000
PHY Basic Status Register BSR, when link is down, shows the following status:
No T4 ability
TX with full duplex ability
TX with half duplex ability
10 Mbps with full duplex ability
10 Mbps with half duplex ability
Auto-negotiate process not completed
No remote fault
Able to perform auto-negotiation function
Link is down
No jabber condition detected.
Supports extended capabilities registers
When link is up, BSR shows (of course) that link is up, and also that
the auto-negotiate process completed.
The PHY Special Control/Status Register (PHYSCSR), when link is down, does not
have a meaningful speed indication (000), or anything else. When link is up,
it shows speed as 100BASE-TX full-duplex (110), and that auto-negotiation is
done.
The PHY Interrupt Source Flag Register (PHYISFR), when link is down, shows
Auto-Negotiation LP Acknowledge, Link Down (link status negated), and ENERGYON
generated. When link is up, we get Auto-Negotiation Page Received,
Auto-Negotiation LP Acknowledge, ENERGYON generated, and Wake on LAN (WoL)
event detected.
The PHY Special Modes Register (PHYSMR), when link is either up or down, shows
the same value: 0x60E0. This means that PHYAD=00000 (PHY address), and
MODE=111 (transceiver mode of operation is set to “All capable.
Auto-negotiation enabled.”.
The PHY Special Control/Status Indications Register (PHYSCSIR), when link is
up, shows Reversed polarity of 10BASE-T, even though link is 100 Mbps.
SoC PMCSETR has two fields set: ETH1_SEL is set to 100, meaning RMII, and
ETH1_REF_CLK_SEL is set to 1, meaning that the reference clock (RMII mode)
comes from the RCC.
Painfully obvious in retrospect, but the problem was that PLL3, from which
we’ve derived the Ethernet clock, was set to fractional mode:
rcc_oscinitstructure.PLL3.PLLFRACV = 0x1a04;
rcc_oscinitstructure.PLL3.PLLMODE = RCC_PLL_FRACTIONAL;If instead we derive the clock from PLL4, which is already set to integer
mode, then sending the Ethernet frame just works, and the link gets up and
stays up:
rcc_oscinitstructure.PLL4.PLLFRACV = 0;
rcc_oscinitstructure.PLL4.PLLMODE = RCC_PLL_INTEGER;
// ...
pclk.PeriphClockSelection = RCC_PERIPHCLK_ETH1;
pclk.Eth1ClockSelection = RCC_ETH1CLKSOURCE_PLL4;Of course! Ethernet requires a perfectly precise 50 MHz clock, up to about 50 ppm. On the eval board that was not a problem: the PHY had its own crystal, and it returned a good 50 MHz clock directly back to the SoC’s MAC.

This morning it occurred to me that I’m really not looking forward to going to the office, for I’ll have to continue doing something that I spent two days on already, and it’s still not working. I can easily think of many other such things that I’d rather not do, and as it happens each of them comes with a “positive”, or attractive aspect (written in brackets):
Going out to meet someone, despite being tired after work (but enjoying the company of a really nice person with a different perspective on life)
Having to separate from loved ones when time comes (but being happy to be with them for now)
Dying (but being being alive)
These are generalized examples; my real list is longer and more specific, but I won’t bore you with the details since anyone can easily write down their own, personally relevant version.
The point of these contrasts is not so much that the “bad” part of the stick is to be borne because the “good” part is worth so much more. The point is not even to try and forget about the bad part by various means (distraction, expression, repression, suppression), even though that’s what I end up doing most of the time. The point is to try and see them as a single “yin-yang” unit: black in white, white in black.
These contrasts are inevitable, so why waste time fighting them, denying their existence? Relax into the reality, let go of the fear and dread by feeling it directly until your brain gets tired of it. I’m not saying, “stop fearing the inevitable”, as the fear itself is in fact part of the inevitable. The lake would not try to hide its waves when a stone is thrown into it; its waves radiate outwards until they stop. In fact they never really stop, so the lake does not reject them.
Somewhere in the Tao Te Ching it is said that the great power of water (wearing down mountains, etc.) is because it’s not loath to take the lowest, humblest part, where no one wants to be. Elsewhere there’s the image of the malformed tree surviving, while the straight, useful ones are cut down for the carpenter. I wonder if peace can be had in the face of the above mentioned “dreadful” future situations by sinking, in each of them, to the most dreadful point. Assume the most broken, useless mental state: be angry and sad, afraid and trembling, and watch things come and go. Strength in weakness?
On a practical note: each day, do the “dreadful” thing first to avoid wasting too much time and effort doing pointless other things. Looking back, avoidance behaviors are often much more exhausting than what they supposedly protect me from. Or, in someone’s wise words: “Procrastination is not worth the time it takes.”

In this writeup we’ll go through the steps needed to bring up the LCD/CTP peripheral on the custom STM32MP135 board.
I am using the Rocktech RK050HR01-CT LCD display, connecting to the
STM32MP135FAE SoC, as follows:
| LCD pin | LCD signal | SoC signal | SoC pin | Alt. Fn. |
|---|---|---|---|---|
| 1, 2 | VLED+/- | PB15/TIM1_CH3N | B12 | AF1 |
| 8 | R3 | PB12/LCD_R3 | D9 | AF13 |
| 9 | R4 | PE3/LCD_R4 | D13 | AF13 |
| 10 | R5 | PF5/LCD_R5 | B2 | AF14 |
| 11 | R6 | PF0/LCD_R6 | C13 | AF13 |
| 12 | R7 | PF6/LCD_R7 | G2 | AF13 |
| 15 | G2 | PF7/LCD_G2 | M1 | AF14 |
| 16 | G3 | PE6/LCD_G3 | N1 | AF14 |
| 17 | G4 | PG5/LCD_G4 | F2 | AF11 |
| 18 | G5 | PG0/LCD_G5 | D7 | AF14 |
| 19 | G6 | PA12/LCD_G6 | E3 | AF14 |
| 20 | G7 | PA15/LCD_G7 | E6 | AF11 |
| 24 | B3 | PG15/LCD_B3 | G4 | AF14 |
| 25 | B4 | PB2/LCD_B4 | H4 | AF14 |
| 26 | B5 | PH9/LCD_B5 | A9 | AF9 |
| 27 | B6 | PF4/LCD_B6 | L2 | AF13 |
| 28 | B7 | PB6/LCD_B7 | C1 | AF14 |
| 30 | DCLK | PD9/LCD_CLK | E8 | AF13 |
| 31 | DISP | PG7 | C9 | — |
| 32 | HSYNC | PE1/LCD_HSYNC | B5 | AF9 |
| 33 | VSYNC | PE12/LCD_VSYNC | B4 | AF9 |
| 34 | DE | PG6/LCD_DE | A14 | AF13 |
The easiest thing to check is the display backlight, since it’s just a single GPIO pin to turn on/off, or a simple PWM to control the brightness via the duty cycle.
In our case, the backlight pin is connected to TIM1_CH3N, which is alternate
function 1:
GPIO_InitTypeDef gpio;
gpio.Pin = GPIO_PIN_15;
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
gpio.Alternate = GPIO_AF1_TIM1;
HAL_GPIO_Init(GPIOB, &gpio);ChatGPT can write the PWM configuration:
__HAL_RCC_TIM1_CLK_ENABLE();
htim1.Instance = TIM1;
htim1.Init.Prescaler = 99U;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 999U;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
HAL_TIM_PWM_Init(&htim1);
TIM_OC_InitTypeDef oc;
oc.OCMode = TIM_OCMODE_PWM1;
oc.Pulse = 500U;
oc.OCPolarity = TIM_OCPOLARITY_HIGH;
oc.OCNPolarity = TIM_OCNPOLARITY_HIGH;
oc.OCIdleState = TIM_OCIDLESTATE_RESET;
oc.OCNIdleState = TIM_OCNIDLESTATE_RESET;
oc.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim1, &oc, TIM_CHANNEL_3);
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_3);
htim1.Instance->BDTR |= TIM_BDTR_MOE;The only “tricky” part, or the part that AI got wrong, was that we have to use
HAL_TIMEx_PWMN_Start() instead of HAL_TIM_PWM_Start(), since we’re dealing
with the complementary output. With that fixed, the brightness pin showed a
clean square wave output, with duty cycle adjustable in units of percent:
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_3,
(htim1.Init.Period + 1U) * percent / 100U);Unfortunately, the PCB reversed all pins and the connector is single sided, so we cannot directly check if the above works on the actual display or not. Nonetheless, we can see a nice 2.088893 kHz square wave with 50 duty cycle, and we can tune it from 0% to 100%.
The Rocktech RK050HR01-CT LCD display includes a capacitive touchpad (CTP),
connecting to the STM32MP135FAE SoC, as follows:
| CPT pin | CPT signal | SoC signal | SoC pin | Alt. Fn. |
|---|---|---|---|---|
| 1 | SCL | PH13/I2C5_SCL | A10 | AF4 |
| 8 | SDA | PF3/I2C5_SDA | B10 | AF4 |
| 4 | RST | PB7 | A4 | — |
| 5 | INT | PH12 | C2 | — |
Luckily the 6-pin CTP connector, albeit wired in reverse, has contacts on both top and bottom sides, so we can simply flip the ribbon cable. With entirely usual I2C configuration it simply works. Check out the final result here.
My GT911 driver is just under 300 lines of code; it’s very interesting that it takes ST almost 3,000 (yes, it has more features ... Whatever, I don’t need them!)
stm32cubemp13-v1-2-0/STM32Cube_FW_MP13_V1.2.0/Drivers/BSP/Components/gt911$ cloc .
12 text files.
12 unique files.
1 file ignored.
github.com/AlDanial/cloc v 1.90 T=0.10 s (109.2 files/s, 48189.3 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
CSS 1 209 56 1446
C 2 223 636 940
C/C++ Header 3 159 614 421
Markdown 2 24 0 62
HTML 1 0 3 56
SVG 2 0 0 4
-------------------------------------------------------------------------------
SUM: 11 615 1309 2929
-------------------------------------------------------------------------------My example code prints out the touch coordinates whenever the touch interrupt fires. Not much more to do, since the CTP will be used within some application which will implement more advanced features. The only reason to include this in the bootloader code is to verify that the I2C connection works.
The custom board is wired backwards, but we can verify that the code is correct
on the eval board. Besides forgetting to turn the LCD_DISP signal on, it all
worked. You set up a framebuffer somewhere (I just used the beginning of the DDR
memory), and write bits there, and magically the picture appears on the display.
For example, to display solid colors:
volatile uint8_t *lcd_fb = (volatile uint8_t *)DRAM_MEM_BASE;
for (uint32_t y = 0; y < RK043FN48H_HEIGHT; y++) {
for (uint32_t x = 0; x < RK043FN48H_WIDTH; x++) {
uint32_t p = (y * RK043FN48H_WIDTH + x) * 3U;
lcd_fb[p + 0] = b; // blue
lcd_fb[p + 1] = g; // green
lcd_fb[p + 2] = r; // red
}
}
/* make sure CPU writes reach DDR before LTDC reads */
L1C_CleanDCacheAll();Making use of an adapter from the 40-pin FFC ribbon cable to jumper wires, we can verify the signals also on the custom board. We see:
R[3:7] signal when screen set to red, otherwise low
G[3:7] signal when screen set to green, otherwise low
B[3:7] signal when screen set to blue, otherwise low
DCLK: 10 MHz
DISP: 3.3V
HSYNC: 17.6688 kHz, 92.76% duty cycle
VSYNC: 61.779 Hz, 96.5% duty cycle
DE: 16.7--16.9 kHz, ~84% duty cycle
We can see the brightness change when adjusting the duty cycle of the backlight.
Left ~2/3 of the screen shows white vertical stripes, the exact pattern of these stripes depending on what “color” the screen is set to. The right ~1/3 of the screen is black. This is to be expected, since we’re using the same settings for both displays. Here’s the settings which work fine on the eval board:
#define LCD_WIDTH 480U // LCD PIXEL WIDTH
#define LCD_HEIGHT 272U // LCD PIXEL HEIGHT
#define LCD_HSYNC 41U // Horizontal synchronization
#define LCD_HBP 13U // Horizontal back porch
#define LCD_HFP 32U // Horizontal front porch
#define LCD_VSYNC 10U // Vertical synchronization
#define LCD_VBP 2U // Vertical back porch
#define LCD_VFP 2U // Vertical front porchThe custom board uses a different display, so let’s try different settings:
#define LCD_WIDTH 800U
#define LCD_HEIGHT 480U
#define LCD_HSYNC 1U
#define LCD_HBP 8U
#define LCD_HFP 8U
#define LCD_VSYNC 1U
#define LCD_VBP 16U
#define LCD_VFP 16UNow the screen is totally white, regardless of which color we send it. We notice
that the LCD datasheet specifies a minimum clock frequency of 10 MHz. Note that
on the STM32MP135, the LCD clock comes from PLL4Q. Raising the DCLK to 24
MHz, the screen works! We get to see all the colors. The PLL4 configuration
that works for me is
rcc_oscinitstructure.PLL4.PLLState = RCC_PLL_ON;
rcc_oscinitstructure.PLL4.PLLSource = RCC_PLL4SOURCE_HSE;
rcc_oscinitstructure.PLL4.PLLM = 2;
rcc_oscinitstructure.PLL4.PLLN = 50;
rcc_oscinitstructure.PLL4.PLLP = 12;
rcc_oscinitstructure.PLL4.PLLQ = 25;
rcc_oscinitstructure.PLL4.PLLR = 6;
rcc_oscinitstructure.PLL4.PLLRGE = RCC_PLL4IFRANGE_1;
rcc_oscinitstructure.PLL4.PLLFRACV = 0;
rcc_oscinitstructure.PLL4.PLLMODE =
RCC_PLL_INTEGER;Unfortunately, just as the LCD becomes configured correctly and is able to display the solid red, green, or blue colors, I noticed that the USB MSC interface disappeared. If I comment out the LCD init code, so it does not run, then USB comes back. How could they possibly interact?
Even more interesting, the USB stops working only if both of the following
functions are called: lcd_backlight_init(), which configures the backlight
brightness PWM, and lcd_panel_init(), which does panel timing and pin
configuration.
As it turns out, my 3.3V supply was set with a 0.1A current limit. Having enabled so many peripherals, the current draw can be a bit higher now. Increasing the current limit up to 0.2A, and everything works fine. In the steady state, after init is complete, the board draws just under 0.1A from the 3.3V supply. (For the record, I’m drawing about 0.26A from the combined 1.25V / 1.35V supply.)
Bringing up the LCD on the custom board ultimately came down to matching the
panel’s exact timing and, critically, running the pixel clock within the range
specified by the datasheet. Once the LTDC geometry and PLL4Q frequency were
correct, the display worked immediately, confirming that the signal wiring and
framebuffer logic were sound.

There are some animals, as well as most plants, that can grow back a lost limb. Humans are like that in relation to the mask we wear. As soon as we take off a mask, we begin to grow another one.
When refusing to play a role, one is merely playing a different role. Nevertheless, this means that one is not stuck with the same role forever; it’s only a matter of reading a new job description, learning the new skills required, and assuming the new behaviors.
Identity is a tool, not a prison.