pyOCD icon indicating copy to clipboard operation
pyOCD copied to clipboard

`pyocd load`/`flash` fails on micro:bit V2 trying to erase sectors

Open microbit-carlos opened this issue 2 years ago • 12 comments

  • Intel Hex file used for testing, but should be the same with any other: CALL_ME_MICROBIT.hex.zip

  • micro:bit V2.0 with the factory 0255 release (the "Firmware for micro:bit V2" button): https://microbit.org/get-started/user-guide/firmware/

Chip erase works, but the flashing via load or flash commands, with and without the --target nrf52833 flag fail:

 $ pyocd --version
0.32.1
 $ pyocd list
  #   Probe                     Unique ID
----------------------------------------------------------------------------------
  0   micro:bit v2 [nrf52833]   9904360249624e45005160190000003b000000009796990b
 $ pyocd erase --chip --target nrf52833
0002165:INFO:eraser:Erasing chip...
0010320:INFO:eraser:Done
 $ pyocd load CALL_ME_MICROBIT.hex
0001357:INFO:load_cmd:Loading /Users/microbit-carlos/Downloads/CALL_ME_MICROBIT.hex
[                                                  ]   0%0002229:CRITICAL:__main__:flash erase sector failure (address 0x00000000; result code 0x67)
Traceback (most recent call last):
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/__main__.py", line 150, in run
    status = cmd.invoke()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/subcommands/load_cmd.py", line 120, in invoke
    file_format=self._args.format)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/file_programmer.py", line 171, in program
    self._loader.commit()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/loader.py", line 293, in commit
    keep_unwritten=self._keep_unwritten)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 509, in program
    flash_operation = self._sector_erase_program_double_buffer(progress_cb)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 911, in _sector_erase_program_double_buffer
    self.flash.erase_sector(sector.addr)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/flash.py", line 374, in erase_sector
    raise FlashEraseFailure('flash erase sector failure', address=address, result_code=result)
pyocd.core.exceptions.FlashEraseFailure: flash erase sector failure (address 0x00000000; result code 0x67)
 $ pyocd load CALL_ME_MICROBIT.hex  --target nrf52833
0001507:INFO:load_cmd:Loading /Users/microbit-carlos/Downloads/CALL_ME_MICROBIT.hex
[                                                  ]   0%0002362:CRITICAL:__main__:flash erase sector failure (address 0x00000000; result code 0x67)
Traceback (most recent call last):
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/__main__.py", line 150, in run
    status = cmd.invoke()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/subcommands/load_cmd.py", line 120, in invoke
    file_format=self._args.format)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/file_programmer.py", line 171, in program
    self._loader.commit()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/loader.py", line 293, in commit
    keep_unwritten=self._keep_unwritten)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 509, in program
    flash_operation = self._sector_erase_program_double_buffer(progress_cb)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 911, in _sector_erase_program_double_buffer
    self.flash.erase_sector(sector.addr)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/flash.py", line 374, in erase_sector
    raise FlashEraseFailure('flash erase sector failure', address=address, result_code=result)
pyocd.core.exceptions.FlashEraseFailure: flash erase sector failure (address 0x00000000; result code 0x67)
$ pyocd flash CALL_ME_MICROBIT.hex --target nrf52833
0001497:INFO:load_cmd:Loading /Users/microbit-carlos/Downloads/CALL_ME_MICROBIT.hex
[                                                  ]   0%0002355:CRITICAL:__main__:flash erase sector failure (address 0x00000000; result code 0x67)
Traceback (most recent call last):
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/__main__.py", line 150, in run
    status = cmd.invoke()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/subcommands/load_cmd.py", line 120, in invoke
    file_format=self._args.format)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/file_programmer.py", line 171, in program
    self._loader.commit()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/loader.py", line 293, in commit
    keep_unwritten=self._keep_unwritten)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 509, in program
    flash_operation = self._sector_erase_program_double_buffer(progress_cb)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 911, in _sector_erase_program_double_buffer
    self.flash.erase_sector(sector.addr)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/flash.py", line 374, in erase_sector
    raise FlashEraseFailure('flash erase sector failure', address=address, result_code=result)
