Merge pull request 'audiocore-i2s-driver' (#3) from audiocore-i2s-driver into main
All checks were successful
Check code formatting / Check-C-Format (push) Successful in 1h6m31s
Check code formatting / Check-Python-Flake8 (push) Successful in 11s
Run unit tests on host / Run-Unit-Tests (push) Successful in 12s

Reviewed-on: #3
Reviewed-by: stefank <kratochwil-la@gmx.de>
This commit was merged in pull request #3.
This commit is contained in:
2024-07-29 18:48:17 +00:00
22 changed files with 919 additions and 10 deletions

View File

@@ -0,0 +1,29 @@
name: Check code formatting
on:
push:
jobs:
Check-C-Format:
runs-on: ubuntu-22.04-full
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Run clang format
run: |
cmake software -B build
cmake --build build -- check-format
Check-Python-Flake8:
runs-on: ubuntu-22.04-full
steps:
- name: Get Flake8
run: |
python -m venv flake-venv
flake-venv/bin/pip install flake8==7.0
- name: Check out repository code
uses: actions/checkout@v4
with:
path: git
- name: Check python
run: |
cd git/software/src &&
find . -iname '*.py' -exec ../../../flake-venv/bin/flake8 {} +

View File

@@ -0,0 +1,22 @@
name: Run unit tests on host
on:
push:
jobs:
Run-Unit-Tests:
runs-on: ubuntu-22.04-full
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Build and test
run: |
cmake software -B build
cmake --build build
ctest --test-dir build
- name: Upload test report
uses: actions/upload-artifact@v3
with:
name: junit-xml
path: build/junit.xml
# Use always() to always run this step to publish test results when there are test failures
if: ${{ always() }}

5
.gitignore vendored
View File

@@ -1,2 +1,5 @@
hardware/tonberry-pico/tonberry-pico-backups/
~*
*~
*.kicad_sch-bak
*.kicad_sch.lck
software/build

61
software/CMakeLists.txt Normal file
View File

@@ -0,0 +1,61 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
cmake_minimum_required(VERSION 3.29)
project(tonberry-pico LANGUAGES C)
include(FetchContent)
FetchContent_Declare(unity
GIT_REPOSITORY https://github.com/ThrowTheSwitch/Unity.git
GIT_TAG v2.6.0
)
FetchContent_MakeAvailable(unity)
include(CTest)
function(make_unity_test )
set(options "")
set(oneValueArgs NAME TEST_SOURCE)
set(multiValueArgs SOURCES INCLUDES)
cmake_parse_arguments(PARSE_ARGV 0 MAKE_UNITY_TEST
"${options}" "${oneValueArgs}" "${multiValueArgs}"
)
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${MAKE_UNITY_TEST_NAME}_runner.c
COMMAND ${unity_SOURCE_DIR}/auto/generate_test_runner.rb ${MAKE_UNITY_TEST_TEST_SOURCE} ${CMAKE_CURRENT_BINARY_DIR}/${MAKE_UNITY_TEST_NAME}_runner.c
DEPENDS ${MAKE_UNITY_TEST_TEST_SOURCE}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
add_executable(${MAKE_UNITY_TEST_NAME}
${MAKE_UNITY_TEST_TEST_SOURCE}
${MAKE_UNITY_TEST_SOURCES}
${CMAKE_CURRENT_BINARY_DIR}/${MAKE_UNITY_TEST_NAME}_runner.c
)
target_link_libraries(${MAKE_UNITY_TEST_NAME} PUBLIC unity::framework)
target_include_directories(${MAKE_UNITY_TEST_NAME}
PRIVATE ${MAKE_UNITY_TEST_INCLUDES})
add_test(NAME ${MAKE_UNITY_TEST_NAME}
COMMAND sh -c "$<TARGET_FILE:${MAKE_UNITY_TEST_NAME}> > ${PROJECT_BINARY_DIR}/reports/${MAKE_UNITY_TEST_NAME}.testresults"
)
set_tests_properties(${MAKE_UNITY_TEST_NAME} PROPERTIES FIXTURES_REQUIRED Report)
endfunction()
add_test(NAME clean-reports
COMMAND sh -c "rm -rf ${PROJECT_BINARY_DIR}/reports; mkdir ${PROJECT_BINARY_DIR}/reports"
)
add_test(NAME generate-xml-report
COMMAND ${unity_SOURCE_DIR}/auto/stylize_as_junit.rb -r ${PROJECT_BINARY_DIR}/reports/ -o junit.xml
)
set_tests_properties(clean-reports PROPERTIES FIXTURES_SETUP "Report")
set_tests_properties(generate-xml-report PROPERTIES FIXTURES_CLEANUP "Report")
add_subdirectory(src/audiocore)
add_custom_target(check-format
find . -iname '*.[ch]' -exec clang-format -Werror --dry-run {} +
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
)
add_custom_target(clang-format
find . -iname '*.[ch]' -exec clang-format -i {} +
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
)

View File

@@ -6,3 +6,5 @@ require("bundle-networking")
require("aioble")
module("rp2_neopixel.py", "../../src")
require("sdcard")
require("aiorepl")

View File

@@ -7,7 +7,8 @@ set -eu
( cd lib/micropython
make -C mpy-cross -j $(nproc)
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR=$TOPDIR/boards/RPI_PICO_W clean
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR=$TOPDIR/boards/RPI_PICO_W -j $(nproc)
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR=$TOPDIR/boards/RPI_PICO_W \
USER_C_MODULES=$TOPDIR/src/audiocore/micropython.cmake -j $(nproc)
)
echo "Output in lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2"

View File

@@ -7,5 +7,7 @@ while [ ! -e "$DEVICEPATH" ] ; do sleep 1; echo 'Waiting for RP2...'; done
set -eu
while [ ! -e "$DEVICEPATH" ] ; do sleep 1; echo 'Waiting for RP2...'; done
udisksctl mount -b "$DEVICEPATH"
cp "$IMAGEPATH" "$(findmnt "$DEVICEPATH" -n -o TARGET)"

View File

@@ -0,0 +1,6 @@
# -*- mode: yaml -*-
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 120
BreakBeforeBraces: WebKit # Only break at start of functions

2
software/src/.flake8 Normal file
View File

@@ -0,0 +1,2 @@
[flake8]
max-line-length = 120

View File

@@ -0,0 +1,8 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
make_unity_test(NAME test_audiocore
TEST_SOURCE test/test_audiocore.c
SOURCES audiocore.c
INCLUDES "${CMAKE_CURRENT_SOURCE_DIR}/../test/include" "${CMAKE_CURRENT_SOURCE_DIR}"
)

View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#include "audiocore.h"
#include "i2s.h"
#include "py/mperrno.h"
void core1_main(void)
{
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base, shared_context.samplerate)) {
multicore_fifo_push_blocking(MP_EIO);
return;
}
multicore_fifo_push_blocking(0);
uint32_t cmd;
while ((cmd = multicore_fifo_pop_blocking()) != AUDIOCORE_CMD_SHUTDOWN) {
switch (cmd) {
default:
break;
}
}
i2s_deinit();
multicore_fifo_push_blocking(0);
}

