This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| en:multiasm:paarm:chapter_5_9 [2024/09/27 21:01] – pczekalski | en:multiasm:paarm:chapter_5_9 [2025/12/04 14:59] (current) – [Pulse Width Modulation] eriks.klavins | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Peripheral Management in RPi ====== | ====== Peripheral Management in RPi ====== | ||
| + | |||
| + | The Raspberry Pi has ported out some General-Purpose Input/ | ||
| + | |||
| + | {{: | ||
| + | |||
| + | |||
| + | For example, to enable UART communication on GPIO 14 and 15, the “./ | ||
| + | * '' | ||
| + | * '' | ||
| + | |||
| + | There are libraries designed to work with GPIO and/or communication interfaces such as UART, SPI, and I2C. For example, to work with GPIO, a special library in C or Python can be used, or GPIO can be accessed via memory mapping of IO registers. Note that GPIO parameters must be set before using them as outputs or inputs, like setting pull-up or pull-down resistors, setting direction, etc. On Linux, using libraries such as libgpiod or wiringPi/ | ||
| + | |||
| + | ===== General Purpose Inputs and Outputs (GPIO) ===== | ||
| + | |||
| + | Usable GPIO pins on the Raspberry Pi 5 are in IO_BANK0, and this provides information on the addresses available for GPIO programming. Other banks, IO_BANK1 and IO_BANK2, are reserved for internal use inside the board. The base address for IO_BANK0 is 0x400D0000, and the remaining addresses for GPIO programming are documented in the section “3.1.4.Registers” at the [[https:// | ||
| + | < | ||
| + | |||
| + | In the previous section, the basic GPIO pin toggling was implemented. GPIOs can be used for many purposes, such as replacing hardware components that are not available on the current board. The technique is called bit-banging. A Raspberry Pi has only one I2C interface, and let's assume that two or more sensors must be connected to the board and share the same I2C device address. This means that only one sensor per I2C channel can be connected, and as the Raspberry Pi has only one interface, only one sensor can be used. With the bit-bang technique, it is possible to create many communication interfaces if at least two GPIO pins are available. Note that the bit-banging technique uses processor power, so there are also limits for the number of interfaces. That’s because I2C must maintain a 100kHz clock rate for successful communication, | ||
| + | |||
| + | The following code example will be executed in the User space and may require root permissions to access the “''/ | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | .global _start | ||
| + | .section .text | ||
| + | |||
| + | @ Constants | ||
| + | .equ GPIO_BASE, | ||
| + | .equ GPFSEL0, | ||
| + | .equ GPSET0, | ||
| + | .equ GPCLR0, | ||
| + | .equ GPLEV0, | ||
| + | |||
| + | .equ SDA_BIT, | ||
| + | .equ SCL_BIT, | ||
| + | |||
| + | _start: | ||
| + | @ Open / | ||
| + | MOV X8, #56 @ syscall openat | ||
| + | MOV X0, #-100 @ AT_FDCWD | ||
| + | LDR X1, =path | ||
| + | MOV X2, #2 @ O_RDWR | ||
| + | MOV X3, #0 | ||
| + | SVC #0 @ open -> fd in X0 | ||
| + | MOV X19, X0 @ save fd | ||
| + | |||
| + | @ mmap GPIO | ||
| + | MOV X8, #222 @ syscall mmap | ||
| + | MOV X0, #0 @ addr | ||
| + | MOV X1, #4096 @ length | ||
| + | MOV X2, #3 @ PROT_READ|PROT_WRITE | ||
| + | MOV X3, #1 @ MAP_SHARED | ||
| + | MOV X4, X19 @ fd | ||
| + | MOV X5, #0 @ offset | ||
| + | SVC #0 | ||
| + | MOV X20, X0 @ base addr | ||
| + | |||
| + | @ configure pins 2,3 as outputs | ||
| + | LDR W1, [X20, #GPFSEL0] | ||
| + | BIC W1, W1, #(0b111 << 6) @ clear bits for GPIO2 | ||
| + | ORR W1, W1, #(0b001 << 6) @ set output | ||
| + | BIC W1, W1, #(0b111 << 9) @ clear bits for GPIO3 | ||
| + | ORR W1, W1, #(0b001 << 9) | ||
| + | STR W1, [X20, #GPFSEL0] | ||
| + | |||
| + | @ Send start condition: SDA low while SCL high | ||
| + | BL scl_high | ||
| + | BL delay | ||
| + | BL sda_low | ||
| + | BL delay | ||
| + | |||
| + | @ Send byte 0xA5 (1010 0101) | ||
| + | MOV W0, #0xA5 | ||
| + | MOV W1, #8 | ||
| + | send_bit: | ||
| + | BL scl_low | ||
| + | TST W0, #0x80 | ||
| + | BEQ send_zero | ||
| + | BL sda_high | ||
| + | B after_bit | ||
| + | send_zero: | ||
| + | BL sda_low | ||
| + | after_bit: | ||
| + | BL delay | ||
| + | BL scl_high | ||
| + | BL delay | ||
| + | BL scl_low | ||
| + | LSL W0, W0, #1 | ||
| + | SUBS W1, W1, #1 | ||
| + | B.NE send_bit | ||
| + | |||
| + | @ Stop: SDA high while SCL high | ||
| + | BL sda_low | ||
| + | BL scl_high | ||
| + | BL delay | ||
| + | BL sda_high | ||
| + | |||
| + | @ Exit | ||
| + | MOV X8, #93 | ||
| + | MOV X0, #0 | ||
| + | SVC #0 | ||
| + | |||
| + | @ ---- Subroutines ---- | ||
| + | sda_high: | ||
| + | MOV W2, #SDA_BIT | ||
| + | STR W2, [X20, #GPSET0] | ||
| + | RET | ||
| + | sda_low: | ||
| + | MOV W2, #SDA_BIT | ||
| + | STR W2, [X20, #GPCLR0] | ||
| + | RET | ||
| + | scl_high: | ||
| + | MOV W2, #SCL_BIT | ||
| + | STR W2, [X20, #GPSET0] | ||
| + | RET | ||
| + | scl_low: | ||
| + | MOV W2, #SCL_BIT | ||
| + | STR W2, [X20, #GPCLR0] | ||
| + | RET | ||
| + | delay: | ||
| + | MOV W3, #100 | ||
| + | delay_loop: | ||
| + | SUBS W3, W3, #1 | ||
| + | B.NE delay_loop | ||
| + | RET | ||
| + | |||
| + | .section .rodata | ||
| + | path: .asciz "/ | ||
| + | </ | ||
| + | </ | ||
| + | < | ||
| + | Overall, working with any peripheral requires studying its documentation to identify its base addresses and the offset addresses needed to control it. | ||
| + | |||
| + | ===== Communication interfaces ===== | ||
| + | |||
| + | The hardware can be used to send and receive a byte through I2C. The hardware itself performs all the control over digital signals. Everything that is needed again, find the base addresses for the hardware. Unfortunately, | ||
| + | |||
| + | In chapter 2 of the [[https:// | ||
| + | |||
| + | First, it is necessary to determine which interfaces are available on the Raspberry Pi and, of course, to find the device addresses. Another option is to use Linux. To use any communication interface, it must be enabled in the OS. The easiest way is to use the standard Linux i.e. i2c-dev interface, and it can be used with the following function calls: | ||
| + | * open("/ | ||
| + | * write(fd, & | ||
| + | * read(fd, & | ||
| + | |||
| + | In the assembler code, the I2C device must be opened by calling the OS system function “'' | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | path_i2c: | ||
| + | .asciz "/ | ||
| + | .section .text | ||
| + | .global _start | ||
| + | _start: | ||
| + | MOV X0, #-100 @ #AT_FDCWD | ||
| + | LDR X1, =path_i2c | ||
| + | MOV X2, #2 @ O_RDWR - read and write | ||
| + | MOV X3, #0 | ||
| + | MOV X8, #56 @ #SYS_openat | ||
| + | SVC #0 | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | |||
| + | After the system call is executed, the ''< | ||
| + | |||
| + | < | ||
| + | < | ||
| + | < | ||
| + | I2Cbyte: | ||
| + | .byte 0xAB | ||
| + | MOV X0, X19 | ||
| + | LDR X1, =I2Cbyte | ||
| + | MOV X2, #1 @ nuber of bytes | ||
| + | MOV X8, # | ||
| + | SVC #0 | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | The code sends the data; the device address is not set in these examples. The system call with number ''< | ||
| + | |||
| + | To unlock the full potential of any communication interface on Raspberry Pi, it will take a lot of effort to find the register addresses, digital lines, and their parameters, and even then, something will be missing. For example, the same code that works on Raspberry Pi version 3 or 4 will not work on version 5. The hardware addresses differ, and it seems the Operating System is translating them into 32-bit addresses. The communication interface depends heavily on the Operating System kernel version, kernel modules, and configuration. It is recommended to use system calls for other communication interfaces as well, as experimenting with the hardware without complete documentation is not recommended. As a result, the code may take harmful actions, damaging the Raspberry Pi. | ||
| + | |||
| + | ===== Pulse Width Modulation ===== | ||
| + | |||
| + | Basically, with a single GPIO line, you can do a lot: control almost any hardware, read or take measurements, | ||
| + | |||
| + | In Raspberry Pi 5, the RP1 chip documentation contains much more information on pulse-width modulation than on basic communication interfaces. PWM registers are on the internal peripheral bus; the base addresses for PWM0 and PWM1 are '' | ||
| + | |||
| + | Before proceeding, the base addresses must be checked at least three times (**NO JOKES**) and, if needed, replaced. The PWM0_BASE and IO_BANK0_BASE addresses are already mapped and known for the Raspberry Pi 5. In the example, the GPIO line 18 will be used. | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | .equ PWM0_BASE, | ||
| + | .equ IO_BANK0_BASE, | ||
| + | .equ PWM_CHAN2_CTRL, | ||
| + | .equ PWM_CHAN2_RANGE, | ||
| + | .equ PWM_CHAN2_DUTY, | ||
| + | .equ GPIO18_CTRL, | ||
| + | .equ FUNC_PWM0_2, | ||
| + | </ | ||
| + | </ | ||
| + | These are the constants used later on in the code. Note that three constants are holding dummy values – these values depend on the hardware. The rest of the code will control GPIO18 and generate a pulse at the specified frequency and duty cycle. The frequency and duty cycle parameters can be passed to the code. The ''< | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | .global pwm_init_chan2 | ||
| + | pwm_init_chan2: | ||
| + | LDR X2, =IO_BANK0_BASE | ||
| + | ADD X2, X2, # | ||
| + | LDR W3, [X2] @ read current CTRL | ||
| + | BIC W3, W3, #0x1f @ clear FUNCSEL bits [4:0] | ||
| + | ORR W3, W3, # | ||
| + | STR W3, [X2] | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | At this moment, the GPIO line is ready and internally connected to the PWM generator. The following code lines provide the PWM generator with all the necessary parameters: period and duty cycle. | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | LDR X4, =PWM0_BASE | ||
| + | ADD X5, X4, # | ||
| + | STR W0, [X5] @ low 32 bits used | ||
| + | ADD X5, X4, # | ||
| + | STR W1, [X5] | ||
| + | </ | ||
| + | </ | ||
| + | Note that the code uses only 32-bit values of the ''< | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | ADD X5, X4, # | ||
| + | LDR W6, [X5] | ||
| + | @ Check the datasheet -> clear existing MODE bits (example) | ||
| + | BIC W6, W6, #(0x7) | ||
| + | @ set mode to trailing-edge | ||
| + | ORR W6, W6, #0x1 | ||
| + | @ set enable bit (placeholder) check datasheet | ||
| + | ORR W6, W6, #(1 << 8) | ||
| + | STR W6, [X5] | ||
| + | RET | ||
| + | </ | ||
| + | </ | ||
| + | |||
| + | This code can be used as a kernel module, but it cannot be executed directly from a regular user program on Pi OS. That’s because the mapping is involved, and with that, the kernel is also involved (because of the PCIe mapping). | ||
| + | |||
| + | ** The second approach ** | ||
| + | |||
| + | The second approach is similar to I2C communication: | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | @ void write_file(const char *path, const char *str) | ||
| + | write_file: | ||
| + | @ x0 = path, x1 = string | ||
| + | @ openat(AT_FDCWD, | ||
| + | MOV X2, #O_WRONLY | ||
| + | MOV X3, #0 | ||
| + | MOV X8, #SYS_OPENAT | ||
| + | MOV X4, #AT_FDCWD | ||
| + | MOV X0, X4 @ AT_FDCWD | ||
| + | @ x1 already holds path | ||
| + | SVC # | ||
| + | MOV X19, X0 @ save fd | ||
| + | @ find string length | ||
| + | MOV X0, X1 @ pointer to string | ||
| + | 1: LDRB W2, [X0], #1 | ||
| + | CBZ W2, 2f @ jump to label 2 (f means forward) | ||
| + | B | ||
| + | 2: | ||
| + | @ now X0 points past NUL; length = (X0 - 1) - str | ||
| + | SUB X2, X0, X1 @ remove the NUL as it is not needed | ||
| + | SUB X2, X2, #1 | ||
| + | @ write(fd, str, len) | ||
| + | MOV X0, X19 | ||
| + | MOV X8, #SYS_write | ||
| + | SVC #0 | ||
| + | @ close(fd) | ||
| + | MOV X0, X19 | ||
| + | MOV X8, #SYS_close | ||
| + | SVC #0 | ||
| + | RET | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | Now, the code to activate the PWM generator sets the period and duty. It is necessary to know which file to edit and which values to write to the files. Required constants for this example are: | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | .equ SYS_openat, 56 | ||
| + | .equ SYS_write, | ||
| + | .equ SYS_close, | ||
| + | .equ SYS_exit, | ||
| + | .equ AT_FDCWD, | ||
| + | .equ O_WRONLY, | ||
| + | |||
| + | .section .rodata | ||
| + | |||
| + | path_export: | ||
| + | .asciz "/ | ||
| + | path_period: | ||
| + | .asciz "/ | ||
| + | path_duty: | ||
| + | .asciz "/ | ||
| + | path_enable: | ||
| + | .asciz "/ | ||
| + | str_chan0: | ||
| + | .asciz " | ||
| + | str_period: | ||
| + | .asciz " | ||
| + | str_duty: | ||
| + | .asciz " | ||
| + | str_enable: | ||
| + | .asciz " | ||
| + | </ | ||
| + | </ | ||
| + | And the main code, which sets all values: | ||
| + | < | ||
| + | < | ||
| + | < | ||
| + | LDR X0, =path_export | ||
| + | LDR X1, =str_chan0 | ||
| + | BL write_file | ||
| + | |||
| + | LDR X0, =path_period | ||
| + | LDR X1, =str_period | ||
| + | BL write_file | ||
| + | |||
| + | LDR X0, =path_duty | ||
| + | LDR X1, =str_duty | ||
| + | BL write_file | ||
| + | |||
| + | LDR X0, =path_enable | ||
| + | LDR X1, =str_enable | ||
| + | BL write_file | ||
| + | |||
| + | MOV X0, #0 @ exit | ||
| + | MOV X8, #SYS_exit | ||
| + | SVC #0 | ||
| + | |||
| + | </ | ||
| + | </ | ||
| + | The code can be upgraded to accept the arguments: period and duty cycle. This would be similar to the write_file function, which takes two arguments. | ||
| + | |||