pyocd.core.exceptions.FlashEraseFailure: flash erase sector failure (address 0x00000000; result code 0x67)
$ pyocd flash CALL_ME_MICROBIT.hex
0001354:INFO:load_cmd:Loading /Users/microbit-carlos/Downloads/CALL_ME_MICROBIT.hex
[                                                  ]   0%0002218:CRITICAL:__main__:flash erase sector failure (address 0x00000000; result code 0x67)
Traceback (most recent call last):
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/__main__.py", line 150, in run
    status = cmd.invoke()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/subcommands/load_cmd.py", line 120, in invoke
    file_format=self._args.format)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/file_programmer.py", line 171, in program
    self._loader.commit()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/loader.py", line 293, in commit
    keep_unwritten=self._keep_unwritten)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 509, in program
    flash_operation = self._sector_erase_program_double_buffer(progress_cb)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 911, in _sector_erase_program_double_buffer
    self.flash.erase_sector(sector.addr)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/flash.py", line 374, in erase_sector
    raise FlashEraseFailure('flash erase sector failure', address=address, result_code=result)
pyocd.core.exceptions.FlashEraseFailure: flash erase sector failure (address 0x00000000; result code 0x67)

Running the sector erase command directly doesn't seem to be doing anything:

$ pyocd erase --sector --target nrf52833
0001524:WARNING:eraser:No operation performed
$ pyocd erase --sector
0001524:WARNING:eraser:No operation performed

Is the error code 0x67 (103) from DAPLink or PyOCD? That value is too large for this DAPLink enum: https://github.com/microbit-foundation/DAPLink-microbit/blob/7839c6cef09eb1b3ae66e180450753e237559bd7/source/daplink/error.h#L31-L87

microbit-carlos avatar Dec 10 '21 13:12 microbit-carlos

Doesn't look like the --erase chip flag is being applied, but something different is happening because in this case it takes a few more senconds to throw the error:

$ pyocd load --erase chip CALL_ME_MICROBIT.hex --verbose
0001569:INFO:board:Target type is nrf52833
0001611:INFO:dap:DP IDR = 0x2ba01477 (v1 rev2)
0001685:INFO:ap:AHB-AP#0 IDR = 0x24770011 (AHB-AP var1 rev2)
0001707:INFO:ap:AP#1 IDR = 0x02880000 (AP var0 rev0)
0001709:INFO:target_nRF52:NRF52833 not in secure state
0001802:INFO:rom_table:AHB-AP#0 Class 0x1 ROM table #0 @ 0xe00ff000 (designer=244 part=00d)
0001836:INFO:rom_table:[0]<e000e000:SCS v7-M class=14 designer=43b part=00c>
0001854:INFO:rom_table:[1]<e0001000:DWT v7-M class=14 designer=43b part=002>
0001857:INFO:rom_table:[2]<e0002000:FPB v7-M class=14 designer=43b part=003>
0001860:INFO:rom_table:[3]<e0000000:ITM v7-M class=14 designer=43b part=001>
0001862:INFO:rom_table:[4]<e0040000:TPIU M4 class=9 designer=43b part=9a1 devtype=11 archid=0000 devid=ca1:0:0>
0001865:INFO:rom_table:[5]<e0041000:ETM M4 class=9 designer=43b part=925 devtype=13 archid=0000 devid=0:0:0>
0001866:INFO:cortex_m:CPU core #0 is Cortex-M4 r0p1
0001870:INFO:cortex_m:FPU present: FPv4-SP-D16-M
0001871:INFO:dwt:4 hardware watchpoints
0001873:INFO:fpb:6 hardware breakpoints, 4 literal comparators
0001880:INFO:load_cmd:Loading /Users/microbit-carlos/Downloads/CALL_ME_MICROBIT.hex
[                                                  ]   1%0011993:CRITICAL:__main__:flash program page failure (address 0x00000000; result code 0x67)
Traceback (most recent call last):
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/__main__.py", line 150, in run
    status = cmd.invoke()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/subcommands/load_cmd.py", line 120, in invoke
    file_format=self._args.format)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/file_programmer.py", line 171, in program
    self._loader.commit()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/loader.py", line 293, in commit
    keep_unwritten=self._keep_unwritten)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 503, in program
    flash_operation = self._chip_erase_program_double_buffer(progress_cb)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/builder.py", line 779, in _chip_erase_program_double_buffer
    raise FlashProgramFailure('flash program page failure', address=current_addr, result_code=result)
