48 Commits

Author SHA1 Message Date
5f9bdb2517 Fastest sd clockrate possible with sandisk ultra 32gb card
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m22s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
2025-10-14 20:27:41 +02:00
502805e2e8 hwconfig: Fix pad config for SD SPI, move clockrate to hwconfig
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m19s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
The code snippet in hwconfig to adjust the drive strength was incorrect:
It was adjusting the wrong pins. This was probably not updated during
some pin shuffling in the breadboard phase.

Fix this by adjusting the correct pins. Experimentation shows that both
setting the slew rate to fast or setting the drive strength to 8 mA
(default: slow and 4 mA) is sufficient to make SD SPI work at 25 MHz on
the Rev1 PCB. For now, the combination 8 mA and slow is chosen (slow
slew rate should result in less high frequency EMI).

Also make the SD clock rate adjustable in hwconfig, and set it to 25 MHz
for Rev1 and 15 MHz for the breadboard setup.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-10-13 23:38:33 +02:00
4a15b2c221 Merge pull request 'hw-update-and-pcb' (#40) from hw-update-and-pcb into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m23s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 12s
Reviewed-on: #40
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-10-07 20:19:56 +00:00
d3674e46aa scripts: Add HW revision support to flash.sh
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m25s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-10-07 22:12:44 +02:00
1fa3b3c887 rp2_sd: Increase timeout for SD card initialization
Spurious failures were observed with a SanDisk Ultra 32GB card that no
longer occur with the increased timeouts.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-10-07 22:12:44 +02:00
4d295501eb build: Fix check-format and clang-format paths
The check-format and clang-format targets were not adjusted when the
code was reorganized in commit 7f8282315e ("Restructure sources"). Fix
it to find the C sources that are now in the modules directory.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-10-07 22:12:44 +02:00
c9150eb21a audiocore: Support swapping dclk and lrclk pins for I2S
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-10-07 22:12:44 +02:00
da90228ab5 Make hardware configurable
Move hardware-specifics (pin assignments, power management) to
hwconfig_*.py.

The build system will build a firmware image
firmware-filesystem-$variant.uf2 for all variants for which a
hwconfig_$variant.py file exits. Inside the filesystem image, the
selected variants hwconfig_$variant.py file will always be named
hwconfig.py.

At runtime, main.py will attempt to import hwconfig which will load the
configuration for the correct variant.

Currently, the hwconfig_* modules are expected to define the pin mapping
and implement a board_init method.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-10-07 22:12:44 +02:00
bd17197fef Merge pull request 'sd: Fix SDSC card support' (#41) from fix-sdsc-card-blocksize into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m22s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
Reviewed-on: #41
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-10-07 19:54:11 +00:00
d39157ba0a sd: Fix SDSC card support
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m18s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Run pytests / Check-Pytest (push) Successful in 10s
- Old SDSC cards could have a native blocksize != 512 bytes, but they
  should support the SET_BLOCKLEN command to set the blocksize to 512.
  Use that so we can just assume 512 everywhere else.

- SDSC cards used byte addresses, not sector numbers, for the argument
  of READ_BLOCK and WRITE_BLOCK. Check the card type and multiply the
  sector number with 512 if necessary.

Tested with a noname chinese 128 MiB-ish card.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-09-07 15:53:22 +02:00
09c8f522b8 hw: Clean up schematic, add RUN reset
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m20s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Clean up schematic and put battery power, buttons and run/reset in
subblocks.

Add missing switch on RUN to reset the RP2040. In the PCB this is
actually a jumper instead of a switch right now, so it fits in the
existing layout.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-09-02 22:17:59 +02:00
0ce0b51f1c Add battery charger, PCB rev 1
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m18s
Check code formatting / Check-C-Format (push) Successful in 53m0s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-09-02 22:17:59 +02:00
e33fefc552 Merge pull request 'add-pytest-infrastructure' (#38) from add-pytest-infrastructure into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m20s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Reviewed-on: #38
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-08-28 15:37:09 +00:00
95b3924736 ci: flake8 on all python folders; Run pytest in CI
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m20s
Check code formatting / Check-C-Format (push) Successful in 6s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 10s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-08-27 13:43:12 +02:00
27110b7b62 Add infrastructure for pytest and mypy
Make mypy run with proper type stubs for micropython RP2.
Add basic structure for pytest testing.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-08-27 13:42:08 +02:00
fb36ac8ed2 Merge pull request 'Switch btree to use mpy stack' (#37) from btree-use-mpy-stack into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m12s
Check code formatting / Check-C-Format (push) Successful in 6s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Reviewed-on: #37
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-08-27 11:30:05 +00:00
4c7ce78201 Switch btree to use mpy stack
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m23s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
2025-08-24 17:51:35 +02:00
10de110375 Merge remote-tracking branch 'origin/feature/upgrade_micropython_1_26_0_plus_fixes'
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m21s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
2025-08-24 17:47:30 +02:00
5c2df891d9 micropython: upgrade to 1.26.0, reverted single commit.
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m21s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 8s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 7s
Migrated all patches from the previous version.

A change in the Findpicotool.cmake script in the pico-sdk led to a
downgrade of picotool, which incorporated mbedtls into the build, which
itself is not buildable with cmake versions < 3.5.

The commit which made this isolated change was reverted. Future versions
of micropython will use pico-sdk 2.2.0 or newer, where this problem is
fixed, and picotool is pinned to a release version.
2025-08-24 17:43:26 +02:00
e72443ff1f Merge pull request 'misc-minor-fixes' (#35) from misc-minor-fixes into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m23s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Reviewed-on: #35
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-08-24 15:36:59 +00:00
3f0eeb837a micropython: Increase micropython stack allocation
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m18s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-08-23 12:54:04 +02:00
3bd81f01e2 rp2_sd: Increase write timeout
Some SD cards seem to need a bit more time...
2025-08-20 19:51:07 +02:00
6f155ebb55 audiocore: Fix small race window in get_fifo_read_value_blocking
In theory, the FIFO interrupt could occur after getting the
fifo_read_value and reenabling interrupts, but before __wfi is called,
causing a deadlock. Fix this by calling __wfi with interrupts still
disabled. It will return immediately if an interrupt is pending.

Add two __nop to ensure that the CPU has enough instructions between
enabling interrupts and disabling them again at the top of the loop.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-08-20 19:50:51 +02:00
f1de8c6c75 Merge pull request 'micropython: Enable btree module for RPI_PICO_W' (#34) from enable-btree-module into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m0s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 8s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 10s
Reviewed-on: #34
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-08-19 18:10:31 +00:00
3b349af8cf micropython: Enable btree module for RPI_PICO_W
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 4m18s
Check code formatting / Check-C-Format (push) Successful in 9s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
2025-08-19 20:05:01 +02:00
679495bf2b Merge pull request 'Handle partitioned and unpartitioned SD cards' (#32) from sd-partition-support into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m30s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 10s
Reviewed-on: #32
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-08-05 19:51:58 +00:00
e9bd4f72b6 Handle partitioned and unpartitioned SD cards
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m28s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Add utils.MBRPartition to implement basic partitioned device support.

Try to mount partition 1 of SDCard first, if that fails, try to mount
entire device as FAT file system.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-08-05 21:16:09 +02:00
34f9a44cdb schematic: Fix I2S_SD pullup
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m28s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Sparkfun I2S audio board already has MAX98357A SD pin pullup for mono
mode, so external pullup is redundant and can actually cause issues.
Remove it.
2025-07-22 22:07:44 +02:00
2796dbcf16 Merge pull request 'Add SDCard write support' (#31) from standalone-mp3 into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m28s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Reviewed-on: #31
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-07-22 20:04:28 +00:00
ff2a609752 rp2_sd: make debug flags accessible through cmake variables.
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m33s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
2025-07-22 21:39:07 +02:00
2f0d4cc3eb rp2_sd: Add optional read CRC check
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m29s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
2025-07-22 21:30:27 +02:00
7ccab40cd6 rp2_sd: Add write support to SD driver
Add write support to rp2_sd driver.

Cleanup standalone-mp3 test tool and add write test mode.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-07-22 21:30:27 +02:00
96fea9dab6 Add standalone-mp3 test tool 2025-06-26 20:49:03 +02:00
9059da1a70 Merge pull request 'nfc-mp3-demo' (#19) from nfc-mp3-demo into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m3s
Check code formatting / Check-C-Format (push) Successful in 6s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Reviewed-on: #19
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-05-27 18:48:42 +00:00
0353796110 Merge pull request 'micropython: upgrade to 1.25.0 plus necessary changes and fixes.' (#20) from feature/upgrade_micropython_1_25_0_plus_fixes into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m11s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Reviewed-on: #20
Reviewed-by: Matthias Blankertz <matthias@blankertz.org>
2025-05-27 18:28:16 +00:00
ce02daad3a Move playlist handling from mp3player to app
All checks were successful
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 8s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m28s
This completes the move of the nfc-mp3-demo to the new architecture.
2025-05-20 22:04:27 +02:00
7778147b66 app: Implement volume curve 2025-05-20 22:04:27 +02:00
7712c25627 Turn TimerManager into a Singleton 2025-05-20 22:04:27 +02:00
69b6f6e860 build: Copy python files to staging dir for littlefs 2025-05-20 22:04:27 +02:00
903840f982 wip: New architecture
Change PlayerApp to new architecture
 - depedencies injected via named tuple
 - some initial type checking
 - move on button press logic to PlayerApp

TODO: Adapt MP3 player
2025-05-20 22:04:27 +02:00
b477aba94c Add initial button handling 2025-05-20 22:04:27 +02:00
f0c3fe4db8 Use context managers to ensure deinit of audiocore and sd 2025-05-20 22:04:27 +02:00
d02776eea8 Add basic application for playback based on NFC tags
Copy and clean up test.py to main.py. Add app module (currently on
app.py, maybe make it a directory later) for application classes.

Implement app.TimerManager to allow scheduling of delayed events in the
micropython async framework.

Implement app.TagPlaybackManager which handles playing back MP3 files
based on the NFC tag reader. Currently, simply plays all MP3 files in a
folder, for which the folder name matches the tag id, in order. Resume,
random and other features not yet supported.
2025-05-20 22:04:27 +02:00
fb496b6991 nfc: Add tag change notification callback 2025-05-20 22:04:27 +02:00
1b683358d1 micropython: upgrade to 1.25.0 plus necessary changes and fixes.
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m6s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Upgrade and cherry-picks were necessary due to a compilation issue
introduced with gcc 15.1 regarding unterminated string literals. For more
details see https://github.com/micropython/micropython/pull/17269.

Still hot and untested...
2025-05-20 20:13:24 +02:00
91af2087b2 Merge pull request '15-create-filesystem-image-with-firmware' (#17) from 15-create-filesystem-image-with-firmware into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m22s
Check code formatting / Check-C-Format (push) Successful in 32m11s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Reviewed-on: #17
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-04-29 17:37:11 +00:00
7f8282315e Restructure sources
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 5m29s
Check code formatting / Check-C-Format (push) Successful in 9s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
The python and C modules that are supposed to be built into the firmware
image (i.e. those that are in manifest.py or in USER_C_MODULES) have
been moved to the software/modules directory.

The software/src directory should now only contain python scripts and
other files that should be installed to the Picos flash filesystem. The
idea is that these should be those scripts that implement the
application behaviour, as these are the ones that a user who does not
want to build the whole firmware themself wants to modify.
2025-04-01 22:05:30 +02:00
8a8cb85c39 ci: Add firmware with filesystem image to artifacts 2025-04-01 22:05:30 +02:00
81 changed files with 24526 additions and 864 deletions

View File

@@ -1,6 +1,6 @@
---
name: Build RPi Pico firmware image
on:
"on":
push:
jobs:
@@ -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

View File

@@ -25,8 +25,11 @@ jobs:
path: git
- name: Check python
run: |
cd git/software/src &&
find . -iname '*.py' -exec ../../../flake-venv/bin/flake8 {} +
cd git/software && (
find src -iname '*.py' -exec ../../flake-venv/bin/flake8 {} +
find tests -iname '*.py' -exec ../../flake-venv/bin/flake8 {} +
find modules -iname '*.py' -exec ../../flake-venv/bin/flake8 {} +
)
Check-Bash-Shellcheck:
runs-on: ubuntu-22.04-full
steps:

View File

@@ -0,0 +1,24 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
---
name: Run pytests
"on":
push:
jobs:
Check-Pytest:
runs-on: ubuntu-22.04-full
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
path: git
- name: Get dependencies
run: |
python -m venv test-venv
test-venv/bin/pip install -r git/software/tests/requirements.txt
- name: Run pytest
run: |
. test-venv/bin/activate &&
cd git/software &&
pytest

4
.gitignore vendored
View File

@@ -3,5 +3,9 @@ hardware/tonberry-pico/tonberry-pico-backups/
*.kicad_sch-bak
*.kicad_sch.lck
software/build
software/typings
compile_commands.json
.dir-locals.el
.cache
\#*#
__pycache__

3
.gitmodules vendored
View File

@@ -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

11
DEVELOP.md Normal file
View File

@@ -0,0 +1,11 @@
# Developer notes
## How to setup python environment for mypy and pytest
```bash
cd software
python -m venv test-venv
. test-venv/bin/activate
pip install -r tests/requirements.txt
pip install -U micropython-rp2-pico_w-stubs --target typings
```

View File

@@ -0,0 +1,443 @@
(footprint "AZ_Delivery_RC522"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(descr "Through hole straight socket strip, 1x08, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
(tags "Through hole socket strip THT 1x08 2.54mm single row")
(property "Reference" "REF**"
(at 0 -2.77 0)
(layer "F.SilkS")
(uuid "a34ef152-6377-4c69-9f9a-fb0a9cbaaa0f")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Value" "AZ_Delivery_RC522"
(at -1.9 11.03 0)
(layer "F.Fab")
(uuid "eb0c6019-b136-4812-894a-851e8bd21756")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "2d28acfc-38d9-483d-a1f6-5b4a7c5c5208")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "d9f04e4e-c6ea-48a4-82e1-9fd680507783")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr through_hole)
(fp_line
(start -1.33 1.27)
(end -1.33 19.11)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "5c816ce7-1e16-4fef-8a94-56ce4f336409")
)
(fp_line
(start -1.33 1.27)
(end 1.33 1.27)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "31cfc545-00ec-41d5-a909-28f51668cbb9")
)
(fp_line
(start -1.33 19.11)
(end 1.33 19.11)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "6fdb9624-459f-42e6-8474-8476e3e73a19")
)
(fp_line
(start 0 -1.33)
(end 1.33 -1.33)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "673bcfad-3a7d-4a5b-ae2e-68bfc3cb0212")
)
(fp_line
(start 1.33 -1.33)
(end 1.33 0)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "596a7cf5-b26a-4237-9ea9-a0b83ce26be7")
)
(fp_line
(start 1.33 1.27)
(end 1.33 19.11)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "dd0a885d-9fe0-46b4-a266-a488d8626717")
)
(fp_rect
(start -1.9 -9.5)
(end 58.1 30.5)
(stroke
(width 0.1524)
(type solid)
)
(fill no)
(layer "F.SilkS")
(uuid "8f986849-0666-4030-885e-2108faf1da15")
)
(fp_line
(start -1.8 -1.8)
(end 1.75 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "446aed1d-b80f-4b30-b956-5f8297a19941")
)
(fp_line
(start -1.8 19.55)
(end -1.8 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "22ebe2a7-8b67-47c2-bdf8-cdb4cd9951cf")
)
(fp_line
(start 1.75 -1.8)
(end 1.75 19.55)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "fe065b5b-3b98-4eb6-bb1d-7dc77992767f")
)
(fp_line
(start 1.75 19.55)
(end -1.8 19.55)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "3e9ed458-acaf-44bb-9c22-6291112f2c8b")
)
(fp_circle
(center 13.75 -7.02)
(end 16.29 -7.02)
(stroke
(width 0.05)
(type solid)
)
(fill no)
(layer "F.CrtYd")
(uuid "b0fc3695-90f5-41e7-8964-ebecbadeaa08")
)
(fp_circle
(center 13.75 27.98)
(end 16.29 27.98)
(stroke
(width 0.05)
(type solid)
)
(fill no)
(layer "F.CrtYd")
(uuid "37ebb819-cba1-4d26-8219-ba4fc5e3d318")
)
(fp_circle
(center 51.25 -2.22)
(end 53.79 -2.22)
(stroke
(width 0.05)
(type solid)
)
(fill no)
(layer "F.CrtYd")
(uuid "c91279d4-19e9-4d6b-82e4-3abaf6c9450a")
)
(fp_circle
(center 51.25 23.63)
(end 53.79 23.63)
(stroke
(width 0.05)
(type solid)
)
(fill no)
(layer "F.CrtYd")
(uuid "49613ec7-c049-4577-95b9-7914f2d0eeb3")
)
(fp_line
(start -1.27 -1.27)
(end 0.635 -1.27)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "2a668cbf-d67d-4e90-af33-7fe4d2a9e9d2")
)
(fp_line
(start -1.27 19.05)
(end -1.27 -1.27)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "063425b4-bc99-4269-a589-b598140b14bf")
)
(fp_line
(start 0.635 -1.27)
(end 1.27 -0.635)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "a5b3e0e3-31ba-4ee4-a261-36ed072fb6d3")
)
(fp_line
(start 1.27 -0.635)
(end 1.27 19.05)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "626e075f-5f87-420e-9637-1a7362632a52")
)
(fp_line
(start 1.27 19.05)
(end -1.27 19.05)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "92009a7e-4398-40ed-8894-3370eb975c50")
)
(fp_text user "keep out"
(at 29 9 0)
(unlocked yes)
(layer "Dwgs.User")
(uuid "1b8c2096-cda5-41b5-8771-3eac90083181")
(effects
(font
(size 1 1)
(thickness 0.15)
)
(justify left bottom)
)
)
(fp_text user "Antenna"
(at 29 7.5 0)
(unlocked yes)
(layer "Dwgs.User")
(uuid "1b984db0-349b-4503-b267-9f2bd33cff0c")
(effects
(font
(size 1 1)
(thickness 0.15)
)
(justify left bottom)
)
)
(fp_text user "${REFERENCE}"
(at 0 8.89 90)
(layer "F.Fab")
(uuid "1781e910-89a3-48c8-8888-4f06a0dd2fdc")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "" np_thru_hole circle
(at 13.75 -7.02)
(size 3.2 3.2)
(drill 3.2)
(layers "F&B.Cu" "*.Mask")
(uuid "7c71dc53-c556-469e-a5bc-b4c067525bc5")
)
(pad "" np_thru_hole circle
(at 13.75 27.98)
(size 3.2 3.2)
(drill 3.2)
(layers "F&B.Cu" "*.Mask")
(uuid "23726452-e50f-490c-a39e-a24c479dbdd3")
)
(pad "" np_thru_hole circle
(at 51.25 -2.22)
(size 3.2 3.2)
(drill 3.2)
(layers "F&B.Cu" "*.Mask")
(uuid "e3c5ee74-770a-46e8-910d-a5bf1769975b")
)
(pad "" np_thru_hole circle
(at 51.25 23.63)
(size 3.2 3.2)
(drill 3.2)
(layers "F&B.Cu" "*.Mask")
(uuid "d92ad159-9fb8-4328-a573-b78abcdc471f")
)
(pad "1" thru_hole rect
(at 0 0)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "cb8ff318-d964-4ff3-93e9-18ed02ecc716")
)
(pad "2" thru_hole circle
(at 0 2.54)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "171fee8a-00a9-4f1b-bee2-5dc44fbef460")
)
(pad "3" thru_hole circle
(at 0 5.08)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "ff79db69-5af0-439a-b402-c1c0dc0f6a5d")
)
(pad "4" thru_hole circle
(at 0 7.62)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "59f1cdf4-9788-48e6-8ac6-11fe687d19a6")
)
(pad "5" thru_hole circle
(at 0 10.16)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "02a680e0-7902-4b9b-97d5-04111a6ad630")
)
(pad "6" thru_hole circle
(at 0 12.7)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "ea4d2b35-79b2-4d5d-8abe-b0d0675a2a70")
)
(pad "7" thru_hole circle
(at 0 15.24)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "c0db5362-fdbd-4784-9c33-469f195ac9af")
)
(pad "8" thru_hole circle
(at 0 17.78)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "adabdc0d-c196-4362-98fc-523b0680144d")
)
(zone
(net 0)
(net_name "")
(layers "F.Cu" "B.Cu")
(uuid "5e845900-288e-4f42-a8ac-c20c1cc0aa41")
(name "Antenna")
(hatch edge 0.5)
(connect_pads
(clearance 0)
)
(min_thickness 0.25)
(filled_areas_thickness no)
(keepout
(tracks not_allowed)
(vias not_allowed)
(pads not_allowed)
(copperpour not_allowed)
(footprints not_allowed)
)
(placement
(enabled no)
(sheetname "")
)
(fill
(thermal_gap 0.5)
(thermal_bridge_width 0.5)
)
(polygon
(pts
(xy 58.1 -9.5) (xy 15.5 -9.5) (xy 15.5 30.5) (xy 58.1 30.5)
)
)
)
(group ""
(uuid "72a71035-0d89-4c39-b4c6-1fefc66807fa")
(members "23726452-e50f-490c-a39e-a24c479dbdd3" "37ebb819-cba1-4d26-8219-ba4fc5e3d318"
"49613ec7-c049-4577-95b9-7914f2d0eeb3" "7c71dc53-c556-469e-a5bc-b4c067525bc5"
"8f986849-0666-4030-885e-2108faf1da15" "b0fc3695-90f5-41e7-8964-ebecbadeaa08"
"c91279d4-19e9-4d6b-82e4-3abaf6c9450a" "d92ad159-9fb8-4328-a573-b78abcdc471f"
"e3c5ee74-770a-46e8-910d-a5bf1769975b" "eb0c6019-b136-4812-894a-851e8bd21756"
)
)
(embedded_fonts no)
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x08_P2.54mm_Vertical.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)

