├── .gitignore ├── CMakeLists.txt ├── img ├── DSP_IMG_angle.jpg ├── DSP_IMG_top.jpg ├── banner.ai ├── banner_800x300.png └── banner_800x600.png ├── main.cpp ├── pico_extras_import.cmake ├── pico_sdk_import.cmake ├── readme.md └── src ├── AudioPioRingBuffer.cpp ├── AudioPioRingBuffer.h ├── I2S.cpp ├── I2S.h ├── compatability.h ├── iir.cpp ├── iir.h └── pio_i2s.pio /.gitignore: -------------------------------------------------------------------------------- 1 | ### USER ADDED 2 | 3 | build/ 4 | dist/ 5 | .vscode/ 6 | 7 | ### AUTO GENERATED 8 | 9 | # Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,cmake,visualstudiocode 10 | # Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,cmake,visualstudiocode 11 | 12 | ### CMake ### 13 | CMakeLists.txt.user 14 | CMakeCache.txt 15 | CMakeFiles 16 | CMakeScripts 17 | Testing 18 | Makefile 19 | cmake_install.cmake 20 | install_manifest.txt 21 | compile_commands.json 22 | CTestTestfile.cmake 23 | _deps 24 | 25 | ### CMake Patch ### 26 | # External projects 27 | *-prefix/ 28 | 29 | ### Linux ### 30 | *~ 31 | 32 | # temporary files which can be created if a process still has a handle open of a deleted file 33 | .fuse_hidden* 34 | 35 | # KDE directory preferences 36 | .directory 37 | 38 | # Linux trash folder which might appear on any partition or disk 39 | .Trash-* 40 | 41 | # .nfs files are created when an open file is removed but is still being accessed 42 | .nfs* 43 | 44 | ### macOS ### 45 | # General 46 | .DS_Store 47 | .AppleDouble 48 | .LSOverride 49 | 50 | # Icon must end with two \r 51 | Icon 52 | 53 | 54 | # Thumbnails 55 | ._* 56 | 57 | # Files that might appear in the root of a volume 58 | .DocumentRevisions-V100 59 | .fseventsd 60 | .Spotlight-V100 61 | .TemporaryItems 62 | .Trashes 63 | .VolumeIcon.icns 64 | .com.apple.timemachine.donotpresent 65 | 66 | # Directories potentially created on remote AFP share 67 | .AppleDB 68 | .AppleDesktop 69 | Network Trash Folder 70 | Temporary Items 71 | .apdisk 72 | 73 | ### macOS Patch ### 74 | # iCloud generated files 75 | *.icloud 76 | 77 | ### VisualStudioCode ### 78 | .vscode/* 79 | !.vscode/settings.json 80 | !.vscode/tasks.json 81 | !.vscode/launch.json 82 | !.vscode/extensions.json 83 | !.vscode/*.code-snippets 84 | 85 | # Local History for Visual Studio Code 86 | .history/ 87 | 88 | # Built Visual Studio Code Extensions 89 | *.vsix 90 | 91 | ### VisualStudioCode Patch ### 92 | # Ignore all local history of files 93 | .history 94 | .ionide 95 | 96 | # Support for Project snippet scope 97 | .vscode/*.code-snippets 98 | 99 | # Ignore code-workspaces 100 | *.code-workspace 101 | 102 | ### Windows ### 103 | # Windows thumbnail cache files 104 | Thumbs.db 105 | Thumbs.db:encryptable 106 | ehthumbs.db 107 | ehthumbs_vista.db 108 | 109 | # Dump file 110 | *.stackdump 111 | 112 | # Folder config file 113 | [Dd]esktop.ini 114 | 115 | # Recycle Bin used on file shares 116 | $RECYCLE.BIN/ 117 | 118 | # Windows Installer files 119 | *.cab 120 | *.msi 121 | *.msix 122 | *.msm 123 | *.msp 124 | 125 | # Windows shortcuts 126 | *.lnk 127 | 128 | # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,cmake,visualstudiocode 129 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Generated Cmake Pico project file 2 | 3 | cmake_minimum_required(VERSION 3.13) 4 | 5 | set(CMAKE_C_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD 17) 7 | 8 | # Pull in Raspberry Pi Pico SDK (must be before project) 9 | include(pico_sdk_import.cmake) 10 | 11 | include(pico_extras_import.cmake) 12 | 13 | project(pico-dsp C CXX ASM) 14 | set(CMAKE_CXX_FLAGS "-O3 -march=armv6-m -mcpu=cortex-m0plus -mthumb -ffunction-sections -fdata-sections -fno-exceptions -Wall -Wextra") 15 | 16 | # Initialise the Raspberry Pi Pico SDK 17 | pico_sdk_init() 18 | 19 | # Add executable. Default name is the project name, version 0.1 20 | 21 | include_directories(src) 22 | add_executable(pico-dsp 23 | main.cpp 24 | ) 25 | 26 | target_sources(pico-dsp PRIVATE 27 | src/I2S.cpp 28 | src/I2S.h 29 | src/AudioPioRingBuffer.cpp 30 | src/AudioPioRingBuffer.h 31 | src/iir.cpp 32 | src/iir.h 33 | src/compatability.h 34 | ) 35 | 36 | pico_generate_pio_header(pico-dsp ${CMAKE_CURRENT_LIST_DIR}/src/pio_i2s.pio) 37 | 38 | pico_set_program_name(pico-dsp "dsp") 39 | pico_set_program_version(pico-dsp "0.1") 40 | 41 | pico_enable_stdio_uart(pico-dsp 0) 42 | pico_enable_stdio_usb(pico-dsp 1) 43 | 44 | # Add the standard library to the build 45 | target_link_libraries(pico-dsp pico_stdlib) 46 | 47 | # Add any user requested libraries 48 | target_link_libraries(pico-dsp 49 | hardware_pio 50 | hardware_dma 51 | pico_runtime 52 | pico_audio_i2s 53 | pico_multicore 54 | ) 55 | 56 | pico_add_extra_outputs(pico-dsp) 57 | # pico_set_binary_type(pico-dsp no_flash) 58 | # pico_set_binary_type(pico-dsp copy_to_ram) 59 | -------------------------------------------------------------------------------- /img/DSP_IMG_angle.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playduck/pico-dsp/b2a09da4aeecf5e2b9e3a9a4a5e5c3bd8f1aa119/img/DSP_IMG_angle.jpg -------------------------------------------------------------------------------- /img/DSP_IMG_top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playduck/pico-dsp/b2a09da4aeecf5e2b9e3a9a4a5e5c3bd8f1aa119/img/DSP_IMG_top.jpg -------------------------------------------------------------------------------- /img/banner.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playduck/pico-dsp/b2a09da4aeecf5e2b9e3a9a4a5e5c3bd8f1aa119/img/banner.ai -------------------------------------------------------------------------------- /img/banner_800x300.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playduck/pico-dsp/b2a09da4aeecf5e2b9e3a9a4a5e5c3bd8f1aa119/img/banner_800x300.png -------------------------------------------------------------------------------- /img/banner_800x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playduck/pico-dsp/b2a09da4aeecf5e2b9e3a9a4a5e5c3bd8f1aa119/img/banner_800x600.png -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "compatability.h" 6 | #include "pico/stdlib.h" 7 | #include "pico/multicore.h" 8 | #include "pico/binary_info.h" 9 | 10 | #include "hardware/uart.h" 11 | #include "hardware/gpio.h" 12 | #include "hardware/divider.h" 13 | #include "hardware/pio.h" 14 | #include "hardware/irq.h" 15 | #include "hardware/vreg.h" 16 | 17 | #include "I2S.h" 18 | #include "iir.h" 19 | 20 | #include "pio_i2s.pio.h" 21 | 22 | const int sampleRate = 48000; 23 | const int bitDepth = 32; 24 | const float mclkFactor = 512.0; 25 | 26 | const int input_BCLK_Base = 3; 27 | const int input_DATA = 5; 28 | const int output_BCLK_Base = 6; 29 | const int output_DATA = 8; 30 | const int mclk_pin = 15; 31 | 32 | mutex_t _pioMutex; /* external definition in comaptability.h */ 33 | 34 | int __not_in_flash_func(main)() 35 | { 36 | /* binary info */ 37 | bi_decl(bi_program_description("pico-dsp - a simple audio dsp")); 38 | bi_decl(bi_1pin_with_name(input_BCLK_Base, "I2S Input (ADC) BCLK - DON'T USE (invalid timing)")); 39 | bi_decl(bi_1pin_with_name(input_BCLK_Base + 1, "I2S Input (ADC) LRCK - DON'T USE (invalid timing)")); 40 | bi_decl(bi_1pin_with_name(input_DATA, "I2S Input (ADC) Data")); 41 | bi_decl(bi_1pin_with_name(output_BCLK_Base, "I2S Output (DAC) BCLK")); 42 | bi_decl(bi_1pin_with_name(output_BCLK_Base + 1, "I2S Output (DAC) LRCK")); 43 | bi_decl(bi_1pin_with_name(output_DATA, "I2S Output (DAC) Data")); 44 | bi_decl(bi_1pin_with_name(mclk_pin, "I2S MCLK")); 45 | 46 | /* start stdio 47 | enables printf and friends 48 | uses either uart or usb as defined by cmake 49 | */ 50 | stdio_init_all(); 51 | 52 | /* on-board led */ 53 | gpio_init(PICO_DEFAULT_LED_PIN); 54 | gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT); 55 | gpio_put(PICO_DEFAULT_LED_PIN, 1); 56 | 57 | /* PIOs wait for IRQ7, so enable before loading */ 58 | irq_set_enabled(7, true); 59 | 60 | /* undervolt core voltage 61 | might enable higher clock speeds */ 62 | // vreg_set_voltage(VREG_VOLTAGE_1_00); 63 | // sleep_ms(1000); // vreg settle 64 | 65 | /* overclock system */ 66 | // set_sys_clock_khz(230000, true); 67 | // sleep_ms(100); 68 | 69 | IIR lowpass1(lowpass, 880, BIQUAD_Q_ORDER_4_1, 0.0, sampleRate); 70 | IIR lowpass2(lowpass, 880, BIQUAD_Q_ORDER_4_2, 0.0, sampleRate); 71 | IIR highpass1(highpass, 880, BIQUAD_Q_ORDER_4_1, 0.0, sampleRate); 72 | IIR highpass2(highpass, 880, BIQUAD_Q_ORDER_4_2, 0.0, sampleRate); 73 | 74 | IIR shaping1(peak, 80, BIQUAD_Q_ORDER_2, 6.0, sampleRate); 75 | 76 | /* load mclk pio */ 77 | int off = 0, sm = 0; 78 | PIO pio; 79 | PIOProgram* mclkPio = new PIOProgram(&pio_i2s_mclk_program); 80 | if(mclkPio->prepare(&pio, &sm, &off)) { 81 | pio_i2s_mclk_program_init(pio, sm, off, mclk_pin); 82 | // set mclk to a multiple of fs 83 | float mclkFrequency = mclkFactor * (float)sampleRate; 84 | pio_sm_set_clkdiv(pio, sm, (float)clock_get_hz(clk_sys) / mclkFrequency); 85 | } else { 86 | printf("failed to allocate MCLK PIO"); 87 | while(1); 88 | } 89 | 90 | /* initilize I2S */ 91 | I2S I2S_Output(OUTPUT, output_BCLK_Base, output_DATA, bitDepth); 92 | I2S I2S_Input(INPUT, input_BCLK_Base, input_DATA, bitDepth); 93 | 94 | I2S_Input.setFrequency(sampleRate); 95 | I2S_Output.setFrequency(sampleRate); 96 | 97 | if (!I2S_Output.begin()) 98 | { 99 | printf("failed to initialize I2S Output!"); 100 | while (1); 101 | } 102 | 103 | if (!I2S_Input.begin()) 104 | { 105 | printf("failed to initialize I2S Input!"); 106 | while (1); 107 | } 108 | 109 | /* loop variables */ 110 | int32_t left_rx = 0, right_rx = 0; 111 | int32_t left_tx = 0, right_tx = 0; 112 | 113 | uint32_t counter = 0; 114 | absolute_time_t start = 0, end = 0; 115 | int64_t diff = 0; 116 | 117 | printf("entering main loop"); 118 | gpio_put(PICO_DEFAULT_LED_PIN, 0); 119 | 120 | /* synchronously start all pio0s and clocks */ 121 | pio_enable_sm_mask_in_sync(pio0, 0xF); 122 | /* run PIOs in sync using falling edge of IRQ7 */ 123 | irq_set_enabled(7, false); 124 | 125 | while (1) 126 | { 127 | I2S_Input.read(&left_rx, true); 128 | I2S_Input.read(&right_rx, true); 129 | 130 | start = get_absolute_time(); 131 | 132 | /* scale 24 bit sample to 32 bit range */ 133 | left_tx = left_rx >> 8; 134 | right_tx = right_rx >> 8; 135 | 136 | lowpass1.filter(&left_tx); // +0dB 137 | lowpass2.filter(&left_tx); // +0dB 138 | shaping1.filter(&left_tx); // +6dB 139 | 140 | highpass1.filter(&right_tx); // +0dB 141 | highpass2.filter(&right_tx); // +0dB 142 | 143 | /* makeup gain 144 | +6dB max -> scale by 2^1 145 | headroom is 2^8 - 2^1 -> 2^7 */ 146 | left_tx <<= 7; 147 | right_tx <<= 7; 148 | 149 | I2S_Output.write(left_tx, false); 150 | I2S_Output.write(right_tx, false); 151 | 152 | end = get_absolute_time(); 153 | 154 | diff = (absolute_time_diff_us(start, end)); 155 | counter++; 156 | if(counter % sampleRate == 0) { 157 | printf("%lldus\n", diff); 158 | } 159 | } 160 | 161 | return 0; 162 | } 163 | -------------------------------------------------------------------------------- /pico_extras_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_extras_import.cmake 2 | 3 | # This can be dropped into an external project to help locate pico-extras 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_EXTRAS_PATH} AND (NOT PICO_EXTRAS_PATH)) 7 | set(PICO_EXTRAS_PATH $ENV{PICO_EXTRAS_PATH}) 8 | message("Using PICO_EXTRAS_PATH from environment ('${PICO_EXTRAS_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_EXTRAS_FETCH_FROM_GIT} AND (NOT PICO_EXTRAS_FETCH_FROM_GIT)) 12 | set(PICO_EXTRAS_FETCH_FROM_GIT $ENV{PICO_EXTRAS_FETCH_FROM_GIT}) 13 | message("Using PICO_EXTRAS_FETCH_FROM_GIT from environment ('${PICO_EXTRAS_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_EXTRAS_FETCH_FROM_GIT_PATH} AND (NOT PICO_EXTRAS_FETCH_FROM_GIT_PATH)) 17 | set(PICO_EXTRAS_FETCH_FROM_GIT_PATH $ENV{PICO_EXTRAS_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_EXTRAS_FETCH_FROM_GIT_PATH from environment ('${PICO_EXTRAS_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | if (NOT PICO_EXTRAS_PATH) 22 | if (PICO_EXTRAS_FETCH_FROM_GIT) 23 | include(FetchContent) 24 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 25 | if (PICO_EXTRAS_FETCH_FROM_GIT_PATH) 26 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_EXTRAS_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 27 | endif () 28 | FetchContent_Declare( 29 | pico_extras 30 | GIT_REPOSITORY https://github.com/raspberrypi/pico-extras 31 | GIT_TAG master 32 | ) 33 | if (NOT pico_extras) 34 | message("Downloading Raspberry Pi Pico Extras") 35 | FetchContent_Populate(pico_extras) 36 | set(PICO_EXTRAS_PATH ${pico_extras_SOURCE_DIR}) 37 | endif () 38 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 39 | else () 40 | if (PICO_SDK_PATH AND EXISTS "${PICO_SDK_PATH}/../pico-extras") 41 | set(PICO_EXTRAS_PATH ${PICO_SDK_PATH}/../pico-extras) 42 | message("Defaulting PICO_EXTRAS_PATH as sibling of PICO_SDK_PATH: ${PICO_EXTRAS_PATH}") 43 | else() 44 | message(FATAL_ERROR 45 | "PICO EXTRAS location was not specified. Please set PICO_EXTRAS_PATH or set PICO_EXTRAS_FETCH_FROM_GIT to on to fetch from git." 46 | ) 47 | endif() 48 | endif () 49 | endif () 50 | 51 | set(PICO_EXTRAS_PATH "${PICO_EXTRAS_PATH}" CACHE PATH "Path to the PICO EXTRAS") 52 | set(PICO_EXTRAS_FETCH_FROM_GIT "${PICO_EXTRAS_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of PICO EXTRAS from git if not otherwise locatable") 53 | set(PICO_EXTRAS_FETCH_FROM_GIT_PATH "${PICO_EXTRAS_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download EXTRAS") 54 | 55 | get_filename_component(PICO_EXTRAS_PATH "${PICO_EXTRAS_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 56 | if (NOT EXISTS ${PICO_EXTRAS_PATH}) 57 | message(FATAL_ERROR "Directory '${PICO_EXTRAS_PATH}' not found") 58 | endif () 59 | 60 | set(PICO_EXTRAS_PATH ${PICO_EXTRAS_PATH} CACHE PATH "Path to the PICO EXTRAS" FORCE) 61 | 62 | add_subdirectory(${PICO_EXTRAS_PATH} pico_extras) 63 | -------------------------------------------------------------------------------- /pico_sdk_import.cmake: -------------------------------------------------------------------------------- 1 | # This is a copy of /external/pico_sdk_import.cmake 2 | 3 | # This can be dropped into an external project to help locate this SDK 4 | # It should be include()ed prior to project() 5 | 6 | if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) 7 | set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) 8 | message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") 9 | endif () 10 | 11 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) 12 | set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) 13 | message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") 14 | endif () 15 | 16 | if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) 17 | set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) 18 | message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") 19 | endif () 20 | 21 | set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") 22 | set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") 23 | set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") 24 | 25 | if (NOT PICO_SDK_PATH) 26 | if (PICO_SDK_FETCH_FROM_GIT) 27 | include(FetchContent) 28 | set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) 29 | if (PICO_SDK_FETCH_FROM_GIT_PATH) 30 | get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") 31 | endif () 32 | # GIT_SUBMODULES_RECURSE was added in 3.17 33 | if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") 34 | FetchContent_Declare( 35 | pico_sdk 36 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 37 | GIT_TAG master 38 | GIT_SUBMODULES_RECURSE FALSE 39 | ) 40 | else () 41 | FetchContent_Declare( 42 | pico_sdk 43 | GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk 44 | GIT_TAG master 45 | ) 46 | endif () 47 | 48 | if (NOT pico_sdk) 49 | message("Downloading Raspberry Pi Pico SDK") 50 | FetchContent_Populate(pico_sdk) 51 | set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) 52 | endif () 53 | set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) 54 | else () 55 | message(FATAL_ERROR 56 | "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." 57 | ) 58 | endif () 59 | endif () 60 | 61 | get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") 62 | if (NOT EXISTS ${PICO_SDK_PATH}) 63 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") 64 | endif () 65 | 66 | set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) 67 | if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) 68 | message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") 69 | endif () 70 | 71 | set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) 72 | 73 | include(${PICO_SDK_INIT_CMAKE_FILE}) 74 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # PICO-DSP 2 | 3 |

