Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
en:multiasm:paarm:chapter_5_9 [2025/12/04 14:34] – [General Purpose Inputs and Outputs (GPIO)] eriks.klavinsen:multiasm:paarm:chapter_5_9 [2025/12/04 14:59] (current) – [Pulse Width Modulation] eriks.klavins
Line 183: Line 183:
  
 ===== Pulse Width Modulation ===== ===== Pulse Width Modulation =====
 +
 +Basically, with a single GPIO line, you can do a lot: control almost any hardware, read or take measurements, generate wireless signals, and more. Of course, it is easier to control hardware designed for specific purposes, such as I2C communication in the previously described examples. As with the bit-banging technique, it is possible to generate periodic digital signals and control their duty cycle. 
 +
 +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 ''0x40098000'' and ''0x4009C000'', respectively. This hardware is located in the RP1 chip and accessed through PCIe. The Linux OS sets up hardware address mapping, and this mapping is not exposed as a simple fixed physical address that can be accessed with just ''<fc #800000>LDR</fc>''/''<fc #800000>STR</fc>'' instructions from user space. To access the PWM registers, it is necessary to execute the code at least at the EL1 level and to know already the PCIe mapping, or the mapping can be implemented manually. Again, this is too advanced and carries a risk of breaking something.
 +
 +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. 
 +<codeblock code_label>
 +<caption>Address mapping</caption>
 +<code>
 +.equ PWM0_BASE,      0xXXXXXXXX      @ filled in by platform code
 +.equ IO_BANK0_BASE,  0xYYYYYYYY      @ filled in by platform code
 +.equ PWM_CHAN2_CTRL, 0x34
 +.equ PWM_CHAN2_RANGE,0x38
 +.equ PWM_CHAN2_DUTY, 0x40
 +.equ GPIO18_CTRL,    0x094
 +.equ FUNC_PWM0_2,    0xZZ           @ FUNCSEL value for PWM0[2] on GPIO18
 +</code>
 +</codeblock>
 +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 ''<fc #008000>X0</fc>'' register will hold the period value, and the ''<fc #008000>X1</fc>'' register will hold the duty cycle value.
 +<codeblock code_label>
 +<caption>PWM initalisation</caption>
 +<code>
 +.global pwm_init_chan2
 +pwm_init_chan2:
 +    LDR     X2, =IO_BANK0_BASE
 +    ADD     X2, X2, #GPIO18_CTRL
 +    LDR     W3, [X2]                @ read current CTRL
 +    BIC     W3, W3, #0x1f  @ clear FUNCSEL bits [4:0]
 +    ORR     W3, W3, #FUNC_PWM0_2 @ set FUNCSEL to FUNC_PWM0_2
 +    STR     W3, [X2]
 +
 +</code>
 +</codeblock>
 +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.
 +<codeblock code_label>
 +<caption>set PWM duty and frequency</caption>
 +<code>
 +    LDR     X4, =PWM0_BASE
 +    ADD     X5, X4, #PWM_CHAN2_RANGE    @ X5=PWM0_BASE+PWM_CHAN2_RANGE
 +    STR     W0, [X5]                    @ low 32 bits used
 +    ADD     X5, X4, #PWM_CHAN2_DUTY
 +    STR     W1, [X5]
 +</code>
 +</codeblock>
 +Note that the code uses only 32-bit values of the ''<fc #008000>X0</fc>'' and ''<fc #008000>X1</fc>'' registers, where the input arguments are stored. The last step is to activate the PWM generator for Channel 2, setting its parameters, such as the PWM generation mode to trailing edge.
 +<codeblock code_label>
 +<caption>Enable PWM</caption>
 +<code>
 +    ADD     X5, X4, #PWM_CHAN2_CTRL   @ x5=PWM0_BASE+PWM_CHAN2_CTRL
 +    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
 +</code>
 +</codeblock>
 +
 +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: it uses system calls. The Raspberry Pi must be prepared with a device-tree overlay, for example, “''dtoverlay=pwm-2chan,pin=18,func=4''”. Note that to enable PWM, the string must be used, not just the value. Now it is time to create system calls. In the I2C example, three different system calls were made; the only difference between the code fragments is the system call value and arguments. Everything needed to activate PWM generation is to open the file, write some variables to it, and save it by closing it. 
 +<codeblock code_label>
 +<caption>Write_file function</caption>
 +<code>
 +@ void write_file(const char *path, const char *str)
 +write_file:
 +    @ x0 = path, x1 = string
 +    @ openat(AT_FDCWD, path, O_WRONLY, 0)
 +    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     #             @ X0 = fd
 +    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       1b              @ jump to label 1 (b means backward)
 +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
 +
 +</code>
 +</codeblock>
 +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:
 +<codeblock code_label>
 +<caption>Constants</caption>
 +<code>
 +    .equ SYS_openat, 56
 +    .equ SYS_write,  64
 +    .equ SYS_close,  57
 +    .equ SYS_exit,   93
 +    .equ AT_FDCWD,  -100
 +    .equ O_WRONLY,   1
 +
 +    .section .rodata
 +
 +path_export:
 +    .asciz "/sys/class/pwm/pwmchip0/export"
 +path_period:
 +    .asciz "/sys/class/pwm/pwmchip0/pwm0/period"
 +path_duty:
 +    .asciz "/sys/class/pwm/pwmchip0/pwm0/duty_cycle"
 +path_enable:
 +    .asciz "/sys/class/pwm/pwmchip0/pwm0/enable"
 +str_chan0:
 +    .asciz "0\n"        
 +str_period:
 +    .asciz "20000000\n" @ 20 ms period  = 20000000 ns  (for example, servo-style period)
 +str_duty:
 +    .asciz "5000000\n"  @ 5 ms high time = 5000000 ns  (25% duty)
 +str_enable:
 +    .asciz "1\n"          @ enable PWM
 +</code>
 +</codeblock>
 +And the main code, which sets all values:
 +<codeblock code_label>
 +<caption>Main code</caption>
 +<code>
 +    LDR     X0, =path_export    @ export PWM channel 0
 +    LDR     X1, =str_chan0
 +    BL      write_file
 +
 +    LDR     X0, =path_period    @ set period
 +    LDR     X1, =str_period
 +    BL      write_file
 +
 +    LDR     X0, =path_duty      @ set duty_cycle
 +    LDR     X1, =str_duty
 +    BL      write_file
 +
 +    LDR     X0, =path_enable    @ enable PWM generation
 +    LDR     X1, =str_enable
 +    BL      write_file
 +
 +    MOV     X0, #0               @ exit
 +    MOV     X8, #SYS_exit
 +    SVC     #0
 +
 +</code>
 +</codeblock>
 +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. 
  
en/multiasm/paarm/chapter_5_9.1764858862.txt.gz · Last modified: 2025/12/04 14:34 by eriks.klavins
CC Attribution-Share Alike 4.0 International
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0