View File

@@ -0,0 +1,355 @@
(footprint "Adafruit_bq25185"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(descr "Through hole straight socket strip, 1x07, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
(tags "Through hole socket strip THT 1x07 2.54mm single row")
(property "Reference" "REF**"
(at 0 -8.255 0)
(layer "F.SilkS")
(uuid "6e575542-8fac-4808-b8c9-ded669296d0d")
(effects
(font
(size 1.016 1.016)
(thickness 0.1524)
)
)
)
(property "Value" "Adafruit_bq25185"
(at -3.81 23.495 0)
(layer "F.Fab")
(uuid "86edab5c-7f9d-4119-b32b-b12277ace150")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "178de254-c544-4662-881b-f069a6a8f6a5")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "66b522fa-0a20-4d7b-9c1c-01db271771b4")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr through_hole)
(fp_line
(start -1.33 15.24)
(end -1.33 -2.54)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "aca72a9b-9330-4c35-bf1b-82d487a9447f")
)
(fp_line
(start -1.33 17.78)
(end -1.33 16.51)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "518da0df-6fd0-4f31-af5f-48e331dc849c")
)
(fp_line
(start 1.33 15.24)
(end -1.33 15.24)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "e4083416-4300-4fb2-b50c-b38793ec4834")
)
(fp_line
(start 1.33 15.24)
(end 1.33 -2.54)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "9b378274-151c-4b84-ae74-fca52b0e1635")
)
(fp_rect
(start -16.51 -6.985)
(end 2.54 22.225)
(stroke
(width 0.1524)
(type solid)
)
(fill no)
(layer "F.SilkS")
(uuid "07d7f04c-4960-4dfe-92c3-4d957a4244ce")
)
(fp_line
(start -1.8 -1.8)
(end -1.799999 -2.652907)
(stroke
(width 0.05)
(type default)
)
(layer "F.CrtYd")
(uuid "4452e51e-0893-4b70-92fd-70f0b54ced32")
)
(fp_line
(start -1.778371 17.931627)
(end -1.8 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "9f38d32c-752e-406c-81c2-56a23e2124ab")
)
(fp_line
(start 1.75 -1.8)
(end 1.75 -2.604051)
(stroke
(width 0.05)
(type default)
)
(layer "F.CrtYd")
(uuid "355fe774-8482-4f16-ae08-c09c0defb5e5")
)
(fp_line
(start 1.75 -1.8)
(end 1.778373 17.931628)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "f10f2b1d-14a2-4182-8823-7cb2b08a980c")
)
(fp_arc
(start -1.8 -2.652908)
(mid 0.034954 -6.984758)
(end 1.75 -2.604051)
(stroke
(width 0.05)
(type default)
)
(layer "F.CrtYd")
(uuid "f25457eb-8a07-4acd-907f-7cd787994a5c")
)
(fp_arc
(start 1.778373 17.931627)
(mid 0 22.224999)
(end -1.778373 17.931627)
(stroke
(width 0.05)
(type default)
)
(layer "F.CrtYd")
(uuid "b284c369-20df-4fe6-a2a0-f28fcc3b58d6")
)
(fp_circle
(center -13.97 -4.42)
(end -16.485 -4.42)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "bd44fb04-efc3-4813-ad81-2f8b403680df")
)
(fp_circle
(center -13.97 19.71)
(end -16.485 19.71)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "cf8bdd37-9205-40af-8a51-17c2a30f9014")
)
(fp_line
(start -1.27 -2.501639)
(end 1.27 -2.501639)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "7afbc36b-dc93-48ce-b6e3-213a0cc77565")
)
(fp_line
(start -1.27 17.145)
(end -1.27 -2.501639)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "2c7bc528-2d0c-40c9-9e97-d56236d26fe6")
)
(fp_line
(start -0.635 17.78)
(end -1.27 17.145)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "a12a655f-03d7-4c2a-87ca-b0238aca19c3")
)
(fp_line
(start 1.27 -2.501639)
(end 1.27 17.78)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "7a7136ae-feda-4d2a-9391-81bb7d3312d2")
)
(fp_line
(start 1.27 17.78)
(end -0.635 17.78)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "c13c8715-e3c4-4d03-8849-e2a1e26d40a1")
)
(fp_text user "${REFERENCE}"
(at -2.54 12.7 270)
(layer "F.Fab")
(uuid "a8c74cf2-2d56-4366-b45f-d04e448ebc72")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "" np_thru_hole circle
(at -13.97 -4.445)
(size 2.54 2.54)
(drill 2.54)
(layers "F&B.Cu" "*.Mask")
(uuid "356e8d03-c3ad-4b24-bf9f-f35de4f1636d")
)
(pad "" np_thru_hole circle
(at -13.97 19.685)
(size 2.54 2.54)
(drill 2.54)
(layers "F&B.Cu" "*.Mask")
(uuid "c342310e-eb02-4174-9d9c-eb95934aebf5")
)
(pad "" np_thru_hole circle
(at 0 -4.445)
(size 2.54 2.54)
(drill 2.54)
(layers "F&B.Cu" "*.Mask")
(uuid "6306c281-706a-4b36-9e39-5aa956be2d80")
)
(pad "" np_thru_hole circle
(at 0 19.685)
(size 2.54 2.54)
(drill 2.54)
(layers "F&B.Cu" "*.Mask")
(uuid "d01f277d-a18c-4fb7-8bce-ad82f53d5330")
)
(pad "1" thru_hole rect
(at 0 16.51 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "9c199252-bdac-484c-a687-6b4ef26cc3e6")
)
(pad "2" thru_hole circle
(at 0 13.97 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "be025cc7-4905-4574-955b-178576ccc1cc")
)
(pad "3" thru_hole circle
(at 0 11.43 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "e46efbed-3393-457c-95ba-f30245b6478b")
)
(pad "4" thru_hole circle
(at 0 8.89 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "5bb680ed-7bca-4b4a-852d-e6849574c8a6")
)
(pad "5" thru_hole circle
(at 0 6.35 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "59961279-9785-4a81-948d-7855e0e08516")
)
(pad "6" thru_hole circle
(at 0 3.81 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "eda46f52-d12e-47b7-951c-bc4005bc694e")
)
(pad "7" thru_hole circle
(at 0 1.27 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "2cb79b81-6688-44c2-92c7-e75f5df30e91")
)
(pad "8" thru_hole circle
(at 0 -1.27 180)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "b2d6f12f-bf69-4a9b-a3e9-164231e4f483")
)
(embedded_fonts no)
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x08_P2.54mm_Vertical.step"
(offset
(xyz 0 1.25 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz -0 -0 -0)
)
)
)