4 | 5 |

6 | 7 | ## Functionality 8 | 9 | Audio DSP Project for the Raspberry Pi Pico (RP2040). 10 | The project reads an I2S input stream, performs signal processing and outputs it as an I2S stream. 11 | The pico acts as the I2S master generating all required clocks (including MCLK). 12 | 13 | DSP is done at 44.1kHz at 32 Bits fixed point. 14 | Fixed point is a hard requirement, as the RP2040 doesn't have an FPU. 15 | Running just one floating point IIR filter would require overclocking the core to 230MHz. 16 | 17 | ## Usage 18 | 19 | ### Firmware 20 | 21 | The I2S interfaces are realized via the PIOs. 22 | The project takes strong influence from the [`pico-extras`](https://github.com/raspberrypi/pico-extras) and the [`arduino-pico`](https://github.com/earlephilhower/arduino-pico) repositories. 23 | 24 | The I2S communication works by using 3 PIOs. 25 | One transmitter, one receiver and a clock generator. 26 | The transmitter and receiver are clocked at the bitclock frequency (Bits * Channels * FS), while MCLK is run at it's own frequency (x * FS). 27 | These are synchronized using IRQ7. 28 | This could perhaps be consolidated into just two or even only one PIO. 29 | 30 | ### Hardware 31 | 32 | **Use the DAC Clocks (DAC WS and DAC BCK) for both the ADC and DAC**. 33 | The ADC Clocks are shifted by one `nop` statement. 34 | 35 |

