Compare commits
26 Commits
docu/archi
...
bug/playba
| Author | SHA1 | Date | |
|---|---|---|---|
| 41cfe794bf | |||
| f1de8c6c75 | |||
| 3b349af8cf | |||
| 679495bf2b | |||
| e9bd4f72b6 | |||
| 34f9a44cdb | |||
| 2796dbcf16 | |||
| ff2a609752 | |||
| 2f0d4cc3eb | |||
| 7ccab40cd6 | |||
| 96fea9dab6 | |||
| 9059da1a70 | |||
| 0353796110 | |||
| ce02daad3a | |||
| 7778147b66 | |||
| 7712c25627 | |||
| 69b6f6e860 | |||
| 903840f982 | |||
| b477aba94c | |||
| f0c3fe4db8 | |||
| d02776eea8 | |||
| fb496b6991 | |||
| 1b683358d1 | |||
| 91af2087b2 | |||
| 7f8282315e | |||
| 8a8cb85c39 |
@@ -18,3 +18,8 @@ jobs:
|
||||
with:
|
||||
name: firmware-RPi-Pico-W
|
||||
path: software/lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2
|
||||
- name: Upload firmware w/ filesystem
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: firmware-RPi-Pico-W-with-fs
|
||||
path: software/lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware-filesystem.uf2
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ hardware/tonberry-pico/tonberry-pico-backups/
|
||||
software/build
|
||||
compile_commands.json
|
||||
.dir-locals.el
|
||||
.cache
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
||||
[submodule "software/lib/microdot"]
|
||||
path = software/lib/microdot
|
||||
url = ../microdot.git
|
||||
[submodule "software/tools/mklittlefs"]
|
||||
path = software/tools/mklittlefs
|
||||
url = https://github.com/earlephilhower/mklittlefs.git
|
||||
|
||||
@@ -2347,15 +2347,15 @@
|
||||
)
|
||||
(uuid "1a911333-582d-4aef-a546-acfaa15e92ce")
|
||||
)
|
||||
(text "I2S_SD:\nLeft/2 + Right/2\nmode"
|
||||
(text "Sparkfun board has pullup\nfor mono mode by default"
|
||||
(exclude_from_sim no)
|
||||
(at 59.182 68.58 0)
|
||||
(at 65.024 91.186 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
(uuid "442b0a7f-27a4-45d5-ba3f-392a624afa5c")
|
||||
(uuid "a299ef28-05a8-4b90-8b36-1e4fa5063a43")
|
||||
)
|
||||
(junction
|
||||
(at 193.04 101.6)
|
||||
@@ -2375,6 +2375,10 @@
|
||||
(color 0 0 0 0)
|
||||
(uuid "e452439a-8818-4be8-bcc4-c26f3276ba13")
|
||||
)
|
||||
(no_connect
|
||||
(at 50.8 92.71)
|
||||
(uuid "10706d1d-1bc6-44cc-835d-b8fa3a3b6bd0")
|
||||
)
|
||||
(no_connect
|
||||
(at 170.18 139.7)
|
||||
(uuid "772cb13b-a39d-4540-a627-d741ae566dfc")
|
||||
@@ -2633,16 +2637,6 @@
|
||||
)
|
||||
(uuid "61d55cac-25ff-4b94-850d-39c3ad6f0e65")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 31.75 92.71) (xy 49.53 92.71)
|
||||
)
|
||||
(stroke
|
||||
(width 0)
|
||||
(type default)
|
||||
)
|
||||
(uuid "638a190f-f331-40ae-8ee9-d088d216e176")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 157.48 59.69) (xy 162.56 59.69)
|
||||
@@ -2943,6 +2937,16 @@
|
||||
)
|
||||
(uuid "b893aba8-4828-4d16-b439-4c8bc498044d")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 31.75 92.71) (xy 50.8 92.71)
|
||||
)
|
||||
(stroke
|
||||
(width 0)
|
||||
(type default)
|
||||
)
|
||||
(uuid "bbffc780-5b57-45bf-b3fa-3b8cba94a267")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 121.92 85.09) (xy 104.14 85.09)
|
||||
@@ -3073,16 +3077,6 @@
|
||||
)
|
||||
(uuid "e5e39417-bae8-449a-bdc3-ccde84eb8a5d")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 49.53 76.2) (xy 49.53 92.71)
|
||||
)
|
||||
(stroke
|
||||
(width 0)
|
||||
(type default)
|
||||
)
|
||||
(uuid "e6a50daf-f91a-47f2-b4b7-a9f8864cafea")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 99.06 62.23) (xy 99.06 72.39)
|
||||
@@ -3115,7 +3109,7 @@
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 43.18 100.33) (xy 43.18 95.25)
|
||||
(xy 43.18 95.25) (xy 43.18 101.6)
|
||||
)
|
||||
(stroke
|
||||
(width 0)
|
||||
@@ -3173,16 +3167,6 @@
|
||||
)
|
||||
(uuid "fc2056d1-3684-4dd8-b246-53d83b7d7512")
|
||||
)
|
||||
(wire
|
||||
(pts
|
||||
(xy 49.53 64.77) (xy 49.53 68.58)
|
||||
)
|
||||
(stroke
|
||||
(width 0)
|
||||
(type default)
|
||||
)
|
||||
(uuid "ff79dc83-d0c0-4e54-a4a0-d73c49bb4af7")
|
||||
)
|
||||
(label "SD_SCK"
|
||||
(at 83.82 67.31 0)
|
||||
(effects
|
||||
@@ -3403,141 +3387,6 @@
|
||||
)
|
||||
(uuid "ef716abf-d5c9-485a-8d9b-cba5d9b64623")
|
||||
)
|
||||
(symbol
|
||||
(lib_id "Device:R")
|
||||
(at 49.53 72.39 0)
|
||||
(unit 1)
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(dnp no)
|
||||
(uuid "0dba1186-6524-4282-90a1-e43af2371113")
|
||||
(property "Reference" "R1"
|
||||
(at 52.07 71.1199 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
)
|
||||
)
|
||||
(property "Value" "634k"
|
||||
(at 52.07 73.6599 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(justify left)
|
||||
)
|
||||
)
|
||||
(property "Footprint" ""
|
||||
(at 47.752 72.39 90)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" "~"
|
||||
(at 49.53 72.39 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Description" "Resistor"
|
||||
(at 49.53 72.39 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(pin "2"
|
||||
(uuid "08fd4621-268b-4331-aea1-885698dd7098")
|
||||
)
|
||||
(pin "1"
|
||||
(uuid "dfa35725-41ed-43ce-a906-abde0d386cdb")
|
||||
)
|
||||
(instances
|
||||
(project "tonberry-pico"
|
||||
(path "/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7"
|
||||
(reference "R1")
|
||||
(unit 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol
|
||||
(lib_id "power:+3V3")
|
||||
(at 49.53 64.77 0)
|
||||
(unit 1)
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
(on_board yes)
|
||||
(dnp no)
|
||||
(fields_autoplaced yes)
|
||||
(uuid "155dd1f6-f675-468b-b443-9581eb4514a9")
|
||||
(property "Reference" "#PWR07"
|
||||
(at 49.53 68.58 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Value" "+3V3"
|
||||
(at 49.53 59.69 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
)
|
||||
)
|
||||
(property "Footprint" ""
|
||||
(at 49.53 64.77 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Datasheet" ""
|
||||
(at 49.53 64.77 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(property "Description" "Power symbol creates a global label with name \"+3V3\""
|
||||
(at 49.53 64.77 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
)
|
||||
(hide yes)
|
||||
)
|
||||
)
|
||||
(pin "1"
|
||||
(uuid "aa7566f9-5551-4f75-a9a2-e6ecee097c88")
|
||||
)
|
||||
(instances
|
||||
(project "tonberry-pico"
|
||||
(path "/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7"
|
||||
(reference "#PWR07")
|
||||
(unit 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(symbol
|
||||
(lib_id "power:+5V")
|
||||
(at 45.72 64.77 0)
|
||||
@@ -4486,7 +4335,7 @@
|
||||
)
|
||||
(symbol
|
||||
(lib_id "power:GND")
|
||||
(at 43.18 100.33 0)
|
||||
(at 43.18 101.6 0)
|
||||
(unit 1)
|
||||
(exclude_from_sim no)
|
||||
(in_bom yes)
|
||||
@@ -4495,7 +4344,7 @@
|
||||
(fields_autoplaced yes)
|
||||
(uuid "90f7f757-dce2-492f-8d74-03b43473ca02")
|
||||
(property "Reference" "#PWR05"
|
||||
(at 43.18 106.68 0)
|
||||
(at 43.18 107.95 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
@@ -4504,7 +4353,7 @@
|
||||
)
|
||||
)
|
||||
(property "Value" "GND"
|
||||
(at 43.18 105.41 0)
|
||||
(at 43.18 106.68 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
@@ -4512,7 +4361,7 @@
|
||||
)
|
||||
)
|
||||
(property "Footprint" ""
|
||||
(at 43.18 100.33 0)
|
||||
(at 43.18 101.6 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
@@ -4521,7 +4370,7 @@
|
||||
)
|
||||
)
|
||||
(property "Datasheet" ""
|
||||
(at 43.18 100.33 0)
|
||||
(at 43.18 101.6 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
@@ -4530,7 +4379,7 @@
|
||||
)
|
||||
)
|
||||
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
|
||||
(at 43.18 100.33 0)
|
||||
(at 43.18 101.6 0)
|
||||
(effects
|
||||
(font
|
||||
(size 1.27 1.27)
|
||||
|
||||
@@ -50,7 +50,7 @@ add_test(NAME generate-xml-report
|
||||
set_tests_properties(clean-reports PROPERTIES FIXTURES_SETUP "Report")
|
||||
set_tests_properties(generate-xml-report PROPERTIES FIXTURES_CLEANUP "Report")
|
||||
|
||||
add_subdirectory(src/audiocore)
|
||||
add_subdirectory(modules/audiocore)
|
||||
|
||||
add_custom_target(check-format
|
||||
find . -iname '*.[ch]' -exec clang-format -Werror --dry-run {} +
|
||||
|
||||
@@ -4,5 +4,5 @@ FROM gitea/runner-images:ubuntu-22.04
|
||||
# Install gcc-arm-none-eabi
|
||||
RUN apt update && \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
apt install -y --no-install-recommends cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib \
|
||||
apt install -y --no-install-recommends cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib cpio \
|
||||
&& apt clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -5,8 +5,7 @@ require("bundle-networking")
|
||||
# Bluetooth
|
||||
require("aioble")
|
||||
|
||||
module("rp2_neopixel.py", "../../src")
|
||||
require("sdcard")
|
||||
# AsyncIO REPL
|
||||
require("aiorepl")
|
||||
|
||||
# Third party modules
|
||||
@@ -14,5 +13,5 @@ module("mfrc522.py", "../../lib/micropython-mfrc522/")
|
||||
module("microdot.py", "../../lib/microdot/src/microdot/")
|
||||
|
||||
# TonberryPico modules
|
||||
module("audiocore.py", "../../src/audiocore")
|
||||
package("nfc", base_path="../../src/")
|
||||
module("audiocore.py", "../../modules/audiocore")
|
||||
module("rp2_neopixel.py", "../../modules")
|
||||
|
||||
@@ -10,6 +10,8 @@ set(MICROPY_PY_BLUETOOTH ON)
|
||||
set(MICROPY_BLUETOOTH_BTSTACK ON)
|
||||
set(MICROPY_PY_BLUETOOTH_CYW43 ON)
|
||||
|
||||
set(MICROPY_PY_BTREE ON)
|
||||
|
||||
# Board specific version of the frozen manifest
|
||||
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)
|
||||
|
||||
@@ -18,3 +20,5 @@ set(GEN_PINS_CSV_ARG --board-csv "${GEN_PINS_BOARD_CSV}")
|
||||
|
||||
add_link_options("-Wl,--print-memory-usage")
|
||||
set(PICO_USE_FASTEST_SUPPORTED_CLOCK 1)
|
||||
|
||||
set(MICROPY_C_HEAP_SIZE 8192)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// Board and hardware specific configuration
|
||||
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico W"
|
||||
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico W"
|
||||
|
||||
// todo: We need something to check our binary size
|
||||
#define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024)
|
||||
#define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024)
|
||||
|
||||
// Enable networking.
|
||||
#define MICROPY_PY_NETWORK 1
|
||||
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "PicoW"
|
||||
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "PicoW"
|
||||
|
||||
// CYW43 driver configuration.
|
||||
#define CYW43_USE_SPI (1)
|
||||
@@ -18,10 +18,10 @@
|
||||
// Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose
|
||||
// #define MODUSSL_MBEDTLS_DEBUG_LEVEL 1
|
||||
|
||||
#define MICROPY_HW_PIN_EXT_COUNT CYW43_WL_GPIO_COUNT
|
||||
#define MICROPY_HW_PIN_EXT_COUNT CYW43_WL_GPIO_COUNT
|
||||
|
||||
// If this returns true for a pin then its irq will not be disabled on a soft reboot
|
||||
int mp_hal_is_pin_reserved(int n);
|
||||
#define MICROPY_HW_PIN_RESERVED(i) mp_hal_is_pin_reserved(i)
|
||||
|
||||
#define MICROPY_PY_THREAD (0)
|
||||
#define MICROPY_PY_THREAD (0)
|
||||
|
||||
@@ -8,7 +8,31 @@ set -eu
|
||||
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 \
|
||||
USER_C_MODULES="$TOPDIR"/src/micropython.cmake -j "$(nproc)"
|
||||
USER_C_MODULES="$TOPDIR"/modules/micropython.cmake -j "$(nproc)"
|
||||
)
|
||||
|
||||
echo "Output in lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2"
|
||||
( cd tools/mklittlefs
|
||||
make -j "$(nproc)"
|
||||
)
|
||||
|
||||
PICOTOOL=picotool
|
||||
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||
echo "system picotool not found, checking SDK build dir"
|
||||
PICOTOOL=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/_deps/picotool-build/picotool
|
||||
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||
echo "No picotool found, exiting"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
BUILDDIR=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
|
||||
FS_STAGE_DIR=$(mktemp -d)
|
||||
trap 'rm -rf $FS_STAGE_DIR' EXIT
|
||||
find src/ -iname '*.py' | cpio -pdm "$FS_STAGE_DIR"
|
||||
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/src $BUILDDIR/filesystem.bin
|
||||
truncate -s 2M $BUILDDIR/firmware-filesystem.bin
|
||||
dd if=$BUILDDIR/firmware.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k
|
||||
dd if=$BUILDDIR/filesystem.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k seek=1200
|
||||
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin $BUILDDIR/firmware-filesystem.uf2
|
||||
|
||||
echo "Output in $BUILDDIR/firmware.uf2"
|
||||
echo "Image with filesystem in $BUILDDIR/firmware-filesystem.uf2"
|
||||
|
||||
Submodule software/lib/micropython updated: e4422b860e...281615c157
@@ -5,9 +5,11 @@ from asyncio import ThreadSafeFlag
|
||||
class Audiocore:
|
||||
def __init__(self, pin, sideset):
|
||||
self.notify = ThreadSafeFlag()
|
||||
self._audiocore = _audiocore.Audiocore(pin, sideset, self._interrupt)
|
||||
self.pin = pin
|
||||
self.sideset = sideset
|
||||
self._audiocore = _audiocore.Audiocore(self.pin, self.sideset, self._interrupt)
|
||||
|
||||
def __del__(self):
|
||||
def deinit(self):
|
||||
self._audiocore.deinit()
|
||||
|
||||
def _interrupt(self, _):
|
||||
@@ -35,3 +37,16 @@ class Audiocore:
|
||||
if pos >= len(buffer):
|
||||
return (pos, buf_space, underruns)
|
||||
await self.notify.wait()
|
||||
|
||||
|
||||
class AudioContext:
|
||||
def __init__(self, pin, sideset):
|
||||
self.pin = pin
|
||||
self.sideset = sideset
|
||||
|
||||
def __enter__(self):
|
||||
self._audiocore = Audiocore(self.pin, self.sideset)
|
||||
return self._audiocore
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self._audiocore.deinit()
|
||||
@@ -90,9 +90,9 @@ static mp_obj_t audiocore_put(mp_obj_t self_in, mp_obj_t buffer)
|
||||
(void)self;
|
||||
mp_buffer_info_t bufinfo;
|
||||
if (!mp_get_buffer(buffer, &bufinfo, MP_BUFFER_READ))
|
||||
mp_raise_ValueError("not a read buffer");
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("not a read buffer"));
|
||||
if (bufinfo.typecode != 'b' && bufinfo.typecode != 'B')
|
||||
mp_raise_ValueError("unsupported buffer type");
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("unsupported buffer type"));
|
||||
unsigned to_copy = bufinfo.len;
|
||||
|
||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||
@@ -144,7 +144,7 @@ static mp_obj_t audiocore_set_volume(mp_obj_t self_in, mp_obj_t volume_obj)
|
||||
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||
const int volume = mp_obj_get_int(volume_obj);
|
||||
if (volume < 0 || volume > 255)
|
||||
mp_raise_ValueError("volume out of range");
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("volume out of range"));
|
||||
multicore_fifo_push_blocking(AUDIOCORE_CMD_SET_VOLUME);
|
||||
multicore_fifo_push_blocking(AUDIOCORE_MAX_VOLUME * volume / 255);
|
||||
wake_core1();
|
||||
@@ -66,9 +66,9 @@ static mp_obj_t sdcard_readblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_
|
||||
const int start_block = mp_obj_get_int(block_obj);
|
||||
mp_buffer_info_t bufinfo;
|
||||
if (!mp_get_buffer(buf_obj, &bufinfo, MP_BUFFER_WRITE))
|
||||
mp_raise_ValueError("Not a write buffer");
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("Not a write buffer"));
|
||||
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
||||
mp_raise_ValueError("Buffer length is invalid");
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
|
||||
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
||||
for (int block = 0; block < nblocks; block++) {
|
||||
// TODO: Implement CMD18 read multiple blocks
|
||||
@@ -79,6 +79,25 @@ static mp_obj_t sdcard_readblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_readblocks_obj, sdcard_readblocks);
|
||||
|
||||
static mp_obj_t sdcard_writeblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_t buf_obj)
|
||||
{
|
||||
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||
const int start_block = mp_obj_get_int(block_obj);
|
||||
mp_buffer_info_t bufinfo;
|
||||
if (!mp_get_buffer(buf_obj, &bufinfo, MP_BUFFER_READ))
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("Not a read buffer"));
|
||||
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
||||
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
|
||||
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
||||
for (int block = 0; block < nblocks; block++) {
|
||||
// TODO: Implement CMD25 write multiple blocks
|
||||
if (!sd_writeblock(&self->sd_context, start_block + block, bufinfo.buf + block * SD_SECTOR_SIZE))
|
||||
mp_raise_OSError(MP_EIO);
|
||||
}
|
||||
return mp_const_none;
|
||||
}
|
||||
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_writeblocks_obj, sdcard_writeblocks);
|
||||
|
||||
static mp_obj_t sdcard_ioctl(mp_obj_t self_obj, mp_obj_t op_obj, mp_obj_t arg_obj)
|
||||
{
|
||||
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||
@@ -96,8 +115,10 @@ static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_ioctl_obj, sdcard_ioctl);
|
||||
|
||||
static const mp_rom_map_elem_t sdcard_locals_dict_table[] = {
|
||||
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&sdcard_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&sdcard_deinit_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&sdcard_ioctl_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&sdcard_readblocks_obj)},
|
||||
{MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&sdcard_writeblocks_obj)},
|
||||
};
|
||||
static MP_DEFINE_CONST_DICT(sdcard_locals_dict, sdcard_locals_dict_table);
|
||||
|
||||
@@ -265,3 +265,11 @@ bool sd_readblock_complete(struct sd_context *sd_context)
|
||||
}
|
||||
|
||||
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
|
||||
|
||||
bool sd_writeblock(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE])
|
||||
{
|
||||
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||
return false;
|
||||
|
||||
return sd_cmd_write(24, sector_num, SD_SECTOR_SIZE, buffer);
|
||||
}
|
||||
@@ -21,3 +21,5 @@ bool sd_readblock(struct sd_context *context, size_t sector_num, uint8_t buffer[
|
||||
bool sd_readblock_start(struct sd_context *context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE]);
|
||||
bool sd_readblock_complete(struct sd_context *context);
|
||||
bool sd_readblock_is_complete(struct sd_context *context);
|
||||
|
||||
bool sd_writeblock(struct sd_context *context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE]);
|
||||
@@ -208,9 +208,74 @@ bool sd_cmd_read_complete(void)
|
||||
sd_spi_wait_complete();
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, &buf, 1);
|
||||
#ifdef SD_READ_CRC_CHECK
|
||||
const uint16_t expect_crc = sd_crc16(sd_spi_context.sd_dma_context.len, sd_spi_context.sd_dma_context.read_buf);
|
||||
const uint16_t act_crc = sd_spi_context.sd_dma_context.crc_buf[0] << 8 | sd_spi_context.sd_dma_context.crc_buf[1];
|
||||
if (act_crc != expect_crc) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("read CRC fail: got %04hx, expected %04hx\n", act_crc, expect_crc);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return (sd_spi_context.sd_dma_context.read_token_buf == 0xfe);
|
||||
}
|
||||
|
||||
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen])
|
||||
{
|
||||
uint8_t buf[2];
|
||||
const uint16_t crc = sd_crc16(datalen, data);
|
||||
sd_spi_cmd_send(cmd, arg);
|
||||
// Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero)
|
||||
bool got_r1 = false;
|
||||
for (int timeout = 0; timeout < 8; ++timeout) {
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
if (!(buf[0] & 0x80)) {
|
||||
got_r1 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!got_r1 || buf[0] != 0x00)
|
||||
goto abort;
|
||||
buf[0] = 0xfe;
|
||||
sd_spi_write_blocking(buf, 1);
|
||||
sd_spi_write_blocking(data, datalen);
|
||||
buf[0] = crc >> 8;
|
||||
buf[1] = crc;
|
||||
sd_spi_write_blocking(buf, 2);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
if ((buf[0] & 0x1f) != 0x5) {
|
||||
#ifdef SD_DEBUG
|
||||
printf("Write fail: %2hhx\n", buf[0]);
|
||||
#endif
|
||||
goto abort;
|
||||
}
|
||||
|
||||
int timeout = 0;
|
||||
bool got_done = false;
|
||||
for (timeout = 0; timeout < 8192; ++timeout) {
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
if (buf[0] != 0x0) {
|
||||
got_done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#ifdef SD_DEBUG
|
||||
printf("dbg write end: %d, %2hhx\n", timeout, buf[0]);
|
||||
#endif
|
||||
if (!got_done)
|
||||
goto abort;
|
||||
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
return true;
|
||||
|
||||
abort:
|
||||
gpio_put(sd_spi_context.ss, true);
|
||||
sd_spi_read_blocking(0xff, buf, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool sd_spi_init(int mosi, int miso, int sck, int ss)
|
||||
{
|
||||
if (sd_spi_context.initialized)
|
||||
@@ -243,6 +308,7 @@ bool sd_spi_init(int mosi, int miso, int sck, int ss)
|
||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_cfg, DMA_SIZE_8);
|
||||
sd_spi_context.spi_dma_rd_crc_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_rd_crc);
|
||||
channel_config_set_read_increment(&sd_spi_context.spi_dma_rd_crc_cfg, false);
|
||||
channel_config_set_write_increment(&sd_spi_context.spi_dma_rd_crc_cfg, true);
|
||||
channel_config_set_dreq(&sd_spi_context.spi_dma_rd_crc_cfg, pio_get_dreq(SD_PIO, sd_spi_context.spi_sm, false));
|
||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_crc_cfg, DMA_SIZE_8);
|
||||
sd_spi_context.spi_dma_wr_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_wr);
|
||||
@@ -25,3 +25,5 @@ void sd_spi_dbg_clk(const int div, const int frac);
|
||||
bool sd_cmd_read_start(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen]);
|
||||
bool sd_cmd_read_complete(void);
|
||||
bool sd_cmd_read_is_complete(void);
|
||||
|
||||
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen]);
|
||||
30
software/modules/rp2_sd/sd_util.h
Normal file
30
software/modules/rp2_sd/sd_util.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
inline static uint8_t sd_crc7(size_t len, const uint8_t data[const static len])
|
||||
{
|
||||
const uint8_t poly = 0b1001;
|
||||
uint8_t crc = 0;
|
||||
for (size_t pos = 0; pos < len; ++pos) {
|
||||
crc ^= data[pos];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc << 1) ^ ((crc & 0x80) ? (poly << 1) : 0);
|
||||
}
|
||||
}
|
||||
return crc >> 1;
|
||||
}
|
||||
|
||||
inline static uint16_t sd_crc16(size_t len, const uint8_t data[const static len])
|
||||
{
|
||||
const uint16_t poly = 0b1000000100001;
|
||||
uint16_t crc = 0;
|
||||
for (size_t pos = 0; pos < len; ++pos) {
|
||||
crc ^= data[pos] << 8;
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc << 1) ^ ((crc & 0x8000) ? poly : 0);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
93
software/src/app.py
Normal file
93
software/src/app.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
from collections import namedtuple
|
||||
import os
|
||||
import time
|
||||
from utils import TimerManager
|
||||
|
||||
|
||||
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons'))
|
||||
|
||||
# Should be ~ 6dB steps
|
||||
VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
|
||||
|
||||
|
||||
class PlayerApp:
|
||||
def __init__(self, deps: Dependencies):
|
||||
self.current_tag = None
|
||||
self.current_tag_time = time.ticks_ms()
|
||||
self.timer_manager = TimerManager()
|
||||
self.player = deps.mp3player(self)
|
||||
self.nfc = deps.nfcreader(self)
|
||||
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
||||
self.mp3file = None
|
||||
self.volume_pos = 3
|
||||
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||
|
||||
def __del__(self):
|
||||
if self.mp3file is not None:
|
||||
self.mp3file.close()
|
||||
self.mp3file = None
|
||||
|
||||
def onTagChange(self, new_tag):
|
||||
if new_tag is not None:
|
||||
self.timer_manager.cancel(self.onTagRemoveDelay)
|
||||
if new_tag == self.current_tag:
|
||||
return
|
||||
# Change playlist on new tag
|
||||
if new_tag is not None:
|
||||
self.current_tag_time = time.ticks_ms()
|
||||
self.current_tag = new_tag
|
||||
uid_str = ''.join('{:02x}'.format(x) for x in new_tag)
|
||||
try:
|
||||
testfiles = [f'/sd/{uid_str}/'.encode() + name for name in os.listdir(f'/sd/{uid_str}'.encode())
|
||||
if name.endswith(b'mp3')]
|
||||
except OSError as ex:
|
||||
print(f'Could not get playlist for tag {uid_str}: {ex}')
|
||||
self.current_tag = None
|
||||
self.player.stop()
|
||||
return
|
||||
testfiles.sort()
|
||||
self._set_playlist(testfiles)
|
||||
else:
|
||||
self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay)
|
||||
|
||||
def onTagRemoveDelay(self):
|
||||
if self.current_tag is not None:
|
||||
print('Tag gone, stopping playback')
|
||||
self.current_tag = None
|
||||
self.player.stop()
|
||||
|
||||
def onButtonPressed(self, what):
|
||||
if what == self.buttons.VOLUP:
|
||||
self.volume_pos = min(self.volume_pos + 1, len(VOLUME_CURVE) - 1)
|
||||
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||
elif what == self.buttons.VOLDOWN:
|
||||
self.volume_pos = max(self.volume_pos - 1, 0)
|
||||
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||
elif what == self.buttons.NEXT:
|
||||
self._play_next()
|
||||
|
||||
def onPlaybackDone(self):
|
||||
self.mp3file.close()
|
||||
self.mp3file = None
|
||||
self._play_next()
|
||||
|
||||
def _set_playlist(self, files: list[bytes]):
|
||||
self.playlist_pos = 0
|
||||
self.playlist = files
|
||||
self._play(self.playlist[self.playlist_pos])
|
||||
|
||||
def _play_next(self):
|
||||
if self.playlist_pos + 1 < len(self.playlist):
|
||||
self.playlist_pos += 1
|
||||
self._play(self.playlist[self.playlist_pos])
|
||||
|
||||
def _play(self, filename: bytes):
|
||||
if self.mp3file is not None:
|
||||
self.player.stop()
|
||||
self.mp3file.close()
|
||||
self.mp3file = None
|
||||
self.mp3file = open(filename, 'rb')
|
||||
self.player.play(self.mp3file)
|
||||
84
software/src/main.py
Normal file
84
software/src/main.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import aiorepl
|
||||
import asyncio
|
||||
import machine
|
||||
import micropython
|
||||
import time
|
||||
from machine import Pin
|
||||
from math import pi, sin, pow
|
||||
|
||||
# Own modules
|
||||
import app
|
||||
from audiocore import AudioContext
|
||||
from mfrc522 import MFRC522
|
||||
from mp3player import MP3Player
|
||||
from nfc import Nfc
|
||||
from rp2_neopixel import NeoPixel
|
||||
from utils import Buttons, SDContext, TimerManager
|
||||
|
||||
micropython.alloc_emergency_exception_buf(100)
|
||||
|
||||
|
||||
async def rainbow(np, period=10):
|
||||
def gamma(value, X=2.2):
|
||||
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
|
||||
|
||||
brightness = 0.5
|
||||
count = 0.0
|
||||
leds = len(np)
|
||||
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))
|
||||
|
||||
|
||||
# Machine setup
|
||||
|
||||
# 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
|
||||
# high prio for proc 1
|
||||
machine.mem32[0x40030000 + 0x00] = 0x10
|
||||
|
||||
|
||||
def run():
|
||||
asyncio.new_event_loop()
|
||||
# Setup LEDs
|
||||
pin = Pin.board.GP16
|
||||
np = NeoPixel(pin, 10, sm=1)
|
||||
asyncio.create_task(rainbow(np))
|
||||
|
||||
# Setup MP3 player
|
||||
with SDContext(mosi=Pin(3), miso=Pin(4), sck=Pin(2), ss=Pin(5), baudrate=15000000), \
|
||||
AudioContext(Pin(8), Pin(6)) as audioctx:
|
||||
|
||||
# Setup NFC
|
||||
reader = MFRC522(spi_id=1, sck=10, miso=12, mosi=11, cs=13, rst=9, tocard_retries=20)
|
||||
|
||||
# Setup app
|
||||
deps = app.Dependencies(mp3player=lambda the_app: MP3Player(audioctx, the_app),
|
||||
nfcreader=lambda the_app: Nfc(reader, the_app),
|
||||
buttons=lambda the_app: Buttons(the_app))
|
||||
the_app = app.PlayerApp(deps)
|
||||
|
||||
# Start
|
||||
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
|
||||
'app': the_app}))
|
||||
asyncio.get_event_loop().run_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP).value() != 0:
|
||||
time.sleep(5)
|
||||
run()
|
||||
@@ -3,118 +3,71 @@
|
||||
|
||||
import asyncio
|
||||
from array import array
|
||||
from utils import TimerManager
|
||||
try:
|
||||
from typing import TYPE_CHECKING # type: ignore
|
||||
except ImportError:
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
import typing
|
||||
|
||||
class PlayerCallback(typing.Protocol):
|
||||
def onPlaybackDone(self) -> None: ...
|
||||
|
||||
|
||||
class MP3Player:
|
||||
def __init__(self, audiocore):
|
||||
def __init__(self, audiocore, cb: PlayerCallback):
|
||||
self.audiocore = audiocore
|
||||
self.commands = []
|
||||
self.command_event = asyncio.Event()
|
||||
self.playlist = []
|
||||
self.mp3task = None
|
||||
self.volume = 128
|
||||
self.cb = cb
|
||||
|
||||
def set_playlist(self, mp3files):
|
||||
def play(self, stream):
|
||||
"""
|
||||
Set a new playlist and start playing from the first entry.
|
||||
For convenience a single file name can also be passed.
|
||||
Play from byte stream.
|
||||
"""
|
||||
if type(mp3files) is bytes:
|
||||
self.playlist = [mp3files]
|
||||
else:
|
||||
self.playlist = mp3files
|
||||
self._send_command('newplaylist')
|
||||
|
||||
def play_next(self):
|
||||
"""
|
||||
Skip to the next track in the playlist. Reaching the end of the playlist stops playback.
|
||||
"""
|
||||
self._send_command('next')
|
||||
|
||||
def play_prev(self):
|
||||
"""
|
||||
Skip to the previous track in the playlist.
|
||||
"""
|
||||
self._send_command('prev')
|
||||
if self.mp3task is not None:
|
||||
self.mp3task.cancel()
|
||||
self.mp3task = None
|
||||
self.mp3task = asyncio.create_task(self._play_task(stream))
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stop playback, remembering the current position in the playlist (but not inside a track).
|
||||
Stop playback
|
||||
"""
|
||||
self._send_command('stop')
|
||||
|
||||
def play(self):
|
||||
"""
|
||||
Start playback.
|
||||
"""
|
||||
self._send_command('play')
|
||||
if self.mp3task is not None:
|
||||
self.mp3task.cancel()
|
||||
self.mp3task = None
|
||||
|
||||
def set_volume(self, volume: int):
|
||||
"""
|
||||
Set volume (0..255).
|
||||
"""
|
||||
self.volume = volume
|
||||
self.audiocore.set_volume(volume)
|
||||
|
||||
def _send_command(self, command: str):
|
||||
self.commands.append(command)
|
||||
self.command_event.set()
|
||||
def get_volume(self) -> int:
|
||||
return self.volume
|
||||
|
||||
async def _play_task(self, mp3path):
|
||||
async def _play_task(self, stream):
|
||||
known_underruns = 0
|
||||
send_done = False
|
||||
data = array('b', range(512))
|
||||
try:
|
||||
print(b'Playing ' + mp3path)
|
||||
with open(mp3path, 'rb') as mp3file:
|
||||
while True:
|
||||
bytes_read = mp3file.readinto(data)
|
||||
if bytes_read == 0:
|
||||
# End of file
|
||||
break
|
||||
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
|
||||
if underruns > known_underruns:
|
||||
print(f"{underruns:x}")
|
||||
known_underruns = underruns
|
||||
# Intentionally do not use _send_command, we don't want to set command_event yet
|
||||
self.commands.append('done')
|
||||
while True:
|
||||
bytes_read = stream.readinto(data)
|
||||
if bytes_read == 0:
|
||||
# End of file
|
||||
break
|
||||
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
|
||||
if underruns > known_underruns:
|
||||
print(f"{underruns:x}")
|
||||
known_underruns = underruns
|
||||
# Call onPlaybackDone after flush
|
||||
send_done = True
|
||||
finally:
|
||||
self.audiocore.flush()
|
||||
self.command_event.set()
|
||||
|
||||
def _play(self, mp3path):
|
||||
if self.mp3task is not None:
|
||||
self.mp3task.cancel()
|
||||
self.mp3task = None
|
||||
if mp3path is not None:
|
||||
self.mp3task = asyncio.create_task(self._play_task(mp3path))
|
||||
|
||||
async def task(self):
|
||||
playlist_pos = 0
|
||||
while True:
|
||||
await self.command_event.wait()
|
||||
self.command_event.clear()
|
||||
change_play = False
|
||||
while len(self.commands) > 0:
|
||||
command = self.commands.pop()
|
||||
if command == 'next' or command == 'done':
|
||||
if playlist_pos + 1 < len(self.playlist):
|
||||
playlist_pos += 1
|
||||
change_play = True
|
||||
else:
|
||||
# reaching the end of the playlist stops playback
|
||||
self._play(None)
|
||||
elif command == 'prev':
|
||||
if playlist_pos > 0:
|
||||
playlist_pos -= 1
|
||||
change_play = True
|
||||
elif command == 'stop':
|
||||
self._play(None)
|
||||
elif command == 'play':
|
||||
if self.mp3task is None:
|
||||
change_play = True
|
||||
elif command == 'newplaylist':
|
||||
if len(self.playlist) > 0:
|
||||
playlist_pos = 0
|
||||
change_play = True
|
||||
else:
|
||||
self._play(None)
|
||||
if change_play:
|
||||
self._play(self.playlist[playlist_pos])
|
||||
if send_done:
|
||||
# Only call onPlaybackDone if exit due to end of stream
|
||||
# Use timer with time 0 to call callback "immediately" but from a different task
|
||||
TimerManager().schedule(0, self.cb.onPlaybackDone)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'''
|
||||
SPDX-License-Identifier: MIT
|
||||
Copyright (c) 2025 Stefan Kratochwil (Kratochwil-LA@gmx.de)
|
||||
Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
'''
|
||||
from nfc.nfc import Nfc
|
||||
|
||||
__all__ = ['Nfc']
|
||||
__all__ = (Nfc)
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
'''
|
||||
SPDX-License-Identifier: MIT
|
||||
Copyright (c) 2025 Stefan Kratochwil (Kratochwil-LA@gmx.de)
|
||||
Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from mfrc522 import MFRC522
|
||||
try:
|
||||
from typing import TYPE_CHECKING # type: ignore
|
||||
except ImportError:
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
import typing
|
||||
|
||||
class TagCallback(typing.Protocol):
|
||||
def onTagChange(self, uid: list[int]) -> None: ...
|
||||
|
||||
|
||||
class Nfc:
|
||||
@@ -28,10 +38,11 @@ class Nfc:
|
||||
|
||||
asyncio.run(main())
|
||||
'''
|
||||
def __init__(self, reader: MFRC522):
|
||||
def __init__(self, reader: MFRC522, cb: TagCallback | None = None):
|
||||
self.reader = reader
|
||||
self.last_uid = None
|
||||
self.last_uid_timestamp = None
|
||||
self.last_uid: list[int] | None = None
|
||||
self.last_uid_timestamp: int | None = None
|
||||
self.cb = cb
|
||||
self.task = asyncio.create_task(self._reader_poll_task())
|
||||
|
||||
@staticmethod
|
||||
@@ -41,20 +52,30 @@ class Nfc:
|
||||
'''
|
||||
return '0x' + ''.join(f'{i:02x}' for i in uid)
|
||||
|
||||
def _read_tag_sn(self) -> list[int] | None:
|
||||
(stat, _) = self.reader.request(self.reader.REQIDL)
|
||||
if stat == self.reader.OK:
|
||||
(stat, uid) = self.reader.SelectTagSN()
|
||||
if stat == self.reader.OK:
|
||||
return uid
|
||||
return None
|
||||
|
||||
async def _reader_poll_task(self, poll_interval_ms: int = 50):
|
||||
'''
|
||||
Periodically polls the nfc reader. Stores tag uid and timestamp if a new tag was found.
|
||||
'''
|
||||
last_callback_uid = None
|
||||
while True:
|
||||
self.reader.init()
|
||||
|
||||
# For now we omit the tag type
|
||||
(stat, _) = self.reader.request(self.reader.REQIDL)
|
||||
if stat == self.reader.OK:
|
||||
(stat, uid) = self.reader.SelectTagSN()
|
||||
if stat == self.reader.OK:
|
||||
self.last_uid = uid
|
||||
self.last_uid_timestamp = time.ticks_us()
|
||||
uid = self._read_tag_sn()
|
||||
if uid is not None:
|
||||
self.last_uid = uid
|
||||
self.last_uid_timestamp = time.ticks_us()
|
||||
if self.cb is not None and last_callback_uid != uid:
|
||||
self.cb.onTagChange(uid)
|
||||
last_callback_uid = uid
|
||||
|
||||
await asyncio.sleep_ms(poll_interval_ms)
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
inline static uint8_t sd_crc7(size_t len, const uint8_t data[const static len])
|
||||
{
|
||||
const uint8_t poly = 0b1001;
|
||||
uint8_t crc = 0;
|
||||
for (size_t pos = 0; pos < len; ++pos) {
|
||||
crc ^= data[pos];
|
||||
for (int bit = 0; bit < 8; ++bit) {
|
||||
crc = (crc << 1) ^ ((crc & 0x80) ? (poly << 1) : 0);
|
||||
}
|
||||
}
|
||||
return crc >> 1;
|
||||
}
|
||||
|
||||
/* inline static uint16_t sd_crc16(size_t len, const uint8_t data[const static len]) */
|
||||
/* { */
|
||||
/* const uint16_t poly = 0b1000000100001; */
|
||||
/* uint16_t crc = 0; */
|
||||
/* for (size_t pos = 0; pos < len; ++pos) { */
|
||||
/* crc ^= data[pos] << 8; */
|
||||
/* for (int bit = 0; bit < 8; ++bit) { */
|
||||
/* crc = (crc << 1) ^ ((crc & 0x8000) ? poly : 0); */
|
||||
/* } */
|
||||
/* } */
|
||||
/* return crc; */
|
||||
/* } */
|
||||
9
software/src/utils/__init__.py
Normal file
9
software/src/utils/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
from utils.buttons import Buttons
|
||||
from utils.mbrpartition import MBRPartition
|
||||
from utils.sdcontext import SDContext
|
||||
from utils.timer import TimerManager
|
||||
|
||||
__all__ = ["Buttons", "MBRPartition", "SDContext", "TimerManager"]
|
||||
53
software/src/utils/buttons.py
Normal file
53
software/src/utils/buttons.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import asyncio
|
||||
import machine
|
||||
import micropython
|
||||
import time
|
||||
try:
|
||||
from typing import TYPE_CHECKING # type: ignore
|
||||
except ImportError:
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
import typing
|
||||
|
||||
class ButtonCallback(typing.Protocol):
|
||||
def onButtonPressed(self, what: int) -> None: ...
|
||||
|
||||
|
||||
class Buttons:
|
||||
def __init__(self, cb: ButtonCallback, pin_volup=17, pin_voldown=19, pin_next=18):
|
||||
self.VOLUP = micropython.const(1)
|
||||
self.VOLDOWN = micropython.const(2)
|
||||
self.NEXT = micropython.const(3)
|
||||
self.cb = cb
|
||||
self.buttons = {machine.Pin(pin_volup, machine.Pin.IN, machine.Pin.PULL_UP): self.VOLUP,
|
||||
machine.Pin(pin_voldown, machine.Pin.IN, machine.Pin.PULL_UP): self.VOLDOWN,
|
||||
machine.Pin(pin_next, machine.Pin.IN, machine.Pin.PULL_UP): self.NEXT}
|
||||
self.int_flag = asyncio.ThreadSafeFlag()
|
||||
self.pressed: list[int] = []
|
||||
self.last: dict[int, int] = {}
|
||||
for button in self.buttons.keys():
|
||||
button.irq(handler=self._interrupt, trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING)
|
||||
asyncio.create_task(self.task())
|
||||
|
||||
def _interrupt(self, button):
|
||||
keycode = self.buttons[button]
|
||||
last = self.last.get(keycode, 0)
|
||||
now = time.ticks_ms()
|
||||
self.last[keycode] = now
|
||||
if now - last < 10:
|
||||
# debounce, discard
|
||||
return
|
||||
if button.value() == 0:
|
||||
# print(f'B{keycode} {now}')
|
||||
self.pressed.append(keycode)
|
||||
self.int_flag.set()
|
||||
|
||||
async def task(self):
|
||||
while True:
|
||||
await self.int_flag.wait()
|
||||
while len(self.pressed) > 0:
|
||||
what = self.pressed.pop()
|
||||
self.cb.onButtonPressed(what)
|
||||
46
software/src/utils/mbrpartition.py
Normal file
46
software/src/utils/mbrpartition.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
from array import array
|
||||
import struct
|
||||
|
||||
|
||||
class MBRPartition:
|
||||
def __init__(self, bdev, partno):
|
||||
assert partno >= 0 and partno < 4
|
||||
self.bdev = bdev
|
||||
bdev_len = bdev.ioctl(4, None)
|
||||
bdev_bs = bdev.ioctl(5, None)
|
||||
assert bdev_bs == 512
|
||||
mbr = array('B', 512*b'0')
|
||||
bdev.readblocks(0, mbr)
|
||||
if mbr[510] != 0x55 or mbr[511] != 0xaa:
|
||||
raise ValueError("Not a valid MBR")
|
||||
partofs = 0x1be + partno*16
|
||||
(boot_ind, _, _, _,
|
||||
parttype, _, _, _,
|
||||
lba_start, lba_len) = struct.unpack_from('<BBBBBBBBLL', mbr, partofs)
|
||||
print(f'Partition {partno} bi {boot_ind} type {parttype} start {lba_start} len {lba_len}')
|
||||
if (boot_ind != 0x00 and boot_ind != 0x80) or parttype == 0x00:
|
||||
raise ValueError("Not a valid partition")
|
||||
self.offset = lba_start
|
||||
self.size = lba_len
|
||||
assert lba_start + lba_len <= bdev_len
|
||||
|
||||
def ioctl(self, op, arg):
|
||||
if op == 4:
|
||||
return self.size
|
||||
elif op == 5:
|
||||
return 512
|
||||
else:
|
||||
return None
|
||||
|
||||
def readblocks(self, block, buf):
|
||||
if block >= self.size:
|
||||
raise ValueError("Block out of range")
|
||||
return self.bdev.readblocks(block+self.offset, buf)
|
||||
|
||||
def writeblocks(self, block, buf):
|
||||
if block >= self.size:
|
||||
raise ValueError("Block out of range")
|
||||
return self.bdev.writeblocks(block+self.offset, buf)
|
||||
39
software/src/utils/sdcontext.py
Normal file
39
software/src/utils/sdcontext.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import os
|
||||
|
||||
from . import MBRPartition
|
||||
from rp2_sd import SDCard
|
||||
|
||||
|
||||
class SDContext:
|
||||
def __init__(self, mosi, miso, sck, ss, baudrate):
|
||||
self.mosi = mosi
|
||||
self.miso = miso
|
||||
self.sck = sck
|
||||
self.ss = ss
|
||||
self.baudrate = baudrate
|
||||
|
||||
def __enter__(self):
|
||||
self.sdcard = SDCard(self.mosi, self.miso, self.sck, self.ss, self.baudrate)
|
||||
# Try first partition
|
||||
try:
|
||||
self.part = MBRPartition(self.sdcard, 0)
|
||||
os.mount(self.part, '/sd')
|
||||
return self
|
||||
except Exception:
|
||||
print("Failed to mount SDCard partition, trying whole device...")
|
||||
# Try whole device
|
||||
try:
|
||||
os.mount(self.sdcard, '/sd')
|
||||
return self
|
||||
except Exception as ex:
|
||||
self.sdcard.deinit()
|
||||
raise RuntimeError("Could not mount SD card") from ex
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
try:
|
||||
os.umount('/sd')
|
||||
finally:
|
||||
self.sdcard.deinit()
|
||||
71
software/src/utils/timer.py
Normal file
71
software/src/utils/timer.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||
|
||||
import asyncio
|
||||
import heapq
|
||||
import time
|
||||
|
||||
TIMER_DEBUG = True
|
||||
|
||||
|
||||
class TimerManager(object):
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(TimerManager, cls).__new__(cls)
|
||||
cls._instance.timers = []
|
||||
cls._instance.timer_debug = TIMER_DEBUG
|
||||
cls._instance.task = asyncio.create_task(cls._instance._timer_worker())
|
||||
cls._instance.worker_event = asyncio.Event()
|
||||
return cls._instance
|
||||
|
||||
def schedule(self, when, what):
|
||||
cur_nearest = self.timers[0][0] if len(self.timers) > 0 else None
|
||||
heapq.heappush(self.timers, (when, what))
|
||||
if cur_nearest is None or cur_nearest > self.timers[0][0]:
|
||||
# New timer is closer than previous closest timer
|
||||
if self.timer_debug:
|
||||
print(f'cur_nearest: {cur_nearest}, new next: {self.timers[0][0]}')
|
||||
print("schedule: wake")
|
||||
self.worker_event.set()
|
||||
|
||||
def cancel(self, what):
|
||||
try:
|
||||
(when, _), i = next(filter(lambda item: item[0][1] == what, zip(self.timers, range(len(self.timers)))))
|
||||
except StopIteration:
|
||||
return False
|
||||
del self.timers[i]
|
||||
heapq.heapify(self.timers)
|
||||
if i == 0:
|
||||
# Cancel timer was closest timer
|
||||
if self.timer_debug:
|
||||
print("cancel: wake")
|
||||
self.worker_event.set()
|
||||
return True
|
||||
|
||||
async def _timer_worker(self):
|
||||
while True:
|
||||
if len(self.timers) == 0:
|
||||
# Nothing to do
|
||||
await self.worker_event.wait()
|
||||
if self.timer_debug:
|
||||
print("_timer_worker: event 0")
|
||||
self.worker_event.clear()
|
||||
continue
|
||||
cur_nearest = self.timers[0][0]
|
||||
wait_time = cur_nearest - time.ticks_ms()
|
||||
if wait_time > 0:
|
||||
if self.timer_debug:
|
||||
print(f"_timer_worker: next is {self.timers[0]}, sleep {wait_time} ms")
|
||||
try:
|
||||
await asyncio.wait_for_ms(self.worker_event.wait(), wait_time)
|
||||
if self.timer_debug:
|
||||
print("_timer_worker: event 1")
|
||||
# got woken up due to event
|
||||
self.worker_event.clear()
|
||||
continue
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
_, callback = heapq.heappop(self.timers)
|
||||
callback()
|
||||
1
software/tools/mklittlefs
Submodule
1
software/tools/mklittlefs
Submodule
Submodule software/tools/mklittlefs added at db0513ade5
67
software/tools/standalone-mp3/CMakeLists.txt
Normal file
67
software/tools/standalone-mp3/CMakeLists.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Workaround for pico-sdk host toolchain issue, see directory for details
|
||||
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../lib/micropython/ports/rp2/tools_patch")
|
||||
|
||||
# initialize pico-sdk from submodule
|
||||
# note: this must happen before project()
|
||||
include(../../lib/micropython/lib/pico-sdk/pico_sdk_init.cmake)
|
||||
|
||||
project(standalone_mp3)
|
||||
|
||||
option(ENABLE_WRITE_TEST "Enable write test" OFF)
|
||||
option(ENABLE_PLAY_TEST "Enable mp3 playback test" OFF)
|
||||
option(ENABLE_SD_READ_CRC "Enable crc check when reading from sd card" OFF)
|
||||
option(ENABLE_SD_DEBUG "Enable debug output for sd card driver" OFF)
|
||||
|
||||
# initialize the Raspberry Pi Pico SDK
|
||||
pico_sdk_init()
|
||||
|
||||
set(SD_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../../modules/rp2_sd")
|
||||
|
||||
set(SD_LIB_SRCS
|
||||
"${SD_LIB_DIR}/sd.c"
|
||||
"${SD_LIB_DIR}/sd_spi.c"
|
||||
)
|
||||
|
||||
|
||||
add_executable(standalone_mp3
|
||||
main.c
|
||||
i2s.c
|
||||
${CMAKE_CURRENT_BINARY_DIR}/sd_spi_pio.pio.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/i2s_max98357.pio.h
|
||||
${SD_LIB_SRCS}
|
||||
)
|
||||
|
||||
if(ENABLE_WRITE_TEST)
|
||||
target_compile_definitions(standalone_mp3 PRIVATE WRITE_TEST)
|
||||
endif()
|
||||
|
||||
if(ENABLE_PLAY_TEST)
|
||||
target_compile_definitions(standalone_mp3 PRIVATE PLAY_TEST)
|
||||
endif()
|
||||
|
||||
if(ENABLE_SD_READ_CRC)
|
||||
target_compile_definitions(standalone_mp3 PRIVATE SD_READ_CRC_CHECK)
|
||||
endif()
|
||||
|
||||
if(ENABLE_SD_DEBUG)
|
||||
target_compile_definitions(standalone_mp3 PRIVATE SD_DEBUG)
|
||||
endif()
|
||||
|
||||
pico_generate_pio_header(standalone_mp3 ${SD_LIB_DIR}/sd_spi_pio.pio)
|
||||
|
||||
pico_generate_pio_header(standalone_mp3 ${CMAKE_CURRENT_LIST_DIR}/i2s_max98357.pio)
|
||||
|
||||
add_subdirectory(../../lib/helix_mp3 helix_mp3)
|
||||
|
||||
target_link_libraries(standalone_mp3 PRIVATE pico_stdlib hardware_dma hardware_spi hardware_sync hardware_pio helix_mp3)
|
||||
target_include_directories(standalone_mp3 PRIVATE ${SD_LIB_DIR})
|
||||
target_compile_options(standalone_mp3 PRIVATE -Og -DSD_DEBUG)
|
||||
|
||||
|
||||
pico_add_extra_outputs(standalone_mp3)
|
||||
pico_enable_stdio_uart(standalone_mp3 1)
|
||||
|
||||
set_property(TARGET standalone_mp3 APPEND_STRING PROPERTY LINK_FLAGS "-Wl,--print-memory-usage")
|
||||
|
||||
116
software/tools/standalone-mp3/i2s.c
Normal file
116
software/tools/standalone-mp3/i2s.c
Normal file
@@ -0,0 +1,116 @@
|
||||
#include "i2s.h"
|
||||
#include "i2s_max98357.pio.h"
|
||||
|
||||
#include <hardware/dma.h>
|
||||
#include <hardware/sync.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define audiocore_pio pio1
|
||||
|
||||
#define AUDIO_BUFS 3
|
||||
|
||||
struct i2s_context {
|
||||
unsigned pio_program_offset;
|
||||
int pio_sm;
|
||||
int dma_ch;
|
||||
dma_channel_config dma_config;
|
||||
uint32_t dma_buf[AUDIO_BUFS][I2S_DMA_BUF_SIZE];
|
||||
int cur_playing;
|
||||
bool has_data[AUDIO_BUFS];
|
||||
bool playback_active;
|
||||
};
|
||||
|
||||
static struct i2s_context i2s_context;
|
||||
|
||||
#define OUT_PIN 8
|
||||
#define SIDESET_BASE 6
|
||||
|
||||
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 int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||
if (i2s_context.playback_active && i2s_context.has_data[next_buf]) {
|
||||
i2s_context.cur_playing = next_buf;
|
||||
i2s_context.has_data[next_buf] = false;
|
||||
} else {
|
||||
memset(i2s_context.dma_buf[i2s_context.cur_playing], 0, sizeof(uint32_t) * I2S_DMA_BUF_SIZE);
|
||||
if (i2s_context.playback_active)
|
||||
printf("x");
|
||||
}
|
||||
dma_channel_transfer_from_buffer_now(i2s_context.dma_ch, i2s_context.dma_buf[i2s_context.cur_playing], 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));
|
||||
}
|
||||
|
||||
uint32_t *i2s_next_buf(void)
|
||||
{
|
||||
const long flags = save_and_disable_interrupts();
|
||||
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||
uint32_t *ret = NULL;
|
||||
if (!i2s_context.has_data[next_buf]) {
|
||||
ret = i2s_context.dma_buf[next_buf];
|
||||
} else {
|
||||
const int next_buf_2 = (next_buf + 1) % AUDIO_BUFS;
|
||||
if (!i2s_context.has_data[next_buf_2]) {
|
||||
ret = i2s_context.dma_buf[next_buf_2];
|
||||
}
|
||||
}
|
||||
restore_interrupts(flags);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void i2s_commit_buf(uint32_t *buf)
|
||||
{
|
||||
const long flags = save_and_disable_interrupts();
|
||||
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||
const int next_buf_2 = (next_buf + 1) % AUDIO_BUFS;
|
||||
if (i2s_context.dma_buf[next_buf] == buf) {
|
||||
i2s_context.has_data[next_buf] = true;
|
||||
} else if (i2s_context.dma_buf[next_buf_2] == buf) {
|
||||
i2s_context.has_data[next_buf_2] = true;
|
||||
i2s_context.playback_active = true;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
restore_interrupts(flags);
|
||||
}
|
||||
|
||||
bool i2s_init(int samplerate)
|
||||
{
|
||||
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * 2* I2S_DMA_BUF_SIZE);
|
||||
if (!pio_can_add_program(audiocore_pio, (const pio_program_t *)&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, (const pio_program_t *)&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;
|
||||
i2s_context.playback_active = false;
|
||||
setup_dma_config();
|
||||
irq_add_shared_handler(DMA_IRQ_1, &dma_isr, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
|
||||
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, (const pio_program_t *)&i2s_max98357_program, i2s_context.pio_program_offset);
|
||||
pio_sm_unclaim(audiocore_pio, i2s_context.pio_sm);
|
||||
return false;
|
||||
}
|
||||
11
software/tools/standalone-mp3/i2s.h
Normal file
11
software/tools/standalone-mp3/i2s.h
Normal file
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define I2S_DMA_BUF_SIZE (1152)
|
||||
|
||||
bool i2s_init(int samplerate);
|
||||
|
||||
uint32_t *i2s_next_buf(void);
|
||||
void i2s_commit_buf(uint32_t *buf);
|
||||
59
software/tools/standalone-mp3/i2s_max98357.pio
Normal file
59
software/tools/standalone-mp3/i2s_max98357.pio
Normal 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);
|
||||
}
|
||||
%}
|
||||
188
software/tools/standalone-mp3/main.c
Normal file
188
software/tools/standalone-mp3/main.c
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "i2s.h"
|
||||
#include "sd.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <hardware/clocks.h>
|
||||
#include <hardware/gpio.h>
|
||||
#include <hardware/spi.h>
|
||||
#include <hardware/sync.h>
|
||||
#include <pico/stdio.h>
|
||||
#include <pico/stdlib.h>
|
||||
#include <pico/time.h>
|
||||
|
||||
#include "mp3dec.h"
|
||||
#include "sd_spi.h"
|
||||
|
||||
extern void sd_spi_dbg_clk(const int div, const int frac);
|
||||
|
||||
extern void sd_spi_dbg_loop(void);
|
||||
|
||||
#define MAX_VOLUME 0x8000u
|
||||
|
||||
void __time_critical_func(volume_adjust)(int16_t *restrict buf, size_t samples, uint16_t scalef)
|
||||
{
|
||||
for (size_t pos = 0; pos < samples; ++pos) {
|
||||
buf[pos] = ((int32_t)buf[pos] * scalef) >> 15;
|
||||
}
|
||||
}
|
||||
|
||||
static int __time_critical_func(play_mp3)(struct sd_context *sd_context)
|
||||
{
|
||||
HMP3Decoder mp3dec = MP3InitDecoder();
|
||||
|
||||
if (!i2s_init(44100)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t mp3buffer[4 * 512];
|
||||
for (int i = 0; i < sizeof(mp3buffer) / 512; ++i) {
|
||||
sd_readblock(sd_context, i, mp3buffer + 512 * i);
|
||||
}
|
||||
size_t next_sector = sizeof(mp3buffer) / 512;
|
||||
|
||||
unsigned char *readptr = mp3buffer;
|
||||
int bytes_left = sizeof(mp3buffer);
|
||||
|
||||
bool first = true;
|
||||
bool pending_read = false;
|
||||
bool synced = false;
|
||||
while (true) {
|
||||
/* Get some input data */
|
||||
if (pending_read && sd_readblock_is_complete(sd_context)) {
|
||||
sd_readblock_complete(sd_context);
|
||||
bytes_left += 512;
|
||||
pending_read = false;
|
||||
}
|
||||
if (!pending_read && (sizeof(mp3buffer) - bytes_left >= 512)) {
|
||||
// If there is not enough space for an mp3 frame, or if there is less than one SD block to the end, move
|
||||
// remaining data to start of buffer
|
||||
if (readptr - mp3buffer >= sizeof(mp3buffer) - 1044 ||
|
||||
readptr - mp3buffer > sizeof(mp3buffer) - 512 - bytes_left) {
|
||||
memmove(mp3buffer, readptr, bytes_left);
|
||||
readptr = mp3buffer;
|
||||
}
|
||||
sd_readblock_start(sd_context, next_sector++, readptr + bytes_left);
|
||||
pending_read = true;
|
||||
}
|
||||
if (bytes_left == 0) {
|
||||
// Can't do anything without input, wait and try again
|
||||
__wfe();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Synchronize MP3 stream if neccessary
|
||||
if (!synced) {
|
||||
const int ofs = MP3FindSyncWord(readptr, bytes_left);
|
||||
if (ofs == -1) {
|
||||
printf("MP3 sync word not found\n");
|
||||
readptr += bytes_left;
|
||||
bytes_left = 0;
|
||||
continue; // try again
|
||||
}
|
||||
readptr += ofs;
|
||||
bytes_left -= ofs;
|
||||
printf("MP3 sync word found after %zu bytes\n", ofs);
|
||||
synced = true;
|
||||
}
|
||||
|
||||
// Get an output buffer
|
||||
uint32_t *const buf = i2s_next_buf();
|
||||
if (!buf) {
|
||||
// No output needed, wait and try again
|
||||
__wfe();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Decode one frame
|
||||
unsigned char *const old_readptr = readptr;
|
||||
const int old_bytes_left = bytes_left;
|
||||
const int status = MP3Decode(mp3dec, &readptr, &bytes_left, (short *)buf, 0);
|
||||
if (status) {
|
||||
if (status == ERR_MP3_INDATA_UNDERFLOW) {
|
||||
readptr = old_readptr;
|
||||
bytes_left = old_bytes_left;
|
||||
printf("INDATA_UNDERFLOW\n");
|
||||
sd_readblock_complete(sd_context);
|
||||
continue;
|
||||
} else /*if (status== ERR_MP3_MAINDATA_UNDERFLOW)*/ {
|
||||
--bytes_left;
|
||||
++readptr;
|
||||
synced = false;
|
||||
continue;
|
||||
}
|
||||
printf("MP3Decode failed: %d\n", status);
|
||||
break;
|
||||
}
|
||||
|
||||
MP3FrameInfo info;
|
||||
MP3GetLastFrameInfo(mp3dec, &info);
|
||||
if (first) {
|
||||
printf("bitrate %d, nChans %d, samprate %d, bitsPerSample %d, outputSamps %d, layer %d, version %d\n",
|
||||
info.bitrate, info.nChans, info.samprate, info.bitsPerSample, info.outputSamps, info.layer,
|
||||
info.version);
|
||||
first = false;
|
||||
}
|
||||
if (info.outputSamps != 2304) {
|
||||
printf("Unexpected number of output samples: %d\n", info.outputSamps);
|
||||
return 1;
|
||||
}
|
||||
volume_adjust((int16_t *)buf, info.outputSamps, MAX_VOLUME >> 4);
|
||||
|
||||
i2s_commit_buf(buf);
|
||||
}
|
||||
}
|
||||
|
||||
static void write_test(struct sd_context *sd_context)
|
||||
{
|
||||
uint8_t data_buffer[4096];
|
||||
do {
|
||||
for (int i = 0; i < sizeof(data_buffer) / SD_SECTOR_SIZE; ++i) {
|
||||
if (!sd_readblock(sd_context, i, data_buffer + SD_SECTOR_SIZE * i)) {
|
||||
printf("sd_readblock(%d) failed\n", i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (int line = 0; line < 32; ++line) {
|
||||
printf("%04hx ", line * 16);
|
||||
for (int item = 0; item < 16; ++item) {
|
||||
printf("%02hhx%c", data_buffer[line * 16 + item], (item == 15) ? '\n' : ' ');
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < SD_SECTOR_SIZE; ++i) {
|
||||
data_buffer[i] ^= 0xff;
|
||||
}
|
||||
|
||||
if(!sd_writeblock(sd_context, 0, data_buffer)) {
|
||||
printf("sd_writeblock failed\n");
|
||||
return;
|
||||
}
|
||||
sleep_ms(1000);
|
||||
} while (data_buffer[SD_SECTOR_SIZE - 1] != 0xAA);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
stdio_init_all();
|
||||
printf("sysclk is %d Hz\n", clock_get_hz(clk_sys));
|
||||
|
||||
struct sd_context sd_context;
|
||||
|
||||
if (!sd_init(&sd_context, 3, 4, 2, 5, 15000000)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
#ifdef WRITE_TEST
|
||||
write_test(&sd_context);
|
||||
#endif
|
||||
|
||||
#ifdef PLAY_TEST
|
||||
play_mp3(&sd_context);
|
||||
#endif
|
||||
|
||||
printf("Done.\n");
|
||||
}
|
||||
@@ -3,5 +3,8 @@
|
||||
set -eu
|
||||
|
||||
git submodule update --init lib
|
||||
git -C lib/micropython submodule update --init lib/pico-sdk lib/mbedtls lib/micropython-lib lib/tinyusb lib/btstack lib/cyw43-driver lib/lwip
|
||||
git -C lib/micropython submodule update --init \
|
||||
lib/pico-sdk lib/mbedtls lib/micropython-lib lib/tinyusb lib/btstack lib/cyw43-driver lib/lwip \
|
||||
lib/berkeley-db-1.xx
|
||||
git -C lib/micropython/lib/pico-sdk submodule update --init lib
|
||||
git submodule update --init --recursive tools/mklittlefs
|
||||
|
||||
Reference in New Issue
Block a user