View File

@@ -0,0 +1,381 @@
(footprint "Sparkfun_MAX98357A"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(descr "Through hole straight socket strip, 1x07, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
(tags "Through hole socket strip THT 1x07 2.54mm single row")
(property "Reference" "REF**"
(at 0 -3.556 0)
(layer "F.SilkS")
(uuid "2656f117-ae27-4bd8-a9c3-f25e38d25a50")
(effects
(font
(size 1.016 1.016)
(thickness 0.1524)
)
)
)
(property "Value" "Sparkfun_MAX98357A"
(at 0 18.01 0)
(layer "F.Fab")
(uuid "e7891e54-c3b2-4045-b729-ec1b29005170")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "aab68a41-9b58-43f5-b6ed-218d51a35c21")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "d5308824-df7c-46bf-b6ad-dfc407c4cafc")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr through_hole)
(fp_line
(start -1.33 -2.54)
(end 19.05 -2.54)
(stroke
(width 0.1524)
(type solid)
)
(layer "F.SilkS")
(uuid "a225c96b-f88a-46ab-8898-d5a961a69e4d")
)
(fp_line
(start -1.33 1.27)
(end -1.33 -2.54)
(stroke
(width 0.1524)
(type solid)
)
(layer "F.SilkS")
(uuid "d9e35813-ad57-481a-bd0f-e96849b81f42")
)
(fp_line
(start -1.33 1.27)
(end -1.33 16.57)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "acaab1d6-f4d6-4ae2-98ff-f3ba2e3cd533")
)
(fp_line
(start -1.33 1.27)
(end 1.33 1.27)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "c2228c31-a76f-4f6c-8dac-e47a00dd4a83")
)
(fp_line
(start -1.33 16.57)
(end 1.33 16.57)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "4067195c-1874-47b5-b0f1-8f78aa17f1fb")
)
(fp_line
(start -1.27 17.78)
(end -1.27 16.57)
(stroke
(width 0.1524)
(type solid)
)
(layer "F.SilkS")
(uuid "b5f022f0-b8d1-4bd3-b372-d667aa83852e")
)
(fp_line
(start 0 -1.33)
(end 1.33 -1.33)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "3672f0c4-cfd3-4ed8-a916-26b23e299b5c")
)
(fp_line
(start 1.33 -1.33)
(end 1.33 0)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "e8fb0117-396a-4d0e-92dd-8c1cd555e5c6")
)
(fp_line
(start 1.33 1.27)
(end 1.33 16.57)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "1ac1a480-cb45-4327-a825-2b9d92cf5a54")
)
(fp_line
(start 19.05 -2.54)
(end 19.05 17.78)
(stroke
(width 0.1524)
(type solid)
)
(layer "F.SilkS")
(uuid "51996aa9-4796-4afe-9489-c6b28405d249")
)
(fp_line
(start 19.05 17.78)
(end -1.27 17.78)
(stroke
(width 0.1524)
(type solid)
)
(layer "F.SilkS")
(uuid "18391d8a-34f3-4119-86e5-c63f37e37070")
)
(fp_line
(start -1.8 -1.8)
(end 1.75 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "858f249c-fdd9-466b-b8a1-4dd799afef6b")
)
(fp_line
(start -1.8 17)
(end -1.8 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "1d1e798f-8b02-4758-b297-16af69eea697")
)
(fp_line
(start 1.75 -1.8)
(end 1.75 17)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "43d35c9f-6401-4fe4-80f8-4316968255a0")
)
(fp_line
(start 1.75 17)
(end -1.8 17)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "35349e7d-9f0b-4de6-b84b-46bf4473917f")
)
(fp_circle
(center 15.875 0.635)
(end 18.415 0.635)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "dfaa1b74-8adb-43a3-83c0-f701d5acdf69")
)
(fp_circle
(center 15.875 14.605)
(end 18.415 14.605)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "F.CrtYd")
(uuid "b6741d2d-655e-4405-bdec-635bf83d8ad5")
)
(fp_line
(start -1.27 -1.27)
(end 0.635 -1.27)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "37e9acde-12d2-4319-a282-cdf2fdbe341a")
)
(fp_line
(start -1.27 16.51)
(end -1.27 -1.27)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "56075374-a1e4-485e-b882-8e4cce7262d5")
)
(fp_line
(start 0.635 -1.27)
(end 1.27 -0.635)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "b0ddd3e4-2153-4cae-8cb5-d50cbfc8f47e")
)
(fp_line
(start 1.27 -0.635)
(end 1.27 16.51)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "2c20ee51-27de-4f59-83a5-2a2f58df65fb")
)
(fp_line
(start 1.27 16.51)
(end -1.27 16.51)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "d6de62ad-e175-43b9-85c8-340c8ab61fbc")
)
(fp_rect
(start -1.3716 -2.6416)
(end 19.1516 17.8816)
(stroke
(width 0.05)
(type solid)
)
(fill no)
(layer "F.Fab")
(uuid "6931ddba-5c8b-4684-9522-4250e5a9daf6")
)
(fp_text user "${REFERENCE}"
(at 0 7.62 90)
(layer "F.Fab")
(uuid "d0f4c06e-6fb2-4a19-9b48-8ad674e44ff7")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "" np_thru_hole circle
(at 15.875 0.635)
(size 3.2 3.2)
(drill 3.2)
(layers "F&B.Cu" "*.Mask")
(uuid "71993a8f-c48d-437a-8a3c-3e63eb969208")
)
(pad "" np_thru_hole circle
(at 15.875 14.605)
(size 3.2 3.2)
(drill 3.2)
(layers "F&B.Cu" "*.Mask")
(uuid "ed9c8b97-5d58-4937-959a-98150878726c")
)
(pad "1" thru_hole rect
(at 0 0)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "1c83ddd7-1eb1-4a63-97b8-8cad20ed8adb")
)
(pad "2" thru_hole circle
(at 0 2.54)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "4b5aedc5-52ef-4559-9513-4cfe7a9290f5")
)
(pad "3" thru_hole circle
(at 0 5.08)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "d2e33fa3-db5c-4923-b9dd-22f0a7d95420")
)
(pad "4" thru_hole circle
(at 0 7.62)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "769680bd-a4f8-488a-9497-b63b5c793581")
)
(pad "5" thru_hole circle
(at 0 10.16)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "995ea8f5-e63f-41d7-93fe-dc90eacb16c7")
)
(pad "6" thru_hole circle
(at 0 12.7)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "3060d1e5-b1f1-47ad-9a39-6410cc4f3629")
)
(pad "7" thru_hole circle
(at 0 15.24)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "8651b2dd-3cf3-4c82-aa1a-ccc59e2c1675")
)
(embedded_fonts no)
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x07_P2.54mm_Vertical.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)

