Making the LED Blink in C (PI5): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
Zeile 414: Zeile 414:
=== Compiling and Running ===
=== Compiling and Running ===
Navigate to the LED directory and compile the program with the make command. If everything was successful, you will get a file named kernel_2712.img, which can be copied to an SD card and used in the Raspberry Pi. Turn on the Raspberry Pi, and the LED should start blinking.
Navigate to the LED directory and compile the program with the make command. If everything was successful, you will get a file named kernel_2712.img, which can be copied to an SD card and used in the Raspberry Pi. Turn on the Raspberry Pi, and the LED should start blinking.
You can download the source code as a ZIP file from the following link: https://www.satyria.de/arm/sources/C/led.zip

Version vom 20. August 2024, 19:36 Uhr

Our current goal is to make the built-in LED of the Raspberry Pi blink. I will explain each step and why it has been implemented this way in this project.

Preparing the Directory

First, create a new directory, e.g., LED, and place the Makefile and the linker.ld file in it. Also, create the include directory within LED to organize our header files.

Programming the Startup Behavior of the Raspberry Pi

Unfortunately, we cannot completely avoid assembly language. The first code that is executed should be written in assembly. Usually, some CPU properties are defined here, which is "only" possible in assembly. In this example, it's not necessary yet, but we create this file to be prepared for later projects. To do this, we create the file "boot.S":

//
// boot.S
//

#include "config.h"

.section .init  // Ensure the linker places this at the beginning of the kernel image
.global _start  // Execution starts here

_start:
    ldr x0, =MEM_KERNEL_STACK
    mov sp, x0           // init its stack 
    b sysinit

Explanation:

  • #include "config.h": Includes the configuration file where we define necessary parameters.
  • .section .init: Defines a section that is placed at the beginning of the kernel image. According to the linker script, this is the first position in memory starting from address 0x80000. This is also the address the Raspberry Pi jumps to on initial start.
  • .global _start: Declares `_start` as global so it can be used by all parts of the program.
  • _start: Label marking the starting point of execution.
  • ldr x0, =MEM_KERNEL_STACK: Loads the address of `MEM_KERNEL_STACK` into register `x0`, as defined in "config.h".
  • mov sp, x0: Initializes the stack pointer `SP`.

- `b sysinit`: Jumps to the `sysinit` function, which will be implemented later.

When we start the program, the register `x0` is first loaded with the value of `MEM_KERNEL_STACK`, defined in the include file, and then stored in the stack pointer (SP). With `b` (branch), the code jumps to the label `sysinit`.

Configuration File config.h, a Header File

Meaning and Structure of Header Files

Our "config.h" is a header file. In general, header files are used in programming to organize and make code reusable. Here are the main reasons explained:

  • Structuring: Header files help to divide the code into smaller, clear parts. Functions, variables, and constants used in multiple files can be declared in a header file.
  • Reusability: Code written once in a header file can be used in multiple programs or files without needing to rewrite the code each time.
  • Simplification: Using header files makes the code clearer and easier to understand. Instead of having all functions in a single file, they can be divided into smaller, logical parts.
  • Avoidance of Redundancy: If a function prototype or constant changes, this change needs to be made only in the header file, instead of in every file that uses it.

In summary, header files make the code clearer, more reusable, and easier to maintain.

Our "config.h" is now described as follows:

//
// config.h
//

#ifndef _config_h
#define _config_h

#define MEGABYTE 0x100000

#define MEM_KERNEL_START 0x80000          // Start address of the main program
#define KERNEL_MAX_SIZE (2 * MEGABYTE)
#define MEM_KERNEL_END (MEM_KERNEL_START + KERNEL_MAX_SIZE)
#define KERNEL_STACK_SIZE 0x20000
#define MEM_KERNEL_STACK (MEM_KERNEL_END + KERNEL_STACK_SIZE)

#endif

Explanation:

  • #ifndef _config_h ... #endif: Prevents the file from being included multiple times.
  • #define: Assigns constant values to symbols.
  • MEM_KERNEL_STACK: Calculates the start of the kernel stack based on the start address of the kernel and its maximum size.