View File

@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#pragma once
#include <hardware/sync.h>
#include <stdint.h>
#include <string.h>
/* Access rules
* audiocore processing runs on core 1 and, unless stated otherwise, all
* variables may only be accessed from core 1. Take care of interrupt safety
* where needed. The micropython interface lives in module.c and is invoked from
* the micropython runtime on core 0. Micropython objects may only be handled in
* that context The audiocore_shared_context struct defined below is used for
* communication between the cores.
*/
#define AUDIO_BUFFER_SIZE 2048
// Context shared between the micropython runtime on core0 and the audio task on
// core1 All access must hold "lock" unless otherwise noted
struct audiocore_shared_context {
spin_lock_t *lock;
// Set by module.c before core1 is launched and then never changed, can be read without lock
int out_pin, sideset_base, samplerate;
// Must hold lock
uint32_t audio_buffer[AUDIO_BUFFER_SIZE];
int audio_buffer_write, audio_buffer_read;
int underruns;
};
extern struct audiocore_shared_context shared_context;
static inline unsigned audiocore_get_audio_buffer_space(void)
{
if (shared_context.audio_buffer_write >= shared_context.audio_buffer_read)
return AUDIO_BUFFER_SIZE - 1 - (shared_context.audio_buffer_write - shared_context.audio_buffer_read);
else
return shared_context.audio_buffer_read - shared_context.audio_buffer_write - 1;
}
static inline unsigned audiocore_get_audio_buffer_avail(void)
{
if (shared_context.audio_buffer_write >= shared_context.audio_buffer_read)
return shared_context.audio_buffer_write - shared_context.audio_buffer_read;
else
return AUDIO_BUFFER_SIZE - (shared_context.audio_buffer_read - shared_context.audio_buffer_write);
}
static inline void audiocore_audio_buffer_put(const uint32_t *restrict src, const size_t len)
{
const unsigned end_samples = AUDIO_BUFFER_SIZE - shared_context.audio_buffer_write;
memcpy(shared_context.audio_buffer + shared_context.audio_buffer_write, src,
4 * ((end_samples >= len) ? len : end_samples));
if (end_samples < len) {
memcpy(shared_context.audio_buffer, src + end_samples, 4 * (len - end_samples));
shared_context.audio_buffer_write = len - end_samples;
} else {
shared_context.audio_buffer_write += len;
}
shared_context.audio_buffer_write %= AUDIO_BUFFER_SIZE;
}
static inline void audiocore_audio_buffer_get(uint32_t *restrict dst, const size_t len)
{
const unsigned end_samples = AUDIO_BUFFER_SIZE - shared_context.audio_buffer_read;
memcpy(dst, shared_context.audio_buffer + shared_context.audio_buffer_read,
4 * ((end_samples >= len) ? len : end_samples));
if (end_samples < len) {
memcpy(dst + end_samples, shared_context.audio_buffer, 4 * (len - end_samples));
shared_context.audio_buffer_read = len - end_samples;
} else {
shared_context.audio_buffer_read += len;
}
shared_context.audio_buffer_read %= AUDIO_BUFFER_SIZE;
}
void core1_main(void);
// SHUTDOWN - no arguments - return 0
#define AUDIOCORE_CMD_SHUTDOWN 0xdeadc0de