pyocd.core.exceptions.FlashProgramFailure: flash program page failure (address 0x00000000; result code 0x67)

microbit-carlos avatar Dec 10 '21 13:12 microbit-carlos

As expected, pyocd cmd has the same results:

 $ pyocd cmd --verbose
0001346:INFO:board:Target type is nrf52833
0001372:INFO:dap:DP IDR = 0x2ba01477 (v1 rev2)
0001418:INFO:ap:AHB-AP#0 IDR = 0x24770011 (AHB-AP var1 rev2)
0001436:INFO:ap:AP#1 IDR = 0x02880000 (AP var0 rev0)
0001439:INFO:target_nRF52:NRF52833 not in secure state
0001454:INFO:rom_table:AHB-AP#0 Class 0x1 ROM table #0 @ 0xe00ff000 (designer=244 part=00d)
0001477:INFO:rom_table:[0]<e000e000:SCS v7-M class=14 designer=43b part=00c>
0001516:INFO:rom_table:[1]<e0001000:DWT v7-M class=14 designer=43b part=002>
0001535:INFO:rom_table:[2]<e0002000:FPB v7-M class=14 designer=43b part=003>
0001559:INFO:rom_table:[3]<e0000000:ITM v7-M class=14 designer=43b part=001>
0001594:INFO:rom_table:[4]<e0040000:TPIU M4 class=9 designer=43b part=9a1 devtype=11 archid=0000 devid=ca1:0:0>
0001606:INFO:rom_table:[5]<e0041000:ETM M4 class=9 designer=43b part=925 devtype=13 archid=0000 devid=0:0:0>
0001622:INFO:cortex_m:CPU core #0 is Cortex-M4 r0p1
0001626:INFO:cortex_m:FPU present: FPv4-SP-D16-M
0001629:INFO:dwt:4 hardware watchpoints
0001631:INFO:fpb:6 hardware breakpoints, 4 literal comparators
Connected to NRF52833 [Sleeping]: 9904360249624e45005160190000003b000000009796990b
pyocd> erase
0028549:INFO:eraser:Erasing chip...
0037704:INFO:eraser:Done
pyocd> erase 0 4096
0103317:INFO:eraser:Erasing sector 0x00000000 (4096 bytes)
Error: flash erase sector failure (address 0x00000000; result code 0x67)
Traceback (most recent call last):
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/commands/repl.py", line 96, in run_one_command
    self.context.process_command_line(line)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/commands/execution_context.py", line 299, in process_command_line
    invoc.handler(invoc)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/commands/execution_context.py", line 369, in execute_command
    cmd_object.execute()
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/commands/commands.py", line 896, in execute
    eraser.erase([self.addr])
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/eraser.py", line 76, in erase
    self._sector_erase(addresses)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/eraser.py", line 145, in _sector_erase
    flash.erase_sector(sector_addr)
  File "/Users/microbit-carlos/workspace/microbit-foundation/DAPLink-mbef-fork/venv/lib/python3.6/site-packages/pyocd/flash/flash.py", line 374, in erase_sector
    raise FlashEraseFailure('flash erase sector failure', address=address, result_code=result)
pyocd.core.exceptions.FlashEraseFailure: flash erase sector failure (address 0x00000000; result code 0x67)
pyocd>

microbit-carlos avatar Dec 10 '21 13:12 microbit-carlos

This is because the micro:bit uses Nordic's SoftDevices. The normal flash algo cannot erase the SoftDevice sector because the SoftDevice locks itself when installed.

You need to use either chip or mass erase to remove the SoftDevice.

flit avatar Dec 10 '21 21:12 flit

Btw, with pyocd erase --sector you have to tell pyocd what sectors you want to erase.

The error code 0x67 is from the flash algo. So it would most likely be the Nordic flash driver's error code.

Fyi, Nordic includes an "sde" version of the nRF52 and nRF51 flash algos in their nRF CMSIS Device Family Pack, where "sde" seems to mean "SoftDevice erase". I haven't tried it out to see how it behaves differently, or if it can be used in all cases in place of the normal algo.

flit avatar Dec 10 '21 21:12 flit

I can confirm that a pyocd erase --mass worked for me.

mbrossard avatar Dec 10 '21 21:12 mbrossard

Here's some documentation that I'm writing to cover this in a new "Target family notes" section to be added to the docs.


SoftDevice

The nRF51 and nRF52 series have support for so-called “SoftDevice” firmware, which implements Nordic's Bluetooth LE or other wireless protocol API. When firmware containing a SoftDevice is loaded, the SoftDevice region of flash is locked. In order to reprogram the flash sectors containing the SoftDevice image, a mass erase must first be performed. This can potentially cause issues with flash programming if one is not aware of this requirement.

For a development workflow with firmware using a SoftDevice, no extra steps are required.

PyOCD will by default scan flash sectors when programming flash in order to only erase and program sectors whose contents are changing. Since normally the SoftDevice sectors do not change during development, pyOCD will skip over these sectors.

In addition, a chip erased performed with a SoftDevice in flash will erase only the non-SoftDevice sectors. For example, running pyocd erase --chip on such a device will leave the SoftDevice intact and erase all other sectors.

However, any case where the SoftDevice sectors are being erased requires a prior mass erase. This includes changing the SoftDevice variant or version, as well as switching to firmware that doesn't include a SoftDevice. Mass erase is a separate operation. It mostly functions like a chip erase, but can also be used to [unlock]({% link _docs/security.md %}) devices that have APPROTECT enabled.

To perform a mass erase:

pyocd erase --mass

flit avatar Dec 12 '21 22:12 flit

Here's some documentation that I'm writing to cover this in a new "Target family notes" section to be added to the docs.

Hey @flit is it possible to add "Short Story" and "LongStory" sections approach in the docs? Short Story just contains list of points to make things work (most people just need that). Long Story would contain your description for people that want to understand how/why things (does not) work. I found this approach useful myself when returning to a project after some time and just need a quick reminder how to make it work :nerd_face:

cederom avatar Dec 12 '21 22:12 cederom

That's a great idea! Thanks 😄

flit avatar Dec 12 '21 22:12 flit

Okay, so i can confirm that pyocd erase --chip doesn't work but pyocd erase --mass does work 🎉

Is "chip erase" the same a sector erase, but going through all sectors? And mass erase is a full "erase all"? That caught me a bit by surprise, I would have expect them to be the other way around.

If that's the case pyocd load should probably accept "mass erase" as well?

 $ pyocd load --help
...
load options:
  -e {auto,chip,sector}, --erase {auto,chip,sector}
                        Choose flash erase method. Default is sector.
...
 $ pyocd load --erase mass CALL_ME_MICROBIT.hex --verbose
...
pyocd load: error: argument -e/--erase: invalid choice: 'mass' (choose from 'auto', 'chip', 'sector')

microbit-carlos avatar Dec 14 '21 12:12 microbit-carlos

Chip erase is usually a specific operation that is optimised compared to iterating over and erasing all sectors. On the nRF52, there is a chip erase, I believe. But when a SoftDevice is present, it only erases the non-SoftDevice sectors. It's also quite slow, which leads me to believe that it's erasing individual sectors. Seems faster when there is no SoftDevice present.