View File

@@ -0,0 +1,306 @@
(footprint "Sparkfun_MicroSD_Breakout"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(descr "Through hole straight socket strip, 1x07, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
(tags "Through hole socket strip THT 1x07 2.54mm single row")
(property "Reference" "REF**"
(at 0 -3.81 0)
(layer "F.SilkS")
(uuid "2649026f-8664-4aec-9bb7-08c001d0263d")
(effects
(font
(size 1.016 1.016)
(thickness 0.1524)
)
)
)
(property "Value" "Sparkfun_MicroSD_Breakout"
(at -8.255 19.05 0)
(layer "F.Fab")
(uuid "befe8006-9d37-47c5-9cb4-916482040426")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "bda792b3-ed8e-4973-9481-8397712eba90")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(layer "F.Fab")
(hide yes)
(uuid "2b01e065-879a-4a28-815b-34368d12b6af")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr through_hole)
(fp_line
(start -1.33 1.27)
(end -1.33 16.57)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "c581ca6d-5026-479b-93ae-7bad6b62ddd9")
)
(fp_line
(start -1.33 1.27)
(end 1.33 1.27)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "d49fc77d-4bea-4acf-adb5-43551cb508f1")
)
(fp_line
(start -1.33 16.57)
(end 1.33 16.57)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "fa1edbcf-d278-4f63-83db-6cdbef0fe0fb")
)
(fp_line
(start 0 -1.33)
(end 1.33 -1.33)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "466d8a50-67a4-4117-b2ef-fd8fa3c112f8")
)
(fp_line
(start 1.33 -1.33)
(end 1.33 0)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "cc6f4d87-5f66-4c7a-b151-a392aa8f66ed")
)
(fp_line
(start 1.33 1.27)
(end 1.33 16.57)
(stroke
(width 0.12)
(type solid)
)
(layer "F.SilkS")
(uuid "dc1b7dd4-ca53-4049-ae81-eb11216e81d9")
)
(fp_rect
(start -20.574 -2.794)
(end 1.778 18.034)
(stroke
(width 0.1524)
(type solid)
)
(fill no)
(layer "F.SilkS")
(uuid "0af85dea-fde2-4a4a-83d8-7c1dc2a9aafa")
)
(fp_line
(start -1.8 -1.8)
(end 1.75 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "abe8217b-b617-4a57-8b1b-aaad9a1a9408")
)
(fp_line
(start -1.8 17)
(end -1.8 -1.8)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "f6453894-baf7-4501-8b5d-b696bc45df5b")
)
(fp_line
(start 1.75 -1.8)
(end 1.75 17)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "9c21be3b-ef13-48a3-a28c-f5586af79a94")
)
(fp_line
(start 1.75 17)
(end -1.8 17)
(stroke
(width 0.05)
(type solid)
)
(layer "F.CrtYd")
(uuid "27ab22be-40bf-42f8-a30e-00bb317bcedd")
)
(fp_line
(start -1.27 -1.27)
(end 0.635 -1.27)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "bbaf3582-2515-4a48-ac3f-6c1eac7a9c7a")
)
(fp_line
(start -1.27 16.51)
(end -1.27 -1.27)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "7ba01148-ed5c-4058-bbbe-f7586e6b0fe3")
)
(fp_line
(start 0.635 -1.27)
(end 1.27 -0.635)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "449b7ca7-e50d-486c-95c9-ec3b9328d1e9")
)
(fp_line
(start 1.27 -0.635)
(end 1.27 16.51)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "a8fe924f-576e-43f4-bdbd-bede8f50e4a9")
)
(fp_line
(start 1.27 16.51)
(end -1.27 16.51)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "bc826cb1-1f13-4927-b007-dbc4e7bf9119")
)
(fp_rect
(start -20.5105 -2.8575)
(end 1.7145 18.0975)
(stroke
(width 0.1)
(type solid)
)
(fill no)
(layer "F.Fab")
(uuid "ff136a20-1e67-47d3-967e-862544e6261a")
)
(fp_text user "${REFERENCE}"
(at 0 7.62 90)
(layer "F.Fab")
(uuid "c75ce234-326d-4a91-a01c-aa463620f982")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "1" thru_hole rect
(at 0 0)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "94205f6f-c0ef-4190-8ab9-3ca1f6dfb4f7")
)
(pad "2" thru_hole circle
(at 0 2.54)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "b734434c-14a9-4700-99dc-f466e6509db7")
)
(pad "3" thru_hole circle
(at 0 5.08)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "971175f6-b389-4741-aaaf-0fb15d3e7fff")
)
(pad "4" thru_hole circle
(at 0 7.62)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "dca213e6-9378-4e06-9621-11be8656a233")
)
(pad "5" thru_hole circle
(at 0 10.16)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "c723245c-962a-4ab8-b0e1-229741e9138e")
)
(pad "6" thru_hole circle
(at 0 12.7)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "9b547c20-1f88-4220-82b8-d72f851fb6a1")
)
(pad "7" thru_hole circle
(at 0 15.24)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "a588ed23-dbc2-406f-bc1c-3c6e69dd99ee")
)
(embedded_fonts no)
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x07_P2.54mm_Vertical.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)

