Written By:
Well let’s try to blink the onboard ACT
led. @dwelch67 did the same thing on the Raspberry Pi with the BCM2835
SoC 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
.
arch/arm/boot/dts/bcm2711-rpi-4-b.dts
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.setmode(GPIO.BCM)
GPIO.setup(42,GPIO.OUT)
GPIO.output(42, GPIO.HIGH)
time.sleep(2)
GPIO.output(42, GPIO.LOW)
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] -
dtparam=aci_led_gpio=16
in the config.txt
file in the boot
partition of the SD card.
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 -
GPFSEL{n}
registersGPSET/GPCLR
Warning |
---|
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 |
#include<stdint.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/mman.h>
#include<sys/stat.h>
#include<stdio.h>
#include<stddef.h>
#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]);
while(1){
gpio_base[GPSET1] = (1 << 10);
sleep(1);
gpio_base[GPCLR1] = (1 << 10);
sleep(1);
}
return 0;
}
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);
while(1)
{
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
clean:
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
make
And the .img
file should compile