diff --git a/.dockerignore b/.dockerignore index e241596..bb28666 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ .git build firmware/build +firmware/build-ble docs/firmware/*.fwsc node_modules fm1-1.bin diff --git a/.gitignore b/.gitignore index cd0c4ea..8a7836e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ build/ firmware/build/ firmware/openpocket.bin +firmware/build-ble/ +firmware/openpocket-ble.bin docs/firmware/*.fwsc docs/firmware/manifest.json *.o diff --git a/Makefile b/Makefile index 591ee81..ee6f958 100644 --- a/Makefile +++ b/Makefile @@ -2,18 +2,22 @@ # Docker command lines; Windows calls it from the .bat files. See docs/BUILDING.md. # make doctor check the prerequisites (Node, Docker, the image) # make build firmware/openpocket.bin, the update loader and docs/firmware/*.fwsc + manifest.json +# make ble the advertise-only Bluetooth test image: firmware/openpocket-ble.bin, build/ble/*.fwsc (docs/BLE-TEST.md) # make test the host tests (tests/run_tests.sh) # make package build + build/SHA256SUMS of the outputs # make clean remove every build output (needs no Docker) # make shell a shell in the build container # make image (re)build the container image # make serve the installer and editor on http://localhost:8765/ (offline install) +# make serve-ble the same, with the Bluetooth test packages (build/ble) to install; builds them first if missing # make render IN=guitar.wav [OUT=out.wav] [PRESET=1] a WAV through the engine # make appimage [DUMP=fm1-1.bin] fm1-300.bin for the SD-card recovery # `make NATIVE=1 build` skips Docker (toolchain installed with firmware/setup.sh). -.PHONY: doctor build test package clean shell image serve render appimage -doctor build test package clean shell image serve: +.PHONY: doctor build ble test package clean shell image serve serve-ble render appimage +doctor build ble test package clean shell image serve: @node tools/op.mjs $@ +serve-ble: + @node tools/op.mjs serve ble render: @node tools/op.mjs render "$(IN)" "$(OUT)" "$(PRESET)" appimage: diff --git a/NOTICE.md b/NOTICE.md index c9a68b7..403bce4 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -8,6 +8,8 @@ OpenPocket is GPL-3.0-only (`LICENSE`), except the parts below, which keep their | `loader/` (update loader) | felucca-bsp `loader/`, changed for the Pocket X flash layout | MPL-2.0 | | `docs/pxota.js`, `docs/pxpkg.js`, `tools/pxpkg.py`, `tools/mkloader.py`, `tools/lz4blk.py` | adapted from [Felucca](https://github.com/hugelton/Felucca)'s web installer and build tools (as used by CLIPPER and SLOOP) | GPL-3.0 | | `firmware/sdk/` (I2S and DAC drivers as LLVM bitcode, SPL, cfg files) | JieLi AC79 SDK V1.2.0 (fw-AC79_AIoT_SDK) | Apache-2.0 (`LICENSES/Apache-2.0.txt`) | +| `third_party/jieli-ble/` (JieLi's Bluetooth LE controller, 33 objects of `btctrler.a` and 3 of `cpu.a`, as published LLVM bitcode, used only by the Bluetooth test image `make ble`) | JieLi AC79 SDK V1.2.0 (fw-AC79_AIoT_SDK, commit `0617f4c`), `cpu/wl82/liba/`; see `third_party/jieli-ble/README.md` | Apache-2.0 (`third_party/jieli-ble/LICENSE-JieLi-Apache-2.0`, `LICENSES/Apache-2.0.txt`) | +| `firmware/ble/ble_cfg.c` | values and names from the same SDK's `apps/common/config/log_config/lib_btctrler_config.c` | Apache-2.0 | | `firmware/fonts.h` (bitmaps generated from Inter) | [Inter](https://rsms.me/inter/), Rasmus Andersson | SIL OFL 1.1 (`LICENSES/OFL-1.1-Inter.txt`) | Sound models: the names refer to the gear that inspired each voicing (circuit analyses published by ElectroSmash, diff --git a/README.md b/README.md index 5e58bde..11e1f06 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ replaces the files with this folder's. | `tools/` | package builder, loader wrapper, font converter, flash image tools | | `tests/` | host tests and the simulated pedal | | `flash/` | SD-card recovery with fm1buddy (ESP32-S3) | +| `firmware/ble/`, `third_party/jieli-ble/` | the optional advertise-only Bluetooth test image (`make ble`, [docs/BLE-TEST.md](docs/BLE-TEST.md)) and JieLi's Bluetooth controller it links | ## Licence diff --git a/docs/BLE-TEST.md b/docs/BLE-TEST.md new file mode 100644 index 0000000..138ee32 --- /dev/null +++ b/docs/BLE-TEST.md @@ -0,0 +1,175 @@ +# Bluetooth test image (advertise only) + +An optional build of OpenPocket that turns the pedal's Bluetooth radio on and sends **one advertisement**, so the name +**`OpenPocket-BLE-TEST`** shows up in a phone or Mac Bluetooth scanner. It is stage 2 of the Bluetooth plan: the radio comes up, nothing +more. No connection is ever accepted, there is no pairing, no GATT table and no MIDI over Bluetooth. USB MIDI is not +changed. + +The normal firmware and the installer's packages in `docs/firmware/` are built exactly as before: `make build` gives the +same bytes with or without this feature in the tree. The test image is its own build target, `make ble`. + +> **Status: built and linked. The first flash on a pedal crash-looped, the second stalled at `FAILED 5 3` and the third +> froze in the first `ble_poll`; those faults are fixed. The fourth flash reached `ADVERTISING`, but neither a Mac nor a phone scanner saw the pedal. This build adds proof counters and a valid controller address; it has not been run on a pedal yet, and the scanner check is still pending.** There is no emulator of the radio, so everything below about what the pedal does is the plan, not an +> observation. Read [What is not verified](#what-is-not-verified) before you flash it. + +**First hardware run (fixed).** The first image crashed on every boot, before any advertisement: the hardware stack-limit +guard (`fm1_guard.h`, EMU bit 3) faults any stack pointer outside `[_ustack_lo, _sstack_top]`, and the controller's task +stacks (`ble_os.c`) are in ordinary RAM below that window. The test image is now built with that one guard off +(`OP_GUARD_STACK=0` in `firmware/build.sh`); the write, bus and code-range guards stay on. The normal firmware keeps all four. +`firmware/app.ld` now refuses to link the image if the stack guard is on while the task stacks lie outside the window, so the +mistake cannot come back unnoticed (`OP_BLE=1 OP_GUARD_STACK=1 sh firmware/build.sh` shows the refusal). + +## Build + +``` +make ble # firmware/openpocket-ble.bin and build/ble/PocketX_936.fwsc, BlackBox_936.fwsc, manifest.json +``` + +(`node tools/op.mjs ble` is the same; `OP_BLE=1 sh firmware/build.sh` inside the container.) It prints the SHA-256 of the image and the two packages. Build outputs +are `firmware/build-ble/` and `build/ble/`; `make clean` removes them. + +## Flash it + +This is the pedal's normal update path, with the test packages instead of the release ones. Your pedal must already run +OpenPocket (the stock-firmware scan was skipped on purpose). + +1. `make serve-ble` builds the test packages if they are missing and opens the offline installer on + `http://localhost:8765/` with `build/ble/` as its package folder. (Chrome or Edge, USB cable to the pedal.) +2. **Connect**, pick the pedal, **Install**, as for any update. The pedal restarts into the test image; its start-up + screen reads `v36 BLE TEST`. +3. To go back to the normal firmware: run `make serve` (the release packages) and **Install** again, or use the + **Back to the official firmware** step. The SD-card recovery (`make appimage`) works as before. + +The status text at the bottom right of the screen shows the Bluetooth state: `STARTING` with the stage and the pedal's +usual `C1 42%`, then `ADVERTISING` (the proof counters, scrolling) or `FAILED` (the failure, scrolling, with its short +code first so that it is always on screen). See [Reading the status text](#reading-the-status-text). + +## What you should see + +Open a scanner (nRF Connect, LightBlue, or macOS *Bluetooth Explorer* / *Audio MIDI Setup* are all fine; use active +scanning, which phones do by default): + +| In the scanner | Value | +| --- | --- | +| Name | `OpenPocket-BLE-TEST` (in the scan response) | +| Service UUID | BLE-MIDI `03B80E5A-EDE8-4B33-A751-6CE34EC4C700` (in the advertising data) | +| Type | scannable, not connectable (`ADV_SCAN_IND`), 100 ms interval, channels 37, 38, 39 | +| Address | `02:4F:50:42:4C:54`, a fixed locally administered address set by this image (not the stock firmware's) | + +The name appearing in a scanner is the whole success test. Connecting to it will fail by design. + +If the screen reads `FAILED`, or nothing appears within a few seconds, please report the screen text, the pedal model, +what the scanner shows and whether the pedal still works as a normal OpenPocket (audio, screen, USB MIDI). + +## Reading the status text + +| Text | Meaning | +| --- | --- | +| `STARTING n` | the last stage reached: 1 task created, 2 controller init called, 3 radio front end replay entered, 4 waiting for the controller task, 5 setting the advertisement, 6 enabling it, 7 the replay returned, 8 the baseband asked for its interrupts. The markers are set by two tasks, so 3 and 7 can appear after 4; the number shown is the last one set, not the highest | +| `FAILED s c` | step `s` failed with return value `c`. The status area is only 11 characters wide, so the whole line scrolls like a marquee (2 characters a second, resting 2 s on the start, then looping): `FAILED 2 -7 step btctrler_task_init stage init`. The short code is first, so it can be read before the scroll starts. The usual `C1 42%` is not shown while it has failed. | +| `ADVERTISING` | the controller accepted the command to advertise; it does not mean a packet was sent. The status shows the proof counters instead, as a marquee (below) | + +Failing steps: `1` `task_create` failed (`c` is -1); `2` `btctrler_task_init` returned `c` (not 0); `3` +`btctrler_task_ready` returned `c` (0: the controller task did not answer); `4` `ll_hci_adv_enable` returned `c` (not +0); `5` still `STARTING` after 5 s, and `c` is the stage it stalled in (the list above). At a timeout the line ends with `wait XXXXXXXX`: the address of the semaphore or mutex the controller task is blocked on (look it up in `firmware/build-ble/symbols.txt`; a value below 0x10 is a task state instead). Example: `FAILED 5 3` only means the last marker passed was the entry of the radio replay; it does not say the replay hung. (In the second hardware run it was a deadlock after the radio start-up: see "Found on hardware" below.) The codes and the text come from `firmware/ble/ble_status.h`, checked on the PC by +`tests/test_ble_status.c`. Please report the whole `FAILED s c` text. + +### The advertising line (proof counters) + +The fourth flash showed `ADVERTISING` but neither a Mac nor a phone scanner saw the pedal, so while advertising the status area scrolls a line of counters and registers, with the numbers first: `ADV i12 e5 r0 t340 s0 CON 2001 IEN 101 IPND 0 STAT 80 IC45 3 IC29 3 BEN 1`. + +| Field | Meaning | What it tells | +| --- | --- | --- | +| `i` | baseband event interrupts (IRQ 45), counted by `ble_isr.S` before the library's own handler runs | above 0 and rising: the radio runs and events happen. 0: no event ever reaches the CPU | +| `r` | receive interrupts (IRQ 29) | above 0: a scanner's request was heard, so the antenna works both ways | +| `e` | HCI packets the controller returned (`hci_packet_handler`) | any controller answer | +| `t` | library software timers that fired | the controller's tick source runs | +| `s` | calls of the slot-timer stand-ins (`slot_timer_*`) | above 0: the link scheduler asked for a timer this image does not have (see below) | +| `CON IEN IPND STAT` | baseband registers `0x28000`, `0x28028`, `0x28030`, `0x28038` in hex | enable, interrupt enables, pending, busy flags | +| `IC45 IC29` | the interrupt configuration of the two lines (bit 0 enabled, bits 3:1 priority) | 0: the line is off | +| `BEN` | BLE block enable `0x2FC80` | bit 0 clear: the block is off | + +Reading the next flash: `i` and `t` rising means the radio runs and the cause is on the air side (power, trims, address). `i` 0 with `IC45` 0 means the interrupt was never enabled; `i` 0 with `IC45` 3 and `IEN` 0 means the baseband never enabled its event; `t` 0 means the library's timers never fire. + +**Checked against the Core Specification** (`firmware/ble/ble_adv.h`, tested on the PC): the advertising data is two whole AD structures (flags 0x06, the complete 128-bit UUID list, little endian), 21 of 31 bytes; the scan response is the complete local name, 21 bytes; the PDU is ADV_SCAN_IND at 100 ms on channels 37, 38 and 39 with filter policy 0. One fault was found and fixed: the controller's public address was never set (the library reads it from a stored item this image does not have), so it was all zeros, which is not a valid device address. It is now `02:4F:50:42:4C:54` (locally administered, unicast; not an IEEE-assigned address). The library's own-address type is fixed at public in its HCI layer, so a random static address cannot be used from here. + +**Open suspicion, not a fix:** the link scheduler (`link_task.c`) arms its timer through `slot_timer_set` and its siblings, which are among the empty stand-ins in `ble_stubs.c` (the library's slot timer is not part of the linked archive). Whether a plain advertising event depends on them is not known. `s` counts the calls: if it is above 0 the next step is a real slot timer. + +## Found on hardware + +The second flash showed `FAILED 5 3`. The cause, found by reading the vendor code against the linked image: the controller's link-layer start-up calls `thread_create`, which calls `crc_get_32bit`, which waits for ever on a lock that the vendor function `__crc16_mutex_init()` creates and the SDK's own start-up calls. This image never called it, so the lock stayed "taken" and the controller task slept for ever. `ble_test_start` now calls it. Two more defects were fixed in the same change, because the first one would have exposed them: `ll_hci_adv_set_params` reads six bytes from its direct-address argument (it was NULL, which the bus guard is documented to fault on; now a zero array), and the context switch saved `r4`-`r9` while the compiler preserves `r4`-`r15` (now all of them, the list the compiler's own prologues use). None of the three has run on a pedal yet. + +The third flash froze: the screen was drawn but the controls were dead, and the pedal reset by watchdog after about 8 s (`LAST BOOT FAILED`, `RESET BY: WATCHDOG`, `LOOPS 0`). The scheduler is cooperative, so a vendor call that never returns holds the whole main loop. The scout report (data/openpocket-ble-hang-diagnosis/report.md) found two such places on the advertising path: a vendor assertion that fails because the empty stand-in for `low_power_get` returned a null handle (a failed assertion parks in `P33_SYSTEM_RESET`, an endless loop), and, earlier, an unbounded wait on baseband status bit 1 (`0x28038`) that the repository's own baseband header says can stay set. `low_power_get` now returns a non-null handle. The second wait is untested; if it is the one, the crash screen below names it. + +**Stall watch.** The 10 kHz timer interrupt counts while a `ble_poll()` runs. After 4 s it makes the next tick take the fatal path, so the pedal shows the normal crash screen with `VEC 3F` and the `PC` and `RETS` of the code that did not return (look them up in `firmware/build-ble/symbols.txt`; `PC 02000DC8` is a vendor assertion and `RETS` names it; a `PC` inside `ble_hw_disable` is the status-bit wait). It cannot fire while interrupts are off, and it watches only `ble_poll()`; the radio replay is excluded because it is long and its waits are bounded. + +**After a failed boot Bluetooth stays off.** The next boot shows `LAST BOOT FAILED` and runs the normal firmware (status `OFF SAFE nn%`), so the boot guard never counts two failures in a row and the pedal does not fall back to the loader. The flasher works in that state. The boot after that one tries Bluetooth again. + +## How it works + +``` +openpocket.c (OP_BLE_TEST) ble_test_start() after the screen is up, ble_test_poll() in the main loop +firmware/ble/ble_test.c a task that starts the controller, sets the advertising data and enables advertising +firmware/ble/ble_os.c the operating-system services the library calls (below) +firmware/ble/ble_rf.c the Wi-Fi front-end half of the radio start-up (below) +firmware/ble/ble_cfg.c controller configuration: LE only, advertising + one slave link +firmware/ble/ble_stubs.c empty stand-ins for the parts that are not built +third_party/jieli-ble/ JieLi's controller, as published (Apache-2.0): see its README.md +``` + +* **The controller is JieLi's**, from the Apache-2.0 SDK libraries (`btctrler.a`, 33 of its objects plus 3 helpers from + `cpu.a`): HCI front end, link layer, advertising, the LE radio driver. Only the objects the image reaches are linked. + The host stack (`btstack.a`) is not used; `ble_test.c` calls the controller's own `ll_hci_adv_*` functions. + The OpenPocket code around it is small and separate from the vendor objects (`firmware/ble/`), which are kept + unmodified under `third_party/jieli-ble/` with the licence text and the source URL. +* **No RTOS.** The library expects tasks, queues, semaphores and timers. `ble_os.c` provides a cooperative scheduler: + the controller's one task runs from `ble_poll()` in the main loop, switches only where it waits (queue, semaphore, + delay), and is woken by the two BLE interrupts (IRQ 45 and 29, priority 2: the numbers and priority the stock firmware + uses). The heap is a 24 KB static pool; each task has a 4 KB stack. Against the normal image it needs about 44 KB more RAM (of which the heap + and stacks are 36 KB) and 131 KB more flash; the link fits with about 15 KB of the 96 KB RAM region free. +* **Radio start-up.** The library's `bt_rf_init` calls the Wi-Fi library's `wf_rf_common_init` first. Linking that would + pull in the Wi-Fi MAC/SDIO stack, so `ble_rf.c` runs the already committed capture of the stock start-up instead + (`firmware/bsp/chip/ble_rf_tables_v15.h`, run by `fm1_ble_rf.h`), with this unit's trims measured by `fm1_rf_cal.h`. + The BLE baseband itself is initialised by the library (`le_hw_init`), which makes the same register changes as the stock + firmware's baseband start-up in the analysis (bits 13 and 14 set, bit 4 cleared, IRQs 45 and 29 at priority 2). +* **Nothing is saved.** The library asks for stored items (`syscfg_read`): none are returned. It tries to store calibration + results (`syscfg_write`): the call is accepted and ignored. The VM (`0xA5000`) and BTIF (`0xBE000`) areas are not read + and not written by this image. + +## What is not verified + +All of it was built, linked and size-checked; none of it ran on hardware. The things most likely to go wrong, in the +order I would suspect them: + +1. **The radio start-up.** It replays a capture taken from another firmware, with the stored crystal trim and the other + saved radio records **not applied** (the VM is not read, so those fields stay at the capture's defaults). The carrier + may therefore be off by some kHz. A scanner usually still sees it; a wrong value that stops the PLL would hang the + start-up and the screen would stay on `STARTING`. + The brief asked for the stock TX trims to be left as found. This image does **not** meet that exactly: it neither + reads nor writes them, and the transmit level is whatever the library's power table gives with no saved trim. + Reading them needs the VM item format, which is not documented in this repository; that would be the next task. +2. **Interrupt return.** The library's two interrupt functions end in `rti` without the `csync` the stock ones have + (our compiler does not add it to bitcode; `fm1_isr.S` explains it for our own interrupts). The risk is an interrupt + taken twice. Suspect this if the pedal resets or hangs a moment after `ADVERTISING` appears. +3. **The scheduler.** The context switch (`ble_ctx.S`) is eight instructions assembled from the compiler's own prologue + and epilogue forms, and was not run. The queue, semaphore and timer code is plain C but also unrun. +4. **Flash writes while Bluetooth runs.** The library's interrupt code runs from flash (execute in place). Saving a + preset or an update while advertising could, in principle, fetch code while the flash is busy. Do not save presets + while testing. +5. **Real-time load.** Starting the radio blocks the main loop for the length of the start-up (screen and knobs freeze, + audio keeps running from its interrupt). Afterwards the cost is the two radio interrupts every 100 ms. +6. **Other guards, checked on paper only.** After the stack-limit fault, the library's start-up path was read for the other + guards (`fm1_guard.h`). *Write limits* cover only the two 256-byte stack bands and `0x01C7FD50` upward (boot info, + mailbox, vectors); the controller's data, heap and task stacks are in `.bss`/`.data` between `0x01C08000` and about + `0x01C1C000`, so none of its writes should land there. *Code range*: all of its code is linked into `.text` (`_etext` + bounds it) and none runs from RAM. *Bus-invalid*: only the radio's own registers are touched. *Interrupt stacks*: the two + BLE interrupts run on the system stack as every interrupt does, which is inside the window. None of this has run on + hardware; a fault on the next flash is still possible, and the crash screen's `EMU` and `DBG` values say which guard it was. +7. **Regulation.** The pedal was certified with the stock firmware. This image uses the standard advertising channels, + the 1M PHY and the library's own power table; it is a hobby experiment, not a product. + +## Recovery + +If the image does not start, the existing safety net applies: two failed boots go back to the update loader +(`UBOOT`), and the web installer, the SD-card recovery (`make appimage`) and the official firmware's own updater all work as +before. The image does not touch the flash outside the application area. diff --git a/docs/BUILDING.md b/docs/BUILDING.md index de42e5f..c521ac1 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -28,6 +28,8 @@ Apple silicon and other ARM machines Docker emulates it (Rosetta or QEMU); you c | --- | --- | | `make doctor` | check Node, Docker and the image, one line each (exit 1 when the build cannot work) | | `make build` | `firmware/build.sh` in the container: `firmware/openpocket.bin`, the update loader, `docs/firmware/PocketX_9NN.fwsc`, `BlackBox_9NN.fwsc`, `manifest.json` | +| `make ble` | the advertise-only Bluetooth test image (`OP_BLE=1 firmware/build.sh`): `firmware/openpocket-ble.bin`, `build/ble/*.fwsc`. Own target, own output folders; [BLE-TEST.md](BLE-TEST.md) | +| `make serve-ble` | `make serve` with `build/ble/` as the installer's package folder, to flash the test image | | `make package` | `build`, then `build/SHA256SUMS` | | `make test` | `tests/run_tests.sh` (engine, preset store, screen, simulated pedal, installer, editor; the browser page tests also run when Playwright is installed) | | `make clean` | remove every build output (on the host; needs neither Docker nor the image) | @@ -57,7 +59,7 @@ old one. The toolchain is JieLi's proprietary software. It is **not** in this repository and is not redistributed by it: the image build downloads it from JieLi's own server (`jl-update.oss-cn-shenzhen.aliyuncs.com`, the file `https://pkgman.jieliapp.com/s/linux-toolchain` redirects to). If you publish a container image, do not include the toolchain layer. The Apache-2.0 SDK parts in -`firmware/sdk/` are covered by `NOTICE.md`. +`firmware/sdk/` and `third_party/jieli-ble/` are covered by `NOTICE.md`. ## Reproducibility diff --git a/firmware/app.ld b/firmware/app.ld index fb14fa1..49e4016 100644 --- a/firmware/app.ld +++ b/firmware/app.ld @@ -3,8 +3,10 @@ /* Memory map of a standalone FM-1 app: code and constants run in place from XIP * (app.bin offset 0 = 0x02000120); .data is copied to RAM by fm1_cstart, and so is .ram_text (code that runs * while the flash is busy: the flash driver) into RAMTEXT, the guard's RAM code window. + * The ble build (OP_BLE=1) adds the JieLi controller's own sections: .*_code and .*_const (XIP), .*_data, .*_bss (RAM). * Stacks sit above the RAM with 256-byte guard bands; NOINIT survives resets. * 0x01C7FD50.. is reserved (boot info, mailbox, vector table). */ +PROVIDE(op_guard_stack = 1); /* build.sh passes --defsym op_guard_stack=0 with OP_GUARD_STACK=0 */ MEMORY { XIP (rx) : ORIGIN = 0x02000120, LENGTH = 0x8DFBC @@ -15,17 +17,22 @@ MEMORY } SECTIONS { - .text : { KEEP(*(.text.crt0)) *(.text*) *(.rodata*) . = ALIGN(4); _etext = .; } > XIP + .text : { KEEP(*(.text.crt0)) *(.text*) *(.rodata*) *(.*_code) *(.*_const) . = ALIGN(4); _etext = .; } > XIP .ram_text : { _rt_start = .; KEEP(*(.ram_text*)) . = ALIGN(4); _c1_start = .; KEEP(*(.c1_text*)) . = ALIGN(4); _c1_end = .; /* the second core's loop */ _rt_end = .; } > RAMTEXT AT > XIP _rt_load = LOADADDR(.ram_text); - .data : { . = ALIGN(4); _data_start = .; *(.data*) *(.sdata*) . = ALIGN(4); _data_end = .; } > RAM AT > XIP + .data : { . = ALIGN(4); _data_start = .; *(.data*) *(.sdata*) *(.*_data) . = ALIGN(4); _data_end = .; } > RAM AT > XIP _data_load = LOADADDR(.data); - .bss (NOLOAD) : { _bss_start = .; *(.bss*) *(.sbss*) *(COMMON) . = ALIGN(4); _bss_end = .; } > RAM + .bss (NOLOAD) : { _bss_start = .; _ble_stack_lo = .; *(.bss.ble_stack) _ble_stack_hi = .; *(.bss*) *(.sbss*) *(.*_bss) *(COMMON) . = ALIGN(4); _bss_end = .; } > RAM .noinit (NOLOAD) : { *(.noinit*) } > NOINIT .pool (NOLOAD) : { . = ALIGN(8); *(.pool*) } > POOL _guard0 = 0x01C74000; _ustack_lo = 0x01C74100; _ustack_top = 0x01C7A000; _guard1 = 0x01C7A000; _sstack_lo = 0x01C7A100; _sstack_top = 0x01C7C000; + /* The hardware stack guard (fm1_guard.h) faults any sp outside [_ustack_lo, _sstack_top]. The Bluetooth test image's task + * stacks (ble_os.c) are in RAM below that window, so it must be linked with op_guard_stack=0 (build.sh); with the guard on + * this link fails instead of the pedal crash-looping. Empty in the normal firmware. */ + ASSERT(op_guard_stack == 0 || _ble_stack_lo == _ble_stack_hi || (_ble_stack_lo >= _ustack_lo && _ble_stack_hi <= _sstack_top), + "BLE task stacks lie outside the stack-guard window while the stack guard is enabled") /DISCARD/ : { *(.comment) *(.note*) } } diff --git a/firmware/ble/ble_adv.h b/firmware/ble/ble_adv.h new file mode 100644 index 0000000..87330b4 --- /dev/null +++ b/firmware/ble/ble_adv.h @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* The advertisement the Bluetooth test image sends, as the Bluetooth Core Specification wants it (Vol 3 Part C 11, Vol 6 + * Part B 2.3, Vol 4 Part E 7.8.5-7.8.9). No hardware access: tests/test_ble_status.c checks it on the PC. + * + * PDU ADV_SCAN_IND: scannable, not connectable. Scan requests are answered; no connection is accepted. + * interval 0x00A0 x 0.625 ms = 100 ms (min = max). ADV_SCAN_IND needs at least 100 ms in Core 4.0 and 20 ms since 4.1. + * channels 37, 38, 39 (map 0x07). filter policy 0: scans from anybody. + * address the controller's public address slot (the library's own-address type is a build setting of its HCI layer, + * 0 = public, and cannot be changed from here). Left alone it is zero, which is not a valid device address; + * it is set to a fixed, non-zero, locally administered unicast address. Not an IEEE-assigned one: a test image. + * advertising data flags (LE general discoverable, BR/EDR not supported) + complete list of 128-bit service UUIDs + * (BLE-MIDI 03B80E5A-EDE8-4B33-A751-6CE34EC4C700, little endian): 21 of the 31 bytes + * scan response complete local name "OpenPocket-BLE-TEST": 21 bytes + * Transmit power is the library's own power table (no stored trims are read: docs/BLE-TEST.md). */ +#pragma once +#include + +enum { BLE_ADV_SCAN_IND = 2, BLE_ADV_INTERVAL = 0x00A0, BLE_ADV_CHMAP = 7, BLE_ADV_FILTER = 0 }; + +static uint8_t ble_adv_data[] = { + 2, 0x01, 0x06, /* flags: LE general discoverable, no BR/EDR */ + 17, 0x07, /* complete list of 128-bit service UUIDs, little endian */ + 0x00, 0xC7, 0xC4, 0x4E, 0xE3, 0x6C, 0x51, 0xA7, 0x33, 0x4B, 0xE8, 0xED, 0x5A, 0x0E, 0xB8, 0x03, +}; +static uint8_t ble_scan_rsp[] = { 20, 0x09, 'O','p','e','n','P','o','c','k','e','t','-','B','L','E','-','T','E','S','T' }; +static uint8_t ble_adv_mac[6] = { 0x54, 0x4C, 0x42, 0x50, 0x4F, 0x02 }; /* 02:4F:50:42:4C:54, little endian: bit 1 of the first octet = locally administered, bit 0 = unicast */ +static uint8_t ble_no_addr[6]; /* direct address: unused by ADV_SCAN_IND, but the library reads six bytes */ diff --git a/firmware/ble/ble_cfg.c b/firmware/ble/ble_cfg.c new file mode 100644 index 0000000..53db78e --- /dev/null +++ b/firmware/ble/ble_cfg.c @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Controller configuration for the advertise-only image: the values the JieLi SDK's own + * apps/common/config/log_config/lib_btctrler_config.c (fw-AC79_AIoT_SDK V1.2.0, Apache-2.0, see + * third_party/jieli-ble/) gives a Bluetooth LE only build, reduced to LE advertising and one slave link. The names and + * types are the library's; the values for features we do not build are the SDK's defaults. */ +#include + +#define BIT(n) (1u << (n)) +enum { BT_MODULE_CLASSIC = BIT(0), BT_MODULE_LE = BIT(1) }; +enum { BT_NORMAL = 0x01 }; +enum { LE_MASTER = BIT(0), LE_SLAVE = BIT(1), LE_ADV = BIT(2), LE_SCAN = BIT(3), LE_INIT = BIT(4) }; + +const int config_btctler_modules = BT_MODULE_LE; +int config_btctler_mode = BT_NORMAL; +int config_btctler_hci_standard = 0; +const int config_bt_function = 0; +const int config_btctler_le_tws = 0; +const int CONFIG_BTCTLER_TWS_ENABLE = 0; +const int CONFIG_TWS_AFH_ENABLE = 0; +const int CONFIG_WIFI_DETECT_ENABLE = 0; +const int ESCO_FORWARD_ENABLE = 0; +const int CONFIG_LOW_LATENCY_ENABLE = 0; +const int CONFIG_ESCO_MUX_RX_BULK_ENABLE = 0; +const int CONFIG_BTCTLER_FAST_CONNECT_ENABLE = 0; +const int CONFIG_LMP_NAME_REQ_ENABLE = 1; +const int CONFIG_LMP_PASSKEY_ENABLE = 1; +const int CONFIG_LMP_MASTER_ESCO_ENABLE = 1; +const int CONFIG_INQUIRY_PAGE_OFFSET_ADJUST = 0; +const int config_force_bt_pwr_tab_using_normal_level = 0; /* 0: the power table of each mode (the stock trims) */ +unsigned char config_btctler_bredr_master = 0; +const int config_btctler_dual_a2dp = 0; +const int config_btctler_role_switch_fail_disconnect = 1; +const int config_btctler_role_switch_fail_back_to_slave = 0; +const int config_bredr_afh_user = 0; +const uint8_t rx_fre_offset_adjust_enable = 1; +const int config_fix_fre_enable = 0; +const int ble_disable_wait_enable = 1; +const int config_delete_link_key = 1; +const int config_btstask_auto_exit_sniff = 1; +const int CONFIG_TEST_DUT_CODE = 0; +const int CONFIG_TEST_FCC_CODE = 0; +const int CONFIG_TEST_DUT_ONLY_BOX_CODE = 0; +const int CONFIG_BREDR_INQUIRY = 0; + +const uint64_t config_btctler_le_features = 0; /* no encryption: no connections in this image */ +const int config_btctler_le_roles = LE_ADV | LE_SLAVE; +const int config_btctler_le_afh_en = 0; +const int config_btctler_le_hw_ram_use_static = 1; +const int config_btctler_le_master_multilink = 0; +const int config_multilink_support_esco_enalbe = 0; +const int config_btctler_le_slave_multilink = 1; +const int config_btctler_le_hw_nums = 1; +const int config_btctler_le_rx_nums = 5 * 1; +const int config_btctler_le_acl_packet_length = 27; +const int config_btctler_le_acl_total_nums = 5 * 1; +const uint32_t config_vendor_le_bb = 0; +const int config_btctler_le_slave_conn_update_winden = 2500; +const int config_btctler_single_carrier_en = 0; +const int sniff_support_reset_anchor_point = 0; +const int config_bt_security_vulnerability = 0; diff --git a/firmware/ble/ble_ctx.S b/firmware/ble/ble_ctx.S new file mode 100644 index 0000000..d526bb7 --- /dev/null +++ b/firmware/ble/ble_ctx.S @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: MPL-2.0 */ +/* Context switch for the cooperative scheduler in ble_os.c. A task is suspended by pushing the registers a C call must + * preserve (rets and r4-r15: every register a compiled function saves, the list its own prologue pushes) on its own stack and storing the stack + * pointer; it is resumed by loading that pointer and popping the same list. Push and pop are the same instruction pair + * the compiler emits, so the order in memory does not matter. + * + * void ble_ctx_swap(uint32_t **save, uint32_t *load) suspend here into *save, resume the context in load + * void ble_ctx_enter(uint32_t **save, uint32_t *top, void (*fn)(void)) + * suspend here into *save, call fn on the stack at top + * (fn never returns) + */ + .section .text.ble_ctx,"ax",@progbits + .balign 4 + .globl ble_ctx_swap +ble_ctx_swap: + [--sp] = {rets, r15-r4} + r2 = sp + [r0 + 0] = r2 + sp = r1 + {pc, r15-r4} = [sp++] + + .globl ble_ctx_enter +ble_ctx_enter: + [--sp] = {rets, r15-r4} + r3 = sp + [r0 + 0] = r3 + sp = r1 + call r2 + goto . diff --git a/firmware/ble/ble_isr.S b/firmware/ble/ble_isr.S new file mode 100644 index 0000000..bd4ca3c --- /dev/null +++ b/firmware/ble/ble_isr.S @@ -0,0 +1,29 @@ +/* SPDX-License-Identifier: MPL-2.0 */ +/* The two baseband interrupts of the Bluetooth test image, counted. The library's own interrupt functions are complete + * interrupt entries (they save the registers and end in rti), so this cannot be a C function that calls them: it counts, + * restores the two registers it used and jumps to the library's function, which then runs as if it had been entered + * directly. The jump target is on the stack below the saved registers so that no register is left over. It uses add, load + * and store only (the compiler's own compares are fused with their branches), so the flags the library saves are those + * of the interrupted code. + * + * ble_isr_ev IRQ 45 counts ble_cnt_irq_ev, jumps to *ble_irq_target_ev + * ble_isr_rx IRQ 29 counts ble_cnt_irq_rx, jumps to *ble_irq_target_rx (ble_os.c request_irq sets them up) */ + .macro BLE_ISR name, count, target + .section .text.\name,"ax",@progbits + .globl \name +\name: + sp += -4 + [--sp] = {r1-r0} + r0 = \count + r1 = [r0+0] + r1 += 1 + [r0+0] = r1 + r0 = \target + r0 = [r0+0] + [sp+8] = r0 + {r1-r0} = [sp++] + {pc} = [sp++] + .endm + + BLE_ISR ble_isr_ev, ble_cnt_irq_ev, ble_irq_target_ev + BLE_ISR ble_isr_rx, ble_cnt_irq_rx, ble_irq_target_rx diff --git a/firmware/ble/ble_libc.c b/firmware/ble/ble_libc.c new file mode 100644 index 0000000..130d099 --- /dev/null +++ b/firmware/ble/ble_libc.c @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* The C library routines and diagnostics hooks the controller library calls and px_audio.h does not already supply + * (log_print, printf, config_asser, cpu_assert_debug, P33_SYSTEM_RESET, os_current_task_rom). Logging is switched off in + * the library by its log tags (ble_stubs.c), so the print functions here do nothing. */ +#include +#include +#include "ble_os.h" + +size_t strlen(const char *s) { const char *p = s; while (*p) p++; return (size_t)(p - s); } +int strcmp(const char *a, const char *b) { while (*a && *a == *b) { a++; b++; } return (uint8_t)*a - (uint8_t)*b; } +char *strcpy(char *d, const char *s) { char *r = d; while ((*d++ = *s++)) {} return r; } +char *strncpy(char *d, const char *s, size_t n) { size_t i = 0; for (; i < n && s[i]; i++) d[i] = s[i]; for (; i < n; i++) d[i] = 0; return d; } +void *memmove(void *d, const void *s, size_t n) +{ + uint8_t *dp = d; + const uint8_t *sp = s; + if (dp < sp) + while (n--) *dp++ = *sp++; + else + for (dp += n, sp += n; n--;) *--dp = *--sp; + return d; +} + +void printf_buf(const uint8_t *b, uint32_t n) { (void)b; (void)n; } +void put_buf(const uint8_t *b, uint32_t n) { (void)b; (void)n; } +int puts(const char *s) { (void)s; return 0; } +int putchar(int c) { return c; } diff --git a/firmware/ble/ble_os.c b/firmware/ble/ble_os.c new file mode 100644 index 0000000..342f09a --- /dev/null +++ b/firmware/ble/ble_os.c @@ -0,0 +1,382 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* The few operating-system services the JieLi Bluetooth controller library (third_party/jieli-ble) calls, written for + * OpenPocket's main loop: a cooperative scheduler for the controller's tasks, task message queues, semaphores and + * mutexes, software timers, a heap, and the interrupt registration. Signatures and error numbers follow the SDK's + * public headers (include_lib/system/os/os_api.h, os_error.h, timer.h, asm/hwi.h; Apache-2.0). + * + * Tasks never run by themselves: ble_poll() (called from the main loop) resumes every task that is ready and returns + * when all of them are waiting again. A task gives up the processor only inside os_taskq_pend, os_sem_pend, + * os_mutex_pend or os_time_dly; interrupts only post messages and semaphores. */ +#include +#include +#include "fm1_irq.h" +#include "fm1_time.h" +#include "ble_os.h" +#include "ble_status.h" + +enum { OS_NO_ERR = 0, OS_TIMEOUT = 11, OS_TASKQ = 13, OS_TASK_NOT_EXIST = 14, OS_Q_FULL = 21, OS_Q_EMPTY = 22 }; +enum { OS_TICK_MS = 10 }; /* OS_TICKS_PER_SEC is 100 in the SDK */ + +extern void ble_ctx_swap(uint32_t **save, uint32_t *load); +extern void ble_ctx_enter(uint32_t **save, uint32_t *top, void (*fn)(void)); + +/* ---- interrupts: one level of nesting count, as the SDK's local_irq_disable / local_irq_enable */ +static uint32_t irq_nest; +void local_irq_disable(void) { fm1_irq_off(); irq_nest++; } +void local_irq_enable(void) { if (irq_nest && !--irq_nest) fm1_irq_on(); } +void __local_irq_disable(void) { local_irq_disable(); } +void __local_irq_enable(void) { local_irq_enable(); } +void sys_local_irq_disable(void) { local_irq_disable(); } +void sys_local_irq_enable(void) { local_irq_enable(); } +int cpu_irq_disabled(void) { return !(fm1_icfg() & 0x200u); } /* icfg bit 9: the global enable that cli and sti clear and set */ +int cpu_in_irq(void) { return 0; } + +/* ---- heap: first fit with coalescing on free, 8-byte aligned, one static pool */ +#define HEAP_BYTES (24u * 1024u) +typedef struct blk { uint32_t size; uint32_t used; } blk_t; /* size includes this header */ +static uint8_t heap[HEAP_BYTES] __attribute__((aligned(8))); +static int heap_ready; +static uint32_t heap_free_bytes, heap_min_free; + +static void heap_init(void) +{ + blk_t *b = (blk_t *)heap; + b->size = HEAP_BYTES; + b->used = 0; + heap_free_bytes = heap_min_free = HEAP_BYTES; + heap_ready = 1; +} + +void *malloc(size_t n) +{ + uint32_t need = (uint32_t)((n + sizeof(blk_t) + 7u) & ~7u); + uint8_t *p, *end = heap + HEAP_BYTES; + local_irq_disable(); + if (!heap_ready) + heap_init(); + for (p = heap; p < end; p += ((blk_t *)p)->size) { + blk_t *b = (blk_t *)p; + if (b->used || b->size < need) + continue; + if (b->size >= need + 16u) { /* split */ + blk_t *r = (blk_t *)(p + need); + r->size = b->size - need; + r->used = 0; + b->size = need; + } + b->used = 1; + heap_free_bytes -= b->size; + if (heap_free_bytes < heap_min_free) + heap_min_free = heap_free_bytes; + local_irq_enable(); + return p + sizeof(blk_t); + } + local_irq_enable(); + return 0; +} + +void free(void *ptr) +{ + uint8_t *p, *end = heap + HEAP_BYTES; + blk_t *b; + if (!ptr) + return; + local_irq_disable(); + b = (blk_t *)((uint8_t *)ptr - sizeof(blk_t)); + b->used = 0; + heap_free_bytes += b->size; + for (p = heap; p < end;) { /* merge neighbouring free blocks */ + blk_t *c = (blk_t *)p, *n = (blk_t *)(p + c->size); + if ((uint8_t *)n < end && !c->used && !n->used) + c->size += n->size; + else + p += c->size; + } + local_irq_enable(); +} + +void *zalloc(size_t n) { void *p = malloc(n); if (p) memset(p, 0, n); return p; } +void *ram_malloc(unsigned long n) { return malloc(n); } +void ram_free(void *p) { free(p); } +unsigned int xPortGetPhysiceMemorySize(void) { return HEAP_BYTES; } +uint32_t ble_heap_free(void) { return heap_free_bytes; } +uint32_t ble_heap_min_free(void) { return heap_min_free; } + +/* ---- tasks and their message queues */ +#define MAXTASK 3 /* the library starts one ("btctrler"); room for two more */ +#define STACK_WORDS 1024u /* 4 KB each: the radio start-up replay runs on one */ +#define QDEPTH 16 +#define QARGS 12 /* type + up to 11 words */ +typedef struct { int v[QARGS]; } msg_t; +enum { T_FREE, T_READY, T_WAIT_Q, T_WAIT_SEM, T_SLEEP, T_DEAD }; +typedef struct { + char name[16]; + void (*fn)(void *); + void *arg; + uint32_t *sp; /* saved stack pointer while suspended */ + uint32_t state; + uint32_t wake_ms; /* T_SLEEP, and the timeout of the waits (0: none) */ + void *wait_on; + int timed_out; + msg_t q[QDEPTH]; + uint32_t qh, qn; + uint32_t stack[STACK_WORDS] __attribute__((aligned(8))); +} task_t; +static task_t tasks[MAXTASK] __attribute__((section(".bss.ble_stack"))); /* app.ld brackets it as _ble_stack_lo.._hi */ +static task_t *cur; /* the task being run, 0 in the scheduler */ +static uint32_t *sched_sp; + +static task_t *task_find(const char *name) +{ + int i; + for (i = 0; i < MAXTASK; i++) + if (tasks[i].state != T_FREE && !strcmp(tasks[i].name, name)) + return &tasks[i]; + return 0; +} + +static void task_body(void) +{ + task_t *t = cur; + t->fn(t->arg); + t->state = T_DEAD; + for (;;) + ble_ctx_swap(&t->sp, sched_sp); +} + +static void task_yield(void) { ble_ctx_swap(&cur->sp, sched_sp); } + +int task_create(void (*fn)(void *), void *arg, const char *name) +{ + int i; + for (i = 0; i < MAXTASK; i++) + if (tasks[i].state == T_FREE || tasks[i].state == T_DEAD) + break; + if (i == MAXTASK) + return -1; + memset(&tasks[i], 0, sizeof(task_t) - sizeof(tasks[i].stack)); + strncpy(tasks[i].name, name, sizeof(tasks[i].name) - 1); + tasks[i].fn = fn; + tasks[i].arg = arg; + tasks[i].sp = 0; /* 0: not started, ble_poll enters task_body on first run */ + tasks[i].state = T_READY; + return 0; +} + +int task_kill(const char *name) +{ + task_t *t = task_find(name); + if (!t) + return OS_TASK_NOT_EXIST; + t->state = T_FREE; + return 0; +} + +const char *os_current_task(void) { return cur ? cur->name : "main"; } +/* what a task is blocked on, for the failure line: the semaphore or mutex address, or its state when it is not waiting on one */ +uint32_t ble_task_waiting(const char *name) +{ + task_t *t = task_find(name); + return !t ? 0xFFu : t->state == T_WAIT_SEM ? (uint32_t)(uintptr_t)t->wait_on : t->state; +} + +int os_taskq_post_type(const char *name, int type, int argc, int *argv) +{ + task_t *t = task_find(name); + msg_t *m; + int i; + if (!t) + return OS_TASK_NOT_EXIST; + local_irq_disable(); + if (t->qn == QDEPTH) { + local_irq_enable(); + return OS_Q_FULL; + } + m = &t->q[(t->qh + t->qn++) % QDEPTH]; + m->v[0] = type; + for (i = 0; i < argc && i < QARGS - 1; i++) + m->v[1 + i] = argv[i]; + if (t->state == T_WAIT_Q) + t->state = T_READY; + local_irq_enable(); + return 0; +} + +/* the SDK's os_taskq_pend blocks until a message is queued; argv[0] is the type, argv[1..] the words */ +int os_taskq_pend(const char *fmt, int *argv, int argc) +{ + msg_t m; + int i; + (void)fmt; + if (!cur) + return OS_TIMEOUT; + for (;;) { + local_irq_disable(); + if (cur->qn) { + m = cur->q[cur->qh]; + cur->qh = (cur->qh + 1u) % QDEPTH; + cur->qn--; + local_irq_enable(); + for (i = 0; i < argc && i < QARGS; i++) + argv[i] = m.v[i]; + return OS_TASKQ; + } + cur->state = T_WAIT_Q; + local_irq_enable(); + task_yield(); + } +} + +void os_time_dly(int ticks) +{ + if (!cur) + return; + if (ticks < 1) + ticks = 1; + cur->wake_ms = fm1_uptime_ms() + (uint32_t)ticks * OS_TICK_MS; + cur->state = T_SLEEP; + task_yield(); +} + +/* ---- semaphores and mutexes. The SDK's OS_SEM / OS_MUTEX are opaque and at least 16 bytes: the first word is the + * count (a mutex: 1 free, 0 taken), the second the owner for the mutex. */ +typedef struct { volatile int32_t count; void *owner; } sem_t; + +int os_sem_create(void *s, int cnt) { ((sem_t *)s)->count = cnt; ((sem_t *)s)->owner = 0; return 0; } +int os_sem_set(void *s, uint16_t cnt) { ((sem_t *)s)->count = cnt; return 0; } +int os_sem_post(void *s) +{ + int i; + local_irq_disable(); + ((sem_t *)s)->count++; + for (i = 0; i < MAXTASK; i++) + if (tasks[i].state == T_WAIT_SEM && tasks[i].wait_on == s) + tasks[i].state = T_READY; + local_irq_enable(); + return 0; +} + +int os_sem_pend(void *s, int timeout) +{ + sem_t *sm = s; + uint32_t until = timeout ? fm1_uptime_ms() + (uint32_t)timeout * OS_TICK_MS : 0; + for (;;) { + local_irq_disable(); + if (sm->count > 0) { + sm->count--; + local_irq_enable(); + return 0; + } + if (!cur || (until && (int32_t)(fm1_uptime_ms() - until) >= 0)) { + local_irq_enable(); + return OS_TIMEOUT; + } + cur->state = T_WAIT_SEM; + cur->wait_on = s; + cur->wake_ms = until; + local_irq_enable(); + task_yield(); + } +} + +int os_mutex_create(void *m) { return os_sem_create(m, 1); } +int os_mutex_del(void *m, int force) { (void)m; (void)force; return 0; } +int os_mutex_pend(void *m, int timeout) { return os_sem_pend(m, timeout); } +int os_mutex_post(void *m) { return os_sem_post(m); } + +/* ---- software timers (sys_timer_add: periodic, sys_timeout_add: once), run from ble_poll */ +#define MAXTIMER 16 +typedef struct { + void (*fn)(void *); + void *priv; + uint32_t period, due; + uint8_t used, once; +} timer_t_; +static timer_t_ timers[MAXTIMER]; + +static uint16_t timer_add(void *priv, void (*fn)(void *), uint32_t ms, int once) +{ + int i; + for (i = 0; i < MAXTIMER; i++) + if (!timers[i].used) { + timers[i].fn = fn; timers[i].priv = priv; timers[i].period = ms ? ms : 1u; + timers[i].due = fm1_uptime_ms() + timers[i].period; timers[i].once = (uint8_t)once; timers[i].used = 1; + return (uint16_t)(i + 1); + } + return 0; +} +static void timer_del(uint16_t id) { if (id && id <= MAXTIMER) timers[id - 1].used = 0; } + +uint16_t sys_timer_add(void *p, void (*f)(void *), uint32_t ms) { return timer_add(p, f, ms, 0); } +uint16_t sys_timeout_add(void *p, void (*f)(void *), uint32_t ms) { return timer_add(p, f, ms, 1); } +uint16_t usr_timeout_add(void *p, void (*f)(void *), uint32_t ms, uint8_t prio) { (void)prio; return timer_add(p, f, ms, 1); } +void sys_timer_del(uint16_t id) { timer_del(id); } +void sys_timeout_del(uint16_t id) { timer_del(id); } +void usr_timeout_del(uint16_t id) { timer_del(id); } +void sys_timer_re_run(uint16_t id) { if (id && id <= MAXTIMER && timers[id - 1].used) timers[id - 1].due = fm1_uptime_ms() + timers[id - 1].period; } +int sys_timer_modify(uint16_t id, uint32_t ms) +{ + if (!id || id > MAXTIMER || !timers[id - 1].used) + return -1; + timers[id - 1].period = ms ? ms : 1u; + timers[id - 1].due = fm1_uptime_ms() + timers[id - 1].period; + return 0; +} +void sys_timer_set_user_data(uint16_t id, void *p) { if (id && id <= MAXTIMER) timers[id - 1].priv = p; } +void *sys_timer_get_user_data(uint16_t id) { return id && id <= MAXTIMER ? timers[id - 1].priv : 0; } +uint32_t sys_timer_get_ms(void) { return fm1_uptime_ms(); } + +/* ble_isr.S: counts the baseband interrupts, then continues in the library's own interrupt function (saved here) */ +void (*ble_irq_target_ev)(void), (*ble_irq_target_rx)(void); +extern void ble_isr_ev(void), ble_isr_rx(void); + +/* ---- interrupt registration: the library hands over its own interrupt functions (already complete interrupt entries) */ +void request_irq(uint8_t idx, uint8_t prio, void (*handler)(void), uint8_t cpu) +{ + (void)cpu; + ble_stage_set(BLE_STAGE_BB); /* le_hw_init: the baseband is set up and asks for its interrupts */ + if (idx == 45u) { ble_irq_target_ev = handler; handler = ble_isr_ev; } /* ble_isr.S counts, then runs the library's entry */ + else if (idx == 29u) { ble_irq_target_rx = handler; handler = ble_isr_rx; } + fm1_irq_attach(idx, handler, prio); +} +void unrequest_irq(uint8_t idx, uint8_t cpu) { (void)cpu; FM1_ICFG(idx) &= ~(1u << ((idx & 7u) * 4u)); } +void bit_set_ie(uint8_t idx, uint8_t cpu) { (void)cpu; FM1_ICFG(idx) |= 1u << ((idx & 7u) * 4u); } +void bit_clr_ie(uint8_t idx, uint8_t cpu) { (void)cpu; FM1_ICFG(idx) &= ~(1u << ((idx & 7u) * 4u)); } +uint8_t irq_read(uint32_t idx) { return (uint8_t)((FM1_ICFG(idx) >> ((idx & 7u) * 4u)) & 0xFu); } + +/* ---- the scheduler */ +void ble_poll(void) +{ + int i, ran; + fm1_wdt_feed(); + uint32_t now = fm1_uptime_ms(); + for (i = 0; i < MAXTIMER; i++) { + timer_t_ *t = &timers[i]; + if (t->used && (int32_t)(now - t->due) >= 0) { + if (t->once) + t->used = 0; + else + t->due = now + t->period; + ble_cnt_timer++; + t->fn(t->priv); + } + } + do { + ran = 0; + now = fm1_uptime_ms(); + for (i = 0; i < MAXTASK; i++) { + task_t *t = &tasks[i]; + if ((t->state == T_SLEEP || t->state == T_WAIT_SEM) && t->wake_ms && (int32_t)(now - t->wake_ms) >= 0) + t->state = T_READY; + if (t->state != T_READY) + continue; + cur = t; + ran = 1; + if (!t->sp) + ble_ctx_enter(&sched_sp, t->stack + STACK_WORDS, task_body); + else + ble_ctx_swap(&sched_sp, t->sp); + cur = 0; + } + } while (ran); +} diff --git a/firmware/ble/ble_os.h b/firmware/ble/ble_os.h new file mode 100644 index 0000000..beeb766 --- /dev/null +++ b/firmware/ble/ble_os.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +#pragma once +#include +#include + +/* C library routines the controller library needs (ble_libc.c); memset and memcpy are bsp/chip/libc.c's */ +void *memset(void *d, int c, unsigned n); +void *memcpy(void *d, const void *s, unsigned n); +size_t strlen(const char *s); +int strcmp(const char *a, const char *b); +char *strcpy(char *d, const char *s); +char *strncpy(char *d, const char *s, size_t n); +int memcmp(const void *a, const void *b, unsigned n); +void *memmove(void *d, const void *s, size_t n); +void *malloc(size_t n); +void free(void *p); + +void ble_poll(void); /* the main loop calls this often: runs the controller's timers and tasks */ +uint32_t ble_heap_free(void); +uint32_t ble_heap_min_free(void); + +/* ble_test.c */ +void ble_test_start(void); /* radio on, controller up, one advertisement */ +void ble_test_poll(void); +void ble_test_adv_line(char *out); /* the long advertising line: counters and baseband registers (ble_status.h) */ +void ble_test_state(char *out); /* the short status text for the screen (ble_status.h), 24 bytes */ diff --git a/firmware/ble/ble_rf.c b/firmware/ble/ble_rf.c new file mode 100644 index 0000000..b46af4f --- /dev/null +++ b/firmware/ble/ble_rf.c @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* The part of the radio start-up that the JieLi controller library leaves to the Wi-Fi library: its bt_rf_init (RF.c) + * first calls wf_rf_common_init, which brings up the shared Wi-Fi front end (about 34,000 register writes). Linking + * the Wi-Fi library for that would pull in its SDIO/MAC stack, so the writes come from the committed capture of the + * stock start-up instead (bsp/chip/ble_rf_tables_v15.h, run by bsp/chip/fm1_ble_rf.h: stock's boot-2 order, groups 2 + * to 13), with this unit's trims measured by fm1_rf_cal.h (docs/BLE-TEST.md, "What is not verified"). + * + * The saved radio records (VM items 106, 107, 108, 187) are not read: the VM stays untouched. The records are given + * as zeros, which leaves the crystal trim (106) and the other stored fields at the captured defaults. */ +#include "ble_os.h" +#include "ble_status.h" +#include "fm1_ble_rf.h" + +static const uint8_t no_record[68]; + +void wf_rf_common_init(void) +{ + ble_stage_set(BLE_STAGE_RF); + fm1_wdt_feed(); /* the replay takes a while: keep the watchdog fed around it */ + fm1_rf_cal_on = 1; + ble_busy(0); /* the replay is long, and its waits are bounded */ + fm1_ble_rf_run(no_record, no_record, no_record, no_record); + ble_busy(1); + fm1_wdt_feed(); + ble_stage_set(BLE_STAGE_RF_DONE); /* the replay returned: what follows is the library's own start-up */ +} + +/* The library asks for saved items (trims, bonding): none, as in audio_probe.h. It stores nothing: the VM and BTIF + * areas are never written by this image. */ +int syscfg_read(uint16_t id, void *buf, uint16_t len) { (void)id; (void)buf; (void)len; return -1; } +int syscfg_write(uint16_t id, void *buf, uint16_t len) { (void)id; (void)buf; return len; } +void hw_udelay(uint32_t us) { fm1_delay_us(us); fm1_wdt_feed(); } +uint32_t sys_clk_get(void) { return 360000000u; } /* SYS_CLK in the stock isd_config.ini */ +uint8_t boot_info[64]; /* read by the library's chip-id helpers only */ diff --git a/firmware/ble/ble_rf_tables.h b/firmware/ble/ble_rf_tables.h new file mode 100644 index 0000000..5d84e3a --- /dev/null +++ b/firmware/ble/ble_rf_tables.h @@ -0,0 +1,3 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* fm1_ble_rf.h includes the radio start-up program as "ble_rf_tables.h"; here it is the committed capture. */ +#include "../bsp/chip/ble_rf_tables_v15.h" diff --git a/firmware/ble/ble_status.h b/firmware/ble/ble_status.h new file mode 100644 index 0000000..0df09d2 --- /dev/null +++ b/firmware/ble/ble_status.h @@ -0,0 +1,160 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* What the Bluetooth test image shows on the screen (docs/BLE-TEST.md, "Reading the status text"): where start-up has got + * to, and, when it failed, which step and which return value. No hardware access, so tests/test_ble_status.c compiles it + * on the PC. + * + * STARTING the last stage reached (BLE_STAGE_*): a timeout names the stage it stalled in + * FAILED the step that failed (BLE_FAIL_*) and its return value; for BLE_FAIL_TIMEOUT the code is the stage + * ADVERTISING the controller accepted the command to advertise (not proof of a packet: see the proof counters below) + * and the marquee that scrolls the full failure line across the small status area. */ +#pragma once +#include +#include + +enum { BLE_FAIL_NONE, BLE_FAIL_TASK_CREATE, BLE_FAIL_INIT, BLE_FAIL_READY, BLE_FAIL_ADV_ENABLE, BLE_FAIL_TIMEOUT }; +enum { BLE_STAGE_IDLE, BLE_STAGE_TASK, BLE_STAGE_INIT, BLE_STAGE_RF, BLE_STAGE_READY, BLE_STAGE_ADV, BLE_STAGE_ENABLE, + BLE_STAGE_RF_DONE, BLE_STAGE_BB }; +enum { BLE_OFF, BLE_STARTING, BLE_ADV, BLE_FAILED }; + +static volatile uint32_t ble_state; +static volatile uint32_t ble_stage; +static volatile uint32_t ble_fail_step; +static volatile int32_t ble_fail_code; +static volatile uint32_t ble_wait; /* at a timeout: what the controller task is blocked on (ble_task_waiting) */ + +static void ble_stage_set(uint32_t stage) { ble_stage = stage; } + +/* The stall watch. The scheduler is cooperative, so library code that never returns (a failed assertion parks in + * P33_SYSTEM_RESET, a wait on a register bit) holds the main loop until the watchdog resets, and that leaves no trace. + * The 10 kHz tick (px_usb.h) therefore counts while a ble_poll runs; one that has not returned after BLE_STALL_TICKS + * makes the next tick the crash screen, whose PC and RETS name the code (VEC 63). 0: no poll running, or held off. */ +enum { BLE_STALL_TICKS = 40000 }; /* 4 s: before the watchdog (8 s) */ +static volatile uint32_t ble_busy_ticks; +static void ble_busy(int on) { ble_busy_ticks = on ? 1u : 0u; } +static int ble_stall_tick(void) { return ble_busy_ticks && ++ble_busy_ticks > BLE_STALL_TICKS; } + +static void ble_fail(uint32_t step, int32_t code) +{ + ble_fail_step = step; + ble_fail_code = code; + ble_state = BLE_FAILED; +} + +/* the results of btctrler_task_init and btctrler_task_ready (1 when the controller task answered) -> the failing step, or 0 */ +static uint32_t ble_judge_start(int init_rc, int ready_rc) +{ + if (init_rc) { ble_fail(BLE_FAIL_INIT, init_rc); return BLE_FAIL_INIT; } + if (!ready_rc) { ble_fail(BLE_FAIL_READY, ready_rc); return BLE_FAIL_READY; } + return 0; +} + +static char *ble__put(char *o, const char *s) { while (*s) *o++ = *s++; return o; } + +static char *ble__num(char *o, int32_t v) +{ + char t[12]; int n = 0; + uint32_t u = v < 0 ? 0u - (uint32_t)v : (uint32_t)v; + if (v < 0) *o++ = '-'; + do { t[n++] = (char)('0' + u % 10u); u /= 10u; } while (u); + while (n) *o++ = t[--n]; + return o; +} + +/* the status text into out (at least 24 bytes) */ +static void ble_status_text(char *out) +{ + char *o = out; + if (ble_state == BLE_ADV) o = ble__put(o, "ADVERTISING"); + else if (ble_state == BLE_STARTING) { o = ble__put(o, "STARTING "); o = ble__num(o, (int32_t)ble_stage); } + else if (ble_state == BLE_FAILED) { + o = ble__put(o, "FAILED "); o = ble__num(o, (int32_t)ble_fail_step); + *o++ = ' '; o = ble__num(o, ble_fail_code); + } else o = ble__put(o, "OFF"); + *o = 0; +} + +/* The full failure line, for the marquee: the short code first, then the step and the stage in words (out: 96 bytes) */ +static void ble_status_full(char *out) +{ + static const char *const step[] = { "none", "task_create", "btctrler_task_init", "btctrler_task_ready", "ll_hci_adv_enable", "timeout" }; + static const char *const stage[] = { "idle", "task", "init", "radio-init", "ready-wait", "adv-setup", "enable", "radio-done", "baseband-irq" }; + char *o; + ble_status_text(out); + if (ble_state != BLE_FAILED) return; + o = out + strlen(out); + o = ble__put(o, " step "); + o = ble__put(o, ble_fail_step < 6 ? step[ble_fail_step] : "?"); + o = ble__put(o, " stage "); + o = ble__put(o, ble_stage < 9 ? stage[ble_stage] : "?"); + if (ble_wait) { /* look the address up in firmware/build-ble/symbols.txt */ + int i; + o = ble__put(o, " wait "); + for (i = 28; i >= 0; i -= 4) *o++ = "0123456789ABCDEF"[(ble_wait >> i) & 15u]; + } + *o = 0; +} + +/* A marquee: the text, then a gap, looping; tick counts the 500 ms redraws. It rests for BLE_MARQUEE_PAUSE ticks on the + * start, then moves one character per tick (2 a second). win characters go to out, which gets a terminating 0. */ +enum { BLE_MARQUEE_PAUSE = 4, BLE_MARQUEE_GAP = 3 }; +static void ble_marquee(const char *text, uint32_t tick, int win, char *out) +{ + uint32_t len = (uint32_t)strlen(text), period = len + BLE_MARQUEE_GAP, phase, off; + int i; + if (len <= (uint32_t)win) { memcpy(out, text, len + 1u); return; } /* it fits: no scroll */ + phase = tick % (BLE_MARQUEE_PAUSE + period); + off = phase < BLE_MARQUEE_PAUSE ? 0u : phase - BLE_MARQUEE_PAUSE; + for (i = 0; i < win; i++) { + uint32_t k = (off + (uint32_t)i) % period; + out[i] = k < len ? text[k] : ' '; + } + out[win] = 0; +} + +/* ---- proof counters: does the radio run at all? Shown next to ADVERTISING (docs/BLE-TEST.md, "Reading the status text"). + * ble_isr.S counts the two baseband interrupts as they arrive; the others are counted where the library calls us. */ +uint32_t ble_cnt_irq_ev; /* IRQ 45: baseband event (an advertising event, a transmit done) */ +uint32_t ble_cnt_irq_rx; /* IRQ 29: a packet received (a scanner's request) */ +static volatile uint32_t ble_cnt_timer; /* the library's software timers that fired */ +static volatile uint32_t ble_cnt_hci; /* HCI packets the controller returned (hci_packet_handler) */ +static volatile uint32_t ble_cnt_slot; /* calls of the slot-timer stand-ins: the link scheduler wanting a timer we do not have */ +static volatile uint32_t ble_hci_last; /* type << 8 | event code of the last HCI packet */ + +static int ble_count_hci(uint8_t type, const uint8_t *pkt) +{ + ble_cnt_hci++; + ble_hci_last = ((uint32_t)type << 8) | (pkt ? pkt[0] : 0u); + return 0; +} + +static char *ble__dec(char *o, uint32_t v) { return ble__num(o, (int32_t)(v > 0x7FFFFFFFu ? 0x7FFFFFFF : v)); } +static char *ble__hex(char *o, uint32_t v) +{ + int i = 28; + while (i > 0 && !((v >> i) & 15u)) i -= 4; + for (; i >= 0; i -= 4) *o++ = "0123456789ABCDEF"[(v >> i) & 15u]; + return o; +} + +/* the advertising line, the numbers first so that they are on the screen before the marquee moves: + * "ADV i12 e5 r0 t340 s0 CON 2001 IEN 101 IPND 0 STAT 80 IC45 3 IC29 3 BEN 1" (out: 96 bytes; the registers are read by the caller) + * i interrupts 45 e HCI packets returned r interrupts 29 t library timers s slot-timer stand-in calls + * CON/IEN/IPND/STAT baseband 0x28000/28/30/38 IC45, IC29 the interrupt configuration nibble (bit 0 = enabled, bits 3:1 = + * priority; 0 = the line is off) BEN the BLE block enable 0x2FC80 (bit 0) */ +static void ble_adv_text(char *out, uint32_t con, uint32_t ien, uint32_t ipnd, uint32_t stat, uint32_t ic45, uint32_t ic29, uint32_t ben) +{ + char *o = out; + o = ble__put(o, "ADV i"); o = ble__dec(o, ble_cnt_irq_ev); + o = ble__put(o, " e"); o = ble__dec(o, ble_cnt_hci); + o = ble__put(o, " r"); o = ble__dec(o, ble_cnt_irq_rx); + o = ble__put(o, " t"); o = ble__dec(o, ble_cnt_timer); + o = ble__put(o, " s"); o = ble__dec(o, ble_cnt_slot); + o = ble__put(o, " CON "); o = ble__hex(o, con); + o = ble__put(o, " IEN "); o = ble__hex(o, ien); + o = ble__put(o, " IPND "); o = ble__hex(o, ipnd); + o = ble__put(o, " STAT "); o = ble__hex(o, stat); + o = ble__put(o, " IC45 "); o = ble__hex(o, ic45); + o = ble__put(o, " IC29 "); o = ble__hex(o, ic29); + o = ble__put(o, " BEN "); o = ble__hex(o, ben); + *o = 0; +} diff --git a/firmware/ble/ble_stubs.c b/firmware/ble/ble_stubs.c new file mode 100644 index 0000000..2d04af5 --- /dev/null +++ b/firmware/ble/ble_stubs.c @@ -0,0 +1,209 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* What the controller library references but this image never runs: Bluetooth classic (BR/EDR, LMP, eSCO, TWS), LE + * 5 extended features, controller-side test modes, Wi-Fi coexistence and low-power negotiation. The configuration + * (ble_cfg.c: LE only, advertising and one slave link) keeps the library from calling them; each is an empty function + * that returns 0 and counts the call in ble_stub_calls so a call that should not happen shows on the screen. + * Generated from the link errors of the "ble" target (list the names with: grep BLE_STUB firmware/ble/ble_stubs.c). */ +#include +#include "ble_status.h" + +uint32_t ble_stub_calls; +#define BLE_STUB(name) int name(void) { ble_stub_calls++; return 0; } +#define BLE_STUB_SLOT(name) int name(void) { ble_stub_calls++; ble_cnt_slot++; return 0; } /* the link scheduler's timer: counted apart */ +BLE_STUB(__ble5_hw_anchor_cnt_timeout) +BLE_STUB(__ble5_hw_event_process) +BLE_STUB(__ble5_hw_rx_process) +BLE_STUB(__hw5_free_slots) +BLE_STUB(__ll_receive_length_req) +BLE_STUB(__ll_send_phy_req) +BLE_STUB(__master_ll_phy_update_done) +BLE_STUB(__master_ll_send_phy_update_ind) +BLE_STUB(__master_reset_transmitWindowDelay) +BLE_STUB(__reset_CodedPHY_conn_winsize) +BLE_STUB(__reset_connection_winsize) +BLE_STUB(__set_extended_conn_dead_times) +BLE_STUB(__set_extended_empty_tx_data) +BLE_STUB(__set_extended_tx_data) +BLE_STUB(__set_ll_effective_data_args) +BLE_STUB(__slave_ll_phy_update_done) +BLE_STUB(__slave_ll_send_vendor_wifi_detect_req) +BLE_STUB(__slave_reset_widen) +BLE_STUB(__slave_vendor_wifi_detect_done) +BLE_STUB(aes128_start_enc) +BLE_STUB(ble5_master_primary_event_end_handle) +BLE_STUB(ble_ext_primary_scan_preprocess) +BLE_STUB(ble_master_multilink_schedule) +BLE_STUB(bredr_bd_close) +BLE_STUB(bredr_bd_init) +BLE_STUB(bredr_close_all_scan) +BLE_STUB(bredr_fcc_init) +BLE_STUB(bredr_goto_background) +BLE_STUB(bredr_link_check_used) +BLE_STUB(bredr_link_close) +BLE_STUB(bredr_link_init) +BLE_STUB(bredr_link_reset_power) +BLE_STUB(bredr_power_off) +BLE_STUB(bredr_power_on) +BLE_STUB(bredr_power_resume) +BLE_STUB(bredr_power_suspend) +BLE_STUB(bredr_radiopwrupdn_reset) +BLE_STUB(bredr_read_slot_clk) +BLE_STUB(bredr_rx_bulk_empty) +BLE_STUB(bredr_rx_bulk_resume) +BLE_STUB(bredr_rx_bulk_suspend) +BLE_STUB(bredr_rx_remain_bulk_empty) +BLE_STUB(bredr_tx_bulk_empty) +BLE_STUB(bt_edr_prio_settings) +BLE_STUB(bt_event_notify) +BLE_STUB(bt_sfr_pop) +BLE_STUB(ccm_memory_v2) +BLE_STUB(conn_master_rx_post_handler) +BLE_STUB(dut_bredr_close_recall) +BLE_STUB(dut_hci_emit_le_meta_event) +BLE_STUB(dut_le_hw_open) +BLE_STUB(find_interspace_between_connected) +BLE_STUB(get_bredr_is_init) +BLE_STUB(get_if_exist_ble5) +BLE_STUB(hci_add_cmd_handler) +BLE_STUB(hci_lmp_controller_baseband_cmd_handler) +BLE_STUB(hci_lmp_informational_parameters_cmd_handler) +BLE_STUB(hci_lmp_link_control_cmd_handler) +BLE_STUB(hci_lmp_link_policy_cmd_handler) +BLE_STUB(hci_lmp_status_parameters_cmd_handler) +BLE_STUB(hci_lmp_testing_cmd_handler) +BLE_STUB(hci_lmp_vendor_cmd_handler) +BLE_STUB(hci_push_iso_sdu) +BLE_STUB(hci_vendor_cmd_handler) +BLE_STUB(le_controller_handler_for_ble5) +BLE_STUB(le_dut_hci_reset_handle) +BLE_STUB(le_enhanced_receiver_test_handle) +BLE_STUB(le_enhanced_transmitter_test_handle) +BLE_STUB(le_receive_test_handle) +BLE_STUB(le_test_end_handle) +BLE_STUB(le_transmitter_test_handle) +BLE_STUB(link_bulk_init) +BLE_STUB(link_change_bulk_init) +BLE_STUB(link_change_rx_bulk_size) +BLE_STUB(link_data_extension_init) +BLE_STUB(ll_data_extension_init) +BLE_STUB(ll_exit_ext_legacy_adv_status) +BLE_STUB(ll_ext_destroy) +BLE_STUB(ll_ext_handle_malloc) +BLE_STUB(ll_ext_legacy_adv_scan_req_received) +BLE_STUB(ll_ext_rx_post_process) +BLE_STUB(ll_read_maximum_data_length) +BLE_STUB(ll_read_suggested_default_data_length) +BLE_STUB(ll_set_default_prefers_phys) +BLE_STUB(ll_update_ch_done_wfdet_callback) +BLE_STUB(ll_wifi_detect_ext) +BLE_STUB(ll_wifi_detect_init) +BLE_STUB(ll_wifi_detect_probe_process) +BLE_STUB(ll_write_suggested_default_data_length) +BLE_STUB(lmp_conn_for_address) +BLE_STUB(lmp_conn_for_handle) +BLE_STUB(lmp_conn_resume) +BLE_STUB(lmp_conn_suspend) +BLE_STUB(lmp_esco_conn_for_handle) +BLE_STUB(lmp_event_handler) +BLE_STUB(lmp_get_conn_num) +BLE_STUB(lmp_hci_cmd_handler) +BLE_STUB(lmp_hci_cmd_to_conn_handler) +BLE_STUB(lmp_hci_disconnect) +BLE_STUB(lmp_hci_read_remote_version_information) +BLE_STUB(lmp_hci_send_packet_standard) +BLE_STUB(lmp_init) +BLE_STUB(lmp_reset) +BLE_STUB(lmp_tx_suspend) +BLE_STUB(low_power_exit_request) +BLE_STUB(low_power_get_osc_type) +BLE_STUB(low_power_on) +BLE_STUB(low_power_put) +BLE_STUB(low_power_request) +BLE_STUB(low_power_trace_drift) +BLE_STUB(master_rx_conn_post_handler) +BLE_STUB(master_setup_connection_updatea_first_data_location) +BLE_STUB(master_setup_first_data_location) +BLE_STUB(mult_slave_or_master_check_up_point) +BLE_STUB(mult_slave_or_master_del_conn_id) +BLE_STUB(mult_slave_or_master_event_enter) +BLE_STUB(mult_slave_or_master_fist_con_event_set) +BLE_STUB(mult_slave_or_master_get_con_num) +BLE_STUB(mult_slave_or_master_get_rx_irq_hw) +BLE_STUB(mult_slave_or_master_save_conn_id) +BLE_STUB(mult_slave_or_master_set_adv_interval) +BLE_STUB(mult_slave_or_master_set_init_param) +BLE_STUB(mult_slave_or_master_set_scan_param) +BLE_STUB(ogf_vendor_le_controller) +BLE_STUB(power_is_poweroff_post) +BLE_STUB(record_rx_chsel_algorithm) +BLE_STUB(refind_interspace_between_connected) +BLE_STUB(refresh_phy_param) +BLE_STUB(repair_algorithm2_hop) +BLE_STUB(rf_dc_set_max) +BLE_STUB(rf_dc_set_reload) +BLE_STUB(set_hw_channel_selection_algorithm) +BLE_STUB(set_tx_pdu_channel_selection_algorithm) +BLE_STUB(slave_ll_receive_phy_req) +BLE_STUB_SLOT(slot_timer_disable) +BLE_STUB_SLOT(slot_timer_get) +BLE_STUB_SLOT(slot_timer_put) +BLE_STUB_SLOT(slot_timer_remain) +BLE_STUB_SLOT(slot_timer_reset) +BLE_STUB_SLOT(slot_timer_set) +BLE_STUB(suspend_init) +BLE_STUB(tws_link_ble_slave_conn_establish_handler) +BLE_STUB(tws_link_ble_slave_conn_establish_probe_handler) +BLE_STUB(tws_link_get_ble_role_slave) +BLE_STUB(tws_phone_ble_link_connection) +BLE_STUB(tws_phone_ble_link_disconnection) +BLE_STUB(wf_get_phcom_cnt) + +/* The low-power manager's registration. Not an empty stand-in: the library asserts the handle it gets back (ble_power_get, + * called when advertising starts), and a failed assertion never returns. It only tests the handle afterwards. */ +void *low_power_get(void *priv, void *ops) { static uint8_t handle; (void)priv; (void)ops; return &handle; } + +/* values the library keeps for the radio's offset trims (analog.c): written by it, read by it */ +int keep_BT_CMPUK1; +int keep_BT_CMPUK2; +int keep_BT_CMPUK3; +int keep_BT_OFSI; +int keep_BT_OFSQ; + +/* log tags: the library prints nothing (a tag of 0 turns its log call off) */ +#define TAG(n) const char n = 0; +TAG(log_tag_const_d_HCI_LL) +TAG(log_tag_const_d_HCI_STD) +TAG(log_tag_const_d_LE_BB) +TAG(log_tag_const_d_LL_AFH) +TAG(log_tag_const_d_LL_E) +TAG(log_tag_const_e_HCI_STD) +TAG(log_tag_const_e_LE_BB) +TAG(log_tag_const_e_LL) +TAG(log_tag_const_e_LL_ADV) +TAG(log_tag_const_e_LL_AFH) +TAG(log_tag_const_e_LL_E) +TAG(log_tag_const_e_LL_INIT) +TAG(log_tag_const_e_LL_RL) +TAG(log_tag_const_e_LL_S) +TAG(log_tag_const_e_LL_SCAN) +TAG(log_tag_const_e_LL_WL) +TAG(log_tag_const_e_LMP) +TAG(log_tag_const_i_Analog) +TAG(log_tag_const_i_HCI_LL) +TAG(log_tag_const_i_HCI_LMP) +TAG(log_tag_const_i_HCI_STD) +TAG(log_tag_const_i_LE_BB) +TAG(log_tag_const_i_LL) +TAG(log_tag_const_i_LL_ADV) +TAG(log_tag_const_i_LL_AFH) +TAG(log_tag_const_i_LL_E) +TAG(log_tag_const_i_LL_INIT) +TAG(log_tag_const_i_LL_RL) +TAG(log_tag_const_i_LL_S) +TAG(log_tag_const_i_LL_SCAN) +TAG(log_tag_const_i_LL_WL) +TAG(log_tag_const_i_LMP) +TAG(log_tag_const_i_RF) +TAG(log_tag_const_i_Thread) +TAG(log_tag_const_w_LL_AFH) +TAG(log_tag_const_i_WLC) diff --git a/firmware/ble/ble_test.c b/firmware/ble/ble_test.c new file mode 100644 index 0000000..2e20eb3 --- /dev/null +++ b/firmware/ble/ble_test.c @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0 */ +/* Advertise-only Bluetooth test (build target "ble", docs/BLE-TEST.md): the controller from the JieLi SDK library + * (third_party/jieli-ble) is started, and one scannable, non-connectable advertisement is sent on the three + * advertising channels: + * advertising data flags, the BLE-MIDI service UUID 03B80E5A-EDE8-4B33-A751-6CE34EC4C700 (128 bit) + * scan response the complete local name "OpenPocket-BLE-TEST" + * No connection is ever accepted (the type is ADV_SCAN_IND), no pairing, nothing is sent over Bluetooth but this. */ +#include +#include +#include "fm1_irq.h" +#include "ble_os.h" +#include "ble_status.h" +#include "ble_adv.h" + +/* the controller library's entry points (include_lib/btctrler/btctrler_task.h, ble/hci_ll.h; hci_transport.h) */ +extern int btctrler_task_init(const void *transport, const void *config); +extern int btctrler_task_ready(void); /* blocks until the controller task answers, 1 when it did */ +extern const void *hci_transport_h4_controller_instance(void); +extern void ll_hci_adv_set_params(uint16_t adv_int_min, uint16_t adv_int_max, uint8_t adv_type, uint8_t direct_address_type, + uint8_t *direct_address, uint8_t channel_map, uint8_t filter_policy); +extern void ll_hci_adv_set_data(uint8_t len, uint8_t *data); +extern void ll_hci_adv_scan_response_set_data(uint8_t len, uint8_t *data); +extern int ll_hci_adv_enable(bool enable); +extern int task_create(void (*fn)(void *), void *arg, const char *name); /* ble_os.c */ +extern uint32_t ble_task_waiting(const char *name); /* ble_os.c */ +extern int le_controller_set_mac(const uint8_t *mac); /* cpu.a/btctrler: the public address slot */ +extern int __crc16_mutex_init(void); /* cpu.a crc16.c: creates the lock its CRC helpers take; the SDK's start-up calls it */ + +static uint32_t t_start; +static uint8_t cfg[64]; /* the controller reads its host-side settings here: none */ + +/* btctrler_task_ready waits (on a semaphore) until the controller's own task has started, so this runs as a task of + * its own: the main loop must never block there. */ +static void test_task(void *arg) +{ + int rc; + (void)arg; + ble_stage_set(BLE_STAGE_INIT); /* (BLE_STAGE_RF: ble_rf.c, inside the init) */ + rc = btctrler_task_init(hci_transport_h4_controller_instance(), cfg); + if (rc) { ble_judge_start(rc, 0); return; } + ble_stage_set(BLE_STAGE_READY); + if (ble_judge_start(0, btctrler_task_ready())) return; + ble_stage_set(BLE_STAGE_ADV); + le_controller_set_mac(ble_adv_mac); /* the public address slot is zero otherwise (ble_adv.h) */ + ll_hci_adv_set_params(BLE_ADV_INTERVAL, BLE_ADV_INTERVAL, BLE_ADV_SCAN_IND, 0, ble_no_addr, BLE_ADV_CHMAP, BLE_ADV_FILTER); + ll_hci_adv_set_data(sizeof(ble_adv_data), ble_adv_data); + ll_hci_adv_scan_response_set_data(sizeof(ble_scan_rsp), ble_scan_rsp); + ble_stage_set(BLE_STAGE_ENABLE); + rc = ll_hci_adv_enable(true); + if (rc) ble_fail(BLE_FAIL_ADV_ENABLE, rc); else ble_state = BLE_ADV; +} + +void ble_test_start(void) +{ + __crc16_mutex_init(); /* else thread_create (ll_init) waits for ever in crc_get_32bit */ + ble_state = BLE_STARTING; + ble_stage_set(BLE_STAGE_TASK); + t_start = fm1_uptime_ms(); + if (task_create(test_task, 0, "bletest")) + ble_fail(BLE_FAIL_TASK_CREATE, -1); +} + +void ble_test_poll(void) +{ + ble_busy(1); + ble_poll(); + ble_busy(0); + if (ble_state == BLE_STARTING && fm1_uptime_ms() - t_start > 5000u) + { /* (the task, if it is still waiting, is left alone) */ + ble_wait = ble_task_waiting("btctrler"); + ble_fail(BLE_FAIL_TIMEOUT, (int32_t)ble_stage); + } +} + +/* every packet the controller returns to its host: we have none, so this only counts them (h4_controller_interface.c) */ +int hci_packet_handler(uint8_t type, uint8_t *pkt, uint16_t size) { (void)size; return ble_count_hci(type, pkt); } + +void ble_test_state(char *out) { ble_status_text(out); } + +/* the long advertising line, with the baseband registers read now (0x28000 control, 0x28028 enables, 0x28030 pending, 0x28038 status) */ +void ble_test_adv_line(char *out) +{ + ble_adv_text(out, *(volatile uint32_t *)0x28000u, *(volatile uint32_t *)0x28028u, *(volatile uint32_t *)0x28030u, *(volatile uint32_t *)0x28038u, + (FM1_ICFG(45) >> ((45u & 7u) * 4u)) & 0xFu, (FM1_ICFG(29) >> ((29u & 7u) * 4u)) & 0xFu, *(volatile uint32_t *)0x2FC80u); +} diff --git a/firmware/build.sh b/firmware/build.sh index 506303d..eed31cb 100755 --- a/firmware/build.sh +++ b/firmware/build.sh @@ -1,6 +1,9 @@ #!/bin/sh # Build OpenPocket -> firmware/openpocket.bin (app), build/loader/ota.bin (update loader), # docs/firmware/*.fwsc (packages for the web installer) (in the container: tools/op.mjs build) +# OP_BLE=1 sh firmware/build.sh builds the advertise-only Bluetooth test image instead (docs/BLE-TEST.md): the same +# firmware plus the JieLi controller (third_party/jieli-ble) -> firmware/openpocket-ble.bin and build/ble/*.fwsc. +# Nothing of the normal build's outputs is touched. set -e HERE="$(cd "$(dirname "$0")" && pwd)" BSP="${BSP:-$HERE/bsp}" @@ -17,7 +20,12 @@ if [ ! -x "$TC/pi32v2/bin/ld" ]; then echo "ERROR: linker '$TC/pi32v2/bin/ld' missing (incomplete toolchain)." exit 1 fi -O="$HERE/build"; rm -rf "$O"; mkdir -p "$O" +# the stack guard: on, except in the Bluetooth test image (its task stacks are outside the guard window); app.ld refuses the +# link if the guard is on and those stacks are outside. OP_GUARD_STACK=1 with OP_BLE=1 is how that check is shown to fire. +GUARD_STACK=1; [ -n "$OP_BLE" ] && GUARD_STACK="${OP_GUARD_STACK:-0}" +if [ -n "$OP_BLE" ]; then O="$HERE/build-ble"; APP="$HERE/openpocket-ble.bin"; PKG="$HERE/../build/ble"; BLEDEF="-DOP_BLE_TEST=1 -I$HERE/ble" +else O="$HERE/build"; APP="$HERE/openpocket.bin"; PKG="$HERE/../docs/firmware"; BLEDEF=""; fi +rm -rf "$O"; mkdir -p "$O" CC="$TC/pi32v2/bin/clang -target pi32v2" $CC -c "$BSP/chip/crt0.S" -o "$O/crt0.o" $CC -c "$BSP/chip/fm1_vec.S" -o "$O/vec.o" @@ -31,19 +39,30 @@ fi FP="-mcpu=r3 -mfprev1" DSPDEF="-DPX_SR=44100.0f -DPX_OVERSAMPLE=4" INC="-I$HERE/inc -I$HERE/../engine" -$CC $FP -Os -ffunction-sections -fno-builtin -Wall -Wno-unused-function $DSPDEF $INC \ +$CC $FP -Os -ffunction-sections -fno-builtin -Wall -Wno-unused-function $DSPDEF $BLEDEF -DOP_GUARD_STACK=$GUARD_STACK $INC \ -I"$BSP/chip" -I"$BSP/boards/fm1" -c "$HERE/openpocket.c" -o "$O/openpocket.o" $CC $FP -O2 -fno-builtin $DSPDEF $INC -c "$HERE/../engine/px_dsp.c" -o "$O/px_dsp.o" $CC $FP -O2 -fno-builtin -c "$HERE/px_math.c" -o "$O/px_math.o" +BLEOBJ=""; MAPOPT="" +if [ -n "$OP_BLE" ]; then + MAPOPT="-Map $O/openpocket.map" + # JieLi's LE controller (LLVM bitcode, Apache-2.0, third_party/jieli-ble) -> pi32v2 objects, and our context switch + mkdir -p "$O/jl" + for b in "$HERE"/../third_party/jieli-ble/*/*.bc; do + $CC -Os -c -x ir "$b" -o "$O/jl/$(basename "$(dirname "$b")")_$(basename "$b" .bc).o" + done + rm -f "$O/libjl.a"; "$TC/pi32v2/bin/ar" rcs "$O/libjl.a" "$O"/jl/*.o # an archive: the linker takes only the objects the image calls + $CC -c "$HERE/ble/ble_ctx.S" -o "$O/ble_ctx.o"; $CC -c "$HERE/ble/ble_isr.S" -o "$O/ble_isr.o"; BLEOBJ="$O/ble_ctx.o $O/ble_isr.o $O/libjl.a" +fi # official JieLi I2S driver (LLVM bitcode from the AC79 SDK, Apache-2.0) -> pi32v2 object $CC -Os -c -x ir "$HERE/sdk/iis.bc" -o "$O/iis.o" -"$TC/pi32v2/bin/ld" -T "$HERE/app.ld" "$O/crt0.o" "$O/vec.o" "$O/isr.o" $CPU1OBJ "$O/openpocket.o" "$O/px_dsp.o" "$O/px_math.o" "$O/iis.o" -o "$O/openpocket.elf" +"$TC/pi32v2/bin/ld" --defsym "op_guard_stack=$GUARD_STACK" -T "$HERE/app.ld" "$O/crt0.o" "$O/vec.o" "$O/isr.o" $CPU1OBJ "$O/openpocket.o" "$O/px_dsp.o" "$O/px_math.o" "$O/iis.o" $BLEOBJ $MAPOPT -o "$O/openpocket.elf" # symbol map: used to translate a crash address (PC) into a function name "$TC/common/bin/objdump" -t "$O/openpocket.elf" 2>/dev/null | sort > "$O/symbols.txt" || true "$TC/common/bin/objcopy" -O binary -j .text "$O/openpocket.elf" "$O/text.bin" "$TC/common/bin/objcopy" -O binary -j .data "$O/openpocket.elf" "$O/data.bin" "$TC/common/bin/objcopy" -O binary -j .ram_text "$O/openpocket.elf" "$O/rt.bin" 2>/dev/null || : > "$O/rt.bin" -python3 - "$TC" "$O" "$HERE/openpocket.bin" <<'PY' +python3 - "$TC" "$O" "$APP" <<'PY' import re,subprocess,sys tc,out,dest=sys.argv[1:] syms=subprocess.run([f"{tc}/common/bin/objdump","-t",f"{out}/openpocket.elf"],capture_output=True,text=True,check=True).stdout @@ -79,5 +98,6 @@ python3 "$HERE/../tools/mkloader.py" "$L/loader.bin" "$L/ota.bin" # ---- update packages for the web installer -> docs/firmware/ (PocketX_9NN.fwsc, BlackBox_9NN.fwsc, manifest.json) ---- VER=$(sed -n 's/^#define OP_VERSION_NUM "\([0-9]*\)".*/\1/p' "$HERE/openpocket.c") -python3 "$HERE/../tools/pxpkg.py" "$HERE/openpocket.bin" "$L/ota.bin" "$HERE/../docs/firmware" --version "$VER" \ +mkdir -p "$PKG" +python3 "$HERE/../tools/pxpkg.py" "$APP" "$L/ota.bin" "$PKG" --version "$VER" \ --sdk-files "$HERE/sdk/packager" diff --git a/firmware/openpocket.c b/firmware/openpocket.c index 2a1956f..ab4597b 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -303,6 +303,9 @@ static void fm1_fault(const fm1_crash_t *c) #define PH_DIR (*(volatile uint32_t *)0x501C8u) #define ENC_MASK 0x3u /* PC0=A, PC1=B */ #define OP_VERSION_NUM "36" +#ifndef OP_GUARD_STACK /* hardware stack-limit guard (fm1_guard.h). build.sh turns it off for the Bluetooth test image, */ +#define OP_GUARD_STACK 1 /* whose task stacks (ble_os.c) lie outside [_ustack_lo, _sstack_top]; app.ld checks this at link time */ +#endif #define OP_VERSION "v" OP_VERSION_NUM /* shown at boot: changes with every released build */ #define PROBE 0 /* 1 = GPIO diagnostic: shows the pins that change at the bottom */ #define ADC_PROBE 0 /* 1 = ADC diagnostic: shows the channels that move at the bottom (C2:512) */ @@ -544,6 +547,14 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #include "px_editor.h" /* the web editor (docs/EDITOR-PROTOCOL.md) */ #endif #endif +#if OP_BLE_TEST /* the advertise-only Bluetooth test image (docs/BLE-TEST.md): make ble */ +#include "ble/ble_os.c" +#include "ble/ble_libc.c" +#include "ble/ble_cfg.c" +#include "ble/ble_rf.c" +#include "ble/ble_stubs.c" +#include "ble/ble_test.c" +#endif /* P3_RST_SRC as read at boot: why the chip last reset (fm1_sys.h) */ static void rst_names(char *l, uint32_t r) @@ -576,7 +587,11 @@ static void app_main(void) /* boot screen with the version: shows which image is running */ lcd_rect(0,0,240,240,UI_BG); ft_box(0, 98, 240, "OPENPOCKET", &FT_MD, UI_ACC, UI_BG, 1); +#if OP_BLE_TEST + ft_box(0, 126, 240, OP_VERSION " BLE TEST", &FT_SM, UI_DIM, UI_BG, 1); +#else ft_box(0, 126, 240, OP_VERSION, &FT_SM, UI_DIM, UI_BG, 1); +#endif #if !AUDIO_PROBE { /* the supplies as found -> as set (fm1_cstart): "PWR 3>7 11>15 DCDC" */ char l[32]; l[0] = 0; @@ -653,11 +668,18 @@ static void app_main(void) ui_invalidate(); ui_render(); op_stage(ST_RUN, 0); +#endif +#if OP_BLE_TEST + if (!safe) /* after a failed boot Bluetooth stays off: the pedal runs, and can be re-flashed */ + ble_test_start(); /* radio on, controller up, one advertisement (the pedal keeps working) */ #endif enc_last = (uint8_t)(PC_IN & ENC_MASK); for (;;) { uint32_t now = ms_now(); fm1_wdt_feed(); +#if OP_BLE_TEST + ble_test_poll(); +#endif #if !AUDIO_PROBE px_usb_service(now); /* web installer: handshake, update, UBOOT soft key */ audio_poll(now); /* the sound follows the UI (preset, settings, volume) */ @@ -674,7 +696,25 @@ static void app_main(void) if (l >= 100u) c[i++] = (char)('0' + l / 100u); if (l >= 10u) c[i++] = (char)('0' + (l / 10u) % 10u); c[i++] = (char)('0' + l % 10u); c[i++] = '%'; c[i] = 0; +#if OP_BLE_TEST + { /* "STARTING 4 C1 42%": the Bluetooth state first; FAILED and ADVERTISING scroll their long lines instead (ble_status.h) */ + char b[48], full[96]; int k, j; + static uint32_t scroll; + ble_test_state(b); + if (ble_state == BLE_FAILED || ble_state == BLE_ADV) { /* longer than the status area: scroll it (ble_status.h) */ + if (ble_state == BLE_FAILED) ble_status_full(full); else ble_test_adv_line(full); + ble_marquee(full, scroll++, 11, b); + } else { + for (k = 0; b[k]; k++) ; + b[k++] = ' '; + for (j = 0; c[j]; j++) b[k++] = c[j]; + b[k] = 0; + } + ui_status(b, a_bypass); + } +#else ui_status(c, a_bypass); +#endif } } else if (now - t_tuner >= 100u) { t_tuner = now; tuner_draw(tuner_estimate()); } #endif @@ -831,7 +871,7 @@ void fm1_cstart(void) op_pwr.vdc0 = (uint8_t)vdc0; op_pwr.vdc1 = (uint8_t)fm1_vdc14(); op_pwr.sys0 = (uint8_t)sys0; op_pwr.sys1 = (uint8_t)fm1_sysvdd(); op_pwr.dcdc = (uint8_t)dcdc; op_pwr.rst = (uint8_t)rst0; op_pwr.pinr = (uint8_t)pinr0; - fm1_guard_enable(FM1_GUARD_STACK | FM1_GUARD_WRITE | FM1_GUARD_BUS | FM1_GUARD_PC); + fm1_guard_enable((OP_GUARD_STACK ? FM1_GUARD_STACK : 0) | FM1_GUARD_WRITE | FM1_GUARD_BUS | FM1_GUARD_PC); app_main(); for (;;) ; } diff --git a/firmware/px_audio.h b/firmware/px_audio.h index 183714e..72e12b8 100644 --- a/firmware/px_audio.h +++ b/firmware/px_audio.h @@ -46,9 +46,11 @@ void *os_current_task_rom(void) { return 0; } void cpu_assert_debug(void) { } void P33_SYSTEM_RESET(void) { for (;;) ; } +#if !OP_BLE_TEST /* the ble build has a real heap (ble/ble_os.c); iis_open draws its block from it */ static uint8_t jl_heap[4096] __attribute__((aligned(8))); /* iis_open: 2 channels x 8 x sr_points (256) */ void *malloc(unsigned n) { return (n <= sizeof jl_heap) ? jl_heap : 0; } /* single block: iis_open */ void free(void *p) { (void)p; } +#endif /* memset / memcpy: felucca-bsp libc.c (included by openpocket.c) */ static volatile uint32_t *jl_preg(uint32_t gpio, uint32_t off) diff --git a/firmware/px_usb.h b/firmware/px_usb.h index 851134a..de3e0b7 100644 --- a/firmware/px_usb.h +++ b/firmware/px_usb.h @@ -25,6 +25,9 @@ static const volatile char OP_PRODUCT[32] __attribute__((used, aligned(4))) = "O #define FELUCCA_ID_PTR ((const char *)OP_PRODUCT + 11) #include "fm1_timer.h" +#if OP_BLE_TEST +static int ble_stall_tick(void); /* ble/ble_status.h, included after this file */ +#endif #include "fm1_flash.h" #include "fm1_cpu1_stop.h" #include "usb.c" @@ -36,6 +39,10 @@ static volatile uint32_t px_t5_sub; void fm1_timer5_irq(void) /* 10 kHz; USB at 2 kHz */ { fm1_timer5_ack(); +#if OP_BLE_TEST /* a ble_poll that does not return: the crash screen says where (ble_status.h) */ + if (ble_stall_tick()) + FM1_VEC[FM1_IRQ_TIMER5] = (uint32_t)(uintptr_t)(fm1_fatal_stubs + 6u * FM1_IRQ_TIMER5); +#endif if (++px_t5_sub >= 5u) { px_t5_sub = 0; usb_poll(); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index ff82108..0c582ff 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -25,6 +25,10 @@ echo "== installer" node tests/test_installer.mjs "$1" echo "== build entry point (tools/op.mjs)" node tests/test_op.mjs +echo "== Bluetooth test image sources (third_party/jieli-ble)" +node tests/test_ble_sources.mjs +echo "== Bluetooth test image status codes" +$CC -O1 -Wall -Wno-unused-function -o build/ble_status tests/test_ble_status.c && build/ble_status echo "== editor" $CC -O1 -Wall -Wno-unused-function -o build/edsim tests/edsim.c engine/px_dsp.c firmware/px_math.c -lm node tests/test_editor.mjs diff --git a/tests/test_ble_sources.mjs b/tests/test_ble_sources.mjs new file mode 100644 index 0000000..0a918a2 --- /dev/null +++ b/tests/test_ble_sources.mjs @@ -0,0 +1,21 @@ +// The Bluetooth test image's vendor objects (third_party/jieli-ble, docs/BLE-TEST.md): MEMBERS lists exactly the files +// that are there, each is LLVM bitcode, and the licence text is JieLi's Apache-2.0 file. (The image itself is built by +// `make ble`; this needs no Docker.) +import assert from "node:assert/strict"; +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const repo = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const dir = join(repo, "third_party/jieli-ble"); +const listed = readFileSync(join(dir, "MEMBERS"), "utf8").split("\n").filter((l) => l && !l.startsWith("#")).map((l) => l.split("\t")[2]); +assert.ok(listed.length > 0 && listed.every(Boolean), "MEMBERS has archive, member and file on each line"); +const onDisk = ["btctrler", "cpu"].flatMap((d) => readdirSync(join(dir, d)).map((f) => `${d}/${f}`)); +assert.deepEqual([...listed].sort(), onDisk.sort(), "MEMBERS lists exactly the objects in the directory"); +for (const f of listed) { + const head = readFileSync(join(dir, f)).subarray(0, 4); + assert.deepEqual([...head], [0x42, 0x43, 0xc0, 0xde], `${f} is LLVM bitcode, as published`); + assert.ok(statSync(join(dir, f)).size > 100, `${f} is not empty`); +} +assert.equal(readFileSync(join(dir, "LICENSE-JieLi-Apache-2.0"), "utf8"), readFileSync(join(repo, "firmware/sdk/LICENSE-JieLi-Apache-2.0"), "utf8"), "the licence text is JieLi's"); +console.log(`ble sources: ${listed.length} objects ok`); diff --git a/tests/test_ble_status.c b/tests/test_ble_status.c new file mode 100644 index 0000000..bb5ccd5 --- /dev/null +++ b/tests/test_ble_status.c @@ -0,0 +1,116 @@ +/* The Bluetooth test image's status text and failure codes (firmware/ble/ble_status.h): which step failed, with which + * return value, and the stage a timeout stalled in. The header has no hardware access, so it is compiled as is. */ +#include +#include +#include "../firmware/ble/ble_status.h" +#include "../firmware/ble/ble_adv.h" + +static int bad; +static void is(const char *what, const char *want) +{ + char b[48]; + ble_status_text(b); + if (strcmp(b, want)) { printf("FAIL %s: \"%s\", wanted \"%s\"\n", what, b, want); bad++; } + if (strlen(b) >= 24) { printf("FAIL %s: \"%s\" does not fit 24 bytes\n", what, b); bad++; } +} +static void reset(void) { ble_state = BLE_OFF; ble_stage = 0; ble_fail_step = 0; ble_fail_code = 0; } + +int main(void) +{ + reset(); is("off", "OFF"); + ble_state = BLE_STARTING; ble_stage_set(BLE_STAGE_TASK); is("starting, task", "STARTING 1"); + ble_stage_set(BLE_STAGE_RF); is("starting, radio init", "STARTING 3"); + ble_stage_set(BLE_STAGE_READY); is("starting, waiting for the controller", "STARTING 4"); + + reset(); ble_state = BLE_STARTING; + if (ble_judge_start(0, 1) != 0 || ble_state != BLE_STARTING) { puts("FAIL: a good start was judged failed"); bad++; } + if (ble_judge_start(-7, 0) != BLE_FAIL_INIT) { puts("FAIL: init error not judged as step 2"); bad++; } + is("btctrler_task_init returned -7", "FAILED 2 -7"); + + reset(); ble_state = BLE_STARTING; + if (ble_judge_start(0, 0) != BLE_FAIL_READY) { puts("FAIL: not-ready not judged as step 3"); bad++; } + is("btctrler_task_ready returned 0", "FAILED 3 0"); + + reset(); ble_state = BLE_STARTING; ble_fail(BLE_FAIL_TASK_CREATE, -1); is("task_create failed", "FAILED 1 -1"); + reset(); ble_state = BLE_STARTING; ble_fail(BLE_FAIL_ADV_ENABLE, 12); is("ll_hci_adv_enable returned 12", "FAILED 4 12"); + + reset(); ble_state = BLE_STARTING; ble_stage_set(BLE_STAGE_RF); + ble_fail(BLE_FAIL_TIMEOUT, (int32_t)ble_stage); is("timeout in the radio init", "FAILED 5 3"); + + reset(); ble_fail(BLE_FAIL_INIT, -2147483647 - 1); is("most negative code", "FAILED 2 -2147483648"); + reset(); ble_state = BLE_ADV; is("advertising", "ADVERTISING"); + /* the full line */ + reset(); ble_state = BLE_STARTING; ble_stage_set(BLE_STAGE_READY); ble_judge_start(-7, 0); + { char f[96]; ble_status_full(f); + if (strcmp(f, "FAILED 2 -7 step btctrler_task_init stage ready-wait")) { printf("FAIL full line: \"%s\"\n", f); bad++; } + if (strlen(f) >= 96) { puts("FAIL: full line too long"); bad++; } } + reset(); ble_state = BLE_STARTING; ble_stage_set(BLE_STAGE_RF); ble_fail(BLE_FAIL_TIMEOUT, (int32_t)ble_stage); + { char f[96]; ble_status_full(f); + if (strcmp(f, "FAILED 5 3 step timeout stage radio-init")) { printf("FAIL timeout line: \"%s\"\n", f); bad++; } } + /* the marquee: "ABCDEFGH" in a window of 4, gap 3, rest 4 ticks */ + { static const struct { uint32_t tick; const char *want; } m[] = { + {0, "ABCD"}, {3, "ABCD"}, /* the rest on the start */ + {4, "ABCD"}, {5, "BCDE"}, {8, "EFGH"}, + {9, "FGH "}, {11, "H "}, {12, " A"}, /* through the gap and round */ + {14, " ABC"}, /* the last step of the cycle: one gap column left */ + {15, "ABCD"}, {19, "ABCD"}, /* a whole cycle (rest 4 + period 11 = 15 ticks) is back at the start */ + }; + int i; char o[8]; + for (i = 0; i < (int)(sizeof m / sizeof m[0]); i++) { + ble_marquee("ABCDEFGH", m[i].tick, 4, o); + if (strcmp(o, m[i].want)) { printf("FAIL marquee tick %u: \"%s\", wanted \"%s\"\n", m[i].tick, o, m[i].want); bad++; } + } + ble_marquee("ABCD", 7, 4, o); /* fits: never moves */ + if (strcmp(o, "ABCD")) { puts("FAIL: a text that fits was scrolled"); bad++; } } + reset(); ble_state = BLE_STARTING; ble_stage_set(BLE_STAGE_BB); ble_wait = 0x01C1A5E0u; ble_fail(BLE_FAIL_TIMEOUT, (int32_t)ble_stage); + { char f[96]; ble_status_full(f); ble_wait = 0; + if (strcmp(f, "FAILED 5 8 step timeout stage baseband-irq wait 01C1A5E0")) { printf("FAIL wait line: \"%s\"\n", f); bad++; } } + /* the stall watch: silent while no poll runs, fires once a poll has run for more than BLE_STALL_TICKS ticks */ + { uint32_t i; int fired = 0; + ble_busy(0); for (i = 0; i < 2u * BLE_STALL_TICKS; i++) fired |= ble_stall_tick(); + if (fired) { puts("FAIL: the stall watch fired with no poll running"); bad++; } + ble_busy(1); for (i = 0; i < BLE_STALL_TICKS - 1u; i++) fired |= ble_stall_tick(); + if (fired) { puts("FAIL: the stall watch fired before its limit"); bad++; } + ble_busy(0); ble_busy(1); /* each poll starts the count again */ + for (i = 0; i < BLE_STALL_TICKS - 1u; i++) fired |= ble_stall_tick(); + if (fired) { puts("FAIL: the stall watch did not restart with the poll"); bad++; } + if (!ble_stall_tick()) { puts("FAIL: the stall watch did not fire at its limit"); bad++; } + ble_busy(0); } + /* the proof counters: the advertising line, numbers first, registers in hex */ + { char f[96]; + ble_cnt_irq_ev = 12; ble_cnt_hci = 5; ble_cnt_irq_rx = 0; ble_cnt_timer = 340; ble_cnt_slot = 0; + ble_adv_text(f, 0x2001u, 0x101u, 0u, 0x80u, 3u, 3u, 1u); + if (strcmp(f, "ADV i12 e5 r0 t340 s0 CON 2001 IEN 101 IPND 0 STAT 80 IC45 3 IC29 3 BEN 1")) { printf("FAIL adv line: \"%s\"\n", f); bad++; } + if (strlen(f) >= 96) { puts("FAIL: adv line too long"); bad++; } + ble_cnt_irq_ev = 4000000000u; ble_adv_text(f, 0xFFFFFFFFu, 0, 0, 0, 0, 0, 0); + if (strncmp(f, "ADV i2147483647 ", 16) || strlen(f) >= 96) { printf("FAIL big counter: \"%s\"\n", f); bad++; } + ble_cnt_irq_ev = 0; ble_cnt_hci = 0; + static const uint8_t evt[] = { 0x0E, 4, 1, 0x0A, 0x20, 0 }; /* a command complete event */ + ble_count_hci(4, evt); ble_count_hci(4, evt); + if (ble_cnt_hci != 2 || ble_hci_last != 0x040Eu) { puts("FAIL: HCI packets are not counted"); bad++; } + ble_adv_text(f, 0, 0, 0, 0, 0, 0, 0); + if (strncmp(f, "ADV i0 e2 ", 10)) { printf("FAIL hci in the line: \"%s\"\n", f); bad++; } + char w[16]; ble_marquee(f, 0, 11, w); + if (strncmp(w, "ADV i0 e2 ", 10)) { printf("FAIL: the numbers are not first on screen: \"%s\"\n", w); bad++; } } + /* the advertisement against the Core Specification (ble_adv.h) */ + { static const uint8_t uuid_le[16] = { 0x00,0xC7,0xC4,0x4E,0xE3,0x6C,0x51,0xA7,0x33,0x4B,0xE8,0xED,0x5A,0x0E,0xB8,0x03 }; + static const char name[] = "OpenPocket-BLE-TEST"; + unsigned n, i; int flags = 0, uuid = 0; + for (n = 0; n < sizeof ble_adv_data; ) { /* length-type-value structures, no padding, no overrun */ + unsigned l = ble_adv_data[n]; + if (!l || n + 1u + l > sizeof ble_adv_data) { puts("FAIL: advertising data is not whole AD structures"); bad++; break; } + if (ble_adv_data[n + 1] == 0x01 && l == 2 && ble_adv_data[n + 2] == 0x06) flags = 1; /* general discoverable, no BR/EDR */ + if (ble_adv_data[n + 1] == 0x07 && l == 17 && !memcmp(ble_adv_data + n + 2, uuid_le, 16)) uuid = 1; + n += 1u + l; + } + if (sizeof ble_adv_data > 31) { puts("FAIL: advertising data over 31 bytes"); bad++; } + if (!flags || !uuid) { puts("FAIL: flags or the BLE-MIDI UUID missing from the advertising data"); bad++; } + if (sizeof ble_scan_rsp > 31 || ble_scan_rsp[0] != sizeof ble_scan_rsp - 1u || ble_scan_rsp[1] != 0x09 || + ble_scan_rsp[0] != 1u + strlen(name) || memcmp(ble_scan_rsp + 2, name, strlen(name))) { puts("FAIL: scan response is not the complete local name"); bad++; } + for (i = 0, n = 0; i < 6; i++) n |= ble_adv_mac[i]; + if (!n || (ble_adv_mac[5] & 1u)) { puts("FAIL: address is zero or a group address"); bad++; } + if (BLE_ADV_SCAN_IND != 2 || BLE_ADV_INTERVAL < 0x00A0 || BLE_ADV_INTERVAL > 0x4000 || BLE_ADV_CHMAP != 7 || BLE_ADV_FILTER != 0) { + puts("FAIL: advertising parameters out of range"); bad++; } } + puts(bad ? "ble status: FAILED" : "ble status: ok"); + return bad != 0; +} diff --git a/tests/test_op.mjs b/tests/test_op.mjs index 50598cf..f522730 100644 --- a/tests/test_op.mjs +++ b/tests/test_op.mjs @@ -41,6 +41,9 @@ let r = op(["build"]); assert.equal(r.status, 0); assert.deepEqual(r.calls, [["info"], BUILD, base(["sh", "firmware/build.sh"])], "build"); +r = op(["ble"]); +assert.deepEqual(r.calls.at(-1), base(["sh", "-c", "OP_BLE=1 sh firmware/build.sh && sha256sum firmware/openpocket-ble.bin build/ble/*.fwsc"]), "ble: the test image is its own build"); + r = op(["test"]); assert.deepEqual(r.calls.at(-1), base(["sh", "tests/run_tests.sh"]), "test"); @@ -97,12 +100,12 @@ r = op(["build"], { path: join(tmp, "empty") }); assert.notEqual(r.status, 0); assert.match(r.err, /Docker is not installed or not running/, "no docker at all"); -for (const p of ["build/x", "firmware/build/y", "docs/firmware/PocketX_900.fwsc", "docs/firmware/manifest.json", "engine/px_render"]) touch(p); +for (const p of ["build/x", "firmware/build/y", "firmware/build-ble/z", "firmware/openpocket-ble.bin", "docs/firmware/PocketX_900.fwsc", "docs/firmware/manifest.json", "engine/px_render"]) touch(p); touch("docs/firmware/README.md"); touch("engine/px_dsp.c"); r = op(["clean"], { path: join(tmp, "empty") }); assert.equal(r.status, 0, "clean needs no docker"); -for (const p of ["build", "firmware/build", "firmware/openpocket.bin", "docs/firmware/PocketX_900.fwsc", "docs/firmware/manifest.json", "engine/px_render"]) { +for (const p of ["build", "firmware/build", "firmware/build-ble", "firmware/openpocket-ble.bin", "firmware/openpocket.bin", "docs/firmware/PocketX_900.fwsc", "docs/firmware/manifest.json", "engine/px_render"]) { assert.ok(!existsSync(join(root, p)), `${p} removed`); } for (const p of ["docs/firmware/README.md", "engine/px_dsp.c", "guitar.wav"]) assert.ok(existsSync(join(root, p)), `${p} kept`); diff --git a/third_party/jieli-ble/LICENSE-JieLi-Apache-2.0 b/third_party/jieli-ble/LICENSE-JieLi-Apache-2.0 new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/third_party/jieli-ble/LICENSE-JieLi-Apache-2.0 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/jieli-ble/MEMBERS b/third_party/jieli-ble/MEMBERS new file mode 100644 index 0000000..df16c62 --- /dev/null +++ b/third_party/jieli-ble/MEMBERS @@ -0,0 +1,37 @@ +# archivemember -> file in this directory (read by extract.sh) +btctrler.a RF.c.o btctrler/RF.c.bc +btctrler.a RF_ble.c.o btctrler/RF_ble.c.bc +btctrler.a aes128.c.o btctrler/aes128.c.bc +btctrler.a analog.c.o btctrler/analog.c.bc +btctrler.a analog_ble.c.o btctrler/analog_ble.c.bc +btctrler.a btagc.c.o btctrler/btagc.c.bc +btctrler.a btcontroller_heap.c.o btctrler/btcontroller_heap.c.bc +btctrler.a btcontroller_util.c.o btctrler/btcontroller_util.c.bc +btctrler.a btctrler_task.c.o btctrler/btctrler_task.c.bc +btctrler.a device_manager.c.o btctrler/device_manager.c.bc +btctrler.a h4_controller_interface.c.o btctrler/h4_controller_interface.c.bc +btctrler.a hci_controller.c.o btctrler/hci_controller.c.bc +btctrler.a hci_ll.c.o btctrler/hci_ll.c.bc +btctrler.a hci_ll_cmds.c.o btctrler/hci_ll_cmds.c.bc +btctrler.a hw_dma.c.o btctrler/hw_dma.c.bc +btctrler.a lbuf.c.o btctrler/lbuf.c.bc +btctrler.a le_low_power.c.o btctrler/le_low_power.c.bc +btctrler.a link_layer.c.o btctrler/link_layer.c.bc +btctrler.a link_task.c.o btctrler/link_task.c.bc +btctrler.a ll_adv.c.o btctrler/ll_adv.c.bc +btctrler.a ll_afh.c.o btctrler/ll_afh.c.bc +btctrler.a ll_events.c.o btctrler/ll_events.c.bc +btctrler.a ll_init.c.o btctrler/ll_init.c.bc +btctrler.a ll_keys.c.o btctrler/ll_keys.c.bc +btctrler.a ll_resolve_list.c.o btctrler/ll_resolve_list.c.bc +btctrler.a ll_scan.c.o btctrler/ll_scan.c.bc +btctrler.a ll_slave.c.o btctrler/ll_slave.c.bc +btctrler.a ll_vendor.c.o btctrler/ll_vendor.c.bc +btctrler.a ll_white_list.c.o btctrler/ll_white_list.c.bc +btctrler.a misc_hw.c.o btctrler/misc_hw.c.bc +btctrler.a sys_timer.c.o btctrler/sys_timer.c.bc +btctrler.a thread.c.o btctrler/thread.c.bc +btctrler.a vsprintf.c.o btctrler/vsprintf.c.bc +cpu.a delay.c.o cpu/delay.c.bc +cpu.a crc16.c.o cpu/crc16.c.bc +cpu.a wlc.c.o cpu/wlc.c.bc diff --git a/third_party/jieli-ble/README.md b/third_party/jieli-ble/README.md new file mode 100644 index 0000000..095ba28 --- /dev/null +++ b/third_party/jieli-ble/README.md @@ -0,0 +1,36 @@ +# third_party/jieli-ble: JieLi's Bluetooth controller (Apache-2.0) + +This directory holds JieLi's own Bluetooth LE controller code, **as published, unmodified**, for the advertise-only +Bluetooth test image (`make ble`, [docs/BLE-TEST.md](../../docs/BLE-TEST.md)). Nothing here is part of the normal +OpenPocket firmware or of the USB MIDI image; only the `ble` build target links it. + +| | | +| --- | --- | +| Source | JieLi `fw-AC79_AIoT_SDK`, branch `release/AC79NN_SDK_V1.2.0`, commit `0617f4cc09685332aaf1104893718291c54ee5bb` | +| URL | https://gitee.com/Jieli-Tech/fw-AC79_AIoT_SDK (files under `cpu/wl82/liba/`) | +| Licence | Apache-2.0, the SDK's `LICENSE`, kept here as `LICENSE-JieLi-Apache-2.0` (identical to `firmware/sdk/LICENSE-JieLi-Apache-2.0`) | +| Copyright | Zhuhai Jieli Technology Co., Ltd. | +| Ruling | The project owner ruled that linking the SDK's Apache-2.0 binary libraries is acceptable (the same reading as for `firmware/sdk/iis.bc`, `docs/AUDIO-SDK.md`). The libraries are shipped as LLVM bitcode, not as source. | + +## What is here + +Individual objects (LLVM bitcode, clang 4.0.1, the same compiler as our toolchain), taken as they are from the +archives; `build.sh` compiles each with `clang -target pi32v2 -x ir`, as it does for `firmware/sdk/iis.bc`. + +| Directory | From | Objects | +| --- | --- | --- | +| `btctrler/` | `btctrler.a` (sha256 `d98f2ad9d483cd2c103cab56c2760162edf92cc451a04e5668af02a590c95cb3`) | the LE controller: HCI front end, link layer, advertising, slave link, the LE and BT radio drivers, interrupt and DMA helpers, their utilities (33 of the archive's 106 bitcode members) | +| `cpu/` | `cpu.a` (sha256 `3d3e0876e21afe2c9cb7de713378d3802b1d0f544a1787f07e982269a0e7e67d`) | `delay`, `crc16`, `wlc` (small helpers the controller calls) | + +Not used: the classic Bluetooth, TWS, LE 5 extended/ISO members of `btctrler.a`, and all of `btstack.a` (the host +stack: this image sends its advertisement through the controller's own HCI-level functions). + +`extract.sh` rebuilds this directory from an SDK checkout and checks the archive hashes above: +`sh third_party/jieli-ble/extract.sh /path/to/fw-AC79_AIoT_SDK`. + +## What OpenPocket supplies + +`firmware/ble/` is OpenPocket's own code that these objects need: a small cooperative scheduler with the operating +system calls they make (tasks, queues, semaphores, timers, heap, interrupt registration), the configuration +(`ble_cfg.c`, derived from the SDK's `apps/common/config/log_config/lib_btctrler_config.c`, Apache-2.0), empty stand-ins +for the parts that are not built (`ble_stubs.c`), and the radio start-up replay (`ble_rf.c`). diff --git a/third_party/jieli-ble/btctrler/RF.c.bc b/third_party/jieli-ble/btctrler/RF.c.bc new file mode 100644 index 0000000..ef79d11 Binary files /dev/null and b/third_party/jieli-ble/btctrler/RF.c.bc differ diff --git a/third_party/jieli-ble/btctrler/RF_ble.c.bc b/third_party/jieli-ble/btctrler/RF_ble.c.bc new file mode 100644 index 0000000..a3dd046 Binary files /dev/null and b/third_party/jieli-ble/btctrler/RF_ble.c.bc differ diff --git a/third_party/jieli-ble/btctrler/aes128.c.bc b/third_party/jieli-ble/btctrler/aes128.c.bc new file mode 100644 index 0000000..fcf5462 Binary files /dev/null and b/third_party/jieli-ble/btctrler/aes128.c.bc differ diff --git a/third_party/jieli-ble/btctrler/analog.c.bc b/third_party/jieli-ble/btctrler/analog.c.bc new file mode 100644 index 0000000..5a767ac Binary files /dev/null and b/third_party/jieli-ble/btctrler/analog.c.bc differ diff --git a/third_party/jieli-ble/btctrler/analog_ble.c.bc b/third_party/jieli-ble/btctrler/analog_ble.c.bc new file mode 100644 index 0000000..7a91870 Binary files /dev/null and b/third_party/jieli-ble/btctrler/analog_ble.c.bc differ diff --git a/third_party/jieli-ble/btctrler/btagc.c.bc b/third_party/jieli-ble/btctrler/btagc.c.bc new file mode 100644 index 0000000..2a20f04 Binary files /dev/null and b/third_party/jieli-ble/btctrler/btagc.c.bc differ diff --git a/third_party/jieli-ble/btctrler/btcontroller_heap.c.bc b/third_party/jieli-ble/btctrler/btcontroller_heap.c.bc new file mode 100644 index 0000000..c6897a5 Binary files /dev/null and b/third_party/jieli-ble/btctrler/btcontroller_heap.c.bc differ diff --git a/third_party/jieli-ble/btctrler/btcontroller_util.c.bc b/third_party/jieli-ble/btctrler/btcontroller_util.c.bc new file mode 100644 index 0000000..756cfaf Binary files /dev/null and b/third_party/jieli-ble/btctrler/btcontroller_util.c.bc differ diff --git a/third_party/jieli-ble/btctrler/btctrler_task.c.bc b/third_party/jieli-ble/btctrler/btctrler_task.c.bc new file mode 100644 index 0000000..302b132 Binary files /dev/null and b/third_party/jieli-ble/btctrler/btctrler_task.c.bc differ diff --git a/third_party/jieli-ble/btctrler/device_manager.c.bc b/third_party/jieli-ble/btctrler/device_manager.c.bc new file mode 100644 index 0000000..94ddd6b Binary files /dev/null and b/third_party/jieli-ble/btctrler/device_manager.c.bc differ diff --git a/third_party/jieli-ble/btctrler/h4_controller_interface.c.bc b/third_party/jieli-ble/btctrler/h4_controller_interface.c.bc new file mode 100644 index 0000000..1575fe1 Binary files /dev/null and b/third_party/jieli-ble/btctrler/h4_controller_interface.c.bc differ diff --git a/third_party/jieli-ble/btctrler/hci_controller.c.bc b/third_party/jieli-ble/btctrler/hci_controller.c.bc new file mode 100644 index 0000000..1426709 Binary files /dev/null and b/third_party/jieli-ble/btctrler/hci_controller.c.bc differ diff --git a/third_party/jieli-ble/btctrler/hci_ll.c.bc b/third_party/jieli-ble/btctrler/hci_ll.c.bc new file mode 100644 index 0000000..e0024e9 Binary files /dev/null and b/third_party/jieli-ble/btctrler/hci_ll.c.bc differ diff --git a/third_party/jieli-ble/btctrler/hci_ll_cmds.c.bc b/third_party/jieli-ble/btctrler/hci_ll_cmds.c.bc new file mode 100644 index 0000000..5a4c18f Binary files /dev/null and b/third_party/jieli-ble/btctrler/hci_ll_cmds.c.bc differ diff --git a/third_party/jieli-ble/btctrler/hw_dma.c.bc b/third_party/jieli-ble/btctrler/hw_dma.c.bc new file mode 100644 index 0000000..1b2250d Binary files /dev/null and b/third_party/jieli-ble/btctrler/hw_dma.c.bc differ diff --git a/third_party/jieli-ble/btctrler/lbuf.c.bc b/third_party/jieli-ble/btctrler/lbuf.c.bc new file mode 100644 index 0000000..bcde8e2 Binary files /dev/null and b/third_party/jieli-ble/btctrler/lbuf.c.bc differ diff --git a/third_party/jieli-ble/btctrler/le_low_power.c.bc b/third_party/jieli-ble/btctrler/le_low_power.c.bc new file mode 100644 index 0000000..175e219 Binary files /dev/null and b/third_party/jieli-ble/btctrler/le_low_power.c.bc differ diff --git a/third_party/jieli-ble/btctrler/link_layer.c.bc b/third_party/jieli-ble/btctrler/link_layer.c.bc new file mode 100644 index 0000000..e163b4e Binary files /dev/null and b/third_party/jieli-ble/btctrler/link_layer.c.bc differ diff --git a/third_party/jieli-ble/btctrler/link_task.c.bc b/third_party/jieli-ble/btctrler/link_task.c.bc new file mode 100644 index 0000000..cea85e2 Binary files /dev/null and b/third_party/jieli-ble/btctrler/link_task.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_adv.c.bc b/third_party/jieli-ble/btctrler/ll_adv.c.bc new file mode 100644 index 0000000..f08a552 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_adv.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_afh.c.bc b/third_party/jieli-ble/btctrler/ll_afh.c.bc new file mode 100644 index 0000000..70a2054 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_afh.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_events.c.bc b/third_party/jieli-ble/btctrler/ll_events.c.bc new file mode 100644 index 0000000..87e004d Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_events.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_init.c.bc b/third_party/jieli-ble/btctrler/ll_init.c.bc new file mode 100644 index 0000000..374989c Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_init.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_keys.c.bc b/third_party/jieli-ble/btctrler/ll_keys.c.bc new file mode 100644 index 0000000..263d056 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_keys.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_resolve_list.c.bc b/third_party/jieli-ble/btctrler/ll_resolve_list.c.bc new file mode 100644 index 0000000..86dc1e0 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_resolve_list.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_scan.c.bc b/third_party/jieli-ble/btctrler/ll_scan.c.bc new file mode 100644 index 0000000..0bc64c3 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_scan.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_slave.c.bc b/third_party/jieli-ble/btctrler/ll_slave.c.bc new file mode 100644 index 0000000..7d0adae Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_slave.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_vendor.c.bc b/third_party/jieli-ble/btctrler/ll_vendor.c.bc new file mode 100644 index 0000000..2f05b19 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_vendor.c.bc differ diff --git a/third_party/jieli-ble/btctrler/ll_white_list.c.bc b/third_party/jieli-ble/btctrler/ll_white_list.c.bc new file mode 100644 index 0000000..8b90f41 Binary files /dev/null and b/third_party/jieli-ble/btctrler/ll_white_list.c.bc differ diff --git a/third_party/jieli-ble/btctrler/misc_hw.c.bc b/third_party/jieli-ble/btctrler/misc_hw.c.bc new file mode 100644 index 0000000..e7d2fb7 Binary files /dev/null and b/third_party/jieli-ble/btctrler/misc_hw.c.bc differ diff --git a/third_party/jieli-ble/btctrler/sys_timer.c.bc b/third_party/jieli-ble/btctrler/sys_timer.c.bc new file mode 100644 index 0000000..e47d054 Binary files /dev/null and b/third_party/jieli-ble/btctrler/sys_timer.c.bc differ diff --git a/third_party/jieli-ble/btctrler/thread.c.bc b/third_party/jieli-ble/btctrler/thread.c.bc new file mode 100644 index 0000000..d0c271e Binary files /dev/null and b/third_party/jieli-ble/btctrler/thread.c.bc differ diff --git a/third_party/jieli-ble/btctrler/vsprintf.c.bc b/third_party/jieli-ble/btctrler/vsprintf.c.bc new file mode 100644 index 0000000..0f3efcd Binary files /dev/null and b/third_party/jieli-ble/btctrler/vsprintf.c.bc differ diff --git a/third_party/jieli-ble/cpu/crc16.c.bc b/third_party/jieli-ble/cpu/crc16.c.bc new file mode 100644 index 0000000..504e9d0 Binary files /dev/null and b/third_party/jieli-ble/cpu/crc16.c.bc differ diff --git a/third_party/jieli-ble/cpu/delay.c.bc b/third_party/jieli-ble/cpu/delay.c.bc new file mode 100644 index 0000000..69f2742 Binary files /dev/null and b/third_party/jieli-ble/cpu/delay.c.bc differ diff --git a/third_party/jieli-ble/cpu/wlc.c.bc b/third_party/jieli-ble/cpu/wlc.c.bc new file mode 100644 index 0000000..cf0b3a7 Binary files /dev/null and b/third_party/jieli-ble/cpu/wlc.c.bc differ diff --git a/third_party/jieli-ble/extract.sh b/third_party/jieli-ble/extract.sh new file mode 100755 index 0000000..5d65106 --- /dev/null +++ b/third_party/jieli-ble/extract.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Rebuild third_party/jieli-ble/ from a checkout of JieLi's fw-AC79_AIoT_SDK (release/AC79NN_SDK_V1.2.0, commit +# 0617f4cc09685332aaf1104893718291c54ee5bb): sh third_party/jieli-ble/extract.sh /path/to/fw-AC79_AIoT_SDK +# The archives are checked against the hashes in README.md first; the objects are copied out unchanged. +set -e +SDK="${1:?usage: extract.sh /path/to/fw-AC79_AIoT_SDK}" +HERE="$(cd "$(dirname "$0")" && pwd)" +LIB="$SDK/cpu/wl82/liba" +sum() { sha256sum "$1" 2>/dev/null | cut -d' ' -f1 || shasum -a 256 "$1" | cut -d' ' -f1; } +[ "$(sum "$LIB/btctrler.a")" = d98f2ad9d483cd2c103cab56c2760162edf92cc451a04e5668af02a590c95cb3 ] || { echo "btctrler.a is not the pinned file"; exit 1; } +[ "$(sum "$LIB/cpu.a")" = 3d3e0876e21afe2c9cb7de713378d3802b1d0f544a1787f07e982269a0e7e67d ] || { echo "cpu.a is not the pinned file"; exit 1; } +grep -v '^#' "$HERE/MEMBERS" | while IFS="$(printf '\t')" read -r arch member dest; do + mkdir -p "$HERE/$(dirname "$dest")" + ar p "$LIB/$arch" "$member" > "$HERE/$dest" +done +cp "$SDK/LICENSE" "$HERE/LICENSE-JieLi-Apache-2.0" +echo "extracted $(grep -vc '^#' "$HERE/MEMBERS") objects" diff --git a/tools/op.mjs b/tools/op.mjs index 5159b78..5d034a6 100644 --- a/tools/op.mjs +++ b/tools/op.mjs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only // The one host-side entry point of the OpenPocket build (macOS, Linux, Windows; Node 18+). The Makefile and the // .bat files only call this. Every Docker command line lives here. -// node tools/op.mjs doctor | build | test | package | clean | shell | image | serve [editor|build] +// node tools/op.mjs doctor | build | ble | test | package | clean | shell | image | serve [editor|build|ble] // node tools/op.mjs render in.wav [out.wav] [preset] | node tools/op.mjs appimage [fm1-1.bin] // NATIVE=1 runs the commands on this machine (toolchain from firmware/setup.sh) instead of in the container. import { spawnSync } from "node:child_process"; @@ -72,10 +72,11 @@ const commands = { }, image, build: () => inContainer(["sh", "firmware/build.sh"]), + ble: () => inContainer(["sh", "-c", "OP_BLE=1 sh firmware/build.sh && sha256sum firmware/openpocket-ble.bin build/ble/*.fwsc"]), // the advertise-only Bluetooth test image (docs/BLE-TEST.md) test: () => inContainer(["sh", "tests/run_tests.sh"]), package: () => inContainer(["sh", "-c", `sh firmware/build.sh && mkdir -p build && sha256sum ${OUTS} > build/SHA256SUMS && cat build/SHA256SUMS`]), clean() { - for (const p of ["build", "firmware/build", "firmware/openpocket.bin", "docs/firmware/manifest.json", "engine/px_render"]) { + for (const p of ["build", "firmware/build", "firmware/build-ble", "firmware/openpocket.bin", "firmware/openpocket-ble.bin", "docs/firmware/manifest.json", "engine/px_render"]) { rmSync(resolve(root, p), { recursive: true, force: true }); } const dir = resolve(root, "docs/firmware"); @@ -84,12 +85,13 @@ const commands = { }, shell: () => inContainer(["bash"], { tty: true }), serve([arg]) { - if (arg === "build" || !existsSync(resolve(root, "docs/firmware/manifest.json"))) { - const built = commands.build(); + const ble = arg === "ble"; // the Bluetooth test packages (build/ble) instead of docs/firmware + if (ble ? !existsSync(resolve(root, "build/ble/manifest.json")) : arg === "build" || !existsSync(resolve(root, "docs/firmware/manifest.json"))) { + const built = ble ? commands.ble() : commands.build(); if (built !== 0) return built; } const py = process.platform === "win32" ? ["py", "-3"] : ["python3"]; - const r = spawnSync(py[0], [...py.slice(1), "tools/serve.py", arg === "editor" ? "editor.html" : "index.html"], { stdio: "inherit", cwd: root }); + const r = spawnSync(py[0], [...py.slice(1), "tools/serve.py", arg === "editor" ? "editor.html" : "index.html", ...(ble ? ["build/ble"] : [])], { stdio: "inherit", cwd: root }); if (r.error) fail("Python 3 is needed to serve the installer offline (python.org)."); return r.status ?? 0; }, diff --git a/tools/serve.py b/tools/serve.py index aed2c8a..86a68a5 100755 --- a/tools/serve.py +++ b/tools/serve.py @@ -2,15 +2,24 @@ # SPDX-License-Identifier: GPL-3.0-only """OpenPocket offline installer: serves docs/ (installer, editor, packages) on this computer only, at http://localhost:8765/ (Web MIDI works on localhost). Chrome or Edge. Ctrl+C stops it. - tools/serve.py [index.html|editor.html]""" + tools/serve.py [index.html|editor.html] [firmware-dir] +firmware-dir (for example build/ble) replaces docs/firmware as the place the installer reads the packages from.""" import functools, http.server, os, sys, webbrowser page = sys.argv[1] if len(sys.argv) > 1 else "index.html" -root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "docs") +here = os.path.dirname(os.path.abspath(__file__)) +root = os.path.abspath(os.path.join(here, "..", "docs")) +fwdir = os.path.abspath(os.path.join(here, "..", sys.argv[2])) if len(sys.argv) > 2 else None class Handler(http.server.SimpleHTTPRequestHandler): extensions_map = {**http.server.SimpleHTTPRequestHandler.extensions_map, ".js": "text/javascript", ".mjs": "text/javascript", ".fwsc": "application/octet-stream"} + def translate_path(self, path): + p = super().translate_path(path) + prefix = os.path.join(root, "firmware") + if fwdir and (p == prefix or p.startswith(prefix + os.sep)): + return os.path.join(fwdir, os.path.relpath(p, prefix)) + return p def end_headers(self): self.send_header("Cache-Control", "no-store") super().end_headers()