View File

@@ -0,0 +1,4 @@
(fp_lib_table
(version 7)
(lib (name "Modules")(type "KiCad")(uri "${KIPRJMOD}/Modules.pretty")(options "")(descr ""))
)

View File

@@ -0,0 +1,28 @@
(version 1)
(rule "Pad to Silkscreen"
(constraint silk_clearance (min 0.15mm))
(layer outer)
(condition "A.Type == 'pad' && (B.Type == 'text' || B.Type == 'graphic')"))
(rule "drill hole size (mechanical)"
(constraint hole_size (min 0.3mm) (max 6.3mm)))
(rule "Minimum Via Hole Size"
(constraint hole_size (min 0.3mm))
(condition "A.Type == 'via'"))
(rule "Minimum Via Diameter"
(constraint via_diameter (min 20mil))
(condition "A.Type == 'via'"))
(rule "PTH Hole Size"
(constraint hole_size (min 12mil) (max 6.35mm))
(condition "A.isPlated()"))
(rule "Minimum Non-plated Hole Size"
(constraint hole_size (min 0.5mm))
(condition "A.Type == 'pad' && !A.isPlated()"))
(rule "Pad to Track clearance"
(constraint clearance (min 0.2mm))
(condition "A.isPlated() && A.Type != 'Via' && B.Type == 'track'"))

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,12 @@
{
"board": {
"active_layer": 0,
"active_layer": 5,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [],
"hidden_nets": [
"GND"
],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
@@ -63,9 +65,42 @@
"version": 5
},
"net_inspector_panel": {
"col_hidden": [],
"col_order": [],
"col_widths": [],
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
],
"col_widths": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
@@ -76,7 +111,7 @@
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": -1
"sorting_column": 0
},
"open_jobsets": [],
"project": {

View File

@@ -2,12 +2,267 @@
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"rules": {},
"track_widths": [],
"via_dimensions": []
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 3.2,
"height": 3.2,
"width": 3.2
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"min_clearance": 0.0
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [
[
"items_not_allowed|117348000|131000000|4c513f4f-0c90-4a64-b2d1-bf0189c761db|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|117348000|131000000|fea7aba8-73c4-4553-8cc8-83472c47a83b|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|122420000|136975000|8ffc6a0d-806e-4b99-922f-2baead2bd98b|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|148270000|136975000|804b9d74-91fa-4f75-bc54-8f55b801562f|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|148720706|136551802|f2c1e20e-a272-42d8-a551-3e9074b96921|00000000-0000-0000-0000-000000000000",
""
],
[
"nonmirrored_text_on_back_layer|210811000|131073000|e87ca627-5327-4f7e-ac1a-c1eadd662c0a|00000000-0000-0000-0000-000000000000",
""
],
[
"silk_overlap|115550000|131000000|cd433b4c-40ac-48cc-b03e-5acf3357d88b|fea7aba8-73c4-4553-8cc8-83472c47a83b",
""
]
],
"meta": {
"filename": "board_design_settings.json",
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "error",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "error",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.2032,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.2,
"min_groove_width": 0.0,
"min_hole_clearance": 0.35,
"min_hole_to_hole": 0.45,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 1.0,
"min_text_thickness": 0.15,
"min_through_hole_diameter": 0.3,
"min_track_width": 0.2032,
"min_via_annular_width": 0.15,
"min_via_diameter": 0.5,
"solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0,
0.2032,
0.4064,
0.508
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
},
{
"diameter": 0.6,
"drill": 0.3
},
{
"diameter": 0.8,
"drill": 0.4
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"dist": "",
@@ -240,6 +495,7 @@
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
@@ -257,7 +513,7 @@
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"clearance": 0.1778,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
@@ -268,10 +524,20 @@
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"track_width": 0.2032,
"via_diameter": 0.8,
"via_drill": 0.4,
"wire_width": 6
},
{
"clearance": 0.1778,
"name": "Power",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 0,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.4064,
"via_diameter": 0.8,
"via_drill": 0.4
}
],
"meta": {
@@ -279,14 +545,27 @@
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
"netclass_patterns": [
{
"netclass": "Power",
"pattern": "+*"
},
{
"netclass": "Power",
"pattern": "GND"
},
{
"netclass": "Power",
"pattern": "/*BAT"
}
]
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "",
"plot": "pcb_test/",
"pos_files": "",
"specctra_dsn": "",
"step": "",
@@ -347,12 +626,36 @@
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": false,
"label": "#",
"name": "${ITEM_NUMBER}",
"show": false
},
{
"group_by": false,
"label": "Description",
"name": "Description",
"show": false
},
{
"group_by": false,
"label": "Sim.Pins",
"name": "Sim.Pins",
"show": false
},
{
"group_by": false,
"label": "Sim.Device",
"name": "Sim.Device",
"show": false
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": false,
"name": "Grouped By Value",
"name": "",
"sort_asc": true,
"sort_field": "Reference"
},

