Jun 08, 2020


Written By: Ahaan Dabholkar

Well let’s try to blink the onboard ACT led. @dwelch67 did the same thing on the Raspberry Pi with the BCM2835SoC and I have referred to his code almost completely. The source code files can be found in the repo in the blink directory. The BCM2835 has its onboard ACT led connected to GPIO16 pin, triggering which, turns on/off the led.

How do we know that GPIO16 is the one ?

Well Broadcom basically told us. They released the full schematics of the Raspberry Pi[1] which clearly show the GPIO16 getting mapped to the STATUS_LED_N. No such luck with the RPi4 though. The released reduced schematics[2] are laughably bad for this and hence of no use. Moreover, the ACT led is mapped to the eMMC activity and is a cruel tantalizer. However, this blog[3] takes an interesting approach by analysing the kernel device tree source (*dts) file[4], which leads us to GPIO42.



Just to test it out, lets run a python script to toggle the ACT led by manipulating GPIO42.

import time
import RPi.GPIO as GPIO
GPIO.output(42, GPIO.HIGH)
GPIO.output(42, GPIO.LOW)

🍻 It works !!!

Observe that the ACT led turns on and off. In fact even if the GPIO.output(42, GPIO.LOW) is commented out, the ACT led still die out after a few seconds. This is probably because of the interference with the eMMC trigger. This can be rectified by reassigning the ACT led to some other pin (16) by adding a device tree directive[5] -


in the config.txt file in the boot partition of the SD card.

Direct Register Manipulation

The following C code is supposed to toggle GPIO42 on & off by writing to the registers mapped to the GPIO42 pin. The addresses of the registers, functions details etc. are all mentioned in the ARM Peripherals[6] doc. Memory Mapped GPIOs are basically used as follows -

Do Note that the doc has errors. It lists the GPIO register base address as 0x7E21 5000, which is wrong. The base address is at low peripheral mode addressed OxFE20 0000 or legacy 0x7E20 0000

#define SIZE 4096
#define GPIO_BASE 0xfe200000
#define GPFSEL4 0x4
#define GPSET1 0x8
#define GPCLR1 0xb

volatile uint32_t* gpio_base;

int main()
	int fd;
	if((fd = open("/dev/mem", O_RDWR|O_SYNC)) < 0 )
			printf("/dev/mem Not Opened \n");
			return -1;
	gpio_base = (uint32_t*)mmap(NULL, SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, GPIO_BASE);
	printf("MMAP DONE %p \n", gpio_base);
	printf("GPFSEL4: %u\n", gpio_base[GPFSEL4]);
	gpio_base[GPFSEL4] &= ~(7 << 6);
	gpio_base[GPFSEL4] |= 1 << 6 ;
	printf("MODGPFSEL4: %d\n", gpio_base[GPFSEL4]);
		gpio_base[GPSET1] = (1 << 10);
		gpio_base[GPCLR1] = (1 << 10);
	return 0;

What the code is essentially doing

gpio_base[GPFSEL4] &= ~(7 << 6); sets bits 6-8 to 0 and is sometimes referred to as cleaning. We consider those specific bits as GPIO42 is controlled by GPFSEL4[6:8]

gpio_base[GPFSEL4] |= 1 << 6 ; sets it as an output pin.

gpio_base[GPSET1] = (1 << 10); pulls the pin high(or low)

Compile the above program and run it as root (to register a memory map for /dev/mem).


This involves writing a custom kernel7l.img file which is very similar to the above program. We’ll still be using 32 bit low peripheral mode addresses as that is what the ARM is configured to handle by default. Note that since this time, we will be running our script without a kernel ( our script will be the kernel ) and hence shall need to compile and link it appropriately. I have provided two separate implementations, one discussed below and the other borrowing from @dwelch67

To compile these examples, I used a cross-compiler from the ARM Embedded Toolchain

#define GPIO_BASE 0xFE200000
#define GPFSEL4 0x4
#define GPSET1 0x8
#define GPCLR1 0xB

volatile unsigned int* gpio_base = (unsigned int* )GPIO_BASE;
volatile unsigned int time;

int main(void) __attribute__((naked));
int main(void)
	gpio_base[GPFSEL4] &= ~(7 << 6);
	gpio_base[GPFSEL4] |= (1 << 6);
		for(time=0; time<50000; time++);
		gpio_base[GPSET1] = (1 << 10);

		for(time=0; time<50000; time++);
		gpio_base[GPCLR1] = (1 << 10);


The Makefile for the above script is

ARMGNU ?= arm-none-eabi

COPS = -nostartfiles -mfloat -abi=hard -mfpu=crypto-neon-fp-armv8 -march=armv8-a+crc -mcpu=cortex-a72

all : kernel7l.img

	rm -f *.o
	rm -f *.bin
	rm -f *.elf
	rm -f *.list
	rm -f *.img

blinker.o : blinker.c
	$(ARMGNU)-gcc $(COPS) -c blinker.c -o blinker.o

kernel7l.img : blinker.o
	$(ARMGNU)-ld blinker.o -o blinker.elf
	$(ARMGNU)-objcopy blinker.elf -O binary kernel7l.img		

Make sure, that the $PATH variable has the location of the cross-compiler after which navigate to the directory contating the Makefile and execute


And the .img file should compile


  1. https://www.raspberrypi.org/app/uploads/2012/04/Raspberry-Pi-Schematics-R1.0.pdf
  2. https://www.raspberrypi.org/documentation/hardware/raspberrypi/schematics/rpi_SCH_4b_4p0_reduced.pdf
  3. https://www.valvers.com/open-software/raspberry-pi/bare-metal-programming-in-c-part-1/
  4. https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/arm/boot/dts/bcm2711-rpi-4-b.dts
  5. https://www.raspberrypi.org/documentation/configuration/device-tree.md
  6. https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf