Graphics (PI5): Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
 
(3 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 219: Zeile 219:
''Endless loop'':
''Endless loop'':
*'''999: b 999b''': An endless loop to keep the program running after the pixels are drawn and the error code is displayed.
*'''999: b 999b''': An endless loop to keep the program running after the pixels are drawn and the error code is displayed.
You can download the source code as a ZIP file from the following link: https://www.satyria.de/arm/sources/assem/graphics.zip
-----
{| style="width: 100%;
| style="width: 33%;" | [[Error Handling|< Back (Error Handling)]]
| style="width: 33%; text-align:center;" | [[English|< Home >]]
| style="width: 33%; text-align:right;" | [[Chars (PI5)e|Next (Chars (PI5)) >]]
|}

Aktuelle Version vom 23. August 2024, 11:48 Uhr

Note: On the Raspberry Pi 5, graphics have undergone significant changes, making it more challenging for BareMetal developers to program graphics correctly. There has already been a discussion on GitHub regarding this: Raspberry Pi Firmware Issue #1904.

I am not pleased with this development or decision!

Nevertheless, I managed to implement a display on the Raspberry Pi 5 that is functional. Unlike previous models, the screen is set in the config.txt file and is already available when the first boot occurs.

We use the entry framebuffer_depth=32 in the config.txt file. This entry creates a framebuffer with a depth of 32 bits and a resolution of 1920x1080 pixels.

The Raspberry Pi has multiple processors responsible for different tasks. One of these processors (GPU) manages the graphics. Interestingly, this same processor is also responsible for the initial startup of the Raspberry Pi. After the initial initialization, it hands control over to the ARM processor.

Since each processor works independently, we need a way for these processors to communicate with each other. For this purpose, a type of mailbox has been set up, where we send "letters" to specify what we want from the graphics processor. When the graphics processor responds, it uses the same mailbox. Naturally, as is typical in computer science, the format of the "letter" is defined, and we must adhere to these guidelines.

Structure of a Mailbox Message

A TAG structure always begins with a 32-bit number indicating the length of the entire structure. This is followed by a 32-bit buffer to possibly store a response that will be transmitted during a call. Next come the individual TAGs. Each TAG has a basic structure: first, the name of the TAG is transmitted, followed by the number of values in bytes (32 bits, which is 4 bytes), and another buffer for the response. Then come the values for the respective TAG. The entire structure ends with a NULL TAG.

  • Mailbox Length
  • Mailbox Request Code
    • TAG1 Name
    • TAG1 Length
    • TAG1 Buffer
      • TAG1 Value1
      • TAG1 Value2
      • ...
    • TAG2 Name
    • ...
    • TAGx NULL (End TAG)

For more information, you can refer to GitHub. It is important that each TAG is aligned to 32 bits.

Unlike previous models, it is easier to obtain the memory address of the graphics on the Raspberry Pi 5. Unfortunately, it is no longer possible to configure a desired display, such as the screen resolution. We must use what is available and request the graphics memory address with PROPTAG_ALLOCATE_BUFFER.

Our structure for the mailbox call now looks as follows:

//**********************************************************
// Data
//**********************************************************
.section .data
.align 16
pScreen:
		.int pScreen_end - pScreen
		.int CODE_REQUEST

        .int PROPTAG_ALLOCATE_BUFFER
        .int 8
        .int 4
m_nBufferPtr:
        .int 0
m_nBufferSize:
        .int 0

        .int PROPTAG_END
pScreen_end:

We will pass the address of this structure in x1 to the mailbox function that we will write, and we will write the mailbox channel in w0. For communication (ARM to VC), we use channel 8, which we previously defined with BCM_MAILBOX_PROP_OUT.

// boolean Init_Screen (void)
.globl Init_Screen
Init_Screen:
	stp x29, x30, [sp, -16]!
	mov x29, sp
1:
	ldr w0,=BCM_MAILBOX_PROP_OUT  // Channel
	ldr x1,=pScreen               // Screen structure
	bl BcmMailBox_Write           // Call the mailbox
    
    ldr x0,=m_nBufferPtr          // Load the memory address
    ldr w0,[x0]
    cmp w0,#0
    beq 1b
    and w0,w0,#0x3FFFFFFF         // Adjust the address
    ldr x1,=graphicsAddress       // And store it
    str w0,[x1]

    ldp x29, x30, [sp], 16
    ret

In our tag list, we receive a response from the graphics chip under PROPTAG_ALLOCATE_BUFFER, indicating the address of the graphics memory. We have labeled this position in the tag as m_nBufferPtr so we can directly access it.

If the memory address still holds a null value, we know that communication either has not occurred yet or there was an error. We call the mailbox repeatedly until we get a response.

The address we get back is as seen by the graphics chip. However, since we are working on the ARM processor, we need to adjust the address to our address space. This is done with and w0,w0,#0x3FFFFFFF. Then we store this address under graphicsAddress so it is not lost.

The Mailbox

Currently, we are keeping it very simple and leaving it to the calling program to evaluate the result.

.globl BcmMailBox_Write
// Inputs: x0 = nChannel, x1 = nData
BcmMailBox_Write:
    stp x29, x30, [sp, -16]!
    mov x29, sp
    ldr x2, =MAILBOX1_WRITE
    orr w1, w1, w0
    str w1, [x2]
    ldp x29, x30, [sp], 16
    ret

We store the address of the MAILBOX1_WRITE function in x2, combine the passed values w0 and x1 using the orr command, and write the result to the address.

Note

This is a very simple implementation of the mailbox function. Later, we will extend this, as we should first check if the mailbox is ready to process data. This is not always possible, and we would have to wait until the data in the mailbox is processed.

DrawPixel Function

Now we have the graphics address, but so far, we can't display anything. For this, we create a DrawPixel function.

The function is passed the coordinates in w0 (x) and w1 (y). First, we check if the pixel fits on the screen. If not, we skip this function.

If the pixel fits, we need to calculate the position in memory: Each row can fit 1920 pixels. We multiply y by the number of pixels per row and add x to it. Since we have 32 bits (4 bytes) per pixel, we multiply the result by 4. This gives us the memory area that our screen point would occupy if the graphics address were zero. We simply add the graphics address to it.

Now we have the calculated position where we can store our point. Since our system has a depth of 32 bits, representing the color, we just need to store our color value here.

The Color

The color we use here is called ARGB. Each letter stands for a specific component:

  • A: Alpha (opacity)
  • R: Red component
  • G: Green component
  • B: Blue component

Each value can range from 0 to 255. The higher the value, the more intense the color component or opacity. For simplicity, we will always choose an opacity of 100% and use the hexadecimal system:

Examples of colors:

  • 0xffff0000: Red
  • 0xff00ff00: Green
  • 0xffffff00: Yellow

Before using the DrawPixel function, we store the color value as a variable DrawColor in memory. We read this value from there and copy it to the previously calculated position.

Here is the code for the DrawPixel function:

// void DrawPixel (x=w0, y=w1)
.globl DrawPixel
DrawPixel:
    stp x29, x30, [sp, -16]!
    stp x3, x4, [sp, -16]!
    mov x29, sp

    cmp w0, #SCREEN_X         // Check if the pixel fits on the screen
    bge 1f

    cmp w1, #SCREEN_Y
    bge 1f

    mov w3, SCREEN_X
    mul w3, w3, w1
    add w3, w3, w0

    lsl w3, w3, #2

    ldr x4, =graphicsAddress
    ldr w4, [x4]

    add w3, w3, w4
    ldr x2, =DrawColor
    ldr w2, [x2]
    str w2, [x3]
1:
    ldp x3, x4, [sp], 16
    ldp x29, x30, [sp], 16
    ret

.section .data
DrawColor:
    .int 0xffffffff // for white

DrawPixel Function in Action

Now we use our new function in our kernel:

//
// kernel.S
//
.section .text
.globl main
main:
    bl LED_off
    bl Init_Screen
    mov w10, 100
2:
    cmp w10, #500
    bge 1f

    mov w0, w10
    mov w1, w10
    bl DrawPixel
    add w10, w10, #1
    b 2b
1:
    mov w0, #2
    bl LED_Error // To indicate the end.
999:
    b 999b

We create a loop that counts from 100 to 500. We pass this value to the DrawPixel function as x and y. This creates a line that runs diagonally from the top left to the bottom right. If this is successfully executed, we intentionally go into error code 2 to see that the Raspberry has completed everything.

Kernel Breakdown

  • bl LED_off: Turns off the LED to ensure we have a defined initial state.
  • bl Init_Screen: Initializes the screen to ensure we have correctly obtained the graphics address.

Loop for drawing pixels:

  • mov w10, 100: Sets the start value for the loop.
  • cmp w10, #500: Compares the current value of w10 with 500.
  • bge 1f: Jumps to label 1 if w10 is greater than or equal to 500 to end the loop.
  • mov w0, w10 and mov w1, w10: Sets the x and y coordinates for the pixel to be drawn.
  • bl DrawPixel: Calls the DrawPixel function to draw the pixel.
  • add w10, w10, #1: Increases w10 by 1 to draw the next pixel.
  • b 2b: Jumps back to the beginning of the loop to draw the next pixel.

Error code indication:

  • mov w0, #2 and bl LED_Error: Sets the error code to 2 and calls the LED_Error function to indicate the end of execution.

Endless loop:

  • 999: b 999b: An endless loop to keep the program running after the pixels are drawn and the error code is displayed.

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


< Back (Error Handling) < Home > Next (Chars (PI5)) >