View File

@@ -0,0 +1,87 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#include "audiocore.h"
#include "i2s_max98357.pio.h"
#include <hardware/dma.h>
#include <hardware/sync.h>
#include <string.h>
#define audiocore_pio pio1
#define I2S_DMA_BUF_SIZE 256
struct i2s_context {
unsigned pio_program_offset;
int pio_sm;
int dma_ch;
dma_channel_config dma_config;
uint32_t dma_buf[I2S_DMA_BUF_SIZE];
};
static struct i2s_context i2s_context;
static void dma_isr(void)
{
if (!dma_channel_get_irq1_status(i2s_context.dma_ch))
return;
dma_channel_acknowledge_irq1(i2s_context.dma_ch);
const uint32_t flags = spin_lock_blocking(shared_context.lock);
if (audiocore_get_audio_buffer_avail() >= I2S_DMA_BUF_SIZE) {
audiocore_audio_buffer_get(i2s_context.dma_buf, I2S_DMA_BUF_SIZE);
spin_unlock(shared_context.lock, flags);
} else {
++shared_context.underruns;
spin_unlock(shared_context.lock, flags);
memset(i2s_context.dma_buf, 0, sizeof(uint32_t) * I2S_DMA_BUF_SIZE);
}
dma_channel_transfer_from_buffer_now(i2s_context.dma_ch, i2s_context.dma_buf, I2S_DMA_BUF_SIZE);
}
static void setup_dma_config(void)
{
i2s_context.dma_config = dma_channel_get_default_config(i2s_context.dma_ch);
channel_config_set_dreq(&i2s_context.dma_config, pio_get_dreq(pio1, i2s_context.pio_sm, true));
}
bool i2s_init(int out_pin, int sideset_base, int samplerate)
{
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0]) * I2S_DMA_BUF_SIZE);
if (!pio_can_add_program(audiocore_pio, &i2s_max98357_program))
return false;
i2s_context.pio_sm = pio_claim_unused_sm(audiocore_pio, false);
if (i2s_context.pio_sm == -1)
return false;
i2s_context.pio_program_offset = pio_add_program(audiocore_pio, &i2s_max98357_program);
i2s_max98357_program_init(audiocore_pio, i2s_context.pio_sm, i2s_context.pio_program_offset, out_pin, sideset_base,
samplerate);
i2s_context.dma_ch = dma_claim_unused_channel(false);
if (i2s_context.dma_ch == -1)
goto out_dma_claim;
setup_dma_config();
irq_set_exclusive_handler(DMA_IRQ_1, &dma_isr);
dma_channel_set_irq1_enabled(i2s_context.dma_ch, true);
irq_set_enabled(DMA_IRQ_1, true);
dma_channel_configure(i2s_context.dma_ch, &i2s_context.dma_config, &audiocore_pio->txf[i2s_context.pio_sm],
i2s_context.dma_buf, I2S_DMA_BUF_SIZE, true);
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, true);
return true;
out_dma_claim:
pio_remove_program(audiocore_pio, &i2s_max98357_program, i2s_context.pio_program_offset);
pio_sm_unclaim(audiocore_pio, i2s_context.pio_sm);
return false;
}
void i2s_deinit(void)
{
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, false);
dma_channel_set_irq1_enabled(i2s_context.dma_ch, false);
dma_channel_unclaim(i2s_context.dma_ch);
pio_remove_program(audiocore_pio, &i2s_max98357_program, i2s_context.pio_program_offset);
pio_sm_unclaim(audiocore_pio, i2s_context.pio_sm);
}

View File

@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#pragma once
#include <stdbool.h>
bool i2s_init(int out_pin, int sideset_base, int samplerate);
void i2s_deinit(void);

View File

@@ -0,0 +1,59 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
.program i2s_max98357
.side_set 2
.lang_opt python sideset_init = pico.PIO.OUT_LOW
.lang_opt python out_init = pico.PIO.OUT_LOW
.lang_opt python out_shiftdir = pcio.PIO.SHIFT_LEFT
.lang_opt python autopull = True
// data - DOUT
// sideset - 2-BCLK, 1-LRCLK
set x,15 side 0
nop side 1
startup_loop:
nop side 2
jmp x-- startup_loop side 3
nop side 0
set x, 14 side 1
left_loop:
.wrap_target
out pins, 1 side 0
jmp x-- left_loop side 1
out pins, 1 side 2
set x, 14 side 3
right_loop:
out pins, 1 side 2
jmp x-- right_loop side 3
out pins, 1 side 0
set x, 14 side 1
.wrap
% c-sdk {
#include "hardware/clocks.h"
static inline void i2s_max98357_program_init(PIO pio, uint sm, uint offset, uint pin, uint sideset, uint samplerate) {
pio_gpio_init(pio, pin);
pio_gpio_init(pio, sideset);
pio_gpio_init(pio, sideset+1);
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
pio_sm_set_consecutive_pindirs(pio, sm, sideset, 2, true);
pio_sm_config c = i2s_max98357_program_get_default_config(offset);
sm_config_set_out_pins(&c, pin, 1);
sm_config_set_sideset_pins(&c, sideset);
sm_config_set_out_shift(&c, false, true, 32);
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
const unsigned i2s_freq = samplerate * 2 * 16 * 2;
const float div = clock_get_hz(clk_sys) / (float)i2s_freq;
sm_config_set_clkdiv(&c, div);
pio_sm_init(pio, sm, offset, &c);
//pio_sm_set_enabled(pio, sm, true);
}
%}

View File

@@ -0,0 +1,19 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
add_library(usermod_audiocore INTERFACE)
pico_generate_pio_header(usermod_audiocore ${CMAKE_CURRENT_LIST_DIR}/i2s_max98357.pio)
target_sources(usermod_audiocore INTERFACE
${CMAKE_CURRENT_LIST_DIR}/audiocore.c
${CMAKE_CURRENT_LIST_DIR}/module.c
${CMAKE_CURRENT_LIST_DIR}/i2s.c
${CMAKE_CURRENT_BINARY_DIR}/i2s_max98357.pio.h
)
target_include_directories(usermod_audiocore INTERFACE
${CMAKE_CURRENT_LIST_DIR}
)
target_link_libraries(usermod INTERFACE usermod_audiocore)

