generate audio with PWM and DMA on rp2040
A minimal version:
\ audio - 2023-03-31 wj
\ Pulse Width Modulation on GPIO pins and generating signals from table using DMA
\ PWM workbench 2025-02-11 -- PWM on GPIO pins
\ this version uses noForth (20)250222 on raspberry pi pico rp2040
\ hx and dm are noforth prefixes for hexadecimal and decimal values
\ signal output on GPIO 6 8 10 12
\ funcsel 4=PWM, slice number (CHn) and channel (A/B) per GPIO:
\ 6 CH3 A 8 CH4 A 10 CH5 A 12 CH6 A
\ Audio generation:
\ - sample frequency is fixed
\ - system clock 125 MHz
\ - each waveform table is hx 100 cells, values between 0 and 1000
\ - one cell per sample value
\ - example: set CC with new value every microsecond: gives frequency of 1000000 / 256 = 3906 Hz
\ - DMA to feed wave tables at a standard rate to the CHn-CC registers
\ 256 samples per cycle
\ send all values of the waveform table to CC every period repeatedly
\ HARDWARE CONTROL
hex
\ support words and register constants
\ GPIO PWM -- Offset hx 14 -- GPIO 6 8 10 12 slice CH3 4 5 6
: allGPIOs! ( u adr -- ) 4 0 ?do 2dup i 14 * + ! loop 2drop ;
4005003c constant CH3_CSR \ enable
40050040 constant CH3_DIV \ systemclock frequency divider
40050048 constant CH3_CC \ compare counter with wafeform value - we use channel A only
4005004c constant CH3_TOP \ set for audio sampling frequency 30.517 kHz
\ GPIO funcion setting -- Offset 10 -- for GPIOs 6 8 10 12
: ALLFUNC! ( u adr -- ) 4 0 ?do 2dup i 10 * + ! loop 2drop ;
40014034 constant GPIO6-CTRL \ GPIO function select
\ DMA registers -- offset 40 CH0 1 2 3
: ALLDMA! ( u adr -- ) 4 0 ?do 2dup i 40 * + ! loop 2drop ;
50000000 constant CH0_READ_ADDR \ offset 40 to CH1 and further up to CH11
50000004 constant CH0_WRITE_ADDR
50000008 constant CH0_TRANS_COUNT
5000000c constant CH0_CTRL_TRIG
5000001c constant CH0_TRANS_COUNT_TRIG
50000420 constant TIMER0 \ offset + 4 for TIMER1 .. 3
: INITPWM ( -- ) \ hardware initialisation
0 1 4 lshift + CH3_DIV allGPIOs! \ CHn-DIV div systemclock freq by 1
1000 CH3_TOP allGPIOs! \ CHn-TOP sampling freq 30.517 kHz
800 CH3_CC allGPIOs! \ CHn-CC channel A mid level
1 CH3_CSR allGPIOs! \ enable
4 GPIO6-CTRL allfunc! \ GPIOm-CTRL funcsel PWM, m is 6 8 10 12
\ PADS: The User Bank Pad Control registers start at a base address
\ of 0x4001c000
\ 0x1c GPIO6 Pad control register
\ reset values: 4ma, pulldown, output enabled, input enabled,
\ schmitt trigger enabled, slow slew
\ --> not needed for current PWM application, perhaps a refinement
\ for later (impedance, minimisation of power, etc.)
;
here .
create waves \ wave tables, for now just sine
100 cells here 3ff and - dup . allot \ align at xxxxx400 or xxxxx800 xxxxxD00 address
decimal \ 256 sine values between hx0 and hx 1000
2048 , 2098 , 2148 , 2199 , 2249 , 2299 , 2349 , 2398 , 2448 , 2497 , 2546 , 2594 , 2643 , 2690 , 2738 , 2785 ,
2832 , 2878 , 2924 , 2969 , 3013 , 3057 , 3101 , 3144 , 3186 , 3227 , 3268 , 3308 , 3347 , 3386 , 3423 , 3460 ,
3496 , 3531 , 3565 , 3599 , 3631 , 3663 , 3693 , 3722 , 3751 , 3778 , 3805 , 3830 , 3854 , 3877 , 3899 , 3920 ,
3940 , 3959 , 3976 , 3993 , 4008 , 4022 , 4035 , 4046 , 4057 , 4066 , 4074 , 4081 , 4086 , 4090 , 4094 , 4095 ,
4096 , 4095 , 4094 , 4090 , 4086 , 4081 , 4074 , 4066 , 4057 , 4046 , 4035 , 4022 , 4008 , 3993 , 3976 , 3959 ,
3940 , 3920 , 3899 , 3877 , 3854 , 3830 , 3805 , 3778 , 3751 , 3722 , 3693 , 3663 , 3631 , 3599 , 3565 , 3531 ,
3496 , 3460 , 3423 , 3386 , 3347 , 3308 , 3268 , 3227 , 3186 , 3144 , 3101 , 3057 , 3013 , 2969 , 2924 , 2878 ,
2832 , 2785 , 2738 , 2690 , 2643 , 2594 , 2546 , 2497 , 2448 , 2398 , 2349 , 2299 , 2249 , 2199 , 2148 , 2098 ,
2048 , 1998 , 1948 , 1897 , 1847 , 1797 , 1747 , 1698 , 1648 , 1599 , 1550 , 1502 , 1453 , 1406 , 1358 , 1311 ,
1264 , 1218 , 1172 , 1127 , 1083 , 1039 , 995 , 952 , 910 , 869 , 828 , 788 , 749 , 710 , 673 , 636 ,
600 , 565 , 531 , 497 , 465 , 433 , 403 , 374 , 345 , 318 , 291 , 266 , 242 , 219 , 197 , 176 ,
156 , 137 , 120 , 103 , 88 , 74 , 61 , 50 , 39 , 30 , 22 , 15 , 10 , 6 , 2 , 1 ,
0 , 1 , 2 , 6 , 10 , 15 , 22 , 30 , 39 , 50 , 61 , 74 , 88 , 103 , 120 , 137 ,
156 , 176 , 197 , 219 , 242 , 266 , 291 , 318 , 345 , 374 , 403 , 433 , 465 , 497 , 531 , 565 ,
600 , 636 , 673 , 710 , 749 , 788 , 828 , 869 , 910 , 952 , 995 , 1039 , 1083 , 1127 , 1172 , 1218 ,
1264 , 1311 , 1358 , 1406 , 1453 , 1502 , 1550 , 1599 , 1648 , 1698 , 1747 , 1797 , 1847 , 1897 , 1948 , 1998 ,
hex
here 100 cells - constant sin
waves . sin .
\ bitfield constructors: OR all of the applicable constructors
: bit ( u -- u ) 1 swap lshift ;
\ set for DMA from circular buffer sin to CHn_CC register to produce PWM audio
: CTRL.EN 1 ; \ DMA Channel Enable. When 1, the channel will respond to triggering events
: CTRL.DATA_SIZE 3 bit ; \ 0: byte; 2 bit: half word (16bits); 3 bit: word
: CTRL.INCR_READ 4 bit ; \ If 1, the read address increments with each transfer
: CTRL.INCR_WRITE 0 ; \ 5 bit: the write address increments with each transfer.
: CTRL.RING_SIZE dm 10 6 lshift ; \ Using wave table of 256 cells, 10 address bits will change
: CTRL.RING_SEL 0 ; \ bit 10: If 0, read addresses are wrapped, if 1 write addresses
: CTRL.TREQ_SEL ( i -- n ) 03b + dm 15 lshift ; \ use TIMER0 to pace, add 1 for timer1 etc. until timer 3
: ASM-CHn_CTRL_TRIG ( i -- u )
>r CTRL.EN CTRL.DATA_SIZE or CTRL.INCR_READ or CTRL.INCR_WRITE or CTRL.RING_SIZE or
CTRL.RING_SEL or r> CTRL.TREQ_SEL or ;
\ Pacing Timers TIMERn - n is 0 ... 3 - Each DMA is able to select any of these in CTRL.TREQ_SEL
: TIMER-X/Y ( x y -- u ) swap dm 16 lshift or ;
\ support words: bits and bitfield trace info
\ : .bfx ( u u-lbit u-hbit -- u ) \ bit field extractor [lowbit..highbit]
\ cr over . dup . ( u ul uh )
\ >r dup >r over swap rshift ( u u-shifted r: uh ul )
\ 1 r> r> swap - for 1 lshift 1+ next ( .. mask )
\ and . ; ( u )
\ : .CHn_CTRL_TRIG ( u -- )
\ dm 31 dup .bfx ." AHB_ERROR"
\ dm 30 dup .bfx ." READ_ERROR"
\ dm 29 dup .bfx ." WRITE_ERROR"
\ dm 24 dup .bfx ." BUSY"
\ dm 23 dup .bfx ." SNIFF_EN"
\ dm 22 dup .bfx ." BSWAP"
\ dm 21 dup .bfx ." IRQ_QUIET
\ dm 15 dm 20 .bfx ." TREQ_SEL"
\ dm 11 dm 14 .bfx ." CHAIN_TO"
\ dm 10 dup .bfx ." RING_SEL"
\ dm 6 dm 9 .bfx ." RING_SIZE
\ 5 5 .bfx ." INCR_WRITE"
\ 4 4 .bfx ." INCR_READ"
\ 2 3 .bfx ." DATA_SIZE"
\ 1 1 .bfx ." HIGH_PRIORITY"
\ 0 0 .bfx ." EN"
\ cr . cr ;
\ : .TIMERn ( u )
\ dm 0 dm 15 .bfx ." Y"
\ dm 16 dm 31 .bfx ." X"
\ cr . cr ;
\ : .datDMA
\ ." DMA CH0_READ_ADDR:" CH0_READ_ADDR @ . ." CH0_WRITE_ADDR:" CH0_WRITE_ADDR @ .
\ ." CH0_TRANS_COUNT:" CH0_TRANS_COUNT @ . ." CH0_CTRL_TRIG:" CH0_CTRL_TRIG @ .CHn_CTRL_TRIG
\ ." TIMER0:" TIMER0 @ .TIMERn cr ;
: initDMA ( -- ) \ hardware initialisation
\ cyclic wavetable to Chn-CC register byte transfer
\ Pacing Timers TIMERn - n is 0 ... 3 - Each DMA is able to select any of these in CTRL.TREQ_SEL
\ example: 125 MHz system clock -> 1us pacing, 256 wave table values: 256 us period
4 0 ?do i 1 + dm 125 timer-x/y i 4 * timer0 + ! loop \ apply desired frequencies later
\ source and destination
\ source READ_ADDR register
sin CH0_READ_ADDR alldma! \ sin is wave table start address
\ destination WRITE_ADDR register
4 0 ?do i 14 * CH3_CC + ( CH3/4/5/6_CC) i 40 * CH0_WRITE_ADDR + ! loop
\ TRANS_COUNT register
fffff CH0_TRANS_COUNT ! \ apply desired number of cycles later
\ CTRL register
4 0 ?do i asm-CHn_ctrl_trig i 40 * CH0_CTRL_TRIG + ! loop
;
\ switch on/off
: start/stop-PWM ( n f -- ) \ n 0..3
if 1 else 0 then swap 14 * CH3_CSR + ! ;
: run-DMA ( count n x y -- ) \ count=#cycles, n 0..3, x y TIMERn settings
timer-x/y swap dup >r 4 * timer0 + !
r> 40 * CH0_TRANS_COUNT_TRIG + ! ;
: demo
4 0 ?do i true start/stop-PWM loop \ start
4 0 ?do 0fffff i 1 i 1 + dm 75 * run-DMA 1000 ms loop \ generate signals
4 0 ?do i false start/stop-PWM loop \ stop
;
\ minimal demo
initpwm initdma
demo
Timeline for 4 channel PWM audio, fed by DMA from wavetable
This is an upgrade from original PIO generated squarewave audio, adding a waveform table
2025-02-06 Study rp2040 datasheet on pulse width modulation (PWM)
2025-02-11 Initial version modulating on TOP register, no DMA, using PWM in freerunning mode
2025-02-13 Modulating on CHn_CC register; PWM TOP value fixed producing a 40 KHz PWM signal, 256 samples per period, CPU feeds single channel with microsecond delay in loop, no DMA (proof of concept)
2025-02-14 Sine wavetable added, 256 bytes
(other priorities)
2025-03-20 First sine wave, good quality
2025-03-21 Start studying DMA chapter in rp2040 datasheet
2025-03-26 First version with bad sound quality caused by byte DMA transfers to the CHn_CC register. When sending byte 6a to the CC register it contains 6a6a6a6a. To solve that: byte wave table replaced by 32 bits wave table
2025-03-27 first DMA working correctly
Incorrect assumptions and lessons learned
- Even in ring mode the DMA will not start if TRANS_COUNT is 0
- Ring size field must be set to the number of changing ring address bits, not the size of the ring. So ring size field value is 8 for a 256 byte table, not 256
- Byte size DMA transfers to register will replicate byte-value over all bytes in register (datasheet 2.1.4: "The Cortex-M0+ and DMA on RP2040 will always replicate narrow data across the bus")
- Simple error: TIMER0 offset was defined as 24 but should be 420, because the timer was not initialised it did not activate the DMA