1 Commits

Author SHA1 Message Date
10fb65ae40 docu: current architecture draft; layout slightly suboptimal.
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m0s
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-05-20 19:09:14 +02:00
78 changed files with 770 additions and 23603 deletions

View File

@@ -18,8 +18,3 @@ 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,11 +25,8 @@ jobs:
path: git
- name: Check python
run: |
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 {} +
)
cd git/software/src &&
find . -iname '*.py' -exec ../../../flake-venv/bin/flake8 {} +
Check-Bash-Shellcheck:
runs-on: ubuntu-22.04-full
steps:

View File

@@ -1,24 +0,0 @@
# 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,9 +3,5 @@ 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,6 +7,3 @@
[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

View File

@@ -1,11 +0,0 @@
# 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

@@ -1,443 +0,0 @@
(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

@@ -1,355 +0,0 @@
(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_1x07_P2.54mm_Vertical.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)

View File

@@ -1,381 +0,0 @@
(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

@@ -1,306 +0,0 @@
(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

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

View File

@@ -1,28 +0,0 @@
(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,12 +1,10 @@
{
"board": {
"active_layer": 5,
"active_layer": 0,
"active_layer_preset": "",
"auto_track_width": true,
"hidden_netclasses": [],
"hidden_nets": [
"GND"
],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
@@ -65,42 +63,9 @@
"version": 5
},
"net_inspector_panel": {
"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
],
"col_hidden": [],
"col_order": [],
"col_widths": [],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
@@ -111,7 +76,7 @@
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 0
"sorting_column": -1
},
"open_jobsets": [],
"project": {

View File

@@ -2,275 +2,12 @@
"board": {
"3dviewports": [],
"design_settings": {
"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",
""
],
[
"lib_footprint_mismatch|117348000|41148000|9810a3ba-e3fe-4afe-955a-bea6c56712c1|00000000-0000-0000-0000-000000000000",
""
],
[
"lib_footprint_mismatch|210176000|131198000|bf46a807-edd1-4c31-a613-56c8fe7b1e6b|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.0,
"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
"defaults": {},
"diff_pair_dimensions": [],
"drc_exclusions": [],
"rules": {},
"track_widths": [],
"via_dimensions": []
},
"ipc2581": {
"dist": "",
@@ -503,7 +240,6 @@
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
@@ -521,7 +257,7 @@
"classes": [
{
"bus_width": 12,
"clearance": 0.1778,
"clearance": 0.2,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
@@ -532,20 +268,10 @@
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2032,
"via_diameter": 0.8,
"via_drill": 0.4,
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"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": {
@@ -553,27 +279,14 @@
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": [
{
"netclass": "Power",
"pattern": "+*"
},
{
"netclass": "Power",
"pattern": "GND"
},
{
"netclass": "Power",
"pattern": "/*BAT"
}
]
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "pcb_test/",
"plot": "",
"pos_files": "",
"specctra_dsn": "",
"step": "",
@@ -634,36 +347,12 @@
"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": "",
"name": "Grouped By Value",
"sort_asc": true,
"sort_field": "Reference"
},

File diff suppressed because it is too large Load Diff

View File

@@ -50,7 +50,7 @@ add_test(NAME generate-xml-report
set_tests_properties(clean-reports PROPERTIES FIXTURES_SETUP "Report")
set_tests_properties(generate-xml-report PROPERTIES FIXTURES_CLEANUP "Report")
add_subdirectory(modules/audiocore)
add_subdirectory(src/audiocore)
add_custom_target(check-format
find . -iname '*.[ch]' -exec clang-format -Werror --dry-run {} +

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 cpio \
apt install -y --no-install-recommends cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib \
&& apt clean && rm -rf /var/lib/apt/lists/*

View File

@@ -5,7 +5,8 @@ require("bundle-networking")
# Bluetooth
require("aioble")
# AsyncIO REPL
module("rp2_neopixel.py", "../../src")
require("sdcard")
require("aiorepl")
# Third party modules
@@ -13,5 +14,5 @@ module("mfrc522.py", "../../lib/micropython-mfrc522/")
module("microdot.py", "../../lib/microdot/src/microdot/")
# TonberryPico modules
module("audiocore.py", "../../modules/audiocore")
module("rp2_neopixel.py", "../../modules")
module("audiocore.py", "../../src/audiocore")
package("nfc", base_path="../../src/")

View File

@@ -10,8 +10,6 @@ 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

@@ -1,12 +1,12 @@
// Board and hardware specific configuration
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico W"
#define MICROPY_HW_BOARD_NAME "Raspberry Pi Pico W"
// todo: We need something to check our binary size
#define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024)
#define MICROPY_HW_FLASH_STORAGE_BYTES (848 * 1024)
// Enable networking.
#define MICROPY_PY_NETWORK 1
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "PicoW"
#define MICROPY_PY_NETWORK_HOSTNAME_DEFAULT "PicoW"
// CYW43 driver configuration.
#define CYW43_USE_SPI (1)
@@ -18,10 +18,10 @@
// Debug level (0-4) 1=warning, 2=info, 3=debug, 4=verbose
// #define MODUSSL_MBEDTLS_DEBUG_LEVEL 1
#define MICROPY_HW_PIN_EXT_COUNT CYW43_WL_GPIO_COUNT
#define MICROPY_HW_PIN_EXT_COUNT CYW43_WL_GPIO_COUNT
// If this returns true for a pin then its irq will not be disabled on a soft reboot
int mp_hal_is_pin_reserved(int n);
#define MICROPY_HW_PIN_RESERVED(i) mp_hal_is_pin_reserved(i)
#define MICROPY_PY_THREAD (0)
#define MICROPY_PY_THREAD (0)

View File

@@ -8,31 +8,7 @@ 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"/modules/micropython.cmake -j "$(nproc)"
USER_C_MODULES="$TOPDIR"/src/micropython.cmake -j "$(nproc)"
)
( cd tools/mklittlefs
make -j "$(nproc)"
)
PICOTOOL=picotool
if ! command -v $PICOTOOL >/dev/null 2>&1; then
echo "system picotool not found, checking SDK build dir"
PICOTOOL=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/_deps/picotool-build/picotool
if ! command -v $PICOTOOL >/dev/null 2>&1; then
echo "No picotool found, exiting"
exit 1
fi
fi
BUILDDIR=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
FS_STAGE_DIR=$(mktemp -d)
trap 'rm -rf $FS_STAGE_DIR' EXIT
find src/ -iname '*.py' | cpio -pdm "$FS_STAGE_DIR"
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/src $BUILDDIR/filesystem.bin
truncate -s 2M $BUILDDIR/firmware-filesystem.bin
dd if=$BUILDDIR/firmware.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k
dd if=$BUILDDIR/filesystem.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k seek=1200
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin $BUILDDIR/firmware-filesystem.uf2
echo "Output in $BUILDDIR/firmware.uf2"
echo "Image with filesystem in $BUILDDIR/firmware-filesystem.uf2"
echo "Output in lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2"

View File

@@ -0,0 +1,60 @@
@startuml
component "Application Context" {
component PlayerApp [
PlayerApp
- manages playlists
- random or sequential
- resumable or not
]
component MP3Player [
MP3Player
- stream bytes to core1
- track playback position in time and bytes
]
component LED [
LED
]
component NFCReader [
NFCReader
- poll nfc
]
component Button [
Button
- debounce
- key repeat
]
}
component Filesystem {
component MP3
component Database
}
[PlayerApp] -left-> [MP3Player] : startPlayback(file)
[PlayerApp] -left-> [MP3Player] : stop()
[PlayerApp] -left-> [MP3Player] : pause() : current_position
[PlayerApp] -up-> [LED] : set()
[MP3Player] -down-> [Audiocore] : mp3_bytestream
[MP3Player] -down-> [Audiocore] : volume
[Audiocore] -up-> [MP3Player] : position
[PlayerApp] -up--> [Filesystem] : readdir
[PlayerApp] -up--> [Filesystem] : open
[NFCReader] -left-> [PlayerApp] : onTagChanged
[NFCReader] <--> [MFRC522]
[Button] -left-> [PlayerApp] : onButtonEvent
[Button] <--> [GPIO]
[WebAPI] <---> [PlayerApp] : ? (e.g. stop)
[WebAPI] ---> [Filesystem]
@enduml

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

@@ -1,9 +0,0 @@
[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

View File

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

View File

@@ -1,93 +0,0 @@
# 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

@@ -5,11 +5,9 @@ from asyncio import ThreadSafeFlag
class Audiocore:
def __init__(self, pin, sideset):
self.notify = ThreadSafeFlag()
self.pin = pin
self.sideset = sideset
self._audiocore = _audiocore.Audiocore(self.pin, self.sideset, self._interrupt)
self._audiocore = _audiocore.Audiocore(pin, sideset, self._interrupt)
def deinit(self):
def __del__(self):
self._audiocore.deinit()
def _interrupt(self, _):
@@ -37,16 +35,3 @@ class Audiocore:
if pos >= len(buffer):
return (pos, buf_space, underruns)
await self.notify.wait()
class AudioContext:
def __init__(self, pin, sideset):
self.pin = pin
self.sideset = sideset
def __enter__(self):
self._audiocore = Audiocore(self.pin, self.sideset)
return self._audiocore
def __exit__(self, exc_type, exc_value, traceback):
self._audiocore.deinit()

View File

@@ -50,14 +50,10 @@ 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);
return value & ~AUDIOCORE_FIFO_DATA_FLAG;
}
__wfi();
restore_interrupts(flags);
__nop(); // Ensure at least two instructions between enable interrupts and subsequent disable
__nop();
if (value & AUDIOCORE_FIFO_DATA_FLAG)
return value & ~AUDIOCORE_FIFO_DATA_FLAG;
__wfi();
}
}
@@ -94,9 +90,9 @@ static mp_obj_t audiocore_put(mp_obj_t self_in, mp_obj_t buffer)
(void)self;
mp_buffer_info_t bufinfo;
if (!mp_get_buffer(buffer, &bufinfo, MP_BUFFER_READ))
mp_raise_ValueError(MP_ERROR_TEXT("not a read buffer"));
mp_raise_ValueError("not a read buffer");
if (bufinfo.typecode != 'b' && bufinfo.typecode != 'B')
mp_raise_ValueError(MP_ERROR_TEXT("unsupported buffer type"));
mp_raise_ValueError("unsupported buffer type");
unsigned to_copy = bufinfo.len;
const uint32_t flags = spin_lock_blocking(shared_context.lock);
@@ -148,7 +144,7 @@ static mp_obj_t audiocore_set_volume(mp_obj_t self_in, mp_obj_t volume_obj)
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
const int volume = mp_obj_get_int(volume_obj);
if (volume < 0 || volume > 255)
mp_raise_ValueError(MP_ERROR_TEXT("volume out of range"));
mp_raise_ValueError("volume out of range");
multicore_fifo_push_blocking(AUDIOCORE_CMD_SET_VOLUME);
multicore_fifo_push_blocking(AUDIOCORE_MAX_VOLUME * volume / 255);
wake_core1();

View File

@@ -1,84 +0,0 @@
# 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 machine import Pin
from math import pi, sin, pow
# Own modules
import app
from audiocore import AudioContext
from mfrc522 import MFRC522
from mp3player import MP3Player
from nfc import Nfc
from rp2_neopixel import NeoPixel
from utils import Buttons, SDContext, TimerManager
micropython.alloc_emergency_exception_buf(100)
async def rainbow(np, period=10):
def gamma(value, X=2.2):
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
brightness = 0.5
count = 0.0
leds = len(np)
while True:
for i in range(leds):
ofs = (count + i) % leds
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
count += 0.2
before = time.ticks_ms()
await np.async_write()
now = time.ticks_ms()
if before + 20 > now:
await asyncio.sleep_ms(20 - (now - before))
# Machine setup
# Set 8 mA drive strength and fast slew rate
machine.mem32[0x4001c004 + 6*4] = 0x67
machine.mem32[0x4001c004 + 7*4] = 0x67
machine.mem32[0x4001c004 + 8*4] = 0x67
# high prio for proc 1
machine.mem32[0x40030000 + 0x00] = 0x10
def run():
asyncio.new_event_loop()
# Setup LEDs
pin = Pin.board.GP16
np = NeoPixel(pin, 10, sm=1)
asyncio.create_task(rainbow(np))
# Setup MP3 player
with SDContext(mosi=Pin(3), miso=Pin(4), sck=Pin(2), ss=Pin(5), baudrate=15000000), \
AudioContext(Pin(8), Pin(6)) as audioctx:
# Setup NFC
reader = MFRC522(spi_id=1, sck=10, miso=12, mosi=11, cs=13, rst=9, tocard_retries=20)
# Setup app
deps = app.Dependencies(mp3player=lambda the_app: MP3Player(audioctx, the_app),
nfcreader=lambda the_app: Nfc(reader, the_app),
buttons=lambda the_app: Buttons(the_app))
the_app = app.PlayerApp(deps)
# Start
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
'app': the_app}))
asyncio.get_event_loop().run_forever()
if __name__ == '__main__':
if machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP).value() != 0:
time.sleep(5)
run()

View File

@@ -3,71 +3,118 @@
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, cb: PlayerCallback):
def __init__(self, audiocore):
self.audiocore = audiocore
self.commands = []
self.command_event = asyncio.Event()
self.playlist = []
self.mp3task = None
self.volume = 128
self.cb = cb
def play(self, stream):
def set_playlist(self, mp3files):
"""
Play from byte stream.
Set a new playlist and start playing from the first entry.
For convenience a single file name can also be passed.
"""
if self.mp3task is not None:
self.mp3task.cancel()
self.mp3task = None
self.mp3task = asyncio.create_task(self._play_task(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')
def stop(self):
"""
Stop playback
Stop playback, remembering the current position in the playlist (but not inside a track).
"""
if self.mp3task is not None:
self.mp3task.cancel()
self.mp3task = None
self._send_command('stop')
def play(self):
"""
Start playback.
"""
self._send_command('play')
def set_volume(self, volume: int):
"""
Set volume (0..255).
"""
self.volume = volume
self.audiocore.set_volume(volume)
def get_volume(self) -> int:
return self.volume
def _send_command(self, command: str):
self.commands.append(command)
self.command_event.set()
async def _play_task(self, stream):
async def _play_task(self, mp3path):
known_underruns = 0
send_done = False
data = array('b', range(512))
try:
while True:
bytes_read = stream.readinto(data)
if bytes_read == 0:
# End of file
break
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
if underruns > known_underruns:
print(f"{underruns:x}")
known_underruns = underruns
# Call onPlaybackDone after flush
send_done = True
print(b'Playing ' + mp3path)
with open(mp3path, 'rb') as mp3file:
while True:
bytes_read = mp3file.readinto(data)
if bytes_read == 0:
# End of file
break
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
if underruns > known_underruns:
print(f"{underruns:x}")
known_underruns = underruns
# Intentionally do not use _send_command, we don't want to set command_event yet
self.commands.append('done')
finally:
self.audiocore.flush()
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)
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])

View File

@@ -1,8 +1,7 @@
'''
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,22 +1,12 @@
'''
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:
@@ -38,11 +28,10 @@ class Nfc:
asyncio.run(main())
'''
def __init__(self, reader: MFRC522, cb: TagCallback | None = None):
def __init__(self, reader: MFRC522):
self.reader = reader
self.last_uid: list[int] | None = None
self.last_uid_timestamp: int | None = None
self.cb = cb
self.last_uid = None
self.last_uid_timestamp = None
self.task = asyncio.create_task(self._reader_poll_task())
@staticmethod
@@ -52,30 +41,20 @@ class Nfc:
'''
return '0x' + ''.join(f'{i:02x}' for i in uid)
def _read_tag_sn(self) -> list[int] | None:
(stat, _) = self.reader.request(self.reader.REQIDL)
if stat == self.reader.OK:
(stat, uid) = self.reader.SelectTagSN()
if stat == self.reader.OK:
return uid
return None
async def _reader_poll_task(self, poll_interval_ms: int = 50):
'''
Periodically polls the nfc reader. Stores tag uid and timestamp if a new tag was found.
'''
last_callback_uid = None
while True:
self.reader.init()
# For now we omit the tag type
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
(stat, _) = self.reader.request(self.reader.REQIDL)
if stat == self.reader.OK:
(stat, uid) = self.reader.SelectTagSN()
if stat == self.reader.OK:
self.last_uid = uid
self.last_uid_timestamp = time.ticks_us()
await asyncio.sleep_ms(poll_interval_ms)

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(MP_ERROR_TEXT("Not a write buffer"));
mp_raise_ValueError("Not a write buffer");
if (bufinfo.len % SD_SECTOR_SIZE != 0)
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
mp_raise_ValueError("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,25 +79,6 @@ 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);
@@ -115,10 +96,8 @@ 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

@@ -265,11 +265,3 @@ bool sd_readblock_complete(struct sd_context *sd_context)
}
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
bool sd_writeblock(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE])
{
if (!sd_context->initialized || sector_num >= sd_context->blocks)
return false;
return sd_cmd_write(24, sector_num, SD_SECTOR_SIZE, buffer);
}

View File

@@ -21,5 +21,3 @@ 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,74 +208,9 @@ 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)
@@ -308,7 +243,6 @@ 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,5 +25,3 @@ 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; */
/* } */

View File

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

View File

@@ -1,53 +0,0 @@
# 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

@@ -1,46 +0,0 @@
# 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

@@ -1,39 +0,0 @@
# 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

@@ -1,71 +0,0 @@
# 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

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

View File

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

View File

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

View File

@@ -1 +0,0 @@
pytest

View File

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

View File

@@ -1,67 +0,0 @@
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

@@ -1,116 +0,0 @@
#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

@@ -1,11 +0,0 @@
#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

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

View File

@@ -1,188 +0,0 @@
#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,8 +3,5 @@
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 \
lib/berkeley-db-1.xx
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/lib/pico-sdk submodule update --init lib
git submodule update --init --recursive tools/mklittlefs