View File

@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#include "audiocore.h"
// Include MicroPython API.
#include "py/mperrno.h"
#include "py/runtime.h"
// This module is RP2 specific
#include "mphalport.h"
#include <string.h>
struct audiocore_shared_context shared_context = {.lock = NULL};
static bool initialized = false;
struct audiocore_Context_obj {
mp_obj_base_t base;
};
/*
* audiocore.Context.deinit(self)
*
* Deinitializes the audiocore.Context and shuts down processing on core1
*/
static mp_obj_t audiocore_Context_deinit(mp_obj_t self_in)
{
struct audiocore_Context_obj *self = MP_OBJ_TO_PTR(self_in);
multicore_fifo_push_blocking(AUDIOCORE_CMD_SHUTDOWN);
multicore_fifo_pop_blocking();
(void)self;
initialized = false;
return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(audiocore_Context_deinit_obj, audiocore_Context_deinit);
/*
* (copied, buf_space, undderuns) = audiocore.Context.put(self, buffer)
*
* Copies as many integers as possible from the buffer to the audiocore ring buffer for playback.
* 'buffer' must be any object supporting the buffer protocol with data in unsigned int (array typecode 'I')
* format. The actual number of elements copied is returned in 'copied', the remaining free ring buffer space
* is in 'buf_space', and the total number of buffer underruns since initialization of the audiocore Context
* is in 'underruns'.
*/
static mp_obj_t audiocore_Context_put(mp_obj_t self_in, mp_obj_t buffer)
{
struct audiocore_Context_obj *self = MP_OBJ_TO_PTR(self_in);
(void)self;
mp_buffer_info_t bufinfo;
if (!mp_get_buffer(buffer, &bufinfo, MP_BUFFER_READ))
mp_raise_ValueError("not a read buffer");
if (bufinfo.typecode != 'I')
mp_raise_ValueError("unsupported buffer type");
unsigned to_copy = bufinfo.len / 4;
const uint32_t flags = spin_lock_blocking(shared_context.lock);
const unsigned buf_space = audiocore_get_audio_buffer_space();
if (to_copy > buf_space)
to_copy = buf_space;
if (to_copy > 0) {
audiocore_audio_buffer_put(bufinfo.buf, to_copy);
}
const unsigned underruns = shared_context.underruns;
spin_unlock(shared_context.lock, flags);
mp_obj_t items[] = {
mp_obj_new_int(to_copy),
mp_obj_new_int(buf_space),
mp_obj_new_int(underruns),
};
return mp_obj_new_tuple(3, items);
}
static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_Context_put_obj, audiocore_Context_put);
static const mp_rom_map_elem_t audiocore_Context_locals_dict_table[] = {
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&audiocore_Context_deinit_obj)},
{MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&audiocore_Context_deinit_obj)},
{MP_ROM_QSTR(MP_QSTR_put), MP_ROM_PTR(&audiocore_Context_put_obj)},
};
static MP_DEFINE_CONST_DICT(audiocore_Context_locals_dict, audiocore_Context_locals_dict_table);
const mp_obj_type_t audiocore_Context_type;
MP_DEFINE_CONST_OBJ_TYPE(audiocore_Context_type, MP_QSTR_Context, MP_TYPE_FLAG_NONE, locals_dict,
&audiocore_Context_locals_dict);
/*
* audiocore.init(pin, sideset, samplerate)
* Returns: audiocore.Context
*
* Initialize the audiocore module, starting the audio processing on core1 and initializing the I2S driver.
* The pin parameter specifies the I2S data pin, the sideset parameter specifies the I2S LRCLK pin, the
* BCLK must be on pin sideset+1.
* Samplerate should be 44100 or 48000.
* Only a single audiocore.Context may exist at a time.
* Return a audiocore.Context object on success, raises a OSError or ValueError on failure.
*/
static mp_obj_t audiocore_init(mp_obj_t pin_obj, mp_obj_t sideset_obj, mp_obj_t samplerate_obj)
{
if (initialized)
mp_raise_OSError(MP_EBUSY);
if (!shared_context.lock) {
// initialize shared context lock on first init
int lock = spin_lock_claim_unused(false);
if (lock == -1)
mp_raise_OSError(MP_ENOMEM);
shared_context.lock = spin_lock_init(lock);
}
shared_context.audio_buffer_write = shared_context.audio_buffer_read = shared_context.underruns = 0;
memset(shared_context.audio_buffer, 0, AUDIO_BUFFER_SIZE * 4);
multicore_reset_core1();
struct audiocore_Context_obj *context = m_malloc_with_finaliser(sizeof(struct audiocore_Context_obj));
context->base.type = &audiocore_Context_type;
mp_hal_pin_obj_t pin = pin_obj == MP_OBJ_NULL ? -1 : mp_hal_get_pin_obj(pin_obj);
if (pin == -1)
mp_raise_ValueError("Invalid out pin");
mp_hal_pin_obj_t sideset_pin = sideset_obj == MP_OBJ_NULL ? -1 : mp_hal_get_pin_obj(sideset_obj);
if (sideset_pin == -1)
mp_raise_ValueError("Invalid sideset base pin");
int samplerate = mp_obj_get_int(samplerate_obj);
shared_context.out_pin = pin;
shared_context.sideset_base = sideset_pin;
shared_context.samplerate = samplerate;
initialized = true;
multicore_launch_core1(&core1_main);
uint32_t result = multicore_fifo_pop_blocking();
if (result != 0) {
multicore_reset_core1();
initialized = false;
mp_raise_OSError(result);
}
return MP_OBJ_FROM_PTR(context);
}
static MP_DEFINE_CONST_FUN_OBJ_3(audiocore_init_obj, audiocore_init);
static const mp_rom_map_elem_t audiocore_module_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_audiocore)},
{MP_ROM_QSTR(MP_QSTR_init), MP_ROM_PTR(&audiocore_init_obj)},
{MP_ROM_QSTR(MP_QSTR_Context), MP_ROM_PTR(&audiocore_Context_type)},
};
static MP_DEFINE_CONST_DICT(audiocore_module_globals, audiocore_module_globals_table);
const mp_obj_module_t audiocore_cmodule = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&audiocore_module_globals,
};
MP_REGISTER_MODULE(MP_QSTR_audiocore, audiocore_cmodule);