Mass erase is a generic pyocd term for the kind of erase that unlocks device security and/or restores the device to factory conditions. What it actually does is device-specific. For most devices, mass erase == chip erase. Pyocd only supports mass erase for Kinetis and nRF devices.

I'm hesitant to add mass erase as an additional erase type for flash programming, since it can have non-standard, device-specific behaviour. It's also something you normally don't need except in rare cases. Switching from SoftDevice to non-SoftDevice firmware is probably pretty rare. And because the SoftDevice is preserved by chip erase, and pyocd's skipping of unchanged sectors, you don't need mass erase for typical development. But it's something I'll keep in mind.

flit avatar Dec 14 '21 20:12 flit

If the nRF52 chip erase goes via the flash algos, then it looks like it's using the same ERASEALL command as the mass erase:

  • Flash Algo chip erase: https://github.com/pyocd/FlashAlgo/blob/ea94915222bc0fadba9c556a424a3663d3a010fa/source/nordic/FlashPrg.c#L150
  • PyOCD mass erase: https://github.com/pyocd/pyOCD/blob/6ce26c7d66f7b9d811e0db0e4dd5f5555b8c104f/pyocd/target/family/target_nRF52.py#L116

The main difference being that the Flash algo uses the NVMC controller and PyOCD uses the CTRL AP, but I was under the impression that these two are essentially running the same operation via two different methods? So I'm a bit surprised there is a difference.

I don't think SoftDevice uses APPROTECT, as that is mostly for debug (and UICR) access and even if it's enabled it doesn't stop NVMC flash pages erases.

One thing to note is that the nRF52820/833/840 have a APL flash protection mechanism, instead of BPROT that some of the other nRF52 parts have. The BPROT can be disabled via the debugger, but having a quick look at the datasheet I didn't find a similar feature in the APL. However, both of these protections only last until the next reset (so the target code has to set these bits on every startup), so I would think that a reset and halt should be sufficient to set the target in an unprotected state?

Using the PyOCD commander I tried to do a reset halt followed by a erase 0 1, but that still threw the same error. Interestingly when running just the erase command to erase everything, no errors were shown, but the softdevice flash regions were untouched.

Switching from SoftDevice to non-SoftDevice firmware is probably pretty rare.

For micro:bit V1 is actually pretty common, as MakeCode hex files have SoftDevice and MicroPython doesn't. However for V2 both environments pack the SoftDevice.

To be fair, if I had read the PyOCD documentation (and the terminal help more carefully) I would have found the mass erase option, but looking back I wasn't the first one to make this mistake: https://github.com/pyocd/pyOCD/issues/1212

microbit-carlos avatar Dec 20 '21 20:12 microbit-carlos

For those interested and mentioned in #1212, another non-command line workaround is to drag-and-drop the user.hex or user.bin onto the microbit drive, which will also remove the SD.

Switching from SoftDevice to non-SoftDevice firmware is probably pretty rare.

For micro:bit V1 is actually pretty common, as MakeCode hex files have SoftDevice and MicroPython doesn't. However for V2 both environments pack the SoftDevice.

Indeed, there are actually a few scenario's I ran into last year where this one-time workaround is needed in the classroom.

  1. A factory new V2 with out-of-box hex (https://microbit.org/get-started/user-guide/out-of-box-experience/) is loaded with a SD. Bare metal programming using another language than MakeCode (ie. we use ADA and Arduino/C++) throws the mentioned error when trying to flash with pyocd.
  2. We loan our Microbits to students for use during different courses. Each course has different project requirements so for some full wireless communication using a BLE stack is preferred and other times the radio stack with minimal flash footprint (and thus a switch) is sufficient.

The seamless workflow with the V1's without workarounds is definitely preferred! Many thanks (also from my future students :D)

aiunderstand avatar Jul 18 '22 14:07 aiunderstand