diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..8613f27 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [], + "compilerPath": "/usr/bin/gcc", + "cStandard": "c11", + "cppStandard": "gnu++14", + "intelliSenseMode": "linux-gcc-x64", + "configurationProvider": "ms-vscode.cmake-tools" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..051dd68 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.associations": { + "pio.h": "c", + "dreq.h": "c", + "pio_instructions.h": "c", + "type_traits": "cpp", + "i2s_microphone.h": "c" + } +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 230b75d..e7ef640 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.12) # initialize pico_sdk from GIT # (note this can come from environment, CMake cache etc) -# set(PICO_SDK_FETCH_FROM_GIT on) +set(ENV{PICO_SDK_PATH} "/home/ttb/raspberry-pi-pico/pico/pico-sdk") # pico_sdk_import.cmake is a single file copied from this SDK # note: this must happen before project() @@ -14,33 +14,39 @@ project(pico_microphone) pico_sdk_init() add_library(pico_pdm_microphone INTERFACE) - target_sources(pico_pdm_microphone INTERFACE ${CMAKE_CURRENT_LIST_DIR}/src/pdm_microphone.c ${CMAKE_CURRENT_LIST_DIR}/src/OpenPDM2PCM/OpenPDMFilter.c ) - target_include_directories(pico_pdm_microphone INTERFACE ${CMAKE_CURRENT_LIST_DIR}/src/include ) - pico_generate_pio_header(pico_pdm_microphone ${CMAKE_CURRENT_LIST_DIR}/src/pdm_microphone.pio) - target_link_libraries(pico_pdm_microphone INTERFACE pico_stdlib hardware_dma hardware_pio) -add_library(pico_analog_microphone INTERFACE) +add_library(pico_i2s_microphone INTERFACE) +target_sources(pico_i2s_microphone INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/src/i2s_microphone.c +) +target_include_directories(pico_i2s_microphone INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/src/include + examples/usb_microphone +) +target_link_libraries(pico_i2s_microphone INTERFACE pico_stdlib hardware_dma hardware_pio) + +add_library(pico_analog_microphone INTERFACE) target_sources(pico_analog_microphone INTERFACE ${CMAKE_CURRENT_LIST_DIR}/src/analog_microphone.c ) - target_include_directories(pico_analog_microphone INTERFACE ${CMAKE_CURRENT_LIST_DIR}/src/include ) - target_link_libraries(pico_analog_microphone INTERFACE pico_stdlib hardware_adc hardware_dma) -add_subdirectory("examples/hello_analog_microphone") add_subdirectory("examples/hello_pdm_microphone") +add_subdirectory("examples/hello_analog_microphone") add_subdirectory("examples/usb_microphone") +add_subdirectory("examples/hello_i2s_microphone") +add_subdirectory("examples/hello_analog_microphone") \ No newline at end of file diff --git a/README.md b/README.md index f671b47..64eed21 100644 --- a/README.md +++ b/README.md @@ -2,65 +2,83 @@ Capture audio from a microphone on your [Raspberry Pi Pico](https://www.raspberrypi.org/products/raspberry-pi-pico/) or any [RP2040](https://www.raspberrypi.org/products/rp2040/) based board. 🎤 - ## Hardware - * RP2040 board - * [Raspberry Pi Pico](https://www.raspberrypi.org/products/raspberry-pi-pico/) - * Microphones - * Analog - * [Electret Microphone Amplifier - MAX9814 with Auto Gain Control](https://www.adafruit.com/product/1713) - * PDM - * [Adafruit PDM MEMS Microphone Breakout](https://www.adafruit.com/product/3492) +* RP2040 board + + * [Raspberry Pi Pico](https://www.raspberrypi.org/products/raspberry-pi-pico/) + +* Microphones + + * I2S + + * [INMP441 I2S Omnidirectional Microphone](https://hshop.vn/products/cam-bien-am-thanh-inmp441-i2s-omnidirectional-microphone) + + * Analog + + * [Electret Microphone Amplifier - MAX9814 with Auto Gain Control](https://www.adafruit.com/product/1713) + + * PDM + + * [Adafruit PDM MEMS Microphone Breakout](https://www.adafruit.com/product/3492) ### Default Pinout +#### I2S Microphone + +| Raspberry Pi Pico / RP2040 | Analog Microphone | +| -------------------------- | ----------------- | +| 3.3V | VCC | +| GND | GND | +| GPIO 2 | SCK | +| GPIO 3 | WS | +| GPIO 6 | SD | +| GND | L/R | + #### Analog Microphone | Raspberry Pi Pico / RP2040 | Analog Microphone | | -------------------------- | ----------------- | -| 3.3V | VCC | -| GND | GND | -| GPIO 26 | OUT | +| 3.3V | VCC | +| GND | GND | +| GPIO 26 | OUT | #### PDM Microphone | Raspberry Pi Pico / RP2040 | PDM Microphone | -| -------------------------- | ----------------- | -| 3.3V | VCC | -| GND | GND | -| GND | SEL | -| GPIO 2 | DAT | -| GPIO 3 | CLK | +| -------------------------- | -------------- | +| 3.3V | VCC | +| GND | GND | +| GND | SEL | +| GPIO 2 | DAT | +| GPIO 3 | CLK | GPIO pins are configurable in examples or API. ## Examples -See [examples](examples/) folder. +[Git Examples about microphone](https://github.com/dattran-itrvn/microphone-library-for-pico/tree/Binh_Dev) +#### Set up Pico SDK -## Cloning +[Set up the Pico C/C++ SDK](https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf) -```sh -git clone https://github.com/ArmDeveloperEcosystem/microphone-library-for-pico.git -``` +1. Set `PICO_SDK_PATH` + + ```sh + export PICO_SDK_PATH=/path/to/pico-sdk + ``` -## Building +2. Create `build` dir, run `cmake` and `make`: + + ``` + mkdir build + cd build + cmake .. -DPICO_BOARD=pico + make + ``` -1. [Set up the Pico C/C++ SDK](https://datasheets.raspberrypi.org/pico/getting-started-with-pico.pdf) -2. Set `PICO_SDK_PATH` -```sh -export PICO_SDK_PATH=/path/to/pico-sdk -``` -3. Create `build` dir, run `cmake` and `make`: -``` -mkdir build -cd build -cmake .. -DPICO_BOARD=pico -make -``` -4. Copy example `.uf2` to Pico when in BOOT mode. +3. Copy example `.uf2` to Pico when in BOOT mode. ## License @@ -68,10 +86,20 @@ make ## Acknowledgements -This project was created on behalf of the [Arm Software Developers](https://developer.arm.com/) team, follow them on Twitter: [@ArmSoftwareDev](https://twitter.com/armsoftwaredev) and YouTube: [Arm Software Developers](https://www.youtube.com/channel/UCHUAckhCfRom2EHDGxwhfOg) for more resources! +##### - Mic Analog or Mic PDM The [OpenPDM2PCM](https://os.mbed.com/teams/ST/code/X_NUCLEO_CCA02M1//file/53f8b511f2a1/Middlewares/OpenPDM2PCM/) library is used to filter raw PDM data into PCM. The [TinyUSB](https://github.com/hathach/tinyusb) library is used in the `usb_microphone` example. --- -Disclaimer: This is not an official Arm product. +##### - Mic I2S + +Get data raw using I2S protocol and using algorithm fill buffer to decode audio. + +Buffer App = Buffer Ring / 4 = Buffer DMA / 8. + +To record data, using [TinyUSB]([GitHub - hathach/tinyusb: An open source cross-platform USB stack for embedded system](https://github.com/hathach/tinyusb)) library is used in the `usb_microphone` example. Buffer output <= 32 bytes <=> 256 bytes in DMA buffer + +Using [Audacity software]([Download | Audacity ®](https://www.audacityteam.org/download/) get audio output + +--- diff --git a/examples/hello_i2s_microphone/CMakeLists.txt b/examples/hello_i2s_microphone/CMakeLists.txt new file mode 100644 index 0000000..3d2c81f --- /dev/null +++ b/examples/hello_i2s_microphone/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.12) + +# rest of your project +add_executable(hello_i2s_microphone + main.c + usb_descriptors.c + usb_microphone.c +) + +target_include_directories(hello_i2s_microphone PRIVATE ${CMAKE_CURRENT_LIST_DIR}) + +target_link_libraries(hello_i2s_microphone PRIVATE tinyusb_device tinyusb_board pico_i2s_microphone) + +# enable usb output, disable uart output +# pico_enable_stdio_usb(hello_i2s_microphone 1) +pico_enable_stdio_uart(hello_i2s_microphone 1) + +# create map/bin/hex/uf2 file in addition to ELF. +pico_add_extra_outputs(hello_i2s_microphone) diff --git a/examples/hello_i2s_microphone/main.c b/examples/hello_i2s_microphone/main.c new file mode 100644 index 0000000..347ddb4 --- /dev/null +++ b/examples/hello_i2s_microphone/main.c @@ -0,0 +1,61 @@ +#include "stdio.h" +#include "tusb.h" +#include "pico/stdlib.h" +#include "pico/i2s_microphone.h" +#include "usb_microphone.h" + + +#define SCK 2 +#define WS 3 // needs to be SCK +1 +#define SD 6 +#define BPS 16 +#define RATE 16000 +#define BUTTON 15 +#define SIZE_APP_BUFFER (SIZEOF_DMA_BUFFER_IN_BYTES/8) + +uint32_t countByte = 0; +uint32_t byteReads = 0; +int16_t sample_buffer[SIZE_APP_BUFFER/2]; +machine_i2s_obj_t* i2s0; +uint8_t id_i2s = 0; + +//// Callback function I2S ///// +void on_i2s_samples_ready(){ + byteReads = machine_i2s_stream_read(i2s0, (void*)&sample_buffer, SIZE_APP_BUFFER); // app buffer len <= dma buffer len / 8 +} + +// Callback function USB // +void on_usb_microphone_tx_ready(){ + usb_microphone_write(sample_buffer, sizeof(sample_buffer)); +} + +void init(){ + stdio_init_all(); + gpio_init(BUTTON); + gpio_set_dir(BUTTON, GPIO_IN); + gpio_pull_up(BUTTON); +} + +int main(){ + init(); + printf("START RECORD\r\n"); + printf("\r\n"); + printf("----- START DATA -----\r\n"); + sleep_ms(500); + + i2s0 = machine_i2s_make_new(id_i2s, SCK, WS, SD, + RX, BPS, MONO, + SIZEOF_DMA_BUFFER_IN_BYTES, // set ring buffer len >= dma buffer len + RATE); + + i2s_microphone_set_samples_ready(on_i2s_samples_ready, id_i2s); + + usb_microphone_init(); + usb_microphone_set_tx_ready_handler(on_usb_microphone_tx_ready); + + while (1){ + usb_microphone_task(); + } + + return 0; +} diff --git a/examples/hello_i2s_microphone/tusb_config.h b/examples/hello_i2s_microphone/tusb_config.h new file mode 100644 index 0000000..21f52d1 --- /dev/null +++ b/examples/hello_i2s_microphone/tusb_config.h @@ -0,0 +1,111 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#if CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX +#define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | OPT_MODE_HIGH_SPEED) +#else +#define CFG_TUSB_RHPORT0_MODE OPT_MODE_DEVICE +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +// CFG_TUSB_DEBUG is defined by compiler in DEBUG build +// #define CFG_TUSB_DEBUG 0 + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_CDC 0 +#define CFG_TUD_MSC 0 +#define CFG_TUD_HID 0 +#define CFG_TUD_MIDI 0 +#define CFG_TUD_AUDIO 1 +#define CFG_TUD_VENDOR 0 + +//-------------------------------------------------------------------- +// AUDIO CLASS DRIVER CONFIGURATION +//-------------------------------------------------------------------- + +// Have a look into audio_device.h for all configurations + +#define CFG_TUD_AUDIO_FUNC_1_DESC_LEN TUD_AUDIO_MIC_ONE_CH_DESC_LEN +#define CFG_TUD_AUDIO_FUNC_1_N_AS_INT 1 // Number of Standard AS Interface Descriptors (4.9.1) defined per audio function - this is required to be able to remember the current alternate settings of these interfaces - We restrict us here to have a constant number for all audio functions (which means this has to be the maximum number of AS interfaces an audio function has and a second audio function with less AS interfaces just wastes a few bytes) +#define CFG_TUD_AUDIO_FUNC_1_CTRL_BUF_SZ 64 // Size of control request buffer + +#define CFG_TUD_AUDIO_ENABLE_EP_IN 1 +#define CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX 2 // Driver gets this info from the descriptors - we define it here to use it to setup the descriptors and to do calculations with it below +#define CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX 1 // Driver gets this info from the descriptors - we define it here to use it to setup the descriptors and to do calculations with it below - be aware: for different number of channels you need another descriptor! +#define CFG_TUD_AUDIO_EP_SZ_IN (16 + 1) * CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX * CFG_TUD_AUDIO_FUNC_1_N_CHANNELS_TX // 16 Samples (16 kHz) x 2 Bytes/Sample x 1 Channel +#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SZ_MAX CFG_TUD_AUDIO_EP_SZ_IN // Maximum EP IN size for all AS alternate settings used +#define CFG_TUD_AUDIO_FUNC_1_EP_IN_SW_BUF_SZ CFG_TUD_AUDIO_EP_SZ_IN + +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_CONFIG_H_ */ diff --git a/examples/hello_i2s_microphone/usb_descriptors.c b/examples/hello_i2s_microphone/usb_descriptors.c new file mode 100644 index 0000000..a4e9dc8 --- /dev/null +++ b/examples/hello_i2s_microphone/usb_descriptors.c @@ -0,0 +1,160 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "tusb.h" + +/* A combination of interfaces must have a unique product id, since PC will save device driver after the first plug. + * Same VID/PID with different interface e.g MSC (first), then CDC (later) will possibly cause system error on PC. + * + * Auto ProductID layout's Bitmap: + * [MSB] AUDIO | MIDI | HID | MSC | CDC [LSB] + */ +#define _PID_MAP(itf, n) ( (CFG_TUD_##itf) << (n) ) +#define USB_PID (0x4000 | _PID_MAP(CDC, 0) | _PID_MAP(MSC, 1) | _PID_MAP(HID, 2) | \ + _PID_MAP(MIDI, 3) | _PID_MAP(AUDIO, 4) | _PID_MAP(VENDOR, 5) ) + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ +tusb_desc_device_t const desc_device = +{ + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + + // Use Interface Association Descriptor (IAD) for CDC + // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1) + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0xCafe, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + +// Invoked when received GET DEVICE DESCRIPTOR +// Application return pointer to descriptor +uint8_t const * tud_descriptor_device_cb(void) +{ + return (uint8_t const *) &desc_device; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor +//--------------------------------------------------------------------+ +enum +{ + ITF_NUM_AUDIO_CONTROL = 0, + ITF_NUM_AUDIO_STREAMING, + ITF_NUM_TOTAL +}; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + CFG_TUD_AUDIO * TUD_AUDIO_MIC_ONE_CH_DESC_LEN) + +#if CFG_TUSB_MCU == OPT_MCU_LPC175X_6X || CFG_TUSB_MCU == OPT_MCU_LPC177X_8X || CFG_TUSB_MCU == OPT_MCU_LPC40XX +// LPC 17xx and 40xx endpoint type (bulk/interrupt/iso) are fixed by its number +// 0 control, 1 In, 2 Bulk, 3 Iso, 4 In etc ... +#define EPNUM_AUDIO 0x03 +#else +#define EPNUM_AUDIO 0x01 +#endif + +uint8_t const desc_configuration[] = +{ + // Interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_AUDIO_MIC_ONE_CH_DESCRIPTOR(/*_itfnum*/ ITF_NUM_AUDIO_CONTROL, /*_stridx*/ 0, /*_nBytesPerSample*/ CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX, /*_nBitsUsedPerSample*/ CFG_TUD_AUDIO_FUNC_1_N_BYTES_PER_SAMPLE_TX*8, /*_epin*/ 0x80 | EPNUM_AUDIO, /*_epsize*/ CFG_TUD_AUDIO_EP_SZ_IN) +}; + +// Invoked when received GET CONFIGURATION DESCRIPTOR +// Application return pointer to descriptor +// Descriptor contents must exist long enough for transfer to complete +uint8_t const * tud_descriptor_configuration_cb(uint8_t index) +{ + (void) index; // for multiple configurations + return desc_configuration; +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +// array of pointer to string descriptors +char const* string_desc_arr [] = +{ + (const char[]) { 0x09, 0x04 }, // 0: is supported language is English (0x0409) + "PaniRCorp", // 1: Manufacturer + "MicNode", // 2: Product + "123456", // 3: Serials, should use chip ID + "UAC2", // 4: Audio Interface +}; + +static uint16_t _desc_str[32]; + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void) langid; + + uint8_t chr_count; + + if ( index == 0) + { + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + }else + { + // Convert ASCII string into UTF-16 + + if ( !(index < sizeof(string_desc_arr)/sizeof(string_desc_arr[0])) ) return NULL; + + const char* str = string_desc_arr[index]; + + // Cap at max char + chr_count = strlen(str); + if ( chr_count > 31 ) chr_count = 31; + + for(uint8_t i=0; ibRequest == AUDIO_CS_REQ_CUR); + + // Page 91 in UAC2 specification + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t ep = TU_U16_LOW(p_request->wIndex); + + (void) channelNum; (void) ctrlSel; (void) ep; + + return false; // Yet not implemented +} + +// Invoked when audio class specific set request received for an interface +bool tud_audio_set_req_itf_cb(uint8_t rhport, tusb_control_request_t const * p_request, uint8_t *pBuff) +{ + (void) rhport; + (void) pBuff; + + // We do not support any set range requests here, only current value requests + TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR); + + // Page 91 in UAC2 specification + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t itf = TU_U16_LOW(p_request->wIndex); + + (void) channelNum; (void) ctrlSel; (void) itf; + + return false; // Yet not implemented +} + +// Invoked when audio class specific set request received for an entity +bool tud_audio_set_req_entity_cb(uint8_t rhport, tusb_control_request_t const * p_request, uint8_t *pBuff) +{ + (void) rhport; + + // Page 91 in UAC2 specification + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t itf = TU_U16_LOW(p_request->wIndex); + uint8_t entityID = TU_U16_HIGH(p_request->wIndex); + + (void) itf; + + // We do not support any set range requests here, only current value requests + TU_VERIFY(p_request->bRequest == AUDIO_CS_REQ_CUR); + + // If request is for our feature unit + if ( entityID == 2 ) + { + switch ( ctrlSel ) + { + case AUDIO_FU_CTRL_MUTE: + // Request uses format layout 1 + TU_VERIFY(p_request->wLength == sizeof(audio_control_cur_1_t)); + + mute[channelNum] = ((audio_control_cur_1_t*) pBuff)->bCur; + + TU_LOG2(" Set Mute: %d of channel: %u\r\n", mute[channelNum], channelNum); + + return true; + + case AUDIO_FU_CTRL_VOLUME: + // Request uses format layout 2 + TU_VERIFY(p_request->wLength == sizeof(audio_control_cur_2_t)); + + volume[channelNum] = ((audio_control_cur_2_t*) pBuff)->bCur; + + TU_LOG2(" Set Volume: %d dB of channel: %u\r\n", volume[channelNum], channelNum); + + return true; + + // Unknown/Unsupported control + default: + TU_BREAKPOINT(); + return false; + } + } + return false; // Yet not implemented +} + +// Invoked when audio class specific get request received for an EP +bool tud_audio_get_req_ep_cb(uint8_t rhport, tusb_control_request_t const * p_request) +{ + (void) rhport; + + // Page 91 in UAC2 specification + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t ep = TU_U16_LOW(p_request->wIndex); + + (void) channelNum; (void) ctrlSel; (void) ep; + + // return tud_control_xfer(rhport, p_request, &tmp, 1); + + return false; // Yet not implemented +} + +// Invoked when audio class specific get request received for an interface +bool tud_audio_get_req_itf_cb(uint8_t rhport, tusb_control_request_t const * p_request) +{ + (void) rhport; + + // Page 91 in UAC2 specification + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + uint8_t itf = TU_U16_LOW(p_request->wIndex); + + (void) channelNum; (void) ctrlSel; (void) itf; + + return false; // Yet not implemented +} + +// Invoked when audio class specific get request received for an entity +bool tud_audio_get_req_entity_cb(uint8_t rhport, tusb_control_request_t const * p_request) +{ + (void) rhport; + + // Page 91 in UAC2 specification + uint8_t channelNum = TU_U16_LOW(p_request->wValue); + uint8_t ctrlSel = TU_U16_HIGH(p_request->wValue); + // uint8_t itf = TU_U16_LOW(p_request->wIndex); // Since we have only one audio function implemented, we do not need the itf value + uint8_t entityID = TU_U16_HIGH(p_request->wIndex); + + // Input terminal (Microphone input) + if (entityID == 1) + { + switch (ctrlSel) + { + case AUDIO_TE_CTRL_CONNECTOR:; + // The terminal connector control only has a get request with only the CUR attribute. + + audio_desc_channel_cluster_t ret; + + // Those are dummy values for now + ret.bNrChannels = 1; + ret.bmChannelConfig = 0; + ret.iChannelNames = 0; + + TU_LOG2(" Get terminal connector\r\n"); + + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, (void*)&ret, sizeof(ret)); + + // Unknown/Unsupported control selector + default: TU_BREAKPOINT(); return false; + } + } + + // Feature unit + if (entityID == 2) + { + switch (ctrlSel) + { + case AUDIO_FU_CTRL_MUTE: + // Audio control mute cur parameter block consists of only one byte - we thus can send it right away + // There does not exist a range parameter block for mute + TU_LOG2(" Get Mute of channel: %u\r\n", channelNum); + return tud_control_xfer(rhport, p_request, &mute[channelNum], 1); + + case AUDIO_FU_CTRL_VOLUME: + + switch (p_request->bRequest) + { + case AUDIO_CS_REQ_CUR: + TU_LOG2(" Get Volume of channel: %u\r\n", channelNum); + return tud_control_xfer(rhport, p_request, &volume[channelNum], sizeof(volume[channelNum])); + case AUDIO_CS_REQ_RANGE: + TU_LOG2(" Get Volume range of channel: %u\r\n", channelNum); + + // Copy values - only for testing - better is version below + audio_control_range_2_n_t(1) ret; + + ret.wNumSubRanges = 1; + ret.subrange[0].bMin = -90; // -90 dB + ret.subrange[0].bMax = 90; // +90 dB + ret.subrange[0].bRes = 1; // 1 dB steps + + return tud_audio_buffer_and_schedule_control_xfer(rhport, p_request, (void*)&ret, sizeof(ret)); + + // Unknown/Unsupported control + default: TU_BREAKPOINT(); return false; + } + + // Unknown/Unsupported control + default: TU_BREAKPOINT(); return false; + } + } + + // Clock Source unit + if (entityID == 4) + { + switch (ctrlSel) + { + case AUDIO_CS_CTRL_SAM_FREQ: + + // channelNum is always zero in this case + + switch (p_request->bRequest) + { + case AUDIO_CS_REQ_CUR: + TU_LOG2(" Get Sample Freq.\r\n"); + return tud_control_xfer(rhport, p_request, &sampFreq, sizeof(sampFreq)); + case AUDIO_CS_REQ_RANGE: + TU_LOG2(" Get Sample Freq. range\r\n"); + return tud_control_xfer(rhport, p_request, &sampleFreqRng, sizeof(sampleFreqRng)); + + // Unknown/Unsupported control + default: TU_BREAKPOINT(); return false; + } + + case AUDIO_CS_CTRL_CLK_VALID: + // Only cur attribute exists for this request + TU_LOG2(" Get Sample Freq. valid\r\n"); + return tud_control_xfer(rhport, p_request, &clkValid, sizeof(clkValid)); + + // Unknown/Unsupported control + default: TU_BREAKPOINT(); return false; + } + } + + TU_LOG2(" Unsupported entity: %d\r\n", entityID); + return false; // Yet not implemented +} + +bool tud_audio_tx_done_pre_load_cb(uint8_t rhport, uint8_t itf, uint8_t ep_in, uint8_t cur_alt_setting) +{ + (void) rhport; + (void) itf; + (void) ep_in; + (void) cur_alt_setting; + + if (usb_microphone_tx_ready_handler) + { + usb_microphone_tx_ready_handler(); + } + + return true; +} + +bool tud_audio_tx_done_post_load_cb(uint8_t rhport, uint16_t n_bytes_copied, uint8_t itf, uint8_t ep_in, uint8_t cur_alt_setting) +{ + (void) rhport; + (void) n_bytes_copied; + (void) itf; + (void) ep_in; + (void) cur_alt_setting; + + return true; +} + +bool tud_audio_set_itf_close_EP_cb(uint8_t rhport, tusb_control_request_t const * p_request) +{ + (void) rhport; + (void) p_request; + + return true; +} diff --git a/examples/hello_i2s_microphone/usb_microphone.h b/examples/hello_i2s_microphone/usb_microphone.h new file mode 100644 index 0000000..cf45b9c --- /dev/null +++ b/examples/hello_i2s_microphone/usb_microphone.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 Arm Limited and Contributors. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ + +#ifndef _USB_MICROPHONE_H_ +#define _USB_MICROPHONE_H_ + +#include "tusb.h" + +#ifndef SAMPLE_RATE +#define SAMPLE_RATE ((CFG_TUD_AUDIO_EP_SZ_IN / 2) - 1) * 1000 +#endif + +#ifndef SAMPLE_BUFFER_SIZE +#define SAMPLE_BUFFER_SIZE ((CFG_TUD_AUDIO_EP_SZ_IN/2) - 1) +#endif + +typedef void (*usb_microphone_tx_ready_handler_t)(void); + +void usb_microphone_init(); +void usb_microphone_set_tx_ready_handler(usb_microphone_tx_ready_handler_t handler); +void usb_microphone_task(); +uint16_t usb_microphone_write(const void * data, uint16_t len); + +#endif diff --git a/src/i2s_microphone.c b/src/i2s_microphone.c new file mode 100644 index 0000000..119f712 --- /dev/null +++ b/src/i2s_microphone.c @@ -0,0 +1,732 @@ +/* + i2s_microphone.c - + I2S digital audio input C library for the Raspberry Pi Pico RP2040 + + Copyright (C) 2022 Sfera Labs S.r.l. - All rights reserved. + + For information, see: + http://www.sferalabs.cc/ + + This code is adapted from the I2S implementation of the RP2 MicroPython port + by Mike Teachman, available at: + https://github.com/micropython/micropython/blob/master/ports/rp2/machine_i2s.c + Retrieved on January 25 2022. +*/ + +/* Original header */ + +/* + * This file is part of the MicroPython project, http://micropython.org/ + * + * The MIT License (MIT) + * + * Copyright (c) 2021 Mike Teachman + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include "pico/i2s_microphone.h" +#include "hardware/pio.h" +#include "hardware/clocks.h" +#include "hardware/gpio.h" +#include "hardware/dma.h" +#include "hardware/irq.h" +#include "pico/i2s_microphone.h" + + +STATIC machine_i2s_obj_t* machine_i2s_obj[MAX_I2S_RP2] = {NULL, NULL}; + +// The frame map is used with the readinto() method to transform the audio sample data coming +// from DMA memory (32-bit stereo) to the format specified +// in the I2S constructor. e.g. 16-bit mono +STATIC const int8_t i2s_frame_map[NUM_I2S_USER_FORMATS][I2S_RX_FRAME_SIZE_IN_BYTES] = { + {-1, -1, 0, 1, -1, -1, -1, -1 }, // Mono, 16-bits + { 0, 1, 2, 3, -1, -1, -1, -1 }, // Mono, 32-bits + {-1, -1, 0, 1, -1, -1, 2, 3 }, // Stereo, 16-bits + { 0, 1, 2, 3, 4, 5, 6, 7 }, // Stereo, 32-bits +}; + +STATIC const PIO pio_instances[NUM_PIOS] = {pio0, pio1}; + +// PIO program for 16-bit write +// set(x, 14) .side(0b01) +// label('left_channel') +// out(pins, 1) .side(0b00) +// jmp(x_dec, "left_channel") .side(0b01) +// out(pins, 1) .side(0b10) +// set(x, 14) .side(0b11) +// label('right_channel') +// out(pins, 1) .side(0b10) +// jmp(x_dec, "right_channel") .side(0b11) +// out(pins, 1) .side(0b00) +STATIC const uint16_t pio_instructions_write_16[] = {59438, 24577, 2113, 28673, 63534, 28673, 6213, 24577}; +STATIC const pio_program_t pio_write_16 = { + pio_instructions_write_16, + sizeof(pio_instructions_write_16) / sizeof(uint16_t), + -1 +}; + +// PIO program for 32-bit write +// set(x, 30) .side(0b01) +// label('left_channel') +// out(pins, 1) .side(0b00) +// jmp(x_dec, "left_channel") .side(0b01) +// out(pins, 1) .side(0b10) +// set(x, 30) .side(0b11) +// label('right_channel') +// out(pins, 1) .side(0b10) +// jmp(x_dec, "right_channel") .side(0b11) +// out(pins, 1) .side(0b00) +STATIC const uint16_t pio_instructions_write_32[] = {59454, 24577, 2113, 28673, 63550, 28673, 6213, 24577}; +STATIC const pio_program_t pio_write_32 = { + pio_instructions_write_32, + sizeof(pio_instructions_write_32) / sizeof(uint16_t), + -1 +}; + +// PIO program for 32-bit read +// set(x, 30) .side(0b00) +// label('left_channel') +// in_(pins, 1) .side(0b01) +// jmp(x_dec, "left_channel") .side(0b00) +// in_(pins, 1) .side(0b11) +// set(x, 30) .side(0b10) +// label('right_channel') +// in_(pins, 1) .side(0b11) +// jmp(x_dec, "right_channel") .side(0b10) +// in_(pins, 1) .side(0b01) +STATIC const uint16_t pio_instructions_read_32[] = {57406, 18433, 65, 22529, 61502, 22529, 4165, 18433}; +STATIC const pio_program_t pio_read_32 = { + pio_instructions_read_32, + sizeof(pio_instructions_read_32) / sizeof(uint16_t), + -1 +}; + +STATIC uint8_t dma_get_bits(i2s_mode_t mode, int8_t bits); +STATIC void dma_irq0_handler(void); +STATIC void dma_irq1_handler(void); + +// Ring Buffer +// Thread safe when used with these constraints: +// - Single Producer, Single Consumer +// - Sequential atomic operations +// One byte of capacity is used to detect buffer empty/full + +STATIC void ringbuf_init(ring_buf_t *rbuf, uint8_t *buffer, size_t size) { + rbuf->buffer = buffer; + rbuf->size = size; + rbuf->head = 0; + rbuf->tail = 0; +} + +STATIC bool ringbuf_push(ring_buf_t *rbuf, uint8_t data) { + size_t next_tail = (rbuf->tail + 1) % rbuf->size; + + if (next_tail != rbuf->head) { + rbuf->buffer[rbuf->tail] = data; + rbuf->tail = next_tail; + return true; + } + + // full + return false; +} + +STATIC bool ringbuf_pop(ring_buf_t *rbuf, uint8_t *data) { + stdio_flush(); + if (rbuf->head == rbuf->tail) { + // empty + return false; + } + + *data = rbuf->buffer[rbuf->head]; + rbuf->head = (rbuf->head + 1) % rbuf->size; + return true; +} + +STATIC bool ringbuf_is_empty(ring_buf_t *rbuf) { + return rbuf->head == rbuf->tail; +} + +STATIC bool ringbuf_is_full(ring_buf_t *rbuf) { + return ((rbuf->tail + 1) % rbuf->size) == rbuf->head; +} + +STATIC size_t ringbuf_available_data(ring_buf_t *rbuf) { + return (rbuf->tail - rbuf->head + rbuf->size) % rbuf->size; +} + +STATIC size_t ringbuf_available_space(ring_buf_t *rbuf) { + return rbuf->size - ringbuf_available_data(rbuf) - 1; +} + +STATIC int8_t get_frame_mapping_index(int8_t bits, format_t format) { + if (format == MONO) { + if (bits == 16) { + return 0; + } else { // 32 bits + return 1; + } + } else { // STEREO + if (bits == 16) { + return 2; + } else { // 32 bits + return 3; + } + } +} + +STATIC uint32_t fill_appbuf_from_ringbuf(machine_i2s_obj_t *self, mp_buffer_info_t *appbuf) { + + // copy audio samples from the ring buffer to the app buffer + // loop, copying samples until the app buffer is filled + // For uasyncio mode, the loop will make an early exit if the ring buffer becomes empty + // Example: + // a MicroPython I2S object is configured for 16-bit mono (2 bytes per audio sample). + // For every frame coming from the ring buffer (8 bytes), 2 bytes are "cherry picked" and + // copied to the supplied app buffer. + // Thus, for every 1 byte copied to the app buffer, 4 bytes are read from the ring buffer. + // If a 8kB app buffer is supplied, 32kB of audio samples is read from the ring buffer. + + uint32_t num_bytes_copied_to_appbuf = 0; + uint8_t *app_p = (uint8_t *)appbuf->buf; + uint8_t appbuf_sample_size_in_bytes = (self->bits == 16? 2 : 4) * (self->format == STEREO ? 2: 1); + uint32_t num_bytes_needed_from_ringbuf = appbuf->len * (I2S_RX_FRAME_SIZE_IN_BYTES / appbuf_sample_size_in_bytes); + uint8_t discard_byte; + while (num_bytes_needed_from_ringbuf) { + + uint8_t f_index = get_frame_mapping_index(self->bits, self->format); + + for (uint8_t i = 0; i < I2S_RX_FRAME_SIZE_IN_BYTES; i++) { + int8_t r_to_a_mapping = i2s_frame_map[f_index][i]; + if (r_to_a_mapping != -1) { + if (self->io_mode == BLOCKING) { + // poll the ringbuf until a sample becomes available, copy into appbuf using the mapping transform + while (ringbuf_pop(&self->ring_buffer, app_p + r_to_a_mapping) == false) { + ; + } + num_bytes_copied_to_appbuf++; + } else if (self->io_mode == UASYNCIO) { + if (ringbuf_pop(&self->ring_buffer, app_p + r_to_a_mapping) == false) { + // ring buffer is empty, exit + goto exit; + } else { + num_bytes_copied_to_appbuf++; + } + } else { + return 0; // should never get here (non-blocking mode does not use this function) + } + } else { // r_a_mapping == -1 + // discard unused byte from ring buffer + if (self->io_mode == BLOCKING) { + // poll the ringbuf until a sample becomes available + while (ringbuf_pop(&self->ring_buffer, &discard_byte) == false) { + ; + } + } else if (self->io_mode == UASYNCIO) { + if (ringbuf_pop(&self->ring_buffer, &discard_byte) == false) { + // ring buffer is empty, exit + goto exit; + } + } else { + return 0; // should never get here (non-blocking mode does not use this function) + } + } + num_bytes_needed_from_ringbuf--; + } + app_p += appbuf_sample_size_in_bytes; + } +exit: + return num_bytes_copied_to_appbuf; +} + +STATIC uint32_t copy_appbuf_to_ringbuf(machine_i2s_obj_t *self, mp_buffer_info_t *appbuf) { + + // copy audio samples from the app buffer to the ring buffer + // loop, reading samples until the app buffer is emptied + // for uasyncio mode, the loop will make an early exit if the ring buffer becomes full + + uint32_t a_index = 0; + + while (a_index < appbuf->len) { + if (self->io_mode == BLOCKING) { + // copy a byte to the ringbuf when space becomes available + while (ringbuf_push(&self->ring_buffer, ((uint8_t *)appbuf->buf)[a_index]) == false) { + ; + } + a_index++; + } else if (self->io_mode == UASYNCIO) { + if (ringbuf_push(&self->ring_buffer, ((uint8_t *)appbuf->buf)[a_index]) == false) { + // ring buffer is full, exit + break; + } else { + a_index++; + } + } else { + return 0; // should never get here (non-blocking mode does not use this function) + } + } + + return a_index; +} + +// function is used in IRQ context +STATIC void empty_dma(machine_i2s_obj_t *self, uint8_t *dma_buffer_p) { + // when space exists, copy samples into ring buffer + if (ringbuf_available_space(&self->ring_buffer) >= SIZEOF_HALF_DMA_BUFFER_IN_BYTES) { + for (uint32_t i = 0; i < SIZEOF_HALF_DMA_BUFFER_IN_BYTES; i++) { + ringbuf_push(&self->ring_buffer, dma_buffer_p[i]); + } + } +} + +// function is used in IRQ context +STATIC void feed_dma(machine_i2s_obj_t *self, uint8_t *dma_buffer_p) { + // when data exists, copy samples from ring buffer + if (ringbuf_available_data(&self->ring_buffer) >= SIZEOF_HALF_DMA_BUFFER_IN_BYTES) { + + // copy a block of samples from the ring buffer to the dma buffer. + // STM32 HAL API has a stereo I2S implementation, but not mono + // mono format is implemented by duplicating each sample into both L and R channels. + if ((self->format == MONO) && (self->bits == 16)) { + for (uint32_t i = 0; i < SIZEOF_HALF_DMA_BUFFER_IN_BYTES / 4; i++) { + for (uint8_t b = 0; b < sizeof(uint16_t); b++) { + ringbuf_pop(&self->ring_buffer, &dma_buffer_p[i * 4 + b]); + dma_buffer_p[i * 4 + b + 2] = dma_buffer_p[i * 4 + b]; // duplicated mono sample + } + } + } else if ((self->format == MONO) && (self->bits == 32)) { + for (uint32_t i = 0; i < SIZEOF_HALF_DMA_BUFFER_IN_BYTES / 8; i++) { + for (uint8_t b = 0; b < sizeof(uint32_t); b++) { + ringbuf_pop(&self->ring_buffer, &dma_buffer_p[i * 8 + b]); + dma_buffer_p[i * 8 + b + 4] = dma_buffer_p[i * 8 + b]; // duplicated mono sample + } + } + } else { // STEREO, both 16-bit and 32-bit + for (uint32_t i = 0; i < SIZEOF_HALF_DMA_BUFFER_IN_BYTES; i++) { + ringbuf_pop(&self->ring_buffer, &dma_buffer_p[i]); + } + } + } else { + // underflow. clear buffer to transmit "silence" on the I2S bus + memset(dma_buffer_p, 0, SIZEOF_HALF_DMA_BUFFER_IN_BYTES); + } +} + +STATIC void irq_configure(machine_i2s_obj_t *self) { + if (self->i2s_id == 0) { + irq_set_exclusive_handler(DMA_IRQ_0, dma_irq0_handler); + irq_set_enabled(DMA_IRQ_0, true); + } else { + irq_set_exclusive_handler(DMA_IRQ_1, dma_irq1_handler); + irq_set_enabled(DMA_IRQ_1, true); + } +} + +STATIC void irq_deinit(machine_i2s_obj_t *self) { + if (self->i2s_id == 0) { + irq_set_enabled(DMA_IRQ_0, false); + irq_remove_handler(DMA_IRQ_0, dma_irq0_handler); + } else { + irq_set_enabled(DMA_IRQ_1, false); + irq_remove_handler(DMA_IRQ_1, dma_irq1_handler); + } +} + +STATIC int pio_configure(machine_i2s_obj_t *self) { + if (self->mode == TX) { + if (self->bits == 16) { + self->pio_program = &pio_write_16; + } else { + self->pio_program = &pio_write_32; + } + } else { // RX + self->pio_program = &pio_read_32; + } + + // find a PIO with a free state machine and adequate program space + PIO candidate_pio; + bool is_free_sm; + bool can_add_program; + for (uint8_t p = 0; p < NUM_PIOS; p++) { + candidate_pio = pio_instances[p]; + is_free_sm = false; + can_add_program = false; + + for (uint8_t sm = 0; sm < NUM_PIO_STATE_MACHINES; sm++) { + if (!pio_sm_is_claimed(candidate_pio, sm)) { + is_free_sm = true; + break; + } + } + + if (pio_can_add_program(candidate_pio, self->pio_program)) { + can_add_program = true; + } + + if (is_free_sm && can_add_program) { + break; + } + } + + if (!is_free_sm) { + return -1; + } + + if (!can_add_program) { + return -2; + } + + self->pio = candidate_pio; + self->sm = pio_claim_unused_sm(self->pio, false); + self->prog_offset = pio_add_program(self->pio, self->pio_program); + pio_sm_init(self->pio, self->sm, self->prog_offset, NULL); + + pio_sm_config config = pio_get_default_sm_config(); + + float pio_freq = self->rate * + SAMPLES_PER_FRAME * + dma_get_bits(self->mode, self->bits) * + PIO_INSTRUCTIONS_PER_BIT; + float clkdiv = clock_get_hz(clk_sys) / pio_freq; + sm_config_set_clkdiv(&config, clkdiv); + + if (self->mode == TX) { + sm_config_set_out_pins(&config, self->sd, 1); + sm_config_set_out_shift(&config, false, true, dma_get_bits(self->mode, self->bits)); + sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_TX); // double TX FIFO size + } else { // RX + sm_config_set_in_pins(&config, self->sd); + sm_config_set_in_shift(&config, false, true, dma_get_bits(self->mode, self->bits)); + sm_config_set_fifo_join(&config, PIO_FIFO_JOIN_RX); // double RX FIFO size + } + + sm_config_set_sideset(&config, 2, false, false); + sm_config_set_sideset_pins(&config, self->sck); + sm_config_set_wrap(&config, self->prog_offset, self->prog_offset + self->pio_program->length - 1); + pio_sm_set_config(self->pio, self->sm, &config); + + return 0; +} + +STATIC void pio_deinit(machine_i2s_obj_t *self) { + if (self->pio) { + pio_sm_set_enabled(self->pio, self->sm, false); + pio_sm_unclaim(self->pio, self->sm); + pio_remove_program(self->pio, self->pio_program, self->prog_offset); + } +} + +STATIC void gpio_init_i2s(PIO pio, uint8_t sm, mp_hal_pin_obj_t pin_num, uint8_t pin_val, gpio_dir_t pin_dir) { + uint32_t pinmask = 1 << pin_num; + pio_sm_set_pins_with_mask(pio, sm, pin_val << pin_num, pinmask); + pio_sm_set_pindirs_with_mask(pio, sm, pin_dir << pin_num, pinmask); + pio_gpio_init(pio, pin_num); +} + +STATIC void gpio_configure(machine_i2s_obj_t *self) { + gpio_init_i2s(self->pio, self->sm, self->sck, 0, GP_OUTPUT); + gpio_init_i2s(self->pio, self->sm, self->ws, 0, GP_OUTPUT); + if (self->mode == TX) { + gpio_init_i2s(self->pio, self->sm, self->sd, 0, GP_OUTPUT); + } else { // RX + gpio_init_i2s(self->pio, self->sm, self->sd, 0, GP_INPUT); + } +} + +STATIC uint8_t dma_get_bits(i2s_mode_t mode, int8_t bits) { + if (mode == TX) { + return bits; + } else { // RX + // always read 32 bit words for I2S e.g. I2S MEMS microphones + return 32; + } +} + +// determine which DMA channel is associated to this IRQ +STATIC uint dma_map_irq_to_channel(uint irq_index) { + for (uint ch = 0; ch < NUM_DMA_CHANNELS; ch++) { + if ((dma_irqn_get_channel_status(irq_index, ch))) { + return ch; + } + } + // This should never happen + return -1; +} + +// note: first DMA channel is mapped to the top half of buffer, second is mapped to the bottom half +STATIC uint8_t *dma_get_buffer(machine_i2s_obj_t *i2s_obj, uint channel) { + for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) { + if (i2s_obj->dma_channel[ch] == channel) { + return i2s_obj->dma_buffer + (SIZEOF_HALF_DMA_BUFFER_IN_BYTES * ch); + } + } + // This should never happen + return NULL; +} + +STATIC int dma_configure(machine_i2s_obj_t *self) { + uint8_t num_free_dma_channels = 0; + for (uint8_t ch = 0; ch < NUM_DMA_CHANNELS; ch++) { + if (!dma_channel_is_claimed(ch)) { + num_free_dma_channels++; + } + } + if (num_free_dma_channels < I2S_NUM_DMA_CHANNELS) { + return -1; + } + + for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) { + self->dma_channel[ch] = dma_claim_unused_channel(false); + } + + // The DMA channels are chained together. The first DMA channel is used to access + // the top half of the DMA buffer. The second DMA channel accesses the bottom half of the DMA buffer. + // With chaining, when one DMA channel has completed a data transfer, the other + // DMA channel automatically starts a new data transfer. + enum dma_channel_transfer_size dma_size = (dma_get_bits(self->mode, self->bits) == 16) ? DMA_SIZE_16 : DMA_SIZE_32; + for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) { + dma_channel_config dma_config = dma_channel_get_default_config(self->dma_channel[ch]); + channel_config_set_transfer_data_size(&dma_config, dma_size); + channel_config_set_chain_to(&dma_config, self->dma_channel[(ch + 1) % I2S_NUM_DMA_CHANNELS]); + + uint8_t *dma_buffer = self->dma_buffer + (SIZEOF_HALF_DMA_BUFFER_IN_BYTES * ch); + if (self->mode == TX) { + channel_config_set_dreq(&dma_config, pio_get_dreq(self->pio, self->sm, true)); + channel_config_set_read_increment(&dma_config, true); + channel_config_set_write_increment(&dma_config, false); + dma_channel_configure(self->dma_channel[ch], + &dma_config, + (void *)&self->pio->txf[self->sm], // dest = PIO TX FIFO + dma_buffer, // src = DMA buffer + SIZEOF_HALF_DMA_BUFFER_IN_BYTES / (dma_get_bits(self->mode, self->bits) / 8), + false); + } else { // RX + channel_config_set_dreq(&dma_config, pio_get_dreq(self->pio, self->sm, false)); + channel_config_set_read_increment(&dma_config, false); + channel_config_set_write_increment(&dma_config, true); + dma_channel_configure(self->dma_channel[ch], + &dma_config, + dma_buffer, // dest = DMA buffer + (void *)&self->pio->rxf[self->sm], // src = PIO RX FIFO + SIZEOF_HALF_DMA_BUFFER_IN_BYTES / (dma_get_bits(self->mode, self->bits) / 8), + false); + } + } + + for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) { + dma_irqn_acknowledge_channel(self->i2s_id, self->dma_channel[ch]); // clear pending. e.g. from SPI + dma_irqn_set_channel_enabled(self->i2s_id, self->dma_channel[ch], true); + } + + return 0; +} + +STATIC void dma_deinit(machine_i2s_obj_t *self) { + for (uint8_t ch = 0; ch < I2S_NUM_DMA_CHANNELS; ch++) { + int channel = self->dma_channel[ch]; + + // unchain the channel to prevent triggering a transfer in the chained-to channel + dma_channel_config dma_config = dma_get_channel_config(channel); + channel_config_set_chain_to(&dma_config, channel); + dma_channel_set_config(channel, &dma_config, false); + + dma_irqn_set_channel_enabled(self->i2s_id, channel, false); + dma_channel_abort(channel); // in case a transfer is in flight + dma_channel_unclaim(channel); + } +} + +void i2s_microphone_set_samples_ready(i2s_samples_ready_handler_t handler, uint8_t I2S_ID){ + machine_i2s_obj[I2S_ID]->samples_ready_handler = handler; +} + +STATIC void dma_irq_handler(uint8_t irq_index) { + int dma_channel = dma_map_irq_to_channel(irq_index); + if (dma_channel == -1) { + // This should never happen + return; + } + + machine_i2s_obj_t *self = machine_i2s_obj[irq_index]; + if (self == NULL) { + // This should never happen + return; + } + + uint8_t *dma_buffer = dma_get_buffer(self, dma_channel); + + if (dma_buffer == NULL) { + // This should never happen + return; + } + + if (self->mode == RX) { + empty_dma(self, dma_buffer); + dma_irqn_acknowledge_channel(irq_index, dma_channel); + dma_channel_set_write_addr(dma_channel, dma_buffer, false); + } + + if (self->samples_ready_handler) { + self->samples_ready_handler(); + } + +} + +STATIC void dma_irq0_handler(void) { + dma_irq_handler(0); +} + +STATIC void dma_irq1_handler(void) { + dma_irq_handler(1); +} + +STATIC int machine_i2s_init_helper(machine_i2s_obj_t *self, + mp_hal_pin_obj_t sck, mp_hal_pin_obj_t ws, mp_hal_pin_obj_t sd, + i2s_mode_t i2s_mode, int8_t i2s_bits, format_t i2s_format, + int32_t ring_buffer_len, int32_t i2s_rate) { + // + // ---- Check validity of arguments ---- + // + + // does WS pin follow SCK pin? + // note: SCK and WS are implemented as PIO sideset pins. Sideset pins must be sequential. + if (ws != (sck + 1)) { + return -1; + } + + // is Mode valid? + if ((i2s_mode != RX) && + (i2s_mode != TX)) { + return -2; + } + + // is Bits valid? + if ((i2s_bits != 16) && + (i2s_bits != 32)) { + return -3; + } + + // is Format valid? + if ((i2s_format != MONO) && + (i2s_format != STEREO)) { + return -4; + } + + // is Rate valid? + // Not checked + + // is Ibuf valid? + if (ring_buffer_len > 0) { + self->ring_buffer_storage = m_new(uint8_t, ring_buffer_len); + ; + ringbuf_init(&self->ring_buffer, self->ring_buffer_storage, ring_buffer_len); + } else { + return -5; + } + + self->sck = sck; + self->ws = ws; + self->sd = sd; + self->mode = i2s_mode; + self->bits = i2s_bits; + self->format = i2s_format; + self->rate = i2s_rate; + self->ibuf = ring_buffer_len; + self->io_mode = BLOCKING; + + irq_configure(self); + int err = pio_configure(self); + if (err != 0) { + return err; + } + gpio_configure(self); + err = dma_configure(self); + if (err != 0) { + return err; + } + + pio_sm_set_enabled(self->pio, self->sm, true); + dma_channel_start(self->dma_channel[0]); + + return 0; +} + +machine_i2s_obj_t* machine_i2s_make_new(uint8_t i2s_id, + mp_hal_pin_obj_t sck, mp_hal_pin_obj_t ws, mp_hal_pin_obj_t sd, + i2s_mode_t i2s_mode, int8_t i2s_bits, format_t i2s_format, + int32_t ring_buffer_len, int32_t i2s_rate) { + if (i2s_id >= MAX_I2S_RP2) { + return NULL; + } + + machine_i2s_obj_t *self; + if (machine_i2s_obj[i2s_id] == NULL) { + self = m_new_obj(machine_i2s_obj_t); + machine_i2s_obj[i2s_id] = self; + self->i2s_id = i2s_id; + } else { + self = machine_i2s_obj[i2s_id]; + machine_i2s_deinit(self); + } + + if (machine_i2s_init_helper(self, sck, ws, sd, i2s_mode, i2s_bits, + i2s_format, ring_buffer_len, i2s_rate) != 0) { + return NULL; + } + return self; +} + +void machine_i2s_deinit(machine_i2s_obj_t *self) { + // use self->pio as in indication that I2S object has already been de-initialized + if (self->pio != NULL) { + pio_deinit(self); + dma_deinit(self); + irq_deinit(self); + free(self->ring_buffer_storage); + self->pio = NULL; // flag object as de-initialized + } +} + +int machine_i2s_stream_read(machine_i2s_obj_t *self, void *buf_in, size_t size) { + if (self->mode != RX) { + return -1; + } + + uint8_t appbuf_sample_size_in_bytes = (self->bits / 8) * (self->format == STEREO ? 2: 1); + if (size % appbuf_sample_size_in_bytes != 0) { + return -2; + } + + if (size == 0) { + return 0; + } + + mp_buffer_info_t appbuf; + appbuf.buf = (void *)buf_in; + appbuf.len = size; + uint32_t num_bytes_read = fill_appbuf_from_ringbuf(self, &appbuf); + + return num_bytes_read; +} \ No newline at end of file diff --git a/src/include/pico/i2s_microphone.h b/src/include/pico/i2s_microphone.h new file mode 100644 index 0000000..374bf44 --- /dev/null +++ b/src/include/pico/i2s_microphone.h @@ -0,0 +1,98 @@ +#ifndef _PICO_I2S_MICROPHONE_H_ +#define _PICO_I2S_MICROPHONE_H_ + +#include "hardware/pio.h" + +#define MAX_I2S_RP2 (2) + +// The DMA buffer size was empirically determined. It is a tradeoff between: +// 1. memory use (smaller buffer size desirable to reduce memory footprint) +// 2. interrupt frequency (larger buffer size desirable to reduce interrupt frequency) +#define SIZEOF_DMA_BUFFER_IN_BYTES (256) +#define SIZEOF_HALF_DMA_BUFFER_IN_BYTES (SIZEOF_DMA_BUFFER_IN_BYTES / 2) +#define I2S_NUM_DMA_CHANNELS (2) + +#define NUM_I2S_USER_FORMATS (4) +#define I2S_RX_FRAME_SIZE_IN_BYTES (8) + +#define SAMPLES_PER_FRAME (2) +#define PIO_INSTRUCTIONS_PER_BIT (2) + +#define STATIC static +#define mp_hal_pin_obj_t uint +#define m_new(type, num) ((type *)(malloc(sizeof(type) * (num)))) +#define m_new_obj(type) (m_new(type, 1)) + +typedef void (*i2s_samples_ready_handler_t)(void); + +typedef enum { + RX, + TX +} i2s_mode_t; + +typedef enum { + MONO, + STEREO +} format_t; + +typedef enum { + BLOCKING, + NON_BLOCKING, + UASYNCIO +} io_mode_t; + +typedef enum { + GP_INPUT = 0, + GP_OUTPUT = 1 +} gpio_dir_t; + +typedef struct _ring_buf_t { + uint8_t *buffer; + size_t head; + size_t tail; + size_t size; +} ring_buf_t; + + +// Buffer protocol +typedef struct _mp_buffer_info_t { + void *buf; // can be NULL if len == 0 + size_t len; // in bytes + int typecode; // as per binary.h +} mp_buffer_info_t; + +typedef struct _machine_i2s_obj_t { + uint8_t i2s_id; + mp_hal_pin_obj_t sck; + mp_hal_pin_obj_t ws; + mp_hal_pin_obj_t sd; + i2s_mode_t mode; + int8_t bits; + format_t format; + int32_t rate; + int32_t ibuf; + io_mode_t io_mode; + PIO pio; + uint8_t sm; + const pio_program_t *pio_program; + uint prog_offset; + int dma_channel[I2S_NUM_DMA_CHANNELS]; + uint8_t dma_buffer[SIZEOF_DMA_BUFFER_IN_BYTES]; + ring_buf_t ring_buffer; + uint8_t *ring_buffer_storage; + i2s_samples_ready_handler_t samples_ready_handler; +} machine_i2s_obj_t; + +machine_i2s_obj_t* machine_i2s_make_new(uint8_t i2s_id, mp_hal_pin_obj_t sck, + mp_hal_pin_obj_t ws, mp_hal_pin_obj_t sd, + i2s_mode_t i2s_mode, int8_t i2s_bits, + format_t i2s_format, int32_t ring_buffer_len, + int32_t i2s_rate); + +void machine_i2s_deinit(machine_i2s_obj_t *self); + +void i2s_microphone_set_samples_ready(i2s_samples_ready_handler_t handler, uint8_t I2S_ID); + +int machine_i2s_stream_read(machine_i2s_obj_t *self, void *buf_in, size_t size); + +#endif \ No newline at end of file