View File

@@ -0,0 +1,192 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#include "audiocore.h"
#include "i2s.h"
#include "py/mperrno.h"
#include "unity.h"
struct audiocore_shared_context shared_context;
static bool i2s_init_return;
static bool i2s_initialized = false;
static unsigned multicore_fifo_push_last;
static unsigned (*multicore_fifo_pop_blocking_cb)(void);
bool i2s_init(int out_pin, int sideset_base, int samplerate)
{
TEST_ASSERT_FALSE(i2s_initialized);
if (i2s_init_return)
i2s_initialized = true;
return i2s_init_return;
}
void i2s_deinit(void)
{
TEST_ASSERT_TRUE(i2s_initialized);
i2s_initialized = false;
}
void multicore_fifo_push_blocking(unsigned val) { multicore_fifo_push_last = val; }
unsigned multicore_fifo_pop_blocking(void)
{
if (multicore_fifo_pop_blocking_cb)
return multicore_fifo_pop_blocking_cb();
return 0;
}
void test_audiocore_handles_i2sinit_failure(void)
{
i2s_init_return = false;
core1_main();
TEST_ASSERT_EQUAL(multicore_fifo_push_last, MP_EIO);
}
unsigned audiocore_init_deinit_pop_cb(void)
{
TEST_ASSERT_EQUAL(0, multicore_fifo_push_last);
TEST_ASSERT_TRUE(i2s_initialized);
return AUDIOCORE_CMD_SHUTDOWN;
}
void test_audiocore_init_deinit(void)
{
multicore_fifo_pop_blocking_cb = &audiocore_init_deinit_pop_cb;
i2s_init_return = true;
core1_main();
TEST_ASSERT_EQUAL(0, multicore_fifo_push_last);
TEST_ASSERT_FALSE(i2s_initialized);
}
void test_audiocore_buffer_space(void)
{
// empty ring buffer
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 0;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_space());
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 23;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_space());
shared_context.audio_buffer_read = shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_space());
// full ring buffer
shared_context.audio_buffer_write = 0;
shared_context.audio_buffer_read = 1;
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_space());
shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
shared_context.audio_buffer_read = 0;
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_space());
// write > read
shared_context.audio_buffer_write = 10;
shared_context.audio_buffer_read = 0;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1 - 10, audiocore_get_audio_buffer_space());
// write < read
shared_context.audio_buffer_write = 0;
shared_context.audio_buffer_read = 10;
TEST_ASSERT_EQUAL(9, audiocore_get_audio_buffer_space());
}
void test_audiocore_buffer_avail(void)
{
// empty ring buffer
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 0;
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_avail());
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 23;
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_avail());
shared_context.audio_buffer_read = shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
TEST_ASSERT_EQUAL(0, audiocore_get_audio_buffer_avail());
// full ring buffer
shared_context.audio_buffer_write = 0;
shared_context.audio_buffer_read = 1;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_avail());
shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
shared_context.audio_buffer_read = 0;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, audiocore_get_audio_buffer_avail());
// write > read
shared_context.audio_buffer_write = 10;
shared_context.audio_buffer_read = 0;
TEST_ASSERT_EQUAL(10, audiocore_get_audio_buffer_avail());
// write < read
shared_context.audio_buffer_write = 0;
shared_context.audio_buffer_read = 10;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 10, audiocore_get_audio_buffer_avail());
}
static unsigned fill_buffer_helper(void)
{
uint32_t test_data[100];
uint32_t ctr = 0;
unsigned avail, filled = 0;
while ((avail = audiocore_get_audio_buffer_space()) > 0) {
const unsigned todo = avail > 100 ? 100 : avail;
for (unsigned i = 0; i < todo; ++i)
test_data[i] = ctr++;
audiocore_audio_buffer_put(test_data, todo);
filled += todo;
}
return filled;
}
void test_audiocore_buffer_put(void)
{
shared_context.audio_buffer_read = shared_context.audio_buffer_write = 0;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, fill_buffer_helper());
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i) {
TEST_ASSERT_EQUAL(i, shared_context.audio_buffer[i]);
}
// test wraparound fill
shared_context.audio_buffer_read = shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 10;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, fill_buffer_helper());
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i) {
TEST_ASSERT_EQUAL(i, shared_context.audio_buffer[(i + AUDIO_BUFFER_SIZE - 10) % AUDIO_BUFFER_SIZE]);
}
}
static unsigned get_buffer_helper(void)
{
uint32_t test_data[100];
uint32_t ctr = 0;
unsigned avail, gotten = 0;
while ((avail = audiocore_get_audio_buffer_avail()) > 0) {
const unsigned todo = avail > 100 ? 100 : avail;
audiocore_audio_buffer_get(test_data, todo);
for (unsigned i = 0; i < todo; ++i)
TEST_ASSERT_EQUAL(ctr++, test_data[i]);
gotten += todo;
}
return gotten;
}
void test_audiocore_buffer_get(void)
{
shared_context.audio_buffer_read = 0;
shared_context.audio_buffer_write = AUDIO_BUFFER_SIZE - 1;
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i)
shared_context.audio_buffer[i] = i;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, get_buffer_helper());
// test wraparound read
shared_context.audio_buffer_read = 10;
shared_context.audio_buffer_write = 9;
for (unsigned i = 0; i < AUDIO_BUFFER_SIZE - 1; ++i)
shared_context.audio_buffer[(i + 10) % AUDIO_BUFFER_SIZE] = i;
TEST_ASSERT_EQUAL(AUDIO_BUFFER_SIZE - 1, get_buffer_helper());
}