File diff suppressed because it is too large Load Diff

View File

@@ -50,13 +50,13 @@ 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 {} +
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/modules
)
add_custom_target(clang-format
find . -iname '*.[ch]' -exec clang-format -i {} +
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/modules
)

View File

@@ -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/*

View File

@@ -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")

View File

@@ -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)

View File

@@ -8,7 +8,38 @@ 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
for hwconfig in src/hwconfig_*.py; do
hwconfig_base=$(basename "$hwconfig")
hwname=${hwconfig_base##hwconfig_}
hwname=${hwname%%.py}
find src/ -iname '*.py' \! -iname 'hwconfig_*.py' | cpio -pdm "$FS_STAGE_DIR"
cp "$hwconfig" "$FS_STAGE_DIR"/src/hwconfig.py
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-"$hwname".uf2
rm -r "${FS_STAGE_DIR:?}"/*
done
echo "Output in $BUILDDIR/firmware.uf2"
echo "Images with filesystem in" ${BUILDDIR}firmware-filesystem-*.uf2

View File

@@ -16,14 +16,15 @@ check_command lsusb
check_command picotool
DEVICEPATH=/dev/disk/by-label/RPI-RP2
IMAGEPATH=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2
IMAGEPATH=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
REVISION=Rev1
flash_via_mountpoint()
{
while [ ! -e "$DEVICEPATH" ] ; do sleep 1; echo 'Waiting for RP2...'; done
udisksctl mount -b "$DEVICEPATH"
cp "$IMAGEPATH" "$(findmnt "$DEVICEPATH" -n -o TARGET)"
cp "$IMAGEFILE" "$(findmnt "$DEVICEPATH" -n -o TARGET)"
}
PID="2e8a"
@@ -40,7 +41,7 @@ flash_via_picotool()
local device="${bus_device[1]//[!0-9]/}"
echo "Found RP2 with serial $serial on Bus $bus Device $device"
picotool load --bus "$bus" --address "$device" "$IMAGEPATH"
picotool load --bus "$bus" --address "$device" "$IMAGEFILE"
}
FLASH_VIA_MOUNTPOINT=0
@@ -52,11 +53,12 @@ usage()
echo
echo " -m, --via-mountpoint Mount first found RP2 and flash image by"
echo " copying to mountpoint."
echo " -r, --revision <rev> Hardware revision to flash. Default is Rev1"
echo " -h, --help Print this text and exit."
exit 2
}
PARSED_ARGUMENTS=$(getopt -a -n "$0" -o mh --long via-mountpoint,help -- "$@")
PARSED_ARGUMENTS=$(getopt -a -n "$0" -o mhr: --long via-mountpoint,revision:,help -- "$@")
# shellcheck disable=SC2181
# Indirect getopt return value checking is okay here
if [ "$?" != "0" ]; then
@@ -68,6 +70,7 @@ while :
do
case "$1" in
-m | --via-mountpoint) FLASH_VIA_MOUNTPOINT=1 ; shift ;;
-r | --revision) REVISION=$2 ; shift 2 ;;
-h | --help) usage ;;
--) shift; break ;;
*) echo "Unexpected option: $1"
@@ -80,6 +83,8 @@ if [ $# -gt 0 ]; then
usage
fi
IMAGEFILE="$IMAGEPATH"/firmware-filesystem-$REVISION.uf2
if [ "$FLASH_VIA_MOUNTPOINT" -eq 0 ]; then
flash_via_picotool
else

View File

@@ -40,7 +40,7 @@ void __time_critical_func(core1_main)(void)
{
uint32_t ret = 0;
bool running = true, playing = false;
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base)) {
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base, shared_context.sideset_dclk_first)) {
ret = MP_EIO;
goto out;
}

View File

@@ -31,6 +31,7 @@ struct audiocore_shared_context {
// Set by module.c before core1 is launched and then never changed, can be read without lock
int out_pin, sideset_base, samplerate;
bool sideset_dclk_first;
// Must hold lock. The indices 0..MP3_BUFFER_PREAREA-1 may only be read and written on core1 (no
// lock needed) The buffer is aligned to, and MP3_BUFFER_PREAREA is a multiple of, the machine

View File

@@ -1,13 +1,19 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
import _audiocore
from asyncio import ThreadSafeFlag
from utils import get_pin_index
class Audiocore:
def __init__(self, pin, sideset):
def __init__(self, din, dclk, lrclk):
# PIO requires sideset pins to be adjacent
assert get_pin_index(lrclk) == get_pin_index(dclk)+1 or get_pin_index(lrclk) == get_pin_index(dclk)-1
self.notify = ThreadSafeFlag()
self._audiocore = _audiocore.Audiocore(pin, sideset, self._interrupt)
self._audiocore = _audiocore.Audiocore(din, dclk, lrclk, self._interrupt)
def __del__(self):
def deinit(self):
self._audiocore.deinit()
def _interrupt(self, _):
@@ -35,3 +41,17 @@ class Audiocore:
if pos >= len(buffer):
return (pos, buf_space, underruns)
await self.notify.wait()
class AudioContext:
def __init__(self, din, dclk, lrclk):
self.din = din
self.dclk = dclk
self.lrclk = lrclk
def __enter__(self):
self._audiocore = Audiocore(self.din, self.dclk, self.lrclk)
return self._audiocore
def __exit__(self, exc_type, exc_value, traceback):
self._audiocore.deinit()

View File

@@ -113,17 +113,18 @@ void i2s_stop(void)
pio_sm_clear_fifos(audiocore_pio, i2s_context.pio_sm);
}
bool i2s_init(int out_pin, int sideset_base)
bool i2s_init(int out_pin, int sideset_base, bool dclk_first)
{
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * AUDIO_BUFS * I2S_DMA_BUF_SIZE);
if (!pio_can_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program))
const pio_program_t *program = dclk_first ? &i2s_max98357_program : &i2s_max98357_lrclk_program;
if (!pio_can_add_program(audiocore_pio, program))
return false;
i2s_context.pio_sm = pio_claim_unused_sm(audiocore_pio, false);
i2s_context.out_pin = out_pin;
i2s_context.sideset_base = sideset_base;
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_context.pio_program_offset = pio_add_program(audiocore_pio, program);
i2s_context.dma_ch = dma_claim_unused_channel(false);
if (i2s_context.dma_ch == -1)

View File

@@ -8,7 +8,7 @@
#define I2S_DMA_BUF_SIZE (1152)
bool i2s_init(int out_pin, int sideset_base);
bool i2s_init(int out_pin, int sideset_base, bool dclk_first);
void i2s_deinit(void);
void i2s_play(int samplerate);

View File

@@ -0,0 +1,91 @@
// 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-LRCLK, 1-BCLK
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
.program i2s_max98357_lrclk
.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 2
startup_loop:
nop side 1
jmp x-- startup_loop side 3
nop side 0
set x, 14 side 2
left_loop:
.wrap_target
out pins, 1 side 0
jmp x-- left_loop side 2
out pins, 1 side 1
set x, 14 side 3
right_loop:
out pins, 1 side 1
jmp x-- right_loop side 3
out pins, 1 side 0
set x, 14 side 2
.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

@@ -50,10 +50,14 @@ static uint32_t get_fifo_read_value_blocking(struct audiocore_obj *obj)
const long flags = save_and_disable_interrupts();
const uint32_t value = obj->fifo_read_value;
obj->fifo_read_value = 0;
if (value & AUDIOCORE_FIFO_DATA_FLAG) {
restore_interrupts(flags);
if (value & AUDIOCORE_FIFO_DATA_FLAG)
return value & ~AUDIOCORE_FIFO_DATA_FLAG;
}
__wfi();
restore_interrupts(flags);
__nop(); // Ensure at least two instructions between enable interrupts and subsequent disable
__nop();
}
}
@@ -90,9 +94,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 +148,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();
@@ -166,10 +170,11 @@ static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_set_volume_obj, audiocore_set_volume)
*/
static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
{
enum { ARG_pin, ARG_sideset, ARG_handler };
enum { ARG_pin, ARG_dclk, ARG_lrclk, ARG_handler };
static const mp_arg_t allowed_args[] = {
{MP_QSTR_pin, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
{MP_QSTR_sideset, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
{MP_QSTR_dclk, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
{MP_QSTR_lrclk, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
{MP_QSTR_handler, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
};
if (initialized)
@@ -184,7 +189,8 @@ static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_ob
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
const mp_hal_pin_obj_t pin = mp_hal_get_pin_obj(args[ARG_pin].u_obj);
const mp_hal_pin_obj_t sideset_pin = mp_hal_get_pin_obj(args[ARG_sideset].u_obj);
const mp_hal_pin_obj_t dclk_pin = mp_hal_get_pin_obj(args[ARG_dclk].u_obj);
const mp_hal_pin_obj_t lrclk_pin = mp_hal_get_pin_obj(args[ARG_lrclk].u_obj);
if (args[ARG_handler].u_obj != MP_OBJ_NULL) {
obj->irq_obj = mp_irq_new(&audiocore_irq_methods, MP_OBJ_FROM_PTR(obj));
obj->irq_obj->handler = args[ARG_handler].u_obj;
@@ -199,7 +205,14 @@ static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_ob
memset(shared_context.mp3_buffer, 0, MP3_BUFFER_PREAREA + MP3_BUFFER_SIZE);
multicore_reset_core1();
shared_context.out_pin = pin;
shared_context.sideset_base = sideset_pin;
// PIO requires sideset pins to be adjacent, but we support both dclk first and lrclk first
if (lrclk_pin == dclk_pin + 1) {
shared_context.sideset_base = dclk_pin;
shared_context.sideset_dclk_first = true;
} else {
shared_context.sideset_base = lrclk_pin;
shared_context.sideset_dclk_first = false;
}
initialized = true;
multicore_launch_core1(&core1_main);
uint32_t result = get_fifo_read_value_blocking(obj);

View File

@@ -15,7 +15,7 @@ static unsigned multicore_fifo_push_last;
static unsigned (*multicore_fifo_pop_blocking_cb)(void);
bool i2s_init(int out_pin, int sideset_base)
bool i2s_init(int out_pin, int sideset_base, bool dclk_first)
{
TEST_ASSERT_FALSE(i2s_initialized);
if (i2s_init_return)

View File

@@ -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);

View File

@@ -23,7 +23,7 @@ static bool sd_acmd(const uint8_t cmd, const uint32_t arg, unsigned resplen, uin
static bool sd_early_init(void)
{
uint8_t buf;
for (int i = 0; i < 5; ++i) {
for (int i = 0; i < 500; ++i) {
if (sd_cmd(0, 0, 1, &buf)) {
#ifdef SD_DEBUG
printf("CMD0 resp %02hhx\n", buf);
@@ -63,7 +63,7 @@ static bool sd_send_op_cond(void)
{
uint8_t buf;
bool use_acmd = true;
for (int timeout = 0; timeout < 500; ++timeout) {
for (int timeout = 0; timeout < 50000; ++timeout) {
bool result = false;
if (use_acmd)
result = sd_acmd(41, 0x40000000, 1, &buf);
@@ -74,6 +74,7 @@ static bool sd_send_op_cond(void)
#ifdef SD_DEBUG
printf("sd_init: card does not understand ACMD41, try CMD1...\n");
#endif
use_acmd = false;
continue;
} else if (buf != 0x01) {
printf("sd_init: send_op_cond failed\n");
@@ -174,6 +175,7 @@ static bool sd_read_csd(struct sd_context *sd_context)
}
}
sd_context->blocks = blocks;
sd_context->blocksize = blocksize;
#ifdef SD_DEBUG
printf("CSD version %u.0, blocksize %u, blocks %u, capacity %llu MiB\n", version, blocksize, blocks,
((uint64_t)blocksize * blocks) / (1024 * 1024));
@@ -225,6 +227,26 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
return false;
}
if (sd_context->blocksize != SD_SECTOR_SIZE) {
if (sd_context->blocksize != 1024 && sd_context->blocksize != 2048) {
printf("sd_init: Unsupported block size %u\n", sd_context->blocksize);
return false;
}
// Attempt SET_BLOCKLEN command
uint8_t resp[1];
if (!sd_cmd(16, SD_SECTOR_SIZE, 1, resp)) {
printf("sd_init: SET_BLOCKLEN failed\n");
return false;
}
// Successfully set blocksize to SD_SECTOR_SIZE, adjust context
sd_context->blocks *= sd_context->blocksize / SD_SECTOR_SIZE;
#ifdef SD_DEBUG
printf("Adjusted blocksize from %u to 512, card now has %u blocks\n", sd_context->blocksize,
sd_context->blocks);
#endif
sd_context->blocksize = SD_SECTOR_SIZE;
}
#ifdef SD_DEBUG
sd_dump_cid();
#endif
@@ -246,14 +268,24 @@ bool sd_readblock(struct sd_context *sd_context, size_t sector_num, uint8_t buff
if (!sd_context->initialized || sector_num >= sd_context->blocks)
return false;
return sd_cmd_read(17, sector_num, SD_SECTOR_SIZE, buffer);
uint32_t addr = sector_num;
if (!sd_context->sdhc_sdxc) {
// SDSC cards used byte addressing
addr *= SD_SECTOR_SIZE;
}
return sd_cmd_read(17, addr, SD_SECTOR_SIZE, buffer);
}
bool sd_readblock_start(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE])
{
if (!sd_context->initialized || sector_num >= sd_context->blocks)
return false;
return sd_cmd_read_start(17, sector_num, SD_SECTOR_SIZE, buffer);
uint32_t addr = sector_num;
if (!sd_context->sdhc_sdxc) {
// SDSC cards used byte addressing
addr *= SD_SECTOR_SIZE;
}
return sd_cmd_read_start(17, addr, SD_SECTOR_SIZE, buffer);
}
bool sd_readblock_complete(struct sd_context *sd_context)
@@ -265,3 +297,16 @@ 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;
uint32_t addr = sector_num;
if (!sd_context->sdhc_sdxc) {
// SDSC cards used byte addressing
addr *= SD_SECTOR_SIZE;
}
return sd_cmd_write(24, addr, SD_SECTOR_SIZE, buffer);
}

View File

@@ -8,6 +8,7 @@
struct sd_context {
size_t blocks;
size_t blocksize;
bool initialized;
bool old_card;
bool sdhc_sdxc;
@@ -21,3 +22,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]);

View File

@@ -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 < 131072; ++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);

View File

@@ -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]);

View 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;
}

9
software/mypy.ini Normal file
View File

@@ -0,0 +1,9 @@
[mypy]
platform = linux
mypy_path = $MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/typings
custom_typeshed_dir = $MYPY_CONFIG_FILE_DIR/typings
follow_imports = silent
exclude = "typings[\\/].*"
follow_imports_for_stubs = true
no_site_packages = true
check_untyped_defs = true

3
software/pytest.ini Normal file
View File

@@ -0,0 +1,3 @@
[pytest]
pythonpath = tests/mocks src
testpaths = tests

93
software/src/app.py Normal file
View 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)

View File

@@ -0,0 +1,65 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
import machine
from machine import Pin
# SD Card SPI
SD_DI = Pin.board.GP3
SD_DO = Pin.board.GP4
SD_SCK = Pin.board.GP2
SD_CS = Pin.board.GP5
SD_CLOCKRATE = 16000000
# MAX98357
I2S_LRCLK = Pin.board.GP6
I2S_DCLK = Pin.board.GP7
I2S_DIN = Pin.board.GP8
I2S_SD = Pin.board.GP9
# RC522
RC522_SPIID = 1
RC522_RST = Pin.board.GP14
RC522_IRQ = Pin.board.GP15
RC522_MOSI = Pin.board.GP11
RC522_MISO = Pin.board.GP12
RC522_SCK = Pin.board.GP10
RC522_SS = Pin.board.GP13
# WS2812
LED_DIN = Pin.board.GP16
LED_COUNT = 1
# Buttons
BUTTON_VOLUP = Pin.board.GP17
BUTTON_VOLDOWN = Pin.board.GP19
BUTTON_NEXT = Pin.board.GP18
BUTTON_POWER = Pin.board.GP21
# Power
POWER_EN = Pin.board.GP22
VBAT_ADC = Pin.board.GP26
def board_init():
# Keep power turned on
# TODO: Implement soft power off
POWER_EN.init(mode=Pin.OUT)
POWER_EN.value(1)
# SD_DO / MISO input doesn't need any special configuration
# Set 8 mA drive strength for SCK and MOSI
machine.mem32[0x4001c004 + 2*4] = 0x71 # SCK
machine.mem32[0x4001c004 + 3*4] = 0x71 # MOSI
# SD_CS doesn't need any special configuration
# Permanently enable amplifier
# TODO: Implement amplifier power management
I2S_SD.init(mode=Pin.OPEN_DRAIN)
I2S_SD.value(1)
def get_battery_voltage():
adc = machine.ADC(VBAT_ADC) # create ADC object on ADC pin
battv = adc.read_u16()/65535.0*3.3*2
return battv

View File

@@ -0,0 +1,49 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
from machine import Pin
# SD Card SPI
SD_DI = Pin.board.GP3
SD_DO = Pin.board.GP4
SD_SCK = Pin.board.GP2
SD_CS = Pin.board.GP5
SD_CLOCKRATE = 15000000
# MAX98357
I2S_LRCLK = Pin.board.GP7
I2S_DCLK = Pin.board.GP6
I2S_DIN = Pin.board.GP8
I2S_SD = None
# RC522
RC522_SPIID = 1
RC522_RST = Pin.board.GP9
RC522_IRQ = Pin.board.GP14
RC522_MOSI = Pin.board.GP11
RC522_MISO = Pin.board.GP12
RC522_SCK = Pin.board.GP10
RC522_SS = Pin.board.GP13
# WS2812
LED_DIN = Pin.board.GP16
LED_COUNT = 1
# Buttons
BUTTON_VOLUP = Pin.board.GP17
BUTTON_VOLDOWN = Pin.board.GP19
BUTTON_NEXT = Pin.board.GP18
BUTTON_POWER = None
# Power
POWER_EN = None
VBAT_ADC = Pin.board.GP26
def board_init():
pass
def get_battery_voltage():
# Not supported on breadboard
return None

89
software/src/main.py Normal file
View File

@@ -0,0 +1,89 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
import aiorepl # type: ignore
import asyncio
import machine
import micropython
import time
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
try:
import hwconfig
except ImportError:
print("Fatal: No hwconfig.py found")
raise
micropython.alloc_emergency_exception_buf(100)
# Machine setup
hwconfig.board_init()
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.05
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.02 * leds
before = time.ticks_ms()
await np.async_write()
now = time.ticks_ms()
if before + 20 > now:
await asyncio.sleep_ms(20 - (now - before))
# high prio for proc 1
machine.mem32[0x40030000 + 0x00] = 0x10
def run():
asyncio.new_event_loop()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, hwconfig.LED_COUNT, sm=1)
asyncio.create_task(rainbow(np))
# Setup MP3 player
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
baudrate=hwconfig.SD_CLOCKRATE), \
AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx:
# Setup NFC
reader = MFRC522(spi_id=hwconfig.RC522_SPIID, sck=hwconfig.RC522_SCK, miso=hwconfig.RC522_MISO,
mosi=hwconfig.RC522_MOSI, cs=hwconfig.RC522_SS, rst=hwconfig.RC522_RST, 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, pin_volup=hwconfig.BUTTON_VOLUP,
pin_voldown=hwconfig.BUTTON_VOLDOWN,
pin_next=hwconfig.BUTTON_NEXT))
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(1)
run()

View File

@@ -3,69 +3,59 @@
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)
bytes_read = stream.readinto(data)
if bytes_read == 0:
# End of file
break
@@ -73,48 +63,11 @@ class MP3Player:
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')
# 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)

