[Core] Allow usage of ChibiOS's SIO driver for split keyboards (#15907)
This commit is contained in:
parent
e44604c256
commit
6d67e9df4b
|
@ -1,125 +1,264 @@
|
||||||
# 'serial' Driver
|
# 'serial' Driver
|
||||||
This driver powers the [Split Keyboard](feature_split_keyboard.md) feature.
|
|
||||||
|
The serial driver powers the [Split Keyboard](feature_split_keyboard.md) feature. Several implementations are available, depending on the platform of your split keyboard. Note that none of the drivers support split keyboards with more then two halves.
|
||||||
|
|
||||||
|
| Driver | AVR | ARM | Connection between halves |
|
||||||
|
| --------------------------------------- | ------------------ | ------------------ | --------------------------------------------------------------------------------------------- |
|
||||||
|
| [Bitbang](#bitbang) | :heavy_check_mark: | :heavy_check_mark: | Single wire communication. One wire is used for reception and transmission. |
|
||||||
|
| [USART Half-duplex](#usart-half-duplex) | | :heavy_check_mark: | Efficient single wire communication. One wire is used for reception and transmission. |
|
||||||
|
| [USART Full-duplex](#usart-full-duplex) | | :heavy_check_mark: | Efficient two wire communication. Two distinct wires are used for reception and transmission. |
|
||||||
|
|
||||||
?> Serial in this context should be read as **sending information one bit at a time**, rather than implementing UART/USART/RS485/RS232 standards.
|
?> Serial in this context should be read as **sending information one bit at a time**, rather than implementing UART/USART/RS485/RS232 standards.
|
||||||
|
|
||||||
Drivers in this category have the following characteristics:
|
<hr>
|
||||||
* bit bang and USART Half-duplex provide data and signaling over a single conductor
|
|
||||||
* USART Full-duplex provide data and signaling over two conductors
|
|
||||||
* They are all limited to single master and single slave communication scheme
|
|
||||||
|
|
||||||
## Supported Driver Types
|
## Bitbang
|
||||||
|
|
||||||
| | AVR | ARM |
|
This is the Default driver, the absence of configuration assumes this driver. It works by [bit banging](https://en.wikipedia.org/wiki/Bit_banging) a GPIO pin using the CPU. It is therefore not as efficient as a dedicated hardware peripheral, which the Half-duplex and Full-duplex drivers use.
|
||||||
| ----------------- | ------------------ | ------------------ |
|
|
||||||
| bit bang | :heavy_check_mark: | :heavy_check_mark: |
|
|
||||||
| USART Half-duplex | | :heavy_check_mark: |
|
|
||||||
| USART Full-duplex | | :heavy_check_mark: |
|
|
||||||
|
|
||||||
## Driver configuration
|
!> On ARM platforms the bitbang driver causes connection issues when using it together with the bitbang WS2812 driver. Choosing alternate drivers for both serial and WS2812 (instead of bitbang) is strongly recommended.
|
||||||
|
|
||||||
### Bitbang
|
### Pin configuration
|
||||||
Default driver, the absence of configuration assumes this driver. To configure it, add this to your rules.mk:
|
|
||||||
|
```
|
||||||
|
LEFT RIGHT
|
||||||
|
+-------+ SERIAL +-------+
|
||||||
|
| SSP |-----------------| SSP |
|
||||||
|
| | VDD | |
|
||||||
|
| |-----------------| |
|
||||||
|
| | GND | |
|
||||||
|
| |-----------------| |
|
||||||
|
+-------+ +-------+
|
||||||
|
```
|
||||||
|
|
||||||
|
One GPIO pin is needed for the bitbang driver, as only one wire is used for receiving and transmitting data. This pin is referred to as the `SOFT_SERIAL_PIN` (SSP) in the configuration. A simple TRS or USB cable provides enough conductors for this driver to work.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
To use the bitbang driver follow these steps to activate it.
|
||||||
|
|
||||||
|
1. Change the `SERIAL_DRIVER` to `bitbang` in your keyboards `rules.mk` file:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
SERIAL_DRIVER = bitbang
|
SERIAL_DRIVER = bitbang
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure the driver via your config.h:
|
2. Configure the GPIO pin of your keyboard via the `config.h` file:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define SOFT_SERIAL_PIN D0 // or D1, D2, D3, E6
|
#define SOFT_SERIAL_PIN D0 // or D1, D2, D3, E6
|
||||||
#define SELECT_SOFT_SERIAL_SPEED 1 // or 0, 2, 3, 4, 5
|
|
||||||
// 0: about 189kbps (Experimental only)
|
|
||||||
// 1: about 137kbps (default)
|
|
||||||
// 2: about 75kbps
|
|
||||||
// 3: about 39kbps
|
|
||||||
// 4: about 26kbps
|
|
||||||
// 5: about 20kbps
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### ARM
|
3. On ARM platforms you must turn on ChibiOS `PAL_USE_CALLBACKS` feature:
|
||||||
|
|
||||||
!> The bitbang driver causes connection issues with bitbang WS2812 driver
|
* In `halconf.h` add the line `#define PAL_USE_CALLBACKS TRUE`.
|
||||||
|
|
||||||
Along with the generic options above, you must also turn on the `PAL_USE_CALLBACKS` feature in your halconf.h.
|
<hr>
|
||||||
|
|
||||||
### USART Half-duplex
|
## USART Half-duplex
|
||||||
Targeting STM32 boards where communication is offloaded to a USART hardware device. The advantage over bitbang is that this provides fast and accurate timings. `SERIAL_PIN_TX` for this driver is the configured USART TX pin. As this Pin is configured in open-drain mode an **external pull-up resistor is needed to keep the line high** (resistor values of 1.5k to 8.2k are known to work). To configure it, add this to your rules.mk:
|
|
||||||
|
Targeting ARM boards based on ChibiOS, where communication is offloaded to a USART hardware device that supports Half-duplex operation. The advantages over bitbanging are fast, accurate timings and reduced CPU usage. Therefore it is advised to choose this driver or the Full-duplex driver whenever possible.
|
||||||
|
|
||||||
|
### Pin configuration
|
||||||
|
|
||||||
|
```
|
||||||
|
LEFT RIGHT
|
||||||
|
+-------+ | | +-------+
|
||||||
|
| | R R | |
|
||||||
|
| | | SERIAL | | |
|
||||||
|
| TX |-----------------| TX |
|
||||||
|
| | VDD | |
|
||||||
|
| |-----------------| |
|
||||||
|
| | GND | |
|
||||||
|
| |-----------------| |
|
||||||
|
+-------+ +-------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Only one GPIO pin is needed for the Half-duplex driver, as only one wire is used for receiving and transmitting data. This pin is refereed to as the `SERIAL_USART_TX_PIN` in the configuration. Take care that the pin you chose can act as the TX pin of the USART peripheral. A simple TRS or USB cable provides enough conductors for this driver to work. As the split connection is configured to work in open-drain mode, an **external pull-up resistor is needed to keep the line high**. Resistor values of 1.5kΩ to 8.2kΩ are known to work.
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
To use the Half-duplex driver follow these steps to activate it.
|
||||||
|
|
||||||
|
1. Change the `SERIAL_DRIVER` to `usart` in your keyboards `rules.mk` file:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
SERIAL_DRIVER = usart
|
SERIAL_DRIVER = usart
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure the hardware via your config.h:
|
2. Configure the hardware of your keyboard via the `config.h` file:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define SOFT_SERIAL_PIN B6 // USART TX pin
|
#define SERIAL_USART_TX_PIN B6 // The GPIO pin that is used split communication.
|
||||||
//#define USART1_REMAP // Remap USART TX and RX pins on STM32F103 MCUs, see table below.
|
|
||||||
#define SELECT_SOFT_SERIAL_SPEED 1 // or 0, 2, 3, 4, 5
|
|
||||||
// 0: about 460800 baud
|
|
||||||
// 1: about 230400 baud (default)
|
|
||||||
// 2: about 115200 baud
|
|
||||||
// 3: about 57600 baud
|
|
||||||
// 4: about 38400 baud
|
|
||||||
// 5: about 19200 baud
|
|
||||||
#define SERIAL_USART_DRIVER SD1 // USART driver of TX pin. default: SD1
|
|
||||||
#define SERIAL_USART_TX_PAL_MODE 7 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 7
|
|
||||||
#define SERIAL_USART_TIMEOUT 20 // USART driver timeout. default 20
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You must also enable the ChibiOS `SERIAL` feature:
|
For STM32 MCUs several GPIO configuration options can be changed as well. See the section ["Alternate Functions for selected STM32 MCUs"](alternate-functions-for-selected-stm32-mcus).
|
||||||
* In your board's halconf.h: `#define HAL_USE_SERIAL TRUE`
|
|
||||||
* In your board's mcuconf.h: `#define STM32_SERIAL_USE_USARTn TRUE` (where 'n' matches the peripheral number of your selected USART on the MCU)
|
|
||||||
|
|
||||||
Do note that the configuration required is for the `SERIAL` peripheral, not the `UART` peripheral.
|
```c
|
||||||
|
#define USART1_REMAP // Remap USART TX and RX pins on STM32F103 MCUs, see table below.
|
||||||
|
#define SERIAL_USART_TX_PAL_MODE 7 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 7
|
||||||
|
```
|
||||||
|
|
||||||
### USART Full-duplex
|
3. Decide either for ChibiOS `SERIAL` or `SIO` subsystem, see the section ["Choosing a ChibiOS driver subsystem"](#choosing-a-chibios-driver-subsystem).
|
||||||
Targeting STM32 boards where communication is offloaded to a USART hardware device. The advantage over bitbang is that this provides fast and accurate timings. USART Full-Duplex requires two conductors **without** pull-up resistors instead of one conductor with a pull-up resistor unlike the Half-duplex driver. Due to its internal design it is more efficent, which can result in even faster transmission speeds.
|
|
||||||
|
|
||||||
#### Pin configuration
|
<hr>
|
||||||
|
|
||||||
`SERIAL_USART_TX_PIN` is the USART `TX` pin, `SERIAL_USART_RX_PIN` is the USART `RX` pin. No external pull-up resistors are needed as the `TX` pin operates in push-pull mode. To use this driver the usart peripherals `TX` and `RX` pins must be configured with the correct Alternate-functions. If you are using a Proton-C everything is already setup, same is true for STM32F103 MCUs. For MCUs which are using a modern flexible GPIO configuration you have to specify these by setting `SERIAL_USART_TX_PAL_MODE` and `SERIAL_USART_RX_PAL_MODE`. Refeer to the corresponding datasheets of your MCU or find those settings in the table below.
|
## USART Full-duplex
|
||||||
|
|
||||||
#### Connecting the halves and Pin Swap
|
Targeting ARM boards based on ChibiOS where communication is offloaded to an USART hardware device. The advantages over bitbanging are fast, accurate timings and reduced CPU usage. Therefore it is advised to choose this driver or the Full-duplex driver whenever possible. Due to its internal design it is slightly more efficient then the Half-duplex driver, but it should be primarily chosen if Half-duplex operation is not supported by the USART peripheral.
|
||||||
Please note that `TX` of the master half has to be connected with the `RX` pin of the slave half and `RX` of the master half has to be connected with the `TX` pin of the slave half! Usually this pin swap has to be done outside of the MCU e.g. with cables or on the pcb. Some MCUs like the STM32F303 used on the Proton-C allow this pin swap directly inside the MCU, this feature can be enabled using `#define SERIAL_USART_PIN_SWAP` in your config.h.
|
|
||||||
|
|
||||||
#### Setup
|
### Pin configuration
|
||||||
To use the driver, add this to your rules.mk:
|
|
||||||
|
```
|
||||||
|
LEFT RIGHT
|
||||||
|
+-------+ +-------+
|
||||||
|
| | SERIAL | |
|
||||||
|
| TX |-----------------| RX |
|
||||||
|
| | SERIAL | |
|
||||||
|
| RX |-----------------| TX |
|
||||||
|
| | VDD | |
|
||||||
|
| |-----------------| |
|
||||||
|
| | GND | |
|
||||||
|
| |-----------------| |
|
||||||
|
+-------+ +-------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Two GPIO pins are needed for the Full-duplex driver, as two distinct wires are used for receiving and transmitting data. The pin transmitting data is the `TX` pin and refereed to as the `SERIAL_USART_TX_PIN`, the pin receiving data is the `RX` pin and refereed to as the `SERIAL_USART_RX_PIN` in this configuration. Please note that `TX` pin of the master half has to be connected with the `RX` pin of the slave half and the `RX` pin of the master half has to be connected with the `TX` pin of the slave half! Usually this pin swap has to be done outside of the MCU e.g. with cables or on the PCB. Some MCUs like the STM32F303 used on the Proton-C allow this pin swap directly inside the MCU. A simple TRRS or USB cable provides enough conductors for this driver to work.
|
||||||
|
|
||||||
|
To use this driver the usart peripherals `TX` and `RX` pins must be configured with the correct Alternate-functions. If you are using a Proton-C everything is already setup, same is true for STM32F103 MCUs. For MCUs which are using a modern flexible GPIO configuration you have to specify these by setting `SERIAL_USART_TX_PAL_MODE` and `SERIAL_USART_RX_PAL_MODE`. Reefer to the corresponding datasheets of your MCU or find those settings in the section ["Alternate Functions for selected STM32 MCUs"](#alternate-functions-for-selected-stm32-mcus).
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
To use the Full-duplex driver follow these steps to activate it.
|
||||||
|
|
||||||
|
1. Change the `SERIAL_DRIVER` to `usart` in your keyboards `rules.mk` file:
|
||||||
|
|
||||||
```make
|
```make
|
||||||
SERIAL_DRIVER = usart
|
SERIAL_DRIVER = usart
|
||||||
```
|
```
|
||||||
|
|
||||||
Next configure the hardware via your config.h:
|
2. Configure the hardware of your keyboard via the `config.h` file:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
#define SERIAL_USART_FULL_DUPLEX // Enable full duplex operation mode.
|
#define SERIAL_USART_FULL_DUPLEX // Enable full duplex operation mode.
|
||||||
#define SERIAL_USART_TX_PIN B6 // USART TX pin
|
#define SERIAL_USART_TX_PIN B6 // USART TX pin
|
||||||
#define SERIAL_USART_RX_PIN B7 // USART RX pin
|
#define SERIAL_USART_RX_PIN B7 // USART RX pin
|
||||||
//#define USART1_REMAP // Remap USART TX and RX pins on STM32F103 MCUs, see table below.
|
```
|
||||||
//#define SERIAL_USART_PIN_SWAP // Swap TX and RX pins if keyboard is master halve.
|
|
||||||
// Check if this feature is necessary with your keyboard design and available on the mcu.
|
For STM32 MCUs several GPIO configuration options, including the ability for `TX` to `RX` pin swapping, can be changed as well. See the section ["Alternate Functions for selected STM32 MCUs"](alternate-functions-for-selected-stm32-mcus).
|
||||||
#define SELECT_SOFT_SERIAL_SPEED 1 // or 0, 2, 3, 4, 5
|
|
||||||
// 0: 460800 baud
|
```c
|
||||||
// 1: 230400 baud (default)
|
#define SERIAL_USART_PIN_SWAP // Swap TX and RX pins if keyboard is master halve. (Only available on some MCUs)
|
||||||
// 2: 115200 baud
|
#define USART1_REMAP // Remap USART TX and RX pins on STM32F103 MCUs, see table below.
|
||||||
// 3: 57600 baud
|
|
||||||
// 4: 38400 baud
|
|
||||||
// 5: 19200 baud
|
|
||||||
#define SERIAL_USART_DRIVER SD1 // USART driver of TX and RX pin. default: SD1
|
|
||||||
#define SERIAL_USART_TX_PAL_MODE 7 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 7
|
#define SERIAL_USART_TX_PAL_MODE 7 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 7
|
||||||
#define SERIAL_USART_RX_PAL_MODE 7 // Pin "alternate function", see the respective datasheet for the appropriate values for your MCU. default: 7
|
```
|
||||||
|
|
||||||
|
3. Decide either for ChibiOS `SERIAL` or `SIO` subsystem, see the section ["Choosing a ChibiOS driver subsystem"](#choosing-a-chibios-driver-subsystem).
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
## Choosing a ChibiOS driver subsystem
|
||||||
|
|
||||||
|
### The `SERIAL` driver
|
||||||
|
|
||||||
|
The `SERIAL` Subsystem is supported for the majority of ChibiOS MCUs and should be used whenever supported. Follow these steps in order to activate it:
|
||||||
|
|
||||||
|
1. In your keyboards `halconf.h` add:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define HAL_USE_SERIAL TRUE
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In your keyboards `mcuconf.h`: activate the USART peripheral that is used on your MCU. The shown example is for an STM32 MCU, so this will not work on MCUs by other manufacturers. You can find the correct names in the `mcuconf.h` files of your MCU that ship with ChibiOS.
|
||||||
|
|
||||||
|
Just below `#include_next <mcuconf.h>` add:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include_next <mcuconf.h>
|
||||||
|
|
||||||
|
#undef STM32_SERIAL_USE_USARTn
|
||||||
|
#define STM32_SERIAL_USE_USARTn TRUE
|
||||||
|
```
|
||||||
|
|
||||||
|
Where 'n' matches the peripheral number of your selected USART on the MCU.
|
||||||
|
|
||||||
|
3. In you keyboards `config.h`: override the default USART `SERIAL` driver if you use a USART peripheral that does not belong to the default selected `SD1` driver. For instance, if you selected `STM32_SERIAL_USE_USART3` the matching driver would be `SD3`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define SERIAL_USART_DRIVER SD3
|
||||||
|
```
|
||||||
|
|
||||||
|
### The `SIO` driver
|
||||||
|
|
||||||
|
The `SIO` Subsystem was added to ChibiOS with the 21.11 release and is only supported on selected MCUs. It should only be chosen when the `SERIAL` subsystem is not supported by your MCU.
|
||||||
|
|
||||||
|
Follow these steps in order to activate it:
|
||||||
|
|
||||||
|
1. In your keyboards `halconf.h` add:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define HAL_USE_SIO TRUE
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In your keyboards `mcuconf.h:` activate the USART peripheral that is used on your MCU. The shown example is for an STM32 MCU, so this will not work on MCUs by other manufacturers. You can find the correct names in the `mcuconf.h` files of your MCU that ship with ChibiOS.
|
||||||
|
|
||||||
|
Just below `#include_next <mcuconf.h>` add:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include_next <mcuconf.h>
|
||||||
|
|
||||||
|
#undef STM32_SIO_USE_USARTn
|
||||||
|
#define STM32_SIO_USE_USARTn TRUE
|
||||||
|
```
|
||||||
|
|
||||||
|
Where 'n' matches the peripheral number of your selected USART on the MCU.
|
||||||
|
|
||||||
|
3. In you keyboards `config.h`: override the default USART `SIO` driver if you use a USART peripheral that does not belong to the default selected `SIOD1` driver. For instance, if you selected `STM32_SERIAL_USE_USART3` the matching driver would be `SIOD3`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define SERIAL_USART_DRIVER SIOD3
|
||||||
|
```
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
## Advanced Configuration
|
||||||
|
|
||||||
|
There are several advanced configuration options that can be defined in your keyboards `config.h` file:
|
||||||
|
|
||||||
|
### Baudrate
|
||||||
|
|
||||||
|
If you're having issues or need a higher baudrate with serial communication, you can change the baudrate which in turn controls the communication speed for serial. You want to lower the baudrate if you experience failed transactions.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define SELECT_SOFT_SERIAL_SPEED {#}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Speed | Bitbang | Half-duplex and Full-duplex |
|
||||||
|
| ----- | -------------------------- | --------------------------- |
|
||||||
|
| `0` | 189000 baud (experimental) | 460800 baud |
|
||||||
|
| `1` | 137000 baud (default) | 230400 baud (default) |
|
||||||
|
| `2` | 75000 baud | 115200 baud |
|
||||||
|
| `3` | 39000 baud | 57600 baud |
|
||||||
|
| `4` | 26000 baud | 38400 baud |
|
||||||
|
| `5` | 20000 baud | 19200 baud |
|
||||||
|
|
||||||
|
Alternatively you can specify the baudrate directly by defining `SERIAL_USART_SPEED`.
|
||||||
|
|
||||||
|
### Timeout
|
||||||
|
|
||||||
|
This is the default time window in milliseconds in which a successful communication has to complete. Usually you don't want to change this value. But you can do so anyways by defining an alternate one in your keyboards `config.h` file:
|
||||||
|
|
||||||
|
```c
|
||||||
#define SERIAL_USART_TIMEOUT 20 // USART driver timeout. default 20
|
#define SERIAL_USART_TIMEOUT 20 // USART driver timeout. default 20
|
||||||
```
|
```
|
||||||
|
|
||||||
You must also enable the ChibiOS `SERIAL` feature:
|
<hr>
|
||||||
* In your board's halconf.h: `#define HAL_USE_SERIAL TRUE`
|
|
||||||
* In your board's mcuconf.h: `#define STM32_SERIAL_USE_USARTn TRUE` (where 'n' matches the peripheral number of your selected USART on the MCU)
|
|
||||||
|
|
||||||
Do note that the configuration required is for the `SERIAL` peripheral, not the `UART` peripheral.
|
## Alternate Functions for selected STM32 MCUs
|
||||||
|
|
||||||
#### Pins for USART Peripherals with Alternate Functions for selected STM32 MCUs
|
Pins for USART Peripherals with
|
||||||
|
|
||||||
##### STM32F303 / Proton-C [Datasheet](https://www.st.com/resource/en/datasheet/stm32f303cc.pdf)
|
### STM32F303 / Proton-C [Datasheet](https://www.st.com/resource/en/datasheet/stm32f303cc.pdf)
|
||||||
|
|
||||||
Pin Swap available: :heavy_check_mark:
|
Pin Swap available: :heavy_check_mark:
|
||||||
|
|
||||||
|
@ -151,7 +290,7 @@ Pin Swap available: :heavy_check_mark:
|
||||||
| PD8 | TX | AF7 |
|
| PD8 | TX | AF7 |
|
||||||
| PD9 | RX | AF7 |
|
| PD9 | RX | AF7 |
|
||||||
|
|
||||||
##### STM32F072 [Datasheet](https://www.st.com/resource/en/datasheet/stm32f072c8.pdf)
|
### STM32F072 [Datasheet](https://www.st.com/resource/en/datasheet/stm32f072c8.pdf)
|
||||||
|
|
||||||
Pin Swap available: :heavy_check_mark:
|
Pin Swap available: :heavy_check_mark:
|
||||||
|
|
||||||
|
@ -180,7 +319,7 @@ Pin Swap available: :heavy_check_mark:
|
||||||
| PA0 | TX | AF4 |
|
| PA0 | TX | AF4 |
|
||||||
| PA1 | RX | AF4 |
|
| PA1 | RX | AF4 |
|
||||||
|
|
||||||
##### STM32F103 Medium Density (C8-CB) [Datasheet](https://www.st.com/resource/en/datasheet/stm32f103c8.pdf)
|
### STM32F103 Medium Density (C8-CB) [Datasheet](https://www.st.com/resource/en/datasheet/stm32f103c8.pdf)
|
||||||
|
|
||||||
Pin Swap available: N/A
|
Pin Swap available: N/A
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,19 @@
|
||||||
/* Copyright 2021 QMK
|
// Copyright 2021 QMK
|
||||||
*
|
// Copyright 2022 Stefan Kerkmann
|
||||||
* This program is free software: you can redistribute it and/or modify
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "serial_usart.h"
|
#include "serial_usart.h"
|
||||||
#include "synchronization_util.h"
|
#include "synchronization_util.h"
|
||||||
|
|
||||||
#if defined(SERIAL_USART_CONFIG)
|
#if defined(SERIAL_USART_CONFIG)
|
||||||
static SerialConfig serial_config = SERIAL_USART_CONFIG;
|
static QMKSerialConfig serial_config = SERIAL_USART_CONFIG;
|
||||||
#else
|
#else
|
||||||
static SerialConfig serial_config = {
|
static QMKSerialConfig serial_config = {
|
||||||
.speed = (SERIAL_USART_SPEED), /* speed - mandatory */
|
# if HAL_USE_SERIAL
|
||||||
|
.speed = (SERIAL_USART_SPEED), /* baudrate - mandatory */
|
||||||
|
# else
|
||||||
|
.baud = (SERIAL_USART_SPEED), /* baudrate - mandatory */
|
||||||
|
# endif
|
||||||
.cr1 = (SERIAL_USART_CR1),
|
.cr1 = (SERIAL_USART_CR1),
|
||||||
.cr2 = (SERIAL_USART_CR2),
|
.cr2 = (SERIAL_USART_CR2),
|
||||||
# if !defined(SERIAL_USART_FULL_DUPLEX)
|
# if !defined(SERIAL_USART_FULL_DUPLEX)
|
||||||
|
@ -32,13 +24,24 @@ static SerialConfig serial_config = {
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
static SerialDriver* serial_driver = &SERIAL_USART_DRIVER;
|
static QMKSerialDriver* serial_driver = (QMKSerialDriver*)&SERIAL_USART_DRIVER;
|
||||||
|
|
||||||
static inline bool react_to_transactions(void);
|
static inline bool react_to_transactions(void);
|
||||||
static inline bool __attribute__((nonnull)) receive(uint8_t* destination, const size_t size);
|
static inline bool __attribute__((nonnull)) receive(uint8_t* destination, const size_t size);
|
||||||
|
static inline bool __attribute__((nonnull)) receive_blocking(uint8_t* destination, const size_t size);
|
||||||
static inline bool __attribute__((nonnull)) send(const uint8_t* source, const size_t size);
|
static inline bool __attribute__((nonnull)) send(const uint8_t* source, const size_t size);
|
||||||
static inline bool initiate_transaction(uint8_t sstd_index);
|
static inline bool initiate_transaction(uint8_t sstd_index);
|
||||||
static inline void usart_clear(void);
|
static inline void usart_clear(void);
|
||||||
|
static inline void usart_driver_start(void);
|
||||||
|
|
||||||
|
#if HAL_USE_SERIAL
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief SERIAL Driver startup routine.
|
||||||
|
*/
|
||||||
|
static inline void usart_driver_start(void) {
|
||||||
|
sdStart(serial_driver, &serial_config);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Clear the receive input queue.
|
* @brief Clear the receive input queue.
|
||||||
|
@ -64,6 +67,45 @@ static inline void usart_clear(void) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#elif HAL_USE_SIO
|
||||||
|
|
||||||
|
void clear_rx_evt_cb(SIODriver* siop) {
|
||||||
|
osalSysLockFromISR();
|
||||||
|
/* If errors occured during transactions this callback is invoked. We just
|
||||||
|
* clear the error sources and move on. We rely on the fact that we check
|
||||||
|
* for the success of the transaction by comparing the received/send bytes
|
||||||
|
* with the actual received/send bytes in the send/receive functions. */
|
||||||
|
sioGetAndClearEventsI(serial_driver);
|
||||||
|
osalSysUnlockFromISR();
|
||||||
|
}
|
||||||
|
|
||||||
|
static const SIOOperation serial_usart_operation = {.rx_cb = NULL, .rx_idle_cb = NULL, .tx_cb = NULL, .tx_end_cb = NULL, .rx_evt_cb = &clear_rx_evt_cb};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief SIO Driver startup routine.
|
||||||
|
*/
|
||||||
|
static inline void usart_driver_start(void) {
|
||||||
|
sioStart(serial_driver, &serial_config);
|
||||||
|
sioStartOperation(serial_driver, &serial_usart_operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Clear the receive input queue, as some MCUs have built-in hardware FIFOs.
|
||||||
|
*/
|
||||||
|
static inline void usart_clear(void) {
|
||||||
|
osalSysLock();
|
||||||
|
while (!sioIsRXEmptyX(serial_driver)) {
|
||||||
|
(void)sioGetX(serial_driver);
|
||||||
|
}
|
||||||
|
osalSysUnlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
# error Either the SERIAL or SIO driver has to be activated to use the usart driver for split keyboards.
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Blocking send of buffer with timeout.
|
* @brief Blocking send of buffer with timeout.
|
||||||
*
|
*
|
||||||
|
@ -71,15 +113,46 @@ static inline void usart_clear(void) {
|
||||||
* @return false Send failed.
|
* @return false Send failed.
|
||||||
*/
|
*/
|
||||||
static inline bool send(const uint8_t* source, const size_t size) {
|
static inline bool send(const uint8_t* source, const size_t size) {
|
||||||
bool success = (size_t)sdWriteTimeout(serial_driver, source, size, TIME_MS2I(SERIAL_USART_TIMEOUT)) == size;
|
bool success = (size_t)chnWriteTimeout(serial_driver, source, size, TIME_MS2I(SERIAL_USART_TIMEOUT)) == size;
|
||||||
|
|
||||||
#if !defined(SERIAL_USART_FULL_DUPLEX)
|
#if !defined(SERIAL_USART_FULL_DUPLEX)
|
||||||
if (success) {
|
/* Half duplex fills the input queue with the data we wrote - just throw it away. */
|
||||||
/* Half duplex fills the input queue with the data we wrote - just throw it away.
|
if (likely(success)) {
|
||||||
Under the right circumstances (e.g. bad cables paired with high baud rates)
|
size_t bytes_left = size;
|
||||||
less bytes can be present in the input queue, therefore a timeout is needed. */
|
# if HAL_USE_SERIAL
|
||||||
uint8_t dump[size];
|
/* The SERIAL driver uses large soft FIFOs that are filled from an IRQ
|
||||||
return receive(dump, size);
|
* context, so there is a delay between receiving the data and it
|
||||||
|
* becoming actually available, therefore we have to apply a timeout
|
||||||
|
* mechanism. Under the right circumstances (e.g. bad cables paired with
|
||||||
|
* high baud rates) less bytes can be present in the input queue as
|
||||||
|
* well. */
|
||||||
|
uint8_t dump[64];
|
||||||
|
|
||||||
|
while (unlikely(bytes_left >= 64)) {
|
||||||
|
if (unlikely(!receive(dump, 64))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bytes_left -= 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
return receive(dump, bytes_left);
|
||||||
|
# else
|
||||||
|
/* The SIO driver directly accesses the hardware FIFOs of the USART
|
||||||
|
* peripheral. As these are limited in depth, the RX FIFO might have been
|
||||||
|
* overflowed by a large that we just send. Therefore we attempt to read
|
||||||
|
* back all the data we send or until the FIFO runs empty in case it
|
||||||
|
* overflowed and data was truncated. */
|
||||||
|
if (unlikely(sioSynchronizeTXEnd(serial_driver, TIME_MS2I(SERIAL_USART_TIMEOUT)) < MSG_OK)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
osalSysLock();
|
||||||
|
while (bytes_left > 0 && !sioIsRXEmptyX(serial_driver)) {
|
||||||
|
(void)sioGetX(serial_driver);
|
||||||
|
bytes_left--;
|
||||||
|
}
|
||||||
|
osalSysUnlock();
|
||||||
|
# endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -90,10 +163,21 @@ static inline bool send(const uint8_t* source, const size_t size) {
|
||||||
* @brief Blocking receive of size * bytes with timeout.
|
* @brief Blocking receive of size * bytes with timeout.
|
||||||
*
|
*
|
||||||
* @return true Receive success.
|
* @return true Receive success.
|
||||||
* @return false Receive failed.
|
* @return false Receive failed, e.g. by timeout.
|
||||||
*/
|
*/
|
||||||
static inline bool receive(uint8_t* destination, const size_t size) {
|
static inline bool receive(uint8_t* destination, const size_t size) {
|
||||||
bool success = (size_t)sdReadTimeout(serial_driver, destination, size, TIME_MS2I(SERIAL_USART_TIMEOUT)) == size;
|
bool success = (size_t)chnReadTimeout(serial_driver, destination, size, TIME_MS2I(SERIAL_USART_TIMEOUT)) == size;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Blocking receive of size * bytes.
|
||||||
|
*
|
||||||
|
* @return true Receive success.
|
||||||
|
* @return false Receive failed.
|
||||||
|
*/
|
||||||
|
static inline bool receive_blocking(uint8_t* destination, const size_t size) {
|
||||||
|
bool success = (size_t)chnRead(serial_driver, destination, size) == size;
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +230,7 @@ __attribute__((weak)) void usart_init(void) {
|
||||||
/**
|
/**
|
||||||
* @brief Overridable master specific initializations.
|
* @brief Overridable master specific initializations.
|
||||||
*/
|
*/
|
||||||
__attribute__((weak, nonnull)) void usart_master_init(SerialDriver** driver) {
|
__attribute__((weak, nonnull)) void usart_master_init(QMKSerialDriver** driver) {
|
||||||
(void)driver;
|
(void)driver;
|
||||||
usart_init();
|
usart_init();
|
||||||
}
|
}
|
||||||
|
@ -154,7 +238,7 @@ __attribute__((weak, nonnull)) void usart_master_init(SerialDriver** driver) {
|
||||||
/**
|
/**
|
||||||
* @brief Overridable slave specific initializations.
|
* @brief Overridable slave specific initializations.
|
||||||
*/
|
*/
|
||||||
__attribute__((weak, nonnull)) void usart_slave_init(SerialDriver** driver) {
|
__attribute__((weak, nonnull)) void usart_slave_init(QMKSerialDriver** driver) {
|
||||||
(void)driver;
|
(void)driver;
|
||||||
usart_init();
|
usart_init();
|
||||||
}
|
}
|
||||||
|
@ -169,7 +253,7 @@ static THD_FUNCTION(SlaveThread, arg) {
|
||||||
chRegSetThreadName("usart_tx_rx");
|
chRegSetThreadName("usart_tx_rx");
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!react_to_transactions()) {
|
if (unlikely(!react_to_transactions())) {
|
||||||
/* Clear the receive queue, to start with a clean slate.
|
/* Clear the receive queue, to start with a clean slate.
|
||||||
* Parts of failed transactions or spurious bytes could still be in it. */
|
* Parts of failed transactions or spurious bytes could still be in it. */
|
||||||
usart_clear();
|
usart_clear();
|
||||||
|
@ -184,7 +268,7 @@ static THD_FUNCTION(SlaveThread, arg) {
|
||||||
void soft_serial_target_init(void) {
|
void soft_serial_target_init(void) {
|
||||||
usart_slave_init(&serial_driver);
|
usart_slave_init(&serial_driver);
|
||||||
|
|
||||||
sdStart(serial_driver, &serial_config);
|
usart_driver_start();
|
||||||
|
|
||||||
/* Start transport thread. */
|
/* Start transport thread. */
|
||||||
chThdCreateStatic(waSlaveThread, sizeof(waSlaveThread), HIGHPRIO, SlaveThread, NULL);
|
chThdCreateStatic(waSlaveThread, sizeof(waSlaveThread), HIGHPRIO, SlaveThread, NULL);
|
||||||
|
@ -195,10 +279,11 @@ void soft_serial_target_init(void) {
|
||||||
*/
|
*/
|
||||||
static inline bool react_to_transactions(void) {
|
static inline bool react_to_transactions(void) {
|
||||||
/* Wait until there is a transaction for us. */
|
/* Wait until there is a transaction for us. */
|
||||||
uint8_t sstd_index = (uint8_t)sdGet(serial_driver);
|
uint8_t sstd_index = 0;
|
||||||
|
receive_blocking(&sstd_index, sizeof(sstd_index));
|
||||||
|
|
||||||
/* Sanity check that we are actually responding to a valid transaction. */
|
/* Sanity check that we are actually responding to a valid transaction. */
|
||||||
if (sstd_index >= NUM_TOTAL_TRANSACTIONS) {
|
if (unlikely(sstd_index >= NUM_TOTAL_TRANSACTIONS)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,13 +293,13 @@ static inline bool react_to_transactions(void) {
|
||||||
/* Send back the handshake which is XORed as a simple checksum,
|
/* Send back the handshake which is XORed as a simple checksum,
|
||||||
to signal that the slave is ready to receive possible transaction buffers */
|
to signal that the slave is ready to receive possible transaction buffers */
|
||||||
sstd_index ^= HANDSHAKE_MAGIC;
|
sstd_index ^= HANDSHAKE_MAGIC;
|
||||||
if (!send(&sstd_index, sizeof(sstd_index))) {
|
if (unlikely(!send(&sstd_index, sizeof(sstd_index)))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Receive transaction buffer from the master. If this transaction requires it.*/
|
/* Receive transaction buffer from the master. If this transaction requires it.*/
|
||||||
if (trans->initiator2target_buffer_size) {
|
if (trans->initiator2target_buffer_size) {
|
||||||
if (!receive(split_trans_initiator2target_buffer(trans), trans->initiator2target_buffer_size)) {
|
if (unlikely(!receive(split_trans_initiator2target_buffer(trans), trans->initiator2target_buffer_size))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,7 +311,7 @@ static inline bool react_to_transactions(void) {
|
||||||
|
|
||||||
/* Send transaction buffer to the master. If this transaction requires it. */
|
/* Send transaction buffer to the master. If this transaction requires it. */
|
||||||
if (trans->target2initiator_buffer_size) {
|
if (trans->target2initiator_buffer_size) {
|
||||||
if (!send(split_trans_target2initiator_buffer(trans), trans->target2initiator_buffer_size)) {
|
if (unlikely(!send(split_trans_target2initiator_buffer(trans), trans->target2initiator_buffer_size))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -244,7 +329,7 @@ void soft_serial_initiator_init(void) {
|
||||||
serial_config.cr2 |= USART_CR2_SWAP; // master has swapped TX/RX pins
|
serial_config.cr2 |= USART_CR2_SWAP; // master has swapped TX/RX pins
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
sdStart(serial_driver, &serial_config);
|
usart_driver_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -270,7 +355,7 @@ bool soft_serial_transaction(int index) {
|
||||||
*/
|
*/
|
||||||
static inline bool initiate_transaction(uint8_t sstd_index) {
|
static inline bool initiate_transaction(uint8_t sstd_index) {
|
||||||
/* Sanity check that we are actually starting a valid transaction. */
|
/* Sanity check that we are actually starting a valid transaction. */
|
||||||
if (sstd_index >= NUM_TOTAL_TRANSACTIONS) {
|
if (unlikely(sstd_index >= NUM_TOTAL_TRANSACTIONS)) {
|
||||||
dprintln("USART: Illegal transaction Id.");
|
dprintln("USART: Illegal transaction Id.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -278,7 +363,7 @@ static inline bool initiate_transaction(uint8_t sstd_index) {
|
||||||
split_transaction_desc_t* trans = &split_transaction_table[sstd_index];
|
split_transaction_desc_t* trans = &split_transaction_table[sstd_index];
|
||||||
|
|
||||||
/* Send transaction table index to the slave, which doubles as basic handshake token. */
|
/* Send transaction table index to the slave, which doubles as basic handshake token. */
|
||||||
if (!send(&sstd_index, sizeof(sstd_index))) {
|
if (unlikely(!send(&sstd_index, sizeof(sstd_index)))) {
|
||||||
dprintln("USART: Send Handshake failed.");
|
dprintln("USART: Send Handshake failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -289,14 +374,14 @@ static inline bool initiate_transaction(uint8_t sstd_index) {
|
||||||
* - due to the half duplex limitations on return codes, we always have to read *something*.
|
* - due to the half duplex limitations on return codes, we always have to read *something*.
|
||||||
* - without the read, write only transactions *always* succeed, even during the boot process where the slave is not ready.
|
* - without the read, write only transactions *always* succeed, even during the boot process where the slave is not ready.
|
||||||
*/
|
*/
|
||||||
if (!receive(&sstd_index_shake, sizeof(sstd_index_shake)) || (sstd_index_shake != (sstd_index ^ HANDSHAKE_MAGIC))) {
|
if (unlikely(!receive(&sstd_index_shake, sizeof(sstd_index_shake)) || (sstd_index_shake != (sstd_index ^ HANDSHAKE_MAGIC)))) {
|
||||||
dprintln("USART: Handshake failed.");
|
dprintln("USART: Handshake failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send transaction buffer to the slave. If this transaction requires it. */
|
/* Send transaction buffer to the slave. If this transaction requires it. */
|
||||||
if (trans->initiator2target_buffer_size) {
|
if (trans->initiator2target_buffer_size) {
|
||||||
if (!send(split_trans_initiator2target_buffer(trans), trans->initiator2target_buffer_size)) {
|
if (unlikely(!send(split_trans_initiator2target_buffer(trans), trans->initiator2target_buffer_size))) {
|
||||||
dprintln("USART: Send failed.");
|
dprintln("USART: Send failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -304,7 +389,7 @@ static inline bool initiate_transaction(uint8_t sstd_index) {
|
||||||
|
|
||||||
/* Receive transaction buffer from the slave. If this transaction requires it. */
|
/* Receive transaction buffer from the slave. If this transaction requires it. */
|
||||||
if (trans->target2initiator_buffer_size) {
|
if (trans->target2initiator_buffer_size) {
|
||||||
if (!receive(split_trans_target2initiator_buffer(trans), trans->target2initiator_buffer_size)) {
|
if (unlikely(!receive(split_trans_target2initiator_buffer(trans), trans->target2initiator_buffer_size))) {
|
||||||
dprintln("USART: Receive failed.");
|
dprintln("USART: Receive failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,5 @@
|
||||||
/* Copyright 2021 QMK
|
// Copyright 2021 QMK
|
||||||
*
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
@ -23,8 +10,24 @@
|
||||||
#include <ch.h>
|
#include <ch.h>
|
||||||
#include <hal.h>
|
#include <hal.h>
|
||||||
|
|
||||||
#if !defined(SERIAL_USART_DRIVER)
|
#if HAL_USE_SERIAL
|
||||||
|
|
||||||
|
typedef SerialDriver QMKSerialDriver;
|
||||||
|
typedef SerialConfig QMKSerialConfig;
|
||||||
|
|
||||||
|
# if !defined(SERIAL_USART_DRIVER)
|
||||||
# define SERIAL_USART_DRIVER SD1
|
# define SERIAL_USART_DRIVER SD1
|
||||||
|
# endif
|
||||||
|
|
||||||
|
#elif HAL_USE_SIO
|
||||||
|
|
||||||
|
typedef SIODriver QMKSerialDriver;
|
||||||
|
typedef SIOConfig QMKSerialConfig;
|
||||||
|
|
||||||
|
# if !defined(SERIAL_USART_DRIVER)
|
||||||
|
# define SERIAL_USART_DRIVER SIOD1
|
||||||
|
# endif
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if !defined(USE_GPIOV1)
|
#if !defined(USE_GPIOV1)
|
||||||
|
@ -113,4 +116,4 @@
|
||||||
# define SERIAL_USART_TIMEOUT 20
|
# define SERIAL_USART_TIMEOUT 20
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#define HANDSHAKE_MAGIC 7
|
#define HANDSHAKE_MAGIC 7U
|
||||||
|
|
Loading…
Reference in New Issue