In this header, a condition is checked whether the variable `_config_h` has already been defined. Specifically, whether it has not been defined yet. `#ifndef` means "if not defined". If the variable is not yet defined, everything up to `#endif` is processed. If the variable is already defined, everything up to `#endif` is skipped.

Immediately after the `#ifndef` statement, the variable is defined with `#define _config_h` and is thus known to the compiler.

Why is this checked in a header? The compiler does not like it at all when things are defined multiple times. If this header file has already been called elsewhere, the compiler skips these definitions, avoiding multiple definitions.

Function sysinit

Next, we create the `sysinit` function that initializes the system:

//
// sysinit.S
//

.section .text
.globl sysinit
sysinit:
    b main

Explanation:

  • .section .text: Defines a section for executable code.
  • .globl sysinit: Declares `sysinit` as global.
  • b main: Jumps to the `main` function.

Here, the label `sysinit` is first defined as global so it is known outside the file. It is referred to in the previous code. Unlike the previous code, the section `.text` is used here. This section defines executable code that lies "somewhere" in memory. The exact position is determined by the previously written code and later by the linker. The only command here is `b main`, which means that the program makes a jump (branch) to the label `main`. Later, we will add more code here.

Main Program "main"

Now, let's create the main program:

//
// kernel.c
//

#include "led.h"
#include "time.h"

int main (void)
{
    while(1)
    {
        LED_off();
        wait(0x3F0000);
        LED_on();
        wait(0x3F0000);
    }
}

Explanation:

  • while(1): Creates an infinite loop since 1 is always true.
  • LED_off(): Turns off the LED.
  • wait(0x3F0000): Calls the wait function.
  • LED_on(): Turns on the LED.

With int main (void), we create the label called by sysinit.

First, we define an infinite loop with while. Everything after the curly brace is repeated endlessly since 1 is always true.

The first command LED_off() calls the corresponding function we will write later. This function is supposed to turn off the built-in green LED on the Raspberry Pi 5.

Next, the wait function is called. This function expects a value indicating how long to wait before executing the next command.

After wait is called, the LED_on() function is called, which turns the green LED back on. Then it waits again, and the whole program is repeated infinitely.

LED Control Functions

Let's create the file led.c for LED control:

//
// led.c
//

#include "base.h"
#include "util.h"
#include "types.h"

void LED_off(void)
{
    u32 reg = read32(ARM_GPIO2_DATA0);
    reg &= ~0x200; // Set bit 9 to 0
    write32(ARM_GPIO2_DATA0, reg);
}

void LED_on(void)
{
    u32 reg = read32(ARM_GPIO2_DATA0);
    reg |= 0x200; // Set bit 9 to 1
    write32(ARM_GPIO2_DATA0, reg);
}

Explanation:

  • u32 reg = read32(ARM_GPIO2_DATA0): Reads the content of the ARM_GPIO2_DATA0 register and stores it in reg.
  • reg &= ~0x200: Clears the 9th bit to turn off the LED.
  • write32(ARM_GPIO2_DATA0, reg): Writes the content back to the register.
  • reg |= 0x200: Sets the 9th bit to turn on the LED.

The first command in this source code is #include "base.h". The base.h header file contains some parameters, such as the address of the LED we want to control.

In the Raspberry Pi, many devices (peripherals) can be accessed and directly manipulated via certain addresses. The green LED of the Raspberry Pi 5 is accessible via the GPIO2 register, specifically via the ARM_GPIO2_DATA0 register. The LED occupies bit 9 of this register. Since the other bits in this register have other functions, we must first load the entire content of the register.

The command reg &= ~0x200 performs a bitwise operation to clear a specific bit in the variable reg (set it to 0). Here's how it works:

  • Hexadecimal Number: 0x200 is the hexadecimal representation of the number 512. In binary, this is 0000 0010 0000 0000.
  • Bitwise NOT Operation: The ~ operator performs a bitwise negation, meaning all bits are inverted. For 0x200 (binary 0000 0010 0000 0000), this results in: ~0x200 = 1111 1101 1111 1111.
  • Bitwise AND: The &= operator combines the existing variable reg with the result of the bitwise negation of 0x200 using the bitwise AND operator (&). This means only the bits in reg are retained that are also set in the inverted value of 0x200.

The command reg &= ~0x200 thus sets the ninth bit (counting from zero) of reg to 0 and leaves all other bits unchanged.

Then the value is written back to the register, and the LED is off.

In the LED_on function, the same is done, but we manipulate the value in the other direction:

The command reg |= 0x200 performs a bitwise operation to set a specific bit in the variable reg (set it to 1). Here's how it works:

  • Hexadecimal Number: 0x200 is the hexadecimal representation of the number 512. In binary, this is 0000 0010 0000 0000.
  • Bitwise OR: The |= operator combines the existing variable reg with 0x200 using the bitwise OR operator (|). This means each bit in reg is set to 1 if the corresponding bit in 0x200 is also 1.

The command reg |= 0x200 thus sets the ninth bit (counting from zero) of reg to 1 and leaves all other bits unchanged.

Basic Configuration File base.h

The base.h file contains base addresses for the Raspberry Pi:

//
// base.h
//

#ifndef _base_h
#define _base_h

#define RPI_BASE  0x107C000000UL

// GPIO
#define ARM_GPIO2_BASE RPI_BASE + 0x1517C00
#define ARM_GPIO2_DATA0 ARM_GPIO2_BASE + 0x04

#endif

Explanation:

  • RPI_BASE: Base address of peripheral devices.
  • ARM_GPIO2_BASE: Base address of the GPIO2 register.
  • ARM_GPIO2_DATA0: Address of the data register that controls the LED.

Note: This header will be continuously expanded throughout our course. Currently, we only need the data for our LED.

With RPI_BASE = 0x107C000000UL, we set the base address of peripheral devices. This serves as the foundation for all other devices we use.

This file will later be divided into sections for each individual device. Currently, we only use the GPIO.

With ARM_GPIO2_BASE = RPI_BASE + 0x1517C00, the base address of GPIO2 is set. The data offset is then 0x04, so in total 0x107C000000 + 0x1517C00 + 0x04 equals 0x107D517C04. This is the actual memory address in the Raspberry Pi 5 for the register that controls the LED.

Wait Function "wait"

Lastly, we create the file time.c for the wait function:

//
// time.c
//

#include "types.h"

void wait(u32 cycles) 
{
    volatile u32 i;
    for (i = 0; i < cycles; i++) 
    {
        // Empty loop for delay
    }
}

Explanation:

  • volatile u32 i: Creates the variable i. The volatile keyword prevents the loop from being optimized away by the compiler.
  • for (i = 0; i < cycles; i++): Creates a loop that increments i as long as i is less than cycles.
  • {...}: Here, an empty loop is generated.

write32 and read32

To read a system register or write to it, we need two functions called write32 and read32. Since it involves direct addressing in memory, we simply use assembly code:

// util.s

.globl write32
write32:
    stp x29, x30, [sp, -16]!
    mov x29, sp
    str w1, [x0]
    ldp x29, x30, [sp], 16
    ret

.globl read32
read32:
    stp x29, x30, [sp, -16]!
    mov x29, sp
    ldr w0, [x0]
    ldp x29, x30, [sp], 16
    ret

Additional Header Files

In C, functions are often declared in header files (with the extension .h) before being defined in the actual source code files (with the extension .c). This has several important reasons:

  • Modularity and Reusability: Header files allow the code to be divided into different modules. This way, functions and data structures can be reused in multiple source code files without duplicating the code.
  • Separation of Declaration and Definition: Declaring a function in a header file informs the compiler about the existence and interface of the function (name, return value, parameters) without providing the complete function code. The definition containing the actual code is in the corresponding .c file. This supports the principle of encapsulation by separating implementation details from the interface.
  • Avoidance of Redundant Declarations: Including the header file in multiple source code files (`#include "header.h"`) ensures that all source code files use the same function declarations. This prevents errors caused by inconsistent declarations.
  • Ease of Maintenance: Changes to the function declaration (e.g., changing the parameters) only need to be made in the header file. All source code files that include this header file automatically use the updated declaration.
  • Compilation and Linking: During compilation, the compiler checks the header files to ensure that function calls are correct. During linking, the actual function definitions from the source code files are merged. This also allows large projects to be compiled efficiently, as only the changed files need to be recompiled while the unchanged files from the last compilation can be reused.
  • Internal and External Visibility: Header files can be used to control the visibility of functions and variables. Functions declared in a header file are visible to all source code files that include this header file. Functions defined only in the source code file are visible only in that file (static functions).

In our code, three header files are still missing: led.h, time.h, and types.h.

The led.h

// led.h

#ifndef _ms_led_h
#define _ms_led_h

void LED_off(void);
void LED_on(void);

#endif

This header file defines the function calls LED_off and LED_on. Both function calls expect no parameters and return no parameters. This is indicated by the keyword void.

The time.h

// time.h

#ifndef _ms_time_h
#define _ms_time_h

#include "types.h"

void wait(u32 cycles);

#endif

Here, the function call wait is defined. This function returns no value but expects a u32 value from us. u32 is defined in the next header "types.h". Therefore, this header is included so that the compiler knows what u32 means.

The types.h

// types.h

#ifndef _ms_types_h
#define _ms_types_h

typedef unsigned char        u8;
typedef unsigned short       u16;
typedef unsigned int         u32;

typedef signed char          s8;
typedef signed short         s16;
typedef signed int           s32;

typedef unsigned long        u64;
typedef signed long          s64;

typedef long                 intptr;
typedef unsigned long        uintptr;

typedef unsigned long        size_t;
typedef long                 ssize_t;

typedef char                 boolean;

#define ALIGN(n)             __attribute__((aligned (n)))

#define FALSE                0
#define TRUE                 1

#endif

The types.h header defines various data types and some macros that can be used in a C program. Here is a general description of what happens in this header:

Type Definitions:

  • New data types are created with `typedef` to make the standard types in C clearer and more consistent.
    • Unsigned Types:
      • `u8`: an alias for `unsigned char` (8-bit).
      • `u16`: an alias for `unsigned short` (16-bit).
      • `u32`: an alias for `unsigned int` (32-bit).
      • `u64`: an alias for `unsigned long` (64-bit).
    • Signed Types:
      • `s8`: an alias for `signed char` (8-bit).
      • `s16`: an alias for `signed short` (16-bit).
      • `s32`: an alias for `signed int` (32-bit).
      • `s64`: an alias for `signed long` (64-bit).
    • Pointer Types:
      • `intptr`: an alias for `long`, to store an integer large enough to hold a pointer.
      • `uintptr`: an alias for `unsigned long`, for an unsigned integer large enough to hold a pointer.
    • Size Types:
      • `size_t`: an alias for `unsigned long`, typically used for the size of objects.
      • `ssize_t`: an alias for `long`, typically used for signed size values.

Boolean Type:

  • A new type `boolean` is defined as an alias for `char`.
typedef char boolean;

Macros for Alignment:

  • `ALIGN(n)`: a macro that uses the GCC-specific `__attribute__((aligned(n)))` to enforce the alignment of a data type on `n` bytes.
#define ALIGN(n) __attribute__((aligned (n)))

Boolean Values:

  • Two macros `FALSE` and `TRUE` are defined to represent the values
#define FALSE 0
#define TRUE 1

In summary, this header defines a series of new types and macros that improve the readability and portability of the code by providing standardized names and values.

Compiling and Running

Navigate to the LED directory and compile the program with the make command. If everything was successful, you will get a file named kernel_2712.img, which can be copied to an SD card and used in the Raspberry Pi. Turn on the Raspberry Pi, and the LED should start blinking.

You can download the source code as a ZIP file from the following link: https://www.satyria.de/arm/sources/C/led.zip