mirror of
https://github.com/tillitis/tillitis-key1.git
synced 2025-03-12 18:16:55 -04:00
doc: Update documentation about syscalls
- Revise firmware implementation notes - Document how to do fw syscalls - Document how to trigger a syscall function in the firmware, how to pass arguments, what the caller is responsible for and what is returned. - Describe hardware syscall implementation - how the syscall interrupts are triggered, - the hardware privilege escalation, - the UDS protection. Co-authored-by: Daniel Jobson <jobson@tillitis.se> Co-authored-by: Michael Cardell Widerkrantz <mc@tillitis.se>
This commit is contained in:
parent
7554787678
commit
c52442b54c
@ -11,9 +11,10 @@ The design top level is in `rtl/application_fpga.v`. It contains
|
|||||||
instances of all cores as well as the memory system.
|
instances of all cores as well as the memory system.
|
||||||
|
|
||||||
The memory system allows the CPU to access cores in different ways
|
The memory system allows the CPU to access cores in different ways
|
||||||
given the current execution mode. There are two execution modes -
|
given the current execution mode. There are three execution modes -
|
||||||
firmware and application. Basically, in application mode the access is
|
firmware, application and system call. Each mode give access to a
|
||||||
more restrictive.
|
different set of resources. Where app mode is the most restrictive and
|
||||||
|
firmware mode is the least restrictive.
|
||||||
|
|
||||||
The rest of the components are under `cores`. They typically have
|
The rest of the components are under `cores`. They typically have
|
||||||
their own `README.md` file documenting them and their API in detail.
|
their own `README.md` file documenting them and their API in detail.
|
||||||
@ -34,6 +35,7 @@ Rough memory map:
|
|||||||
| UART | 0xc3 |
|
| UART | 0xc3 |
|
||||||
| Touch | 0xc4 |
|
| Touch | 0xc4 |
|
||||||
| FW\_RAM | 0xd0 |
|
| FW\_RAM | 0xd0 |
|
||||||
|
| Syscall | 0xe1 |
|
||||||
| TK1 | 0xff |
|
| TK1 | 0xff |
|
||||||
|
|
||||||
## `clk_reset_gen`
|
## `clk_reset_gen`
|
||||||
@ -96,6 +98,54 @@ hours, days) there is also a 32 bit prescaler.
|
|||||||
|
|
||||||
The timer is available to use by firmware and applications.
|
The timer is available to use by firmware and applications.
|
||||||
|
|
||||||
|
## Syscall
|
||||||
|
|
||||||
|
System call trigger area. A 32-bit write to address 0xe1000000 will
|
||||||
|
trigger interrupt 31, which in turn triggers a system call in the
|
||||||
|
firmware.
|
||||||
|
|
||||||
|
## Interrupts
|
||||||
|
|
||||||
|
Triggering an interrupt will cause the CPU to execute the interrupt
|
||||||
|
handler att address 0x10.
|
||||||
|
|
||||||
|
The interrupt handler is shared by all PicoRV32 interrupts but only
|
||||||
|
interrupt 31 is enabled on the Tkey. Register `x4` can be inspected to
|
||||||
|
determine the interrupt source. Each interrupt source is assigned one
|
||||||
|
bit in x4. Triggered interrupts have their bit set to `1`.
|
||||||
|
|
||||||
|
| *Source* | *x4 Bit* |
|
||||||
|
|----------|----------|
|
||||||
|
| Syscall | 31 |
|
||||||
|
|
||||||
|
The return address is located in register `x3`. Calling the PicoRV32
|
||||||
|
specific instruction `retirq` exits the interrupt handler and clears
|
||||||
|
the interrupt source.
|
||||||
|
|
||||||
|
No registers are stored/restored when entering/exiting the interrupt
|
||||||
|
handler. It is up to the software to store/restore as necessary.
|
||||||
|
|
||||||
|
Interrupts can be enabled/disabled using the PicoRV32 specific
|
||||||
|
`maskirq` instruction.
|
||||||
|
|
||||||
|
## Restricted resources
|
||||||
|
|
||||||
|
The following table shows resource availablility for each execution
|
||||||
|
mode:
|
||||||
|
|
||||||
|
| *Execution Mode* | *ROM* | *FW RAM* | *SPI* | *UDS* |
|
||||||
|
|------------------|-------|----------|-------|-------|
|
||||||
|
| Firmware mode | r/x | r/w | r/w | r/w* |
|
||||||
|
| Syscall | r/x | r/w | r/w | i |
|
||||||
|
| Application mode | r | i | i | i |
|
||||||
|
|
||||||
|
Legend:
|
||||||
|
r = readable
|
||||||
|
w = writeable
|
||||||
|
x = executable
|
||||||
|
i = invisible
|
||||||
|
* = UDS words are readable only once in firmware mode.
|
||||||
|
|
||||||
## `tk1`
|
## `tk1`
|
||||||
|
|
||||||
See [tk1 README](core/tk1/README.md) for details.
|
See [tk1 README](core/tk1/README.md) for details.
|
||||||
@ -107,7 +157,6 @@ Contains:
|
|||||||
- RGB LED control.
|
- RGB LED control.
|
||||||
- General purpose input/output (GPIO) pin control.
|
- General purpose input/output (GPIO) pin control.
|
||||||
- Application introspection: start address and size of binary.
|
- Application introspection: start address and size of binary.
|
||||||
- BLAKE2s function access.
|
|
||||||
- Compound Device Identity (CDI).
|
- Compound Device Identity (CDI).
|
||||||
- Unique Device Identity (UDI).
|
- Unique Device Identity (UDI).
|
||||||
- RAM memory protection.
|
- RAM memory protection.
|
||||||
@ -135,13 +184,13 @@ should make it infeasible to improve asset extraction by observing
|
|||||||
multiple memory dumps from the same TKey device. The attack should
|
multiple memory dumps from the same TKey device. The attack should
|
||||||
also not directly scale to multiple TKey devices.
|
also not directly scale to multiple TKey devices.
|
||||||
|
|
||||||
The RAM address and data scrambling is done in de RAM core.
|
The RAM address and data scrambling is done in the RAM core.
|
||||||
|
|
||||||
The memory protection is setup by the firmware. Access to the memory
|
The memory protection is setup by the firmware. Access to the memory
|
||||||
protection controls is disabled for applications. Before the memory
|
protection controls is disabled for applications. Before the memory
|
||||||
protecetion is enabled, the RAM is filled with randomised data using
|
protecetion is enabled, the RAM is filled with randomised data using
|
||||||
Xorwow. So during boot the firmware perform the following steps to
|
Xorwow. During boot the firmware perform the following steps to setup
|
||||||
setup the memory protection:
|
the memory protection:
|
||||||
|
|
||||||
1. Get a random 32-bit value from the TRNG to use as data state for
|
1. Get a random 32-bit value from the TRNG to use as data state for
|
||||||
Xorwow.
|
Xorwow.
|
||||||
|
@ -23,17 +23,6 @@ and version of the device. They can be read by FW as well as
|
|||||||
applications.
|
applications.
|
||||||
|
|
||||||
|
|
||||||
### Control of execution mode
|
|
||||||
|
|
||||||
```
|
|
||||||
ADDR_APP_MODE_CTRL: 0x08
|
|
||||||
```
|
|
||||||
|
|
||||||
This register controls if the device is executing in FW mode or in App
|
|
||||||
mode. The register can be written once between power cycles, and only
|
|
||||||
by FW. If set the device is in app mode.
|
|
||||||
|
|
||||||
|
|
||||||
### Control of RGB LED
|
### Control of RGB LED
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -75,19 +64,7 @@ ADDR_APP_SIZE: 0x0d
|
|||||||
These registers provide read only information to the loaded app to
|
These registers provide read only information to the loaded app to
|
||||||
itself - where it was loaded and its size. The values are written by
|
itself - where it was loaded and its size. The values are written by
|
||||||
FW as part of the loading of the app. The registers can't be written
|
FW as part of the loading of the app. The registers can't be written
|
||||||
when the `ADDR_APP_MODE_CTRL` has been set.
|
in application mode.
|
||||||
|
|
||||||
|
|
||||||
### Access to Blake2s
|
|
||||||
|
|
||||||
```
|
|
||||||
ADDR_BLAKE2S: 0x10
|
|
||||||
```
|
|
||||||
|
|
||||||
This register provides the 32-bit function pointer address to the
|
|
||||||
Blake2s hash function in the FW. It is written by FW during boot. The
|
|
||||||
register can't be written to when the `ADDR_APP_MODE_CTRL` has been
|
|
||||||
set.
|
|
||||||
|
|
||||||
|
|
||||||
### Access to CDI
|
### Access to CDI
|
||||||
@ -99,10 +76,10 @@ ADDR_CDI_LAST: 0x27
|
|||||||
|
|
||||||
These registers provide access to the 256-bit compound device secret
|
These registers provide access to the 256-bit compound device secret
|
||||||
calculated by the FW as part of loading an application. The registers
|
calculated by the FW as part of loading an application. The registers
|
||||||
are written by the FW. The register can't be written to when the
|
are written by the FW. The register can't be written in application
|
||||||
`ADDR_APP_MODE_CTRL` has been set. The CDI is readable by apps, which
|
mode. The CDI is readable by apps, which can then use it as a base
|
||||||
can then use it as a base secret for any other secrets required to
|
secret for any other secrets required to carry out their intended use
|
||||||
carry out their intended use case.
|
case.
|
||||||
|
|
||||||
|
|
||||||
### Access to UDI
|
### Access to UDI
|
||||||
|
@ -6,8 +6,7 @@ Unique Device Secret core
|
|||||||
|
|
||||||
This core store and protect the Unique Device Secret (UDS) asset. The
|
This core store and protect the Unique Device Secret (UDS) asset. The
|
||||||
UDS can be accessed as eight separate 32-bit words. The words can only
|
UDS can be accessed as eight separate 32-bit words. The words can only
|
||||||
be accessed as long as the app_mode input is low, implying that the
|
be accessed as long as the `en` input is high.
|
||||||
CPU is executing the FW.
|
|
||||||
|
|
||||||
The UDS words can be accessed in any order, but a given word can only
|
The UDS words can be accessed in any order, but a given word can only
|
||||||
be accessed once between reset cycles. This read once functionality is
|
be accessed once between reset cycles. This read once functionality is
|
||||||
|
@ -1,35 +1,43 @@
|
|||||||
# Firmware
|
# Firmware implementation notes
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
This text is an introduction to, a requirement specification of,
|
This text is specific for the firmware, the piece of software in TKey
|
||||||
and some implementation notes of the TKey firmware. It also gives a
|
ROM. For a more general description on how to implement device apps,
|
||||||
few hint on developing and debugging the firmware.
|
see [the TKey Developer Handbook](https://dev.tillitis.se/).
|
||||||
|
|
||||||
This text is specific for the firmware. For a more general description
|
|
||||||
on how to implement device apps, see [the TKey Developer
|
|
||||||
Handbook](https://dev.tillitis.se/).
|
|
||||||
|
|
||||||
## Definitions
|
## Definitions
|
||||||
|
|
||||||
- Firmware - software in ROM responsible for loading applications. The
|
- Firmware: Software in ROM responsible for loading, measuring, and
|
||||||
firmware is included as part of the FPGA bitstream and not
|
starting applications. The firmware is included as part of the FPGA
|
||||||
replacable on a usual consumer TKey.
|
bitstream and not replacable on a usual consumer TKey.
|
||||||
- Device application or app - software supplied by the client which is
|
- Client: Software running on a computer or a mobile phone the TKey is
|
||||||
received, loaded, measured, and started by the firmware.
|
inserted into.
|
||||||
|
- Device application or app: Software supplied by the client that runs
|
||||||
|
on the TKey.
|
||||||
|
|
||||||
## CPU modes and firmware
|
## CPU modes and firmware
|
||||||
|
|
||||||
The TKey has two modes of software operation: firmware mode and
|
The TKey has two modes of software operation: firmware mode and
|
||||||
application mode. The TKey always starts in firmware mode and starts
|
application mode. The TKey always starts in firmware mode when it
|
||||||
the firmware. When the firmware is about to start the application it
|
starts the firmware. When the application starts the hardware
|
||||||
switches to a more constrained environment, the application mode.
|
automatically switches to a more constrained environment: the
|
||||||
|
application mode.
|
||||||
|
|
||||||
The TKey hardware cores are memory mapped. Firmware has complete
|
The TKey hardware cores are memory mapped but the memory access is
|
||||||
access, except that the UDS is readable only once. The memory map is
|
different depending on mode. Firmware has complete access, except that
|
||||||
constrained when running in application mode, e.g. FW\_RAM and UDS
|
the Unique Device Secret (UDS) words are readable only once even in
|
||||||
isn't readable, and several other hardware addresses are either not
|
firmware mode. The memory map is constrained when running in
|
||||||
readable or not writable for the application.
|
application mode, e.g. FW\_RAM and UDS isn't readable, and several
|
||||||
|
other hardware addresses are either not readable or not writable for
|
||||||
|
the application.
|
||||||
|
|
||||||
|
When doing system calls from a device app the context switches back to
|
||||||
|
firmware mode. However, the UDS is still not available, protected by
|
||||||
|
two measures: 1) the UDS words can only be read out once and have
|
||||||
|
already been read by firmware when measuring the app, and, 2) the UDS
|
||||||
|
is protected by hardware after the execution leaves ROM for the first
|
||||||
|
time.
|
||||||
|
|
||||||
See the table in [the Developer
|
See the table in [the Developer
|
||||||
Handbook](https://dev.tillitis.se/memory/) for an overview about the
|
Handbook](https://dev.tillitis.se/memory/) for an overview about the
|
||||||
@ -72,12 +80,17 @@ Dev Handbook for specific details.
|
|||||||
|
|
||||||
## Memory constraints
|
## Memory constraints
|
||||||
|
|
||||||
- ROM: 6 kByte.
|
| *Name* | *Size* | *FW mode* | *App mode* |
|
||||||
- FW\_RAM: 4 kByte.
|
|---------|-----------|-----------|------------|
|
||||||
- fw stack: 3824 bytes.
|
| ROM | 8 kByte | r-x | r |
|
||||||
- resetinfo: 256 bytes.
|
| FW\_RAM | 4 kByte* | rw- | - |
|
||||||
- rest is available for .data and .bss.
|
| RAM | 128 kByte | rwx | rwx |
|
||||||
- RAM: 128 kByte.
|
|
||||||
|
* FW\_RAM is divided into the following areas:
|
||||||
|
|
||||||
|
- fw stack: 3824 bytes.
|
||||||
|
- resetinfo: 256 bytes.
|
||||||
|
- rest is available for .data and .bss.
|
||||||
|
|
||||||
## Firmware behaviour
|
## Firmware behaviour
|
||||||
|
|
||||||
@ -87,7 +100,7 @@ application received from the client over the USB/UART.
|
|||||||
The firmware binary is part of the FPGA bitstream as the initial
|
The firmware binary is part of the FPGA bitstream as the initial
|
||||||
values of the Block RAMs used to construct the `FW_ROM`. The `FW_ROM`
|
values of the Block RAMs used to construct the `FW_ROM`. The `FW_ROM`
|
||||||
start address is located at `0x0000_0000` in the CPU memory map, which
|
start address is located at `0x0000_0000` in the CPU memory map, which
|
||||||
is the CPU reset vector.
|
is also the CPU reset vector.
|
||||||
|
|
||||||
### Firmware state machine
|
### Firmware state machine
|
||||||
|
|
||||||
@ -151,26 +164,30 @@ is received, when state is changed to "running".
|
|||||||
|
|
||||||
In "running", the loaded device app is measured, the Compound Device
|
In "running", the loaded device app is measured, the Compound Device
|
||||||
Identifier (CDI) is computed, we do some cleanup of firmware data
|
Identifier (CDI) is computed, we do some cleanup of firmware data
|
||||||
structures, flip to application mode, and finally start the app, which
|
structures, enable the system calls, and finally start the app, which
|
||||||
ends the firmware state machine.
|
ends the firmware state machine. Hardware guarantees that we leave
|
||||||
|
firmware mode automatically when the program counter leaves ROM.
|
||||||
|
|
||||||
The device app is now running in application mode. There is no other
|
The device app is now running in application mode. We can, however,
|
||||||
means of getting back from application mode to firmware mode than
|
return to firmware mode (excepting access to the UDS) by doing system
|
||||||
resetting/power cycling the device. Note that ROM is still accessible
|
calls. Note that ROM is still readable, but is now hardware protected
|
||||||
in the memory map, so it's still possible to execute firmware code in
|
from execution, except through the system call mechanism.
|
||||||
application mode, but with no privileged access.
|
|
||||||
|
### Golden path
|
||||||
|
|
||||||
Firmware loads the application at the start of RAM (`0x4000_0000`). It
|
Firmware loads the application at the start of RAM (`0x4000_0000`). It
|
||||||
use a part of the special FW\_RAM for its own stack.
|
use a part of the special FW\_RAM for its own stack.
|
||||||
|
|
||||||
When reset is released, the CPU starts executing the firmware. It
|
When reset is released, the CPU starts executing the firmware. It
|
||||||
begins by clearing all CPU registers, clears all FW\_RAM, sets up a
|
begins in `start.S` by clearing all CPU registers, clears all FW\_RAM,
|
||||||
stack for itself there, and then jumps to `main()`.
|
sets up a stack for itself there, and then jumps to `main()`. Also
|
||||||
|
included in the assembly part of firmware is an interrupt handler for
|
||||||
|
the system calls, but the handler is not yet enabled.
|
||||||
|
|
||||||
Beginning at `main()` it sets up the "system calls", then fills the
|
Beginning at `main()` it fills the entire RAM with pseudo random data
|
||||||
entire RAM with pseudo random data and setting up the RAM address and
|
and setting up the RAM address and data hardware scrambling with
|
||||||
data hardware scrambling with values from the True Random Number
|
values from the True Random Number Generator (TRNG). It then waits for
|
||||||
Generator (TRNG). It then waits for data coming in through the UART.
|
data coming in through the UART.
|
||||||
|
|
||||||
Typical expected use scenario:
|
Typical expected use scenario:
|
||||||
|
|
||||||
@ -199,7 +216,7 @@ Typical expected use scenario:
|
|||||||
([CDI]((#compound-device-identifier-computation))) is then
|
([CDI]((#compound-device-identifier-computation))) is then
|
||||||
computed by doing a new BLAKE2s using the Unique Device Secret
|
computed by doing a new BLAKE2s using the Unique Device Secret
|
||||||
(UDS), the application digest, and any User Supplied Secret
|
(UDS), the application digest, and any User Supplied Secret
|
||||||
(USS).
|
(USS) digest already received.
|
||||||
|
|
||||||
6. The start address of the device app, currently `0x4000_0000`, is
|
6. The start address of the device app, currently `0x4000_0000`, is
|
||||||
written to `APP_ADDR` and the size of the binary to `APP_SIZE` to
|
written to `APP_ADDR` and the size of the binary to `APP_SIZE` to
|
||||||
@ -207,24 +224,22 @@ Typical expected use scenario:
|
|||||||
it is, if it wants to relocate in RAM.
|
it is, if it wants to relocate in RAM.
|
||||||
|
|
||||||
7. The firmware now clears the part of the special `FW_RAM` where it
|
7. The firmware now clears the part of the special `FW_RAM` where it
|
||||||
keeps it stack. After this it performs no more function calls and
|
keeps it stack.
|
||||||
uses no more automatic variables.
|
|
||||||
|
|
||||||
8. Firmware starts the application by first switching from firmware
|
8. The interrupt handler for system calls is enabled.
|
||||||
mode to application mode by writing to the `APP_MODE_CTRL`
|
|
||||||
register. In this mode the MMIO region is restricted, e.g. some
|
9. Firmware starts the application by jumping to the contents of
|
||||||
registers are removed (`UDS`), and some are switched from
|
`APP_ADDR`. Hardware automatically switches from firmware mode to
|
||||||
read/write to read-only (see [the memory
|
application mode. In this mode some memory access is restricted,
|
||||||
|
e.g. some addresses are inaccessible (`UDS`), and some are
|
||||||
|
switched from read/write to read-only (see [the memory
|
||||||
map](https://dev.tillitis.se/memory/)).
|
map](https://dev.tillitis.se/memory/)).
|
||||||
|
|
||||||
Then the firmware jumps to what is in `APP_ADDR` which starts the
|
|
||||||
application.
|
|
||||||
|
|
||||||
If during this whole time any commands are received which are not
|
If during this whole time any commands are received which are not
|
||||||
allowed in the current state, we enter the "failed" state and execute
|
allowed in the current state, or any errors occur, we enter the
|
||||||
an illegal instruction. An illegal instruction traps the CPU and
|
"failed" state and execute an illegal instruction. An illegal
|
||||||
hardware blinks the status LED red until a power cycle. No further
|
instruction traps the CPU and hardware blinks the status LED red until
|
||||||
instructions are executed.
|
a power cycle. No further instructions are executed.
|
||||||
|
|
||||||
### User-supplied Secret (USS)
|
### User-supplied Secret (USS)
|
||||||
|
|
||||||
@ -256,42 +271,43 @@ call `blake2s_update()` with it and then immediately call
|
|||||||
`blake2s_update()` again with the program digest, destroying the UDS
|
`blake2s_update()` again with the program digest, destroying the UDS
|
||||||
stored in the internal context buffer. UDS should now not be in
|
stored in the internal context buffer. UDS should now not be in
|
||||||
`FW_RAM` anymore. We can read UDS only once per power cycle so UDS
|
`FW_RAM` anymore. We can read UDS only once per power cycle so UDS
|
||||||
should now not be available to firmware at all.
|
should now not be available even to firmware.
|
||||||
|
|
||||||
Then we continue with the CDI computation by updating with an optional
|
Then we continue with the CDI computation by updating with an optional
|
||||||
USS and then finalizing the hash, storing the resulting digest in
|
USS digest and finalizing the hash, storing the resulting digest in
|
||||||
`CDI`.
|
`CDI`.
|
||||||
|
|
||||||
### Firmware services
|
### Firmware system calls
|
||||||
|
|
||||||
The firmware exposes a BLAKE2s function through a function pointer
|
The firmware provides a system call mechanism through the use of the
|
||||||
located in MMIO `BLAKE2S` (see [memory
|
PicoRV32 interrupt handler. They are triggered by writing to the
|
||||||
map](system_description.md#memory-mapped-hardware-functions)) with the
|
trigger address: 0xe1000000. It's typically done with a function
|
||||||
with function signature:
|
signature like this:
|
||||||
|
|
||||||
```c
|
|
||||||
int blake2s(void *out, unsigned long outlen, const void *key,
|
|
||||||
unsigned long keylen, const void *in, unsigned long inlen,
|
|
||||||
blake2s_ctx *ctx);
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
int syscall(uint32_t number, uint32_t arg1);
|
||||||
where `blake2s_ctx` is:
|
|
||||||
|
|
||||||
```c
|
|
||||||
typedef struct {
|
|
||||||
uint8_t b[64]; // input buffer
|
|
||||||
uint32_t h[8]; // chained state
|
|
||||||
uint32_t t[2]; // total number of bytes
|
|
||||||
size_t c; // pointer for b[]
|
|
||||||
size_t outlen; // digest size
|
|
||||||
} blake2s_ctx;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `libcommon` library in
|
Arguments are system call number and upto 6 generic arguments passed
|
||||||
[tkey-libs](https://github.com/tillitis/tkey-libs/)
|
to the system call handler. The caller should place the system call
|
||||||
has a wrapper for using this function called `blake2s()` which needs
|
number in the a0 register and the arguments in registers a1 to a7
|
||||||
to be maintained if you do any changes to the firmware call.
|
according to the RISC-V calling convention. The caller is responsible
|
||||||
|
for saving and restoring registers.
|
||||||
|
|
||||||
|
The syscall handler returns execution on the next instruction after
|
||||||
|
the store instruction to the trigger address. The return value from
|
||||||
|
the syscall is now available in x10 (a0).
|
||||||
|
|
||||||
|
To add or change syscalls, see the `syscall_handler()` in
|
||||||
|
`syscall_handler.c`.
|
||||||
|
|
||||||
|
Currently supported syscalls:
|
||||||
|
|
||||||
|
| *Name* | *Number* | *Argument* | *Description* |
|
||||||
|
|-------------|----------|------------|----------------------------------|
|
||||||
|
| RESET | 1 | Unused | Reset the TKey |
|
||||||
|
| SET\_LED | 10 | Colour | Set the colour of the status LED |
|
||||||
|
| GET\_VIDPID | 12 | Unused | Get Vendor and Product ID |
|
||||||
|
|
||||||
## Developing firmware
|
## Developing firmware
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user