View File

@@ -14,17 +14,18 @@ T1 = 2
T2 = 5
T3 = 3
@asm_pio(sideset_init=(PIO.OUT_LOW), fifo_join=PIO.JOIN_TX, autopull=True,
out_shiftdir=PIO.SHIFT_LEFT)
def _ws2812_pio(T1=T1, T2=T2, T3=T3):
label("bitloop")
out(x, 1).side(0).delay(T3-1)
jmp(not_x, "do_zero").side(1) [T1-1]
label("do_one")
jmp("bitloop").side(1) [T2-1]
label("do_zero")
nop().side(0) [T2-1]
wrap()
label("bitloop") # noqa:F821
out(x, 1).side(0).delay(T3-1) # noqa:F821
jmp(not_x, "do_zero").side(1).delay(T1-1) # noqa:F821
label("do_one") # noqa:F821
jmp("bitloop").side(1).delay(T2-1) # noqa:F821
label("do_zero") # noqa:F821
nop().side(0).delay(T2-1) # noqa:F821
wrap() # noqa:F821
class NeoPixel:

129
software/src/test.py Normal file
View File

@@ -0,0 +1,129 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
import aiorepl
import asyncio
import audiocore
import machine
import micropython
import os
import time
from array import array
from machine import Pin, SPI
from math import pi, sin, pow
from micropython import const
from rp2_neopixel import NeoPixel
from sdcard import SDCard
micropython.alloc_emergency_exception_buf(100)
asyncio.create_task(aiorepl.task())
leds = const(10)
brightness = 0.5
def gamma(value, X=2.2):
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
async def rainbow(np, period=10):
count = 0.0
while True:
for i in range(leds):
ofs = (count + i) % leds
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
count += 0.2
before = time.ticks_ms()
await np.async_write()
now = time.ticks_ms()
if before + 20 > now:
await asyncio.sleep_ms(20 - (now - before))
samplerate = 44100
hz = 441
count = 100
amplitude = 0x1fff
buf = array('I', range(count))
for i in range(len(buf)):
val = int(sin(i * hz / samplerate * 2 * pi)*amplitude) & 0xffff
buf[i] = (val << 16) | val
async def output_sound(audioctx):
pos = 0
known_underruns = 0
while True:
pushed, avail, underruns = audioctx.put(buf[pos:])
pos = (pos + pushed) % len(buf)
# print(f"pushed {pushed}, pos {pos}, avail {avail}")
if underruns > known_underruns:
print(f"{underruns:x}")
known_underruns = underruns
await asyncio.sleep(0)
# Set 8 mA drive strength and fast slew rate
machine.mem32[0x4001c004 + 6*4] = 0x67
machine.mem32[0x4001c004 + 7*4] = 0x67
machine.mem32[0x4001c004 + 8*4] = 0x67
def list_sd():
sd_spi = SPI(0, sck=Pin(2), mosi=Pin(3), miso=Pin(4))
try:
sd = SDCard(sd_spi, Pin(5), 25000000)
except OSError:
for i in range(leds):
np[i] = (255, 0, 0)
np.write()
return
try:
os.mount(sd, '/sd')
print(os.listdir('/sd'))
except OSError:
pass
delay_sum = 0
delay_count = 0
max_delay = 0
async def latency_test():
global delay_sum
global delay_count
global max_delay
await asyncio.sleep_ms(1)
while True:
for _ in range(2000):
before = time.ticks_us()
await asyncio.sleep(0)
after = time.ticks_us()
delay = after - before
delay_sum += delay
delay_count += 1
if delay > max_delay:
max_delay = delay
await asyncio.sleep_ms(1)
print(f"Max delay {max_delay} us, average {delay/delay_sum} us")
pin = Pin.board.GP16
np = NeoPixel(pin, leds)
# Test SD card
list_sd()
# Test NeoPixel
asyncio.create_task(rainbow(np))
# Test audio
audioctx = audiocore.init(Pin(8), Pin(6), samplerate)
asyncio.create_task(output_sound(audioctx))
asyncio.create_task(latency_test())
asyncio.get_event_loop().run_forever()

View File

@@ -0,0 +1,10 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
#pragma once
struct spin_lock;
typedef struct spin_lock spin_lock_t;
void multicore_fifo_push_blocking(unsigned val);
unsigned multicore_fifo_pop_blocking(void);

View File

@@ -0,0 +1,5 @@
#pragma once
#include <errno.h>
#define MP_EIO EIO