View File

@@ -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)

View File

@@ -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)
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.
'''
while True:
self.reader.init()
# For now we omit the tag type
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
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)

View File

@@ -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; */
/* } */

View File

@@ -0,0 +1,10 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
from utils.buttons import Buttons
from utils.mbrpartition import MBRPartition
from utils.pinindex import get_pin_index
from utils.sdcontext import SDContext
from utils.timer import TimerManager
__all__ = ["Buttons", "get_pin_index", "MBRPartition", "SDContext", "TimerManager"]

View 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)

View 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)

View File

@@ -0,0 +1,43 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
from machine import Pin
pins = [Pin.board.GP0,
Pin.board.GP1,
Pin.board.GP2,
Pin.board.GP3,
Pin.board.GP4,
Pin.board.GP5,
Pin.board.GP6,
Pin.board.GP7,
Pin.board.GP8,
Pin.board.GP9,
Pin.board.GP10,
Pin.board.GP11,
Pin.board.GP12,
Pin.board.GP13,
Pin.board.GP14,
Pin.board.GP15,
Pin.board.GP16,
Pin.board.GP17,
Pin.board.GP18,
Pin.board.GP19,
Pin.board.GP20,
Pin.board.GP21,
Pin.board.GP22,
None, # 23
None, # 24
None, # 25
Pin.board.GP26,
Pin.board.GP27,
Pin.board.GP28,
]
def get_pin_index(pin: Pin) -> int:
"""
Get the pin index back from a pin object.
Unfortunately, micropython has no built-in function for this.
"""
return pins.index(pin)

View 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()

View 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()

View File

@@ -0,0 +1,40 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
class Pin:
def __init__(self, idx):
self.idx = idx
board = None
class Board:
GP0 = Pin(0)
GP1 = Pin(1)
GP2 = Pin(2)
GP3 = Pin(3)
GP4 = Pin(4)
GP5 = Pin(5)
GP6 = Pin(6)
GP7 = Pin(7)
GP8 = Pin(8)
GP9 = Pin(9)
GP10 = Pin(10)
GP11 = Pin(11)
GP12 = Pin(12)
GP13 = Pin(13)
GP14 = Pin(14)
GP15 = Pin(15)
GP16 = Pin(16)
GP17 = Pin(17)
GP18 = Pin(18)
GP19 = Pin(19)
GP20 = Pin(20)
GP21 = Pin(21)
GP22 = Pin(22)
GP26 = Pin(26)
GP27 = Pin(27)
GP28 = Pin(28)
Pin.board = Board

View File

@@ -0,0 +1,2 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>

View File

@@ -0,0 +1,5 @@
# SPDX-License-Identifier: MIT
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
class SDCard():
pass

View File

@@ -0,0 +1 @@
pytest

View File

@@ -0,0 +1,3 @@
def test_dummy():
# This is just a dummy test to make pytest not fail because no tests are defined
pass

View File

View 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")

View 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;
}

View 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);

View 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");
}

View File

@@ -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