36 | 37 |
38 | DSP PCB top-down view 39 |

40 | 41 | The test PCB consists of a Raspberry Pi Pico, a PCM5102A and a WM8782S on development boards. 42 | The ADC and DAC boards are supplied with +5V from the pico's VBUS connection. 43 | The ADC board requires a jumper between the V+ input and the secondary voltage regulator input to bypass the first regulator. 44 | (The primary regulator steps V+ down to +5V, as VBUS is 5V and the dropout voltage is greater than 0V, I opted to bypass it.) 45 | 46 | The ADC is configured as: 47 | - Slave mode 48 | - Slave clock MCLK input (J3 = S, J1 = Don't care) 49 | - I2S bit format 50 | - 44.1K/48K sample rate 51 | - 24 bit 52 | 53 | The DAC board has no modifications and is left as-is (1L, 2L, H3, 4L). 54 | 55 | ## Building and Compilation 56 | 57 | Requires pico-sdk and pico-extras. 58 | 59 | ```bash 60 | mkdir build 61 | cd build 62 | cmake .. 63 | make 64 | ``` 65 | 66 | Proceed to flash the pico with the generated `.elf`. 67 | 68 | ## TODO 69 | 70 | - [ ] hardware documentation 71 | - [ ] restructure Ringbuffer ? 72 | - [ ] Add more DSP functionality 73 | - [x] Cleanup unused clock outputs (I2S PIO In) 74 | - [x] try to optimize performance 75 | - [ ] more docs 76 | 77 | ### Performance: 78 | 79 | IIR filters using a 64 Bit accumulator require around 2.2us but enables a scaling factor for fixed point arithmetic of 30 or more. 80 | Running 5 filters at this width limits Fs to around 48kHz. 81 | Limiting the IIR's accumulator to 32 Bits drastically increases performance to enabled an Fs of 96kHz. 82 | However the scaling factor must be reduced to 15 and samples must be reduced to a width of 16 Bits. 83 | 32 Bit floating point IIR filters (in DF1) are borderline unusable unless overclocked to around 230MHz. 84 | 85 | ## Further resources 86 | 87 | - The great [earlevel engineering blog](https://www.earlevel.com/main/) is a great resource for IIR Filters and various DSP subjects. 88 | - My own projects [ESP32 Bluetooth DSP](https://github.com/playduck/esp32-bluetooth-dsp) and it's [DSP Playground](https://github.com/playduck/dsp-playground) are peers to this project. 89 | -------------------------------------------------------------------------------- /src/AudioPioRingBuffer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | AudioRingBuffer for Raspnerry Pi Pico RP2040 3 | Implements a ring buffer for PIO DMA for I2S read or write 4 | 5 | Copyright (c) 2022 Earle F. Philhower, III 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | */ 21 | 22 | // #include 23 | #include 24 | #include "hardware/dma.h" 25 | #include "hardware/irq.h" 26 | #include "hardware/pio.h" 27 | #include "pio_i2s.pio.h" 28 | #include "AudioPioRingBuffer.h" 29 | 30 | static int __channelCount = 0; // # of channels left. When we hit 0, then remove our handler 31 | static AudioRingBuffer* __channelMap[12]; // Lets the IRQ handler figure out where to dispatch to 32 | 33 | AudioRingBuffer::AudioRingBuffer(size_t bufferCount, size_t bufferWords, int32_t silenceSample, PinMode direction) { 34 | _running = false; 35 | _silenceSample = silenceSample; 36 | _bufferCount = bufferCount; 37 | _wordsPerBuffer = bufferWords; 38 | _isOutput = direction == OUTPUT; 39 | _overunderflow = false; 40 | _callback = nullptr; 41 | _userBuffer = -1; 42 | _userOff = 0; 43 | for (size_t i = 0; i < bufferCount; i++) { 44 | auto ab = new AudioBuffer; 45 | ab->buff = new uint32_t[_wordsPerBuffer]; 46 | ab->empty = true; 47 | _buffers.push_back(ab); 48 | } 49 | } 50 | 51 | AudioRingBuffer::~AudioRingBuffer() { 52 | if (_running) { 53 | for (auto i = 0; i < 2; i++) { 54 | dma_channel_set_irq0_enabled(_channelDMA[i], false); 55 | dma_channel_unclaim(_channelDMA[i]); 56 | __channelMap[_channelDMA[i]] = nullptr; 57 | } 58 | while (_buffers.size()) { 59 | auto ab = _buffers.back(); 60 | _buffers.pop_back(); 61 | delete[] ab->buff; 62 | delete ab; 63 | } 64 | __channelCount--; 65 | if (!__channelCount) { 66 | irq_set_enabled(DMA_IRQ_0, false); 67 | // TODO - how can we know if there are no other parts of the core using DMA0 IRQ?? 68 | irq_remove_handler(DMA_IRQ_0, _irq); 69 | } 70 | } 71 | } 72 | 73 | void AudioRingBuffer::setCallback(void (*fn)()) { 74 | _callback = fn; 75 | } 76 | 77 | bool AudioRingBuffer::begin(int dreq, volatile void *pioFIFOAddr) { 78 | _running = true; 79 | // Set all buffers to silence, empty 80 | for (auto buff : _buffers) { 81 | buff->empty = true; 82 | // if (_isOutput) { 83 | for (uint32_t x = 0; x < _wordsPerBuffer; x++) { 84 | buff->buff[x] = _silenceSample; 85 | } 86 | // } 87 | } 88 | // Get ping and pong DMA channels 89 | for (auto i = 0; i < 2; i++) { 90 | _channelDMA[i] = dma_claim_unused_channel(true); 91 | if (_channelDMA[i] == -1) { 92 | if (i == 1) { 93 | dma_channel_unclaim(_channelDMA[0]); 94 | } 95 | return false; 96 | } 97 | } 98 | bool needSetIRQ = __channelCount == 0; 99 | // Need to know both channels to set up ping-pong, so do in 2 stages 100 | for (auto i = 0; i < 2; i++) { 101 | dma_channel_config c = dma_channel_get_default_config(_channelDMA[i]); 102 | channel_config_set_transfer_data_size(&c, DMA_SIZE_32); // 32b transfers into PIO FIFO 103 | if (_isOutput) { 104 | channel_config_set_read_increment(&c, true); // Reading incrementing addresses 105 | channel_config_set_write_increment(&c, false); // Writing to the same FIFO address 106 | } else { 107 | channel_config_set_read_increment(&c, false); // Reading same FIFO address 108 | channel_config_set_write_increment(&c, true); // Writing to incrememting buffers 109 | } 110 | channel_config_set_dreq(&c, dreq); // Wait for the PIO TX FIFO specified 111 | channel_config_set_chain_to(&c, _channelDMA[i ^ 1]); // Start other channel when done 112 | channel_config_set_irq_quiet(&c, false); // Need IRQs 113 | 114 | if (_isOutput) { 115 | dma_channel_configure(_channelDMA[i], &c, pioFIFOAddr, _buffers[i]->buff, _wordsPerBuffer, false); 116 | } else { 117 | dma_channel_configure(_channelDMA[i], &c, _buffers[i]->buff, pioFIFOAddr, _wordsPerBuffer, false); 118 | } 119 | dma_channel_set_irq0_enabled(_channelDMA[i], true); 120 | __channelMap[_channelDMA[i]] = this; 121 | __channelCount++; 122 | } 123 | if (needSetIRQ) { 124 | irq_add_shared_handler(DMA_IRQ_0, _irq, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY); 125 | irq_set_enabled(DMA_IRQ_0, true); 126 | } 127 | _curBuffer = 0; 128 | _nextBuffer = 2 % _bufferCount; 129 | dma_channel_start(_channelDMA[0]); 130 | return true; 131 | } 132 | 133 | bool AudioRingBuffer::write(uint32_t v, bool sync) { 134 | if (!_running || !_isOutput) { 135 | return false; 136 | } 137 | if (_userBuffer == -1) { 138 | // First write or overflow, pick spot 2 buffers out 139 | _userBuffer = (_nextBuffer + 2) % _bufferCount; 140 | _userOff = 0; 141 | } 142 | if (!_buffers[_userBuffer]->empty) { 143 | if (!sync) { 144 | return false; 145 | } else { 146 | while (!_buffers[_userBuffer]->empty) { 147 | /* noop busy wait */ 148 | } 149 | } 150 | } 151 | if (_userBuffer == _curBuffer) { 152 | if (!sync) { 153 | return false; 154 | } else { 155 | while (_userBuffer == _curBuffer) { 156 | /* noop busy wait */ 157 | } 158 | } 159 | } 160 | _buffers[_userBuffer]->buff[_userOff++] = v; 161 | if (_userOff == _wordsPerBuffer) { 162 | _buffers[_userBuffer]->empty = false; 163 | _userBuffer = (_userBuffer + 1) % _bufferCount; 164 | _userOff = 0; 165 | } 166 | return true; 167 | } 168 | 169 | bool AudioRingBuffer::read(uint32_t *v, bool sync) { 170 | if (!_running || _isOutput) { 171 | return false; 172 | } 173 | if (_userBuffer == -1) { 174 | // First write or overflow, pick last filled buffer 175 | _userBuffer = (_curBuffer - 1 + _bufferCount) % _bufferCount; 176 | _userOff = 0; 177 | } 178 | if (_buffers[_userBuffer]->empty) { 179 | if (!sync) { 180 | return false; 181 | } else { 182 | while (_buffers[_userBuffer]->empty) { 183 | /* noop busy wait */ 184 | } 185 | } 186 | } 187 | if (_userBuffer == _curBuffer) { 188 | if (!sync) { 189 | return false; 190 | } else { 191 | while (_userBuffer == _curBuffer) { 192 | /* noop busy wait */ 193 | } 194 | } 195 | } 196 | auto ret = _buffers[_userBuffer]->buff[_userOff++]; 197 | if (_userOff == _wordsPerBuffer) { 198 | _buffers[_userBuffer]->empty = true; 199 | _userBuffer = (_userBuffer + 1) % _bufferCount; 200 | _userOff = 0; 201 | } 202 | *v = ret; 203 | return true; 204 | } 205 | 206 | bool AudioRingBuffer::getOverUnderflow() { 207 | bool hold = _overunderflow; 208 | _overunderflow = false; 209 | return hold; 210 | } 211 | 212 | int AudioRingBuffer::available() { 213 | if (!_running) { 214 | return 0; 215 | } 216 | int avail; 217 | avail = _wordsPerBuffer - _userOff; 218 | avail += ((_bufferCount + _curBuffer - _userBuffer) % _bufferCount) * _wordsPerBuffer; 219 | return avail; 220 | } 221 | 222 | void AudioRingBuffer::flush() { 223 | while (_curBuffer != _userBuffer) { 224 | // busy wait 225 | } 226 | } 227 | 228 | void __not_in_flash_func(AudioRingBuffer::_dmaIRQ)(int channel) { 229 | if (_isOutput) { 230 | for (uint32_t x = 0; x < _wordsPerBuffer; x++) { 231 | _buffers[_curBuffer]->buff[x] = _silenceSample; 232 | } 233 | _buffers[_curBuffer]-> empty = true; 234 | _overunderflow = _overunderflow | _buffers[_nextBuffer]->empty; 235 | dma_channel_set_read_addr(channel, _buffers[_nextBuffer]->buff, false); 236 | } else { 237 | _buffers[_curBuffer]-> empty = false; 238 | _overunderflow = _overunderflow | !_buffers[_nextBuffer]->empty; 239 | dma_channel_set_write_addr(channel, _buffers[_nextBuffer]->buff, false); 240 | } 241 | dma_channel_set_trans_count(channel, _wordsPerBuffer, false); 242 | _curBuffer = (_curBuffer + 1) % _bufferCount; 243 | _nextBuffer = (_nextBuffer + 1) % _bufferCount; 244 | dma_channel_acknowledge_irq0(channel); 245 | if (_callback) { 246 | _callback(); 247 | } 248 | } 249 | 250 | void __not_in_flash_func(AudioRingBuffer::_irq)() { 251 | for (size_t i = 0; i < sizeof(__channelMap); i++) { 252 | if (dma_channel_get_irq0_status(i) && __channelMap[i]) { 253 | __channelMap[i]->_dmaIRQ(i); 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/AudioPioRingBuffer.h: -------------------------------------------------------------------------------- 1 | /* 2 | AudioRingBuffer for Rasperry Pi Pico 3 | Implements a ring buffer for PIO DMA for I2S read or write 4 | 5 | Copyright (c) 2022 Earle F. Philhower, III 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | */ 21 | 22 | #pragma once 23 | // #include 24 | #include "compatability.h" 25 | #include 26 | 27 | class AudioRingBuffer { 28 | public: 29 | AudioRingBuffer(size_t bufferCount, size_t bufferWords, int32_t silenceSample, PinMode direction = OUTPUT); 30 | ~AudioRingBuffer(); 31 | 32 | void setCallback(void (*fn)()); 33 | 34 | bool begin(int dreq, volatile void *pioFIFOAddr); 35 | 36 | bool write(uint32_t v, bool sync = true); 37 | bool read(uint32_t *v, bool sync = true); 38 | void flush(); 39 | 40 | bool getOverUnderflow(); 41 | int available(); 42 | 43 | private: 44 | void _dmaIRQ(int channel); 45 | static void _irq(); 46 | 47 | typedef struct { 48 | uint32_t *buff; 49 | volatile bool empty; 50 | } AudioBuffer; 51 | 52 | bool _running = false; 53 | std::vector _buffers; 54 | volatile int _curBuffer; 55 | volatile int _nextBuffer; 56 | size_t _chunkSampleCount; 57 | int _bitsPerSample; 58 | size_t _wordsPerBuffer; 59 | size_t _bufferCount; 60 | bool _isOutput; 61 | int32_t _silenceSample; 62 | int _channelDMA[2]; 63 | void (*_callback)(); 64 | 65 | bool _overunderflow; 66 | 67 | // User buffer pointer 68 | int _userBuffer = -1; 69 | size_t _userOff = 0; 70 | }; 71 | -------------------------------------------------------------------------------- /src/I2S.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | I2SIn and I2SOut for Raspberry Pi Pico 3 | Implements one or more I2S interfaces using DMA 4 | 5 | Copyright (c) 2022 Earle F. Philhower, III 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | */ 21 | // #include 22 | #include "I2S.h" 23 | #include "pio_i2s.pio.h" 24 | 25 | I2S::I2S(PinMode direction, pin_size_t pinBCLK, pin_size_t pinDOUT, int bps) { 26 | _running = false; 27 | _bps = bps; 28 | _writtenHalf = false; 29 | _pinBCLK = pinBCLK; 30 | _pinDOUT = pinDOUT; 31 | _freq = 48000; 32 | _arb = nullptr; 33 | _isOutput = direction == OUTPUT; 34 | _cb = nullptr; 35 | _buffers = 32; 36 | _bufferWords = 64; 37 | _silenceSample = 0; 38 | } 39 | 40 | I2S::~I2S() { 41 | } 42 | 43 | bool I2S::setFrequency(int newFreq) { 44 | _freq = newFreq; 45 | if (_running) { 46 | float bitClk = _freq * _bps * 2.0 /* channels */ * 2.0 /* edges per clock */; 47 | pio_sm_set_clkdiv(_pio, _sm, (float)clock_get_hz(clk_sys) / bitClk); 48 | } 49 | return true; 50 | } 51 | 52 | void I2S::onTransmit(void(*fn)(void)) { 53 | if (_isOutput) { 54 | _cb = fn; 55 | if (_running) { 56 | _arb->setCallback(_cb); 57 | } 58 | } 59 | } 60 | 61 | void I2S::onReceive(void(*fn)(void)) { 62 | if (!_isOutput) { 63 | _cb = fn; 64 | if (_running) { 65 | _arb->setCallback(_cb); 66 | } 67 | } 68 | } 69 | 70 | bool I2S::begin() { 71 | _running = true; 72 | _hasPeeked = false; 73 | int off = 0; 74 | _i2s = new PIOProgram(_isOutput ? &pio_i2s_out_program : &pio_i2s_in_program); 75 | _i2s->prepare(&_pio, &_sm, &off); 76 | if (_isOutput) { 77 | pio_i2s_out_program_init(_pio, _sm, off, _pinDOUT, _pinBCLK, _bps); 78 | } else { 79 | pio_i2s_in_program_init(_pio, _sm, off, _pinDOUT, _bps); 80 | } 81 | setFrequency(_freq); 82 | if (_bps == 8) { 83 | uint8_t a = _silenceSample & 0xff; 84 | _silenceSample = (a << 24) | (a << 16) | (a << 8) | a; 85 | } else if (_bps == 16) { 86 | uint16_t a = _silenceSample & 0xffff; 87 | _silenceSample = (a << 16) | a; 88 | } 89 | _arb = new AudioRingBuffer(_buffers, _bufferWords, _silenceSample, _isOutput ? OUTPUT : INPUT); 90 | _arb->begin(pio_get_dreq(_pio, _sm, _isOutput), _isOutput ? &_pio->txf[_sm] : (volatile void*)&_pio->rxf[_sm]); 91 | _arb->setCallback(_cb); 92 | // pio_sm_set_enabled(_pio, _sm, true); 93 | 94 | return true; 95 | } 96 | 97 | void I2S::end() { 98 | _running = false; 99 | delete _arb; 100 | _arb = nullptr; 101 | delete _i2s; 102 | _i2s = nullptr; 103 | } 104 | 105 | size_t I2S::write(int32_t val, bool sync) { 106 | if (!_running || !_isOutput) { 107 | return 0; 108 | } 109 | return _arb->write(val, sync); 110 | } 111 | 112 | size_t I2S::read(int32_t *val, bool sync) { 113 | if (!_running || _isOutput) { 114 | return 0; 115 | } 116 | return _arb->read((uint32_t *)val, sync); 117 | } 118 | -------------------------------------------------------------------------------- /src/I2S.h: -------------------------------------------------------------------------------- 1 | /* 2 | I2SIn and I2SOut for Raspberry Pi Pico 3 | Implements one or more I2S interfaces using DMA 4 | 5 | Copyright (c) 2022 Earle F. Philhower, III 6 | 7 | This library is free software; you can redistribute it and/or 8 | modify it under the terms of the GNU Lesser General Public 9 | License as published by the Free Software Foundation; either 10 | version 2.1 of the License, or (at your option) any later version. 11 | 12 | This library is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | Lesser General Public License for more details. 16 | 17 | You should have received a copy of the GNU Lesser General Public 18 | License along with this library; if not, write to the Free Software 19 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 20 | */ 21 | 22 | #pragma once 23 | // #include 24 | #include 25 | #include "AudioPioRingBuffer.h" 26 | 27 | class I2S{ 28 | public: 29 | I2S(PinMode direction = OUTPUT, pin_size_t pinBCLK = 0, pin_size_t pinDOUT = 2, int bps = 32); 30 | virtual ~I2S(); 31 | 32 | bool setFrequency(int newFreq); 33 | 34 | bool begin(); 35 | void end(); 36 | 37 | // // from Stream 38 | // virtual int available() override; 39 | // virtual int read() override; 40 | // virtual int peek() override; 41 | // virtual void flush() override; 42 | 43 | // // from Print (see notes on write() methods below) 44 | // virtual size_t write(const uint8_t *buffer, size_t size) override; 45 | // virtual int availableForWrite() override; 46 | 47 | // Try and make I2S::write() do what makes sense, namely write 48 | // one sample (L or R) at the I2S configured bit width 49 | // virtual size_t write(uint8_t s) override { 50 | // return _writeNatural(s & 0xff); 51 | // } 52 | 53 | // Write 32 bit value to port, user responsbile for packing/alignment, etc. 54 | size_t write(int32_t val, bool sync); 55 | 56 | // Write sample to I2S port, will block until completed 57 | size_t write8(int8_t l, int8_t r); 58 | size_t write16(int16_t l, int16_t r); 59 | size_t write24(int32_t l, int32_t r); // Note that 24b must have values left-aligned (i.e. 0xABCDEF00) 60 | size_t write32(int32_t l, int32_t r); 61 | 62 | // Read 32 bit value to port, user responsbile for packing/alignment, etc. 63 | size_t read(int32_t *val, bool sync); 64 | 65 | // Note that these callback are called from **INTERRUPT CONTEXT** and hence 66 | // should be in RAM, not FLASH, and should be quick to execute. 67 | void onTransmit(void(*)(void)); 68 | void onReceive(void(*)(void)); 69 | 70 | private: 71 | pin_size_t _pinBCLK; 72 | pin_size_t _pinDOUT; 73 | int _bps; 74 | int _freq; 75 | size_t _buffers; 76 | size_t _bufferWords; 77 | int32_t _silenceSample; 78 | bool _isOutput; 79 | 80 | bool _running; 81 | 82 | bool _hasPeeked; 83 | int32_t _peekSaved; 84 | 85 | size_t _writeNatural(int32_t s); 86 | uint32_t _writtenData; 87 | bool _writtenHalf; 88 | 89 | int32_t _holdWord = 0; 90 | int _wasHolding = 0; 91 | 92 | void (*_cb)(); 93 | 94 | AudioRingBuffer *_arb; 95 | PIOProgram *_i2s; 96 | PIO _pio; 97 | int _sm; 98 | }; 99 | -------------------------------------------------------------------------------- /src/compatability.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | #include "pico/stdio.h" 7 | #include "pico/stdlib.h" 8 | 9 | #include "hardware/pio.h" 10 | #include "hardware/dma.h" 11 | #include "hardware/clocks.h" 12 | #include "pico/mutex.h" 13 | 14 | #define PinMode uint8_t 15 | 16 | #define OUTPUT (1) 17 | #define INPUT (0) 18 | 19 | typedef uint8_t pin_size_t; 20 | 21 | class CoreMutex 22 | { 23 | public: 24 | CoreMutex(mutex_t *mutex) 25 | { 26 | uint32_t owner; 27 | _mutex = mutex; 28 | _acquired = false; 29 | if (!mutex_try_enter(_mutex, &owner)) 30 | { 31 | if (owner == get_core_num()) 32 | { // Deadlock! 33 | printf("Deadlock detected!\n"); 34 | // DEBUGCORE("CoreMutex - Deadlock detected!\n"); 35 | return; 36 | } 37 | mutex_enter_blocking(_mutex); 38 | } 39 | _acquired = true; 40 | } 41 | 42 | ~CoreMutex() 43 | { 44 | if (_acquired) 45 | { 46 | mutex_exit(_mutex); 47 | } 48 | } 49 | 50 | operator bool() 51 | { 52 | return _acquired; 53 | } 54 | 55 | private: 56 | mutex_t *_mutex; 57 | bool _acquired; 58 | }; 59 | 60 | class PIOProgram 61 | { 62 | public: 63 | PIOProgram(const pio_program_t *pgm) 64 | { 65 | _pgm = pgm; 66 | } 67 | 68 | // Possibly load into a PIO and allocate a SM 69 | bool prepare(PIO *pio, int *sm, int *offset) 70 | { 71 | extern mutex_t _pioMutex; 72 | CoreMutex m(&_pioMutex); 73 | // Is there an open slot to run in, first? 74 | if (!_findFreeSM(pio, sm)) 75 | { 76 | return false; 77 | } 78 | // Is it loaded on that PIO? 79 | if (_offset[pio_get_index(*pio)] < 0) 80 | { 81 | // Nope, need to load it 82 | if (!pio_can_add_program(*pio, _pgm)) 83 | { 84 | return false; 85 | } 86 | _offset[pio_get_index(*pio)] = pio_add_program(*pio, _pgm); 87 | } 88 | // Here it's guaranteed loaded, return values 89 | // PIO and SM already set 90 | *offset = _offset[pio_get_index(*pio)]; 91 | return true; 92 | } 93 | 94 | private: 95 | // Find an unused PIO state machine to grab, returns false when none available 96 | static bool _findFreeSM(PIO *pio, int *sm) 97 | { 98 | int idx = pio_claim_unused_sm(pio0, false); 99 | if (idx >= 0) 100 | { 101 | *pio = pio0; 102 | *sm = idx; 103 | return true; 104 | } 105 | idx = pio_claim_unused_sm(pio1, false); 106 | if (idx >= 0) 107 | { 108 | *pio = pio1; 109 | *sm = idx; 110 | return true; 111 | } 112 | return false; 113 | } 114 | 115 | private: 116 | int _offset[2] = {-1, -1}; 117 | const pio_program_t *_pgm; 118 | }; 119 | -------------------------------------------------------------------------------- /src/iir.cpp: -------------------------------------------------------------------------------- 1 | #include "iir.h" 2 | 3 | void IIR::filter(int32_t *s) 4 | { 5 | /* 6 | The state_error is the truncated part of the accumulator. 7 | This acts as an error, which is fed back (without filter) 8 | resulting in a rudimentary noise shaping feedback loop. 9 | One could potentially add an LSB's worth of TPDF dither ontop. 10 | */ 11 | int64_t accumulator = (int64_t)state_error; 12 | 13 | /* populate the accumulator, the explicit casts are required */ 14 | accumulator += (int64_t)b[0] * (int64_t)(*s); 15 | accumulator += (int64_t)b[1] * (int64_t)x[0]; 16 | accumulator += (int64_t)b[2] * (int64_t)x[1]; 17 | accumulator += (int64_t)a[0] * (int64_t)y[0]; 18 | accumulator += (int64_t)a[1] * (int64_t)y[1]; 19 | 20 | // accumulator = CLAMP(accumulator, ACC_MAX, ACC_MIN); 21 | 22 | /* truncate the result */ 23 | state_error = accumulator & ACC_REM; 24 | int32_t out = (int32_t)(accumulator >> (int64_t)(q)); 25 | 26 | /* shift the delay lines */ 27 | x[1] = x[0]; 28 | y[1] = y[0]; 29 | 30 | /* populate the delay lines */ 31 | x[0] = (*s); 32 | y[0] = out; 33 | 34 | *s = out; 35 | } 36 | 37 | // https://www.earlevel.com/main/2011/01/02/biquad-formulas/ 38 | IIR::IIR(filter_type_t type, float Fc, float Q, float peakGain, float Fs) 39 | { 40 | /* 41 | calculate the iir filter coefficients based on more intuitively 42 | understandable parameters 43 | the coefficients get scaled by the selected scaling factor 44 | */ 45 | 46 | float a0 = 0, a1 = 0, a2 = 0, b1 = 0, b2 = 0, norm = 0; 47 | 48 | float V = powf(10, fabsf(peakGain) / 20); 49 | float K = tanf(M_PI * Fc / Fs); 50 | 51 | switch (type) 52 | { 53 | case lowpass: 54 | norm = 1 / (1 + K / Q + K * K); 55 | a0 = K * K * norm; 56 | a1 = 2 * a0; 57 | a2 = a0; 58 | b1 = 2 * (K * K - 1) * norm; 59 | b2 = (1 - K / Q + K * K) * norm; 60 | break; 61 | 62 | case highpass: 63 | norm = 1 / (1 + K / Q + K * K); 64 | a0 = 1 * norm; 65 | a1 = -2 * a0; 66 | a2 = a0; 67 | b1 = 2 * (K * K - 1) * norm; 68 | b2 = (1 - K / Q + K * K) * norm; 69 | break; 70 | 71 | case bandpass: 72 | norm = 1 / (1 + K / Q + K * K); 73 | a0 = K / Q * norm; 74 | a1 = 0; 75 | a2 = -a0; 76 | b1 = 2 * (K * K - 1) * norm; 77 | b2 = (1 - K / Q + K * K) * norm; 78 | break; 79 | 80 | case notch: 81 | norm = 1 / (1 + K / Q + K * K); 82 | a0 = (1 + K * K) * norm; 83 | a1 = 2 * (K * K - 1) * norm; 84 | a2 = a0; 85 | b1 = a1; 86 | b2 = (1 - K / Q + K * K) * norm; 87 | break; 88 | 89 | case peak: 90 | if (peakGain >= 0) 91 | { // boost 92 | norm = 1 / (1 + 1 / Q * K + K * K); 93 | a0 = (1 + V / Q * K + K * K) * norm; 94 | a1 = 2 * (K * K - 1) * norm; 95 | a2 = (1 - V / Q * K + K * K) * norm; 96 | b1 = a1; 97 | b2 = (1 - 1 / Q * K + K * K) * norm; 98 | } 99 | else 100 | { // cut 101 | norm = 1 / (1 + V / Q * K + K * K); 102 | a0 = (1 + 1 / Q * K + K * K) * norm; 103 | a1 = 2 * (K * K - 1) * norm; 104 | a2 = (1 - 1 / Q * K + K * K) * norm; 105 | b1 = a1; 106 | b2 = (1 - V / Q * K + K * K) * norm; 107 | } 108 | break; 109 | case lowshelf: 110 | if (peakGain >= 0) 111 | { // boost 112 | norm = 1 / (1 + M_SQRT2 * K + K * K); 113 | a0 = (1 + sqrtf(2 * V) * K + V * K * K) * norm; 114 | a1 = 2 * (V * K * K - 1) * norm; 115 | a2 = (1 - sqrtf(2 * V) * K + V * K * K) * norm; 116 | b1 = 2 * (K * K - 1) * norm; 117 | b2 = (1 - M_SQRT2 * K + K * K) * norm; 118 | } 119 | else 120 | { // cut 121 | norm = 1 / (1 + sqrtf(2 * V) * K + V * K * K); 122 | a0 = (1 + M_SQRT2 * K + K * K) * norm; 123 | a1 = 2 * (K * K - 1) * norm; 124 | a2 = (1 - M_SQRT2 * K + K * K) * norm; 125 | b1 = 2 * (V * K * K - 1) * norm; 126 | b2 = (1 - sqrtf(2 * V) * K + V * K * K) * norm; 127 | } 128 | break; 129 | case highshelf: 130 | if (peakGain >= 0) 131 | { // boost 132 | norm = 1 / (1 + M_SQRT2 * K + K * K); 133 | a0 = (V + sqrtf(2 * V) * K + K * K) * norm; 134 | a1 = 2 * (K * K - V) * norm; 135 | a2 = (V - sqrtf(2 * V) * K + K * K) * norm; 136 | b1 = 2 * (K * K - 1) * norm; 137 | b2 = (1 - M_SQRT2 * K + K * K) * norm; 138 | } 139 | else 140 | { // cut 141 | norm = 1 / (V + sqrtf(2 * V) * K + K * K); 142 | a0 = (1 + M_SQRT2 * K + K * K) * norm; 143 | a1 = 2 * (K * K - 1) * norm; 144 | a2 = (1 - M_SQRT2 * K + K * K) * norm; 145 | b1 = 2 * (K * K - V) * norm; 146 | b2 = (V - sqrtf(2 * V) * K + K * K) * norm; 147 | } 148 | break; 149 | case none: 150 | /* fall-through */ 151 | default: 152 | a0 = 1; 153 | a1 = 0; 154 | a2 = 0; 155 | b1 = 0; 156 | b2 = 0; 157 | break; 158 | } 159 | 160 | type = type; 161 | 162 | b[0] = (int32_t)(a0 * scaleQ); 163 | b[1] = (int32_t)(a1 * scaleQ); 164 | b[2] = (int32_t)(a2 * scaleQ); 165 | 166 | a[0] = (int32_t)(-b1 * scaleQ); 167 | a[1] = (int32_t)(-b2 * scaleQ); 168 | 169 | x[0] = 0; 170 | x[1] = 0; 171 | 172 | y[0] = 0; 173 | y[1] = 0; 174 | 175 | state_error = 0; 176 | } 177 | -------------------------------------------------------------------------------- /src/iir.h: -------------------------------------------------------------------------------- 1 | #ifndef IIR_H 2 | #define IIR_H 3 | #pragma once 4 | 5 | #include 6 | #include 7 | 8 | #define CLAMP(x, a, b) (x > a ? a : (x < b ? b : x)) 9 | 10 | #define q (30) 11 | #define scaleQ (powf(2.0, q)) 12 | 13 | #define ACC_MAX ((int64_t) 0x7FFFFFFFFF) 14 | #define ACC_MIN ((int64_t) -0x8000000000) 15 | #define ACC_REM ((uint64_t)0x3FFFFFFFU) 16 | 17 | #define BIQUAD_Q_ORDER_2 0.70710678 18 | #define BIQUAD_Q_ORDER_4_1 0.54119610 19 | #define BIQUAD_Q_ORDER_4_2 1.3065630 20 | 21 | typedef enum 22 | { 23 | lowpass, 24 | highpass, 25 | bandpass, 26 | notch, 27 | peak, 28 | lowshelf, 29 | highshelf, 30 | none 31 | } filter_type_t; 32 | 33 | class IIR { 34 | private: 35 | int32_t a[2]; 36 | int32_t b[3]; 37 | 38 | int32_t x[2]; 39 | int32_t y[2]; 40 | int32_t state_error; 41 | 42 | public: 43 | filter_type_t type; 44 | 45 | void filter(int32_t *s); 46 | IIR(filter_type_t type, float Fc, float Q, float peakGain, float Fs); 47 | }; 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /src/pio_i2s.pio: -------------------------------------------------------------------------------- 1 | ; pio_i2s for the Raspberry Pi Pico RP2040 2 | ; 3 | ; Based loosely off of the MicroPython I2S code in 4 | ; https://github.com/micropython/micropython/blob/master/ports/rp2/machine_i2s.c 5 | ; 6 | ; Copyright (c) 2022 Earle F. Philhower, III 7 | ; 8 | ; This library is free software; you can redistribute it and/or 9 | ; modify it under the terms of the GNU Lesser General Public 10 | ; License as published by the Free Software Foundation; either 11 | ; version 2.1 of the License, or (at your option) any later version. 12 | ; 13 | ; This library is distributed in the hope that it will be useful, 14 | ; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | ; Lesser General Public License for more details. 17 | ; 18 | ; You should have received a copy of the GNU Lesser General Public 19 | ; License along with this library; if not, write to the Free Software 20 | ; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 21 | 22 | 23 | .program pio_i2s_out 24 | .side_set 2 ; 0 = bclk, 1=wclk 25 | 26 | ; The C code should place (number of bits/sample - 2) in Y and 27 | ; also update the SHIFTCTRL to be 24 or 32 as appropriate 28 | 29 | ; +----- WCLK 30 | ; |+---- BCLK 31 | 32 | wait 0 irq 7 side 0b11 33 | .wrap_target 34 | mov x, y side 0b01 35 | left: 36 | out pins, 1 side 0b00 37 | jmp x--, left side 0b01 38 | out pins, 1 side 0b10 ; Last bit of left has WCLK change per I2S spec 39 | 40 | mov x, y side 0b11 41 | right: 42 | out pins, 1 side 0b10 43 | jmp x--, right side 0b11 44 | out pins, 1 side 0b00 ; Last bit of right also has WCLK change 45 | .wrap 46 | 47 | 48 | .program pio_i2s_in ; Note this is the same as _out, just "in" and not "out" 49 | 50 | ; The C code should place (number of bits/sample - 2) in Y and 51 | ; also update the SHIFTCTRL to be 24 or 32 as appropriate 52 | 53 | ; +----- WCLK 54 | ; |+---- BCLK 55 | wait 0 irq 7 56 | nop ; invert clock polarity, to read on leading edge 57 | .wrap_target 58 | mov x, y 59 | left: 60 | in pins, 1 61 | jmp x--, left 62 | in pins, 1 63 | 64 | mov x, y 65 | right: 66 | in pins, 1 67 | jmp x--, right 68 | in pins, 1 69 | .wrap 70 | 71 | .program pio_i2s_mclk 72 | .side_set 1 73 | 74 | wait 0 irq 7 side 0b0 75 | .wrap_target 76 | nop side 0b1 77 | nop side 0b0 78 | .wrap 79 | 80 | % c-sdk { 81 | static inline void pio_i2s_mclk_program_init(PIO pio, uint sm, uint offset, uint clock_pin) { 82 | pio_gpio_init(pio, clock_pin); 83 | 84 | pio_sm_config sm_config = pio_i2s_mclk_program_get_default_config(offset); 85 | 86 | sm_config_set_sideset_pins(&sm_config, clock_pin); 87 | 88 | pio_sm_init(pio, sm, offset, &sm_config); 89 | 90 | uint pin_mask = (1u << clock_pin); 91 | pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); 92 | pio_sm_set_pins(pio, sm, 0); 93 | 94 | // pio_sm_exec(pio, sm, pio_encode_set(pio_y, 0)); 95 | } 96 | 97 | static inline void pio_i2s_out_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base, uint bits) { 98 | pio_gpio_init(pio, data_pin); 99 | pio_gpio_init(pio, clock_pin_base); 100 | pio_gpio_init(pio, clock_pin_base + 1); 101 | 102 | pio_sm_config sm_config = pio_i2s_out_program_get_default_config(offset); 103 | 104 | sm_config_set_out_pins(&sm_config, data_pin, 1); 105 | sm_config_set_sideset_pins(&sm_config, clock_pin_base); 106 | sm_config_set_out_shift(&sm_config, false, true, (bits <= 16) ? 2 * bits : bits); 107 | sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_TX); 108 | 109 | pio_sm_init(pio, sm, offset, &sm_config); 110 | 111 | uint pin_mask = (1u << data_pin) | (3u << clock_pin_base); 112 | pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask); 113 | pio_sm_set_pins(pio, sm, 0); // clear pins 114 | 115 | pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); 116 | } 117 | 118 | static inline void pio_i2s_in_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint bits) { 119 | pio_gpio_init(pio, data_pin); 120 | 121 | pio_sm_config sm_config = pio_i2s_in_program_get_default_config(offset); 122 | 123 | sm_config_set_in_pins(&sm_config, data_pin); 124 | sm_config_set_in_shift(&sm_config, false, true, (bits <= 16) ? 2 * bits : bits); 125 | sm_config_set_fifo_join(&sm_config, PIO_FIFO_JOIN_RX); 126 | 127 | pio_sm_init(pio, sm, offset, &sm_config); 128 | 129 | pio_sm_set_pins(pio, sm, 0); // clear pins 130 | 131 | pio_sm_exec(pio, sm, pio_encode_set(pio_y, bits - 2)); 132 | } 133 | 134 | %} 135 | --------------------------------------------------------------------------------