Commit 67dbbaab by tatsukiishikawa

replacing mic_example

parent ca725554
{
"configurations": [
{
"name": "Pico",
"includePath": [
"${workspaceFolder}/**",
"${userHome}/.pico-sdk/sdk/2.2.0/**"
],
"forcedInclude": [
"${userHome}/.pico-sdk/sdk/2.2.0/src/common/pico_base_headers/include/pico.h",
"${workspaceFolder}/build/generated/pico_base/pico/config_autogen.h"
],
"defines": [],
"compilerPath": "${userHome}/.pico-sdk/toolchain/14_2_Rel1/bin/arm-none-eabi-gcc",
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
"cStandard": "c17",
"cppStandard": "c++14",
"intelliSenseMode": "linux-gcc-arm"
}
],
"version": 4
}
[
{
"name": "Pico",
"compilers": {
"C": "${command:raspberry-pi-pico.getCompilerPath}",
"CXX": "${command:raspberry-pi-pico.getCxxCompilerPath}"
},
"environmentVariables": {
"PATH": "${command:raspberry-pi-pico.getEnvPath};${env:PATH}"
},
"cmakeSettings": {
"Python3_EXECUTABLE": "${command:raspberry-pi-pico.getPythonPath}"
}
}
]
\ No newline at end of file
{
"recommendations": [
"ms-vscode.vscode-serial-monitor",
"raspberry-pi.raspberry-pi-pico",
"ms-vscode.cpptools",
"ms-vscode.cpptools-extension-pack",
"marus25.cortex-debug"
]
}
\ No newline at end of file
{
"version": "0.2.0",
"configurations": [
{
"name": "Pico Debug (Cortex-Debug)",
"cwd": "${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"executable": "${command:raspberry-pi-pico.launchTargetPath}",
"request": "launch",
"type": "cortex-debug",
"servertype": "openocd",
"serverpath": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
"device": "${command:raspberry-pi-pico.getChipUppercase}",
"configFiles": [
"interface/cmsis-dap.cfg",
"target/${command:raspberry-pi-pico.getTarget}.cfg"
],
"svdFile": "${userHome}/.pico-sdk/sdk/2.2.0/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
"runToEntryPoint": "main",
// Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
// Also works fine for flash binaries
"overrideLaunchCommands": [
"monitor reset init",
"load \"${command:raspberry-pi-pico.launchTargetPath}\""
],
"openOCDLaunchCommands": [
"adapter speed 5000"
]
},
{
"name": "Pico Debug (Cortex-Debug with external OpenOCD)",
"cwd": "${workspaceRoot}",
"executable": "${command:raspberry-pi-pico.launchTargetPath}",
"request": "launch",
"type": "cortex-debug",
"servertype": "external",
"gdbTarget": "localhost:3333",
"gdbPath": "${command:raspberry-pi-pico.getGDBPath}",
"device": "${command:raspberry-pi-pico.getChipUppercase}",
"svdFile": "${userHome}/.pico-sdk/sdk/2.2.0/src/${command:raspberry-pi-pico.getChip}/hardware_regs/${command:raspberry-pi-pico.getChipUppercase}.svd",
"runToEntryPoint": "main",
// Fix for no_flash binaries, where monitor reset halt doesn't do what is expected
// Also works fine for flash binaries
"overrideLaunchCommands": [
"monitor reset init",
"load \"${command:raspberry-pi-pico.launchTargetPath}\""
]
},
]
}
{
"cmake.showSystemKits": false,
"cmake.options.statusBarVisibility": "hidden",
"cmake.options.advanced": {
"build": {
"statusBarVisibility": "hidden"
},
"launch": {
"statusBarVisibility": "hidden"
},
"debug": {
"statusBarVisibility": "hidden"
}
},
"cmake.configureOnEdit": true,
"cmake.automaticReconfigure": true,
"cmake.configureOnOpen": true,
"cmake.generator": "Ninja",
"cmake.cmakePath": "${userHome}/.pico-sdk/cmake/v3.31.5/bin/cmake",
"C_Cpp.debugShortcut": false,
"terminal.integrated.env.windows": {
"PICO_SDK_PATH": "${env:USERPROFILE}/.pico-sdk/sdk/2.2.0",
"PICO_TOOLCHAIN_PATH": "${env:USERPROFILE}/.pico-sdk/toolchain/14_2_Rel1",
"Path": "${env:USERPROFILE}/.pico-sdk/toolchain/14_2_Rel1/bin;${env:USERPROFILE}/.pico-sdk/picotool/2.2.0/picotool;${env:USERPROFILE}/.pico-sdk/cmake/v3.31.5/bin;${env:USERPROFILE}/.pico-sdk/ninja/v1.12.1;${env:PATH}"
},
"terminal.integrated.env.osx": {
"PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.2.0",
"PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1",
"PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.2.0/picotool:${env:HOME}/.pico-sdk/cmake/v3.31.5/bin:${env:HOME}/.pico-sdk/ninja/v1.12.1:${env:PATH}"
},
"terminal.integrated.env.linux": {
"PICO_SDK_PATH": "${env:HOME}/.pico-sdk/sdk/2.2.0",
"PICO_TOOLCHAIN_PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1",
"PATH": "${env:HOME}/.pico-sdk/toolchain/14_2_Rel1/bin:${env:HOME}/.pico-sdk/picotool/2.2.0/picotool:${env:HOME}/.pico-sdk/cmake/v3.31.5/bin:${env:HOME}/.pico-sdk/ninja/v1.12.1:${env:PATH}"
},
"raspberry-pi-pico.cmakeAutoConfigure": false,
"raspberry-pi-pico.useCmakeTools": true,
"raspberry-pi-pico.cmakePath": "${HOME}/.pico-sdk/cmake/v3.31.5/bin/cmake",
"raspberry-pi-pico.ninjaPath": "${HOME}/.pico-sdk/ninja/v1.12.1/ninja"
}
{
"version": "2.0.0",
"tasks": [
{
"label": "Compile Project",
"type": "process",
"isBuildCommand": true,
"command": "${userHome}/.pico-sdk/ninja/v1.12.1/ninja",
"args": ["-C", "${workspaceFolder}/build"],
"group": "build",
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": "$gcc",
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/ninja/v1.12.1/ninja.exe"
}
},
{
"label": "Run Project",
"type": "process",
"command": "${env:HOME}/.pico-sdk/picotool/2.2.0/picotool/picotool",
"args": [
"load",
"${command:raspberry-pi-pico.launchTargetPath}",
"-fx"
],
"presentation": {
"reveal": "always",
"panel": "dedicated"
},
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/picotool/2.2.0/picotool/picotool.exe"
}
},
{
"label": "Flash",
"type": "process",
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"args": [
"-s",
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"-f",
"interface/cmsis-dap.cfg",
"-f",
"target/${command:raspberry-pi-pico.getTarget}.cfg",
"-c",
"adapter speed 5000; program \"${command:raspberry-pi-pico.launchTargetPath}\" verify reset exit"
],
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
}
},
{
"label": "Rescue Reset",
"type": "process",
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"args": [
"-s",
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"-f",
"interface/cmsis-dap.cfg",
"-f",
"target/${command:raspberry-pi-pico.getChip}-rescue.cfg",
"-c",
"adapter speed 5000; reset halt; exit"
],
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
}
},
{
"label": "Risc-V Reset (RP2350)",
"type": "process",
"command": "${userHome}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
"args": [
"-s",
"${userHome}/.pico-sdk/openocd/0.12.0+dev/scripts",
"-c",
"set USE_CORE { rv0 rv1 cm0 cm1 }",
"-f",
"interface/cmsis-dap.cfg",
"-f",
"target/rp2350.cfg",
"-c",
"adapter speed 5000; init;",
"-c",
"write_memory 0x40120158 8 { 0x3 }; echo [format \"Info : ARCHSEL 0x%02x\" [read_memory 0x40120158 8 1]];",
"-c",
"reset halt; targets rp2350.rv0; echo [format \"Info : ARCHSEL_STATUS 0x%02x\" [read_memory 0x4012015C 8 1]]; exit"
],
"problemMatcher": [],
"windows": {
"command": "${env:USERPROFILE}/.pico-sdk/openocd/0.12.0+dev/openocd.exe",
}
}
]
}
......@@ -11,72 +11,34 @@ set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake)
if (EXISTS ${picoVscode})
include(${picoVscode})
endif()
# ====================================================================================
# ========================================================================================
set(PICO_BOARD pico CACHE STRING "Board type")
cmake_minimum_required(VERSION 3.13)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
# Pull in Raspberry Pi Pico SDK (must be before project)
include(pico_sdk_import.cmake)
project(main C CXX ASM)
project(pico_microphone C CXX ASM)
# Initialise the Raspberry Pi Pico SDK
pico_sdk_init()
# --- Executable and microphone sources ---
# build driver lib from src/
add_subdirectory(src)
# your application
add_executable(main
main.c
${CMAKE_CURRENT_LIST_DIR}/src/pdm_microphone.c
${CMAKE_CURRENT_LIST_DIR}/src/OpenPDM2PCM/OpenPDMFilter.c
)
# Generate PIO header for the mic
pico_generate_pio_header(main
${CMAKE_CURRENT_LIST_DIR}/src/pdm_microphone.pio
)
# Include directories (note the src/include path gets added)
target_include_directories(main PRIVATE
${CMAKE_CURRENT_LIST_DIR}/src
${CMAKE_CURRENT_LIST_DIR}/src/include
${CMAKE_CURRENT_LIST_DIR}/src/OpenPDM2PCM
${CMAKE_CURRENT_BINARY_DIR} # for staged "pico/pdm_microphone.h"
)
# --- Stage a virtual include so "pico/pdm_microphone.h" resolves ---
set(PDM_HDR_SRC ${CMAKE_CURRENT_LIST_DIR}/src/include/pico/pdm_microphone.h)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/pico/pdm_microphone.h
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/pico
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${PDM_HDR_SRC}
${CMAKE_CURRENT_BINARY_DIR}/pico/pdm_microphone.h
DEPENDS ${PDM_HDR_SRC}
COMMENT "Staging pico/pdm_microphone.h from src/include into build dir"
VERBATIM
)
add_custom_target(pdm_header ALL
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/pico/pdm_microphone.h
)
add_dependencies(main pdm_header)
# Link libraries
target_link_libraries(main PRIVATE
# link your app to the driver lib (and stdlib, for good measure)
target_link_libraries(main
pico_pdm_microphone
pico_stdlib
hardware_dma
hardware_pio
)
# Program metadata and I/O
pico_set_program_name(main "main")
pico_set_program_version(main "0.1")
pico_enable_stdio_uart(main 0)
# enable usb output, disable uart output
pico_enable_stdio_usb(main 1)
pico_enable_stdio_uart(main 0)
# Create UF2, bin, etc.
# create map/bin/hex/uf2 file in addition to ELF.
pico_add_extra_outputs(main)
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include "stdlib.h"
#include "src/include/pico/pdm_microphone.h"
#include "tusb.h"
#define AUDIO_SAMPLE_RATE 16000
#define SAMPLE_BUFFER_SIZE 256
pdm_microphone_config config = {
// PDM ID
.pdm_id = 0,
.dma_irq = DMA_IRQ_0,
// PIO
.pio = pio0,
// GPIO pin for the PDM DAT signal
.gpio_data = 18,
// GPIO pin for the PDM CLK signal
.gpio_clk = 19,
// sample rate in Hz
.sample_rate = AUDIO_SAMPLE_RATE,
// number of samples to buffer
.sample_buffer_size = SAMPLE_BUFFER_SIZE,
};
pdm_mic_obj* pdm_mic;
int16_t sample_buffer[SAMPLE_BUFFER_SIZE];
volatile bool data_valid = false;
void on_pdm_samples_ready(uint8_t pdm_id) {
(void)pdm_id;
data_valid = true;
}
int main(void)
{
stdio_init_all();
while (!tud_cdc_connected()) {
sleep_ms(10);
}
printf("hello PDM microphone\n");
pdm_mic = pdm_microphone_init(&config);
if (!pdm_mic) {
printf("PDM microphone initialization failed!\n");
while (1) { tight_loop_contents(); }
}
printf("PDM microphone initialized\n");
pdm_microphone_set_samples_ready_handler(pdm_mic, on_pdm_samples_ready);
if (pdm_microphone_start(pdm_mic) != 0) {
printf("PDM microphone start failed!\n");
while (1) { tight_loop_contents(); }
}
printf("PDM microphone started\n");
uint32_t printed = 0;
while (true) {
if (data_valid) {
data_valid = false;
int n = pdm_microphone_read(pdm_mic, sample_buffer, SAMPLE_BUFFER_SIZE);
if (n > 0) {
for (int i = 0; i < n; i++) {
printf("%04x\n", (uint16_t)sample_buffer[i]);
}
}
}
sleep_ms(1);
}
}
\ No newline at end of file
#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/pdm_microphone.h"
#include "tusb.h"
// configuration
const struct pdm_microphone_config config = {
// GPIO pin for the PDM DAT signal
.gpio_data = 2,
// GPIO pin for the PDM CLK signal
.gpio_clk = 3,
// PIO instance to use
.pio = pio0,
// PIO State Machine instance to use
.pio_sm = 0,
// sample rate in Hz
.sample_rate = 8000,
// number of samples to buffer
.sample_buffer_size = 256,
};
// variables
int16_t sample_buffer[256];
volatile int samples_read = 0;
void on_pdm_samples_ready()
{
// callback from library when all the samples in the library
// internal sample buffer are ready for reading
samples_read = pdm_microphone_read(sample_buffer, 256);
}
int main( void )
{
// initialize stdio and wait for USB CDC connect
stdio_init_all();
while (!tud_cdc_connected()) {
tight_loop_contents();
}
printf("hello PDM microphone\n");
// initialize the PDM microphone
if (pdm_microphone_init(&config) < 0) {
printf("PDM microphone initialization failed!\n");
while (1) { tight_loop_contents(); }
}
// set callback that is called when all the samples in the library
// internal sample buffer are ready for reading
pdm_microphone_set_samples_ready_handler(on_pdm_samples_ready);
// start capturing data from the PDM microphone
if (pdm_microphone_start() < 0) {
printf("PDM microphone start failed!\n");
while (1) { tight_loop_contents(); }
}
while (1) {
// wait for new samples
while (samples_read == 0) { tight_loop_contents(); }
// store and clear the samples read from the callback
int sample_count = samples_read;
samples_read = 0;
// loop through any new collected samples
for (int i = 0; i < sample_count; i++) {
printf("%d\n", sample_buffer[i]);
}
}
return 0;
}
# No VSCode boilerplate, no pico_sdk_import, no project(), no pico_sdk_init() here
add_library(pico_pdm_microphone STATIC
pdm_microphone.c
OpenPDM2PCM/OpenPDMFilter.c
)
target_include_directories(pico_pdm_microphone PUBLIC
${CMAKE_CURRENT_LIST_DIR}/include
)
# Generate PIO header for the library target
pico_generate_pio_header(pico_pdm_microphone
${CMAKE_CURRENT_LIST_DIR}/pdm_microphone.pio
)
# Link the hardware libs needed by the driver
target_link_libraries(pico_pdm_microphone PUBLIC
pico_stdlib
hardware_dma
hardware_pio
)
# Remove this unless you actually have src/mic_example/
# add_subdirectory(mic_example)
; Receive a mono or stereo I2S audio stream as stereo
; This is 64 bits per sample; can be altered by modifying the "set" params,
; or made programmable by replacing "set x" with "mov x, y" and using Y as a config register.
;
; Autopull must be enabled, with threshold set to 64.
; Since I2S is MSB-first, shift direction should be to left.
; Hence the format of the FIFO word is:
;
; | 31 : 0 | 31 : 0 |
; | sample ws=0 | sample ws=1 |
;
; Data is output at 1 bit per clock. Use clock divider to adjust frequency.
; Fractional divider will probably be needed to get correct bit clock period,
; but for common syslck freqs this should still give a constant word select period.
;
; One output pin is used for the data output.
; Two side-set pins are used. Bit 0 is clock, bit 1 is word select.
; Send 32 bit words to the PIO for mono, 64 bit words for stereo
.program audio_i2s_rx_32b
.side_set 2
; /--- LRCLK
; |/-- BCLK
; ||
.wrap_target
set x, 30 side 0b00
left_channel:
in pins, 1 side 0b01
jmp x-- left_channel side 0b00
in pins, 1 side 0b11
set x, 30 side 0b10
right_channel:
in pins, 1 side 0b11
jmp x-- right_channel side 0b10
in pins, 1 side 0b01
.wrap
% c-sdk {
%}
\ No newline at end of file
; Transmit a mono or stereo I2S audio stream as stereo
; This is 16 bits per sample; can be altered by modifying the "set" params,
; or made programmable by replacing "set x" with "mov x, y" and using Y as a config register.
;
; Autopull must be enabled, with threshold set to 32.
; Since I2S is MSB-first, shift direction should be to left.
; Hence the format of the FIFO word is:
;
; | 31 : 16 | 15 : 0 |
; | sample ws=0 | sample ws=1 |
;
; Data is output at 1 bit per clock. Use clock divider to adjust frequency.
; Fractional divider will probably be needed to get correct bit clock period,
; but for common syslck freqs this should still give a constant word select period.
;
; One output pin is used for the data output.
; Two side-set pins are used. Bit 0 is clock, bit 1 is word select.
; Send 16 bit words to the PIO for mono, 32 bit words for stereo
.program audio_i2s_tx_16b
.side_set 2
; /--- LRCLK
; |/-- BCLK
; ||
.wrap_target
set x, 14 side 0b01
left_channel:
out pins, 1 side 0b00
jmp x-- left_channel side 0b01
out pins, 1 side 0b10
set x, 14 side 0b11
right_channel:
out pins, 1 side 0b10
jmp x-- right_channel side 0b11
public entry_point:
out pins, 1 side 0b00
.wrap
% c-sdk {
static inline void audio_i2s_tx_16b_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base) {
pio_sm_config sm_config = audio_i2s_tx_16b_program_get_default_config(offset);
sm_config_set_out_pins(&sm_config, data_pin, 1);
sm_config_set_sideset_pins(&sm_config, clock_pin_base);
sm_config_set_out_shift(&sm_config, false, true, 32);
pio_sm_init(pio, sm, offset, &sm_config);
uint pin_mask = (1u << data_pin) | (3u << clock_pin_base);
pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask);
pio_sm_set_pins(pio, sm, 0); // clear pins
pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_tx_16b_offset_entry_point));
}
%}
\ No newline at end of file
; Transmit a mono or stereo I2S audio stream as stereo
; This is 64 bits per sample; can be altered by modifying the "set" params,
; or made programmable by replacing "set x" with "mov x, y" and using Y as a config register.
;
; Autopull must be enabled, with threshold set to 64.
; Since I2S is MSB-first, shift direction should be to left.
; Hence the format of the FIFO word is:
;
; | 31 : 0 | 31 : 0 |
; | sample ws=0 | sample ws=1 |
;
; Data is output at 1 bit per clock. Use clock divider to adjust frequency.
; Fractional divider will probably be needed to get correct bit clock period,
; but for common syslck freqs this should still give a constant word select period.
;
; One output pin is used for the data output.
; Two side-set pins are used. Bit 0 is clock, bit 1 is word select.
; Send 32 bit words to the PIO for mono, 64 bit words for stereo
.program audio_i2s_tx_32b
.side_set 2
; /--- LRCLK
; |/-- BCLK
; ||
.wrap_target
set x, 30 side 0b01
left_channel:
out pins, 1 side 0b00
jmp x-- left_channel side 0b01
out pins, 1 side 0b10
set x, 30 side 0b11
right_channel:
out pins, 1 side 0b10
jmp x-- right_channel side 0b11
public entry_point:
out pins, 1 side 0b00
.wrap
% c-sdk {
static inline void audio_i2s_tx_32b_program_init(PIO pio, uint sm, uint offset, uint data_pin, uint clock_pin_base) {
pio_sm_config sm_config = audio_i2s_tx_32b_program_get_default_config(offset);
sm_config_set_out_pins(&sm_config, data_pin, 1);
sm_config_set_sideset_pins(&sm_config, clock_pin_base);
sm_config_set_out_shift(&sm_config, false, true, 32);
pio_sm_init(pio, sm, offset, &sm_config);
uint pin_mask = (1u << data_pin) | (3u << clock_pin_base);
pio_sm_set_pindirs_with_mask(pio, sm, pin_mask, pin_mask);
pio_sm_set_pins(pio, sm, 0); // clear pins
pio_sm_exec(pio, sm, pio_encode_jmp(offset + audio_i2s_tx_32b_offset_entry_point));
}
%}
\ No newline at end of file
#include "pico/dc_offset_filter.h"
void dc_offset_filter_init(dc_offset_filter_t* self, int32_t apply_delay_samples){
self->sample_mean_value_sum = 0;
self->sample_mean_value_cntr = 0;
self->sample_mean_value = 0;
self->apply_delay_samples = apply_delay_samples;
}
int32_t dc_offset_filter_main(dc_offset_filter_t* self, int32_t input_sample, bool update_mean_val){
int32_t sample_mean_value_approx;
// Update mean value
if((update_mean_val == true) && (self->sample_mean_value_cntr >= self->apply_delay_samples)){
self->sample_mean_value = self->sample_mean_value_sum / self->sample_mean_value_cntr;
}
// Use mean value if we have enough input samples
if(self->sample_mean_value_cntr <= self->apply_delay_samples)
sample_mean_value_approx = 0;
else
sample_mean_value_approx = self->sample_mean_value;
// increase counters
self->sample_mean_value_sum = self->sample_mean_value_sum + input_sample;
self->sample_mean_value_cntr ++;
// return output value subtracting mean value:
return input_sample - sample_mean_value_approx;
}
\ No newline at end of file
#ifndef DC_OFFSET_FILTER__H
#define DC_OFFSET_FILTER__H
#include <stdio.h>
#include <stdbool.h>
typedef struct _dc_offset_filter_t {
int64_t sample_mean_value_sum;
int32_t sample_mean_value_cntr;
int32_t sample_mean_value;
int32_t apply_delay_samples;
} dc_offset_filter_t;
void dc_offset_filter_init(dc_offset_filter_t* self, int32_t apply_delay_samples);
int32_t dc_offset_filter_main(dc_offset_filter_t* self, int32_t input_sample, bool update_mean_val);
#endif //DC_OFFSET_FILTER__H
#ifndef DEFAULT_I2S_BOARD_DEFINES__H
#define DEFAULT_I2S_BOARD_DEFINES__H
//-------------------------
// I2s defines
//-------------------------
#define I2S_MIC_INMP441
#ifdef I2S_MIC_INMP441
#ifndef I2S_MIC_SD
#define I2S_MIC_SD 14
#endif //I2S_MIC_SD
#ifndef I2S_MIC_SCK
#define I2S_MIC_SCK 15
#endif //I2S_MIC_SCK
#ifndef I2S_MIC_WS
#define I2S_MIC_WS (I2S_MIC_SCK+1) // needs to be I2S_MIC_SCK +1
#endif //I2S_MIC_WS
#else //I2S_MIC_INMP441
#ifndef I2S_MIC_SPH_DC_OFFSET
#define I2S_MIC_SPH_DC_OFFSET 0xf8c80000
#endif //I2S_MIC_SPH_DC_OFFSET
#ifndef I2S_MIC_SD
#define I2S_MIC_SD 10
#endif //I2S_MIC_SD
#ifndef I2S_MIC_SCK
#define I2S_MIC_SCK 11
#endif //I2S_MIC_SCK
#ifndef I2S_MIC_WS
#define I2S_MIC_WS (I2S_MIC_SCK+1) // needs to be I2S_MIC_SCK +1
#endif //I2S_MIC_WS
#endif //I2S_MIC_INMP441
#ifndef I2S_MIC_BPS
#define I2S_MIC_BPS 32 // 24 is not valid in this implementation, but INMP441 outputs 24 bits samples
#endif //I2S_MIC_BPS
#ifndef I2S_MIC_RATE_DEF
#define I2S_MIC_RATE_DEF (16000)
#endif //I2S_MIC_RATE_DEF
#ifndef I2S_SPK_SD
#define I2S_SPK_SD 2
#endif //I2S_SPK_SD
#ifndef I2S_SPK_SCK
#define I2S_SPK_SCK 3
#endif //I2S_SPK_SCK
#ifndef I2S_SPK_WS
#define I2S_SPK_WS (I2S_SPK_SCK+1) // needs to be SPK_SCK +1
#endif //I2S_SPK_WS
#ifndef I2S_SPK_BPS
#define I2S_SPK_BPS 32
#endif //I2S_SPK_BPS
#ifndef I2S_SPK_RATE_DEF
#define I2S_SPK_RATE_DEF (48000)
#endif //I2S_SPK_RATE_DEF
typedef struct {
uint32_t left;
uint32_t right;
} i2s_32b_audio_sample;
typedef struct {
uint16_t left;
uint16_t right;
} i2s_16b_audio_sample;
//-------------------------
#endif //DEFAULT_I2S_BOARD_DEFINES__H
\ No newline at end of file
#ifndef MACHINE_I2S__H
#define MACHINE_I2S__H
#include <stdlib.h>
#include <string.h>
#include "hardware/pio.h"
#include "hardware/clocks.h"
#include "hardware/gpio.h"
#include "hardware/dma.h"
#include "hardware/irq.h"
#include "pico/ring_buf.h"
// Notes on this port's specific implementation of I2S:
// - the DMA IRQ handler is used to implement the asynchronous background operations, for non-blocking mode
// - the PIO is used to drive the I2S bus signals
// - all sample data transfers use non-blocking DMA
// - the DMA controller is configured with 2 DMA channels in chained mode
#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 (96*8*2) // Max frequency is 96000. in worst case. 1ms contains 96 samples. Each sample is 8 bytes. Need to hold 2 buffers of this size
#define SIZEOF_HALF_DMA_BUFFER_IN_BYTES (SIZEOF_DMA_BUFFER_IN_BYTES / 2)
#define I2S_NUM_DMA_CHANNELS (2)
// For non-blocking mode, to avoid underflow/overflow, sample data is written/read to/from the ring buffer at a rate faster
// than the DMA transfer rate
#define NON_BLOCKING_RATE_MULTIPLIER (4)
#define SIZEOF_NON_BLOCKING_COPY_IN_BYTES (SIZEOF_HALF_DMA_BUFFER_IN_BYTES * NON_BLOCKING_RATE_MULTIPLIER)
#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
#ifndef m_new
#define m_new(type, num) ((type *)(malloc(sizeof(type) * (num))))
#endif //m_new
#ifndef m_new_obj
#define m_new_obj(type) (m_new(type, 1))
#endif //m_new_obj
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;
// 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 _non_blocking_descriptor_t {
mp_buffer_info_t appbuf;
uint32_t index;
bool copy_in_progress;
} non_blocking_descriptor_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;
non_blocking_descriptor_t non_blocking_descriptor;
uint32_t sizeof_half_dma_buffer_in_bytes;
uint32_t sizeof_non_blocking_copy_in_bytes;
} machine_i2s_obj_t;
machine_i2s_obj_t* create_machine_i2s(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);
int machine_i2s_read_stream(machine_i2s_obj_t *self, void *buf_in, size_t size);
int machine_i2s_write_stream(machine_i2s_obj_t *self, void *buf_in, size_t size);
//void update_pio_frequency(machine_i2s_obj_t *self, uint32_t sample_freq);
#endif //MACHINE_I2S__H
\ No newline at end of file
......@@ -10,63 +10,28 @@
#include "hardware/pio.h"
#include "OpenPDMFilter.h"
typedef void (*pdm_samples_ready_handler_t)(void);
#define MAX_PDM_RP2 (2)
#ifndef STATIC
#define STATIC static
#endif //STATIC
#ifndef m_new
#define m_new(type, num) ((type *)(malloc(sizeof(type) * (num))))
#endif //m_new
#ifndef m_new_obj
#define m_new_obj(type) (m_new(type, 1))
#endif //m_new_obj
#define PDM_DECIMATION 64
#define PDM_RAW_BUFFER_COUNT 2
typedef void (*pdm_samples_ready_handler_t)(uint8_t pdm_id);
typedef struct __pdm_microphone_config{
uint8_t pdm_id;
PIO pio;
uint dma_irq;
struct pdm_microphone_config {
uint gpio_data;
uint gpio_clk;
PIO pio;
uint pio_sm;
uint sample_rate;
uint sample_buffer_size;
} pdm_microphone_config;
typedef struct __pdm_mic_obj{
uint8_t pdm_id;
pdm_microphone_config* config;
uint pio_sm;
uint pio_sm_offset;
int dma_channel;
uint8_t* raw_buffer[PDM_RAW_BUFFER_COUNT];
volatile int raw_buffer_write_index;
volatile int raw_buffer_read_index;
uint raw_buffer_size;
TPDMFilter_InitStruct filter;
uint16_t filter_volume;
pdm_samples_ready_handler_t samples_ready_handler;
} pdm_mic_obj;
};
pdm_mic_obj* pdm_microphone_init(pdm_microphone_config* config);
void pdm_microphone_deinit(pdm_mic_obj *pdm_mic);
int pdm_microphone_init(const struct pdm_microphone_config* config);
void pdm_microphone_deinit();
int pdm_microphone_start(pdm_mic_obj *pdm_mic);
void pdm_microphone_stop(pdm_mic_obj *pdm_mic);
int pdm_microphone_start();
void pdm_microphone_stop();
void pdm_microphone_set_samples_ready_handler(pdm_mic_obj *pdm_mic, pdm_samples_ready_handler_t handler);
void pdm_microphone_set_filter_max_volume(pdm_mic_obj *pdm_mic, uint8_t max_volume);
void pdm_microphone_set_filter_gain(pdm_mic_obj *pdm_mic, uint8_t gain);
void pdm_microphone_set_filter_volume(pdm_mic_obj *pdm_mic, uint16_t volume);
void pdm_microphone_set_samples_ready_handler(pdm_samples_ready_handler_t handler);
void pdm_microphone_set_filter_max_volume(uint8_t max_volume);
void pdm_microphone_set_filter_gain(uint8_t gain);
void pdm_microphone_set_filter_volume(uint16_t volume);
int pdm_microphone_read(pdm_mic_obj *pdm_mic, int16_t* buffer, size_t samples);
int pdm_microphone_read(int16_t* buffer, size_t samples);
#endif
#ifndef RING_BUF__H
#define RING_BUF__H
#include <stdio.h>
#include <stdbool.h>
typedef struct _ring_buf_t {
uint8_t *buffer;
size_t head;
size_t tail;
size_t size;
} ring_buf_t;
void ringbuf_init(ring_buf_t *rbuf, uint8_t *buffer, size_t size);
bool ringbuf_push(ring_buf_t *rbuf, uint8_t data);
bool ringbuf_pop(ring_buf_t *rbuf, uint8_t *data);
bool ringbuf_is_empty(ring_buf_t *rbuf);
bool ringbuf_is_full(ring_buf_t *rbuf);
size_t ringbuf_available_data(ring_buf_t *rbuf);
size_t ringbuf_available_space(ring_buf_t *rbuf);
#endif //RING_BUF__H
\ No newline at end of file
#pragma once
#include <stdio.h>
#include <string.h>
#include "pico/stdlib.h"
#include "stdbool.h"
// todo this seemed like aood guess, but is not correct
static const uint16_t db_to_vol[91] = {
0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0002, 0x0002,
0x0002, 0x0002, 0x0003, 0x0003, 0x0004, 0x0004, 0x0005, 0x0005,
0x0006, 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000d, 0x000e,
0x0010, 0x0012, 0x0014, 0x0017, 0x001a, 0x001d, 0x0020, 0x0024,
0x0029, 0x002e, 0x0033, 0x003a, 0x0041, 0x0049, 0x0052, 0x005c,
0x0067, 0x0074, 0x0082, 0x0092, 0x00a4, 0x00b8, 0x00ce, 0x00e7,
0x0104, 0x0124, 0x0147, 0x016f, 0x019c, 0x01ce, 0x0207, 0x0246,
0x028d, 0x02dd, 0x0337, 0x039b, 0x040c, 0x048a, 0x0518, 0x05b7,
0x066a, 0x0732, 0x0813, 0x090f, 0x0a2a, 0x0b68, 0x0ccc, 0x0e5c,
0x101d, 0x1214, 0x1449, 0x16c3, 0x198a, 0x1ca7, 0x2026, 0x2413,
0x287a, 0x2d6a, 0x32f5, 0x392c, 0x4026, 0x47fa, 0x50c3, 0x5a9d,
0x65ac, 0x7214, 0x7fff
};
// actually windows doesn't seem to like this in the middle, so set top range to 0db
#define CENTER_VOLUME_INDEX 91
#define ENCODE_DB(x) ((uint16_t)(int16_t)((x)*256))
#define MIN_VOLUME ENCODE_DB(-CENTER_VOLUME_INDEX)
#define DEFAULT_VOLUME ENCODE_DB(0)
#define MAX_VOLUME ENCODE_DB(count_of(db_to_vol)-CENTER_VOLUME_INDEX)
#define VOLUME_RESOLUTION ENCODE_DB(1)
uint16_t vol_to_db_convert(bool channel_mute, uint16_t channel_volume);
\ No newline at end of file
......@@ -20,27 +20,19 @@ static inline void pdm_microphone_data_init(PIO pio, uint sm, uint offset, float
pio_sm_set_consecutive_pindirs(pio, sm, data_pin, 1, false);
pio_sm_set_consecutive_pindirs(pio, sm, clk_pin, 1, true);
pio_gpio_init(pio, clk_pin);
pio_gpio_init(pio, data_pin);
//gpio_pull_up(pin); //?????
pio_sm_config c = pdm_microphone_data_program_get_default_config(offset);
sm_config_set_in_pins(&c, data_pin); // Data in pi
sm_config_set_sideset_pins(&c, clk_pin); // Clock controlled by side set
sm_config_set_sideset_pins(&c, clk_pin);
sm_config_set_in_pins(&c, data_pin);
// Shift to left, autopush disabled
pio_gpio_init(pio, clk_pin);
pio_gpio_init(pio, data_pin);
sm_config_set_in_shift(&c, false, false, 8);
// Join RX channed to have deeper fifo. TX is not used
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_RX);
// Set clock divider
sm_config_set_clkdiv(&c, clk_div);
pio_sm_init(pio, sm, offset, &c);
// Need to call from app to sync microphones
//pio_sm_set_enabled(pio, sm, true);
}
%}
# This is a copy of <PICO_SDK_PATH>/external/pico_sdk_import.cmake
# This can be dropped into an external project to help locate this SDK
# It should be include()ed prior to project()
# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd.
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH))
set(PICO_SDK_PATH $ENV{PICO_SDK_PATH})
message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT))
set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT})
message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH))
set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH})
message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')")
endif ()
if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG))
set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG})
message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')")
endif ()
if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG)
set(PICO_SDK_FETCH_FROM_GIT_TAG "master")
message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG")
endif()
set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK")
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")
set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK")
set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK")
if (NOT PICO_SDK_PATH)
if (PICO_SDK_FETCH_FROM_GIT)
include(FetchContent)
set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR})
if (PICO_SDK_FETCH_FROM_GIT_PATH)
get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}")
endif ()
FetchContent_Declare(
pico_sdk
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
)
if (NOT pico_sdk)
message("Downloading Raspberry Pi Pico SDK")
# GIT_SUBMODULES_RECURSE was added in 3.17
if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0")
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
GIT_SUBMODULES_RECURSE FALSE
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
else ()
FetchContent_Populate(
pico_sdk
QUIET
GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk
GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG}
SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src
BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build
SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild
)
endif ()
set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR})
endif ()
set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE})
else ()
message(FATAL_ERROR
"SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git."
)
endif ()
endif ()
get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}")
if (NOT EXISTS ${PICO_SDK_PATH})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found")
endif ()
set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake)
if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE})
message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK")
endif ()
set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE)
include(${PICO_SDK_INIT_CMAKE_FILE})
#include "pico/ring_buf.h"
// 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
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;
}
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;
}
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;
}
bool ringbuf_is_empty(ring_buf_t *rbuf) {
return rbuf->head == rbuf->tail;
}
bool ringbuf_is_full(ring_buf_t *rbuf) {
return ((rbuf->tail + 1) % rbuf->size) == rbuf->head;
}
size_t ringbuf_available_data(ring_buf_t *rbuf) {
return (rbuf->tail - rbuf->head + rbuf->size) % rbuf->size;
}
size_t ringbuf_available_space(ring_buf_t *rbuf) {
return rbuf->size - ringbuf_available_data(rbuf) - 1;
}
\ No newline at end of file
#include "pico/volume_ctrl.h"
uint16_t vol_to_db_convert(bool channel_mute, uint16_t channel_volume){
if(channel_mute)
return 0;
// todo interpolate
channel_volume += CENTER_VOLUME_INDEX * 256;
if (channel_volume < 0) channel_volume = 0;
if (channel_volume >= count_of(db_to_vol) * 256) channel_volume = count_of(db_to_vol) * 256 - 1;
uint16_t vol_mul = db_to_vol[((uint16_t)channel_volume) >> 8u];
return vol_mul;
}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment