mirror of
https://github.com/eddyem/stm32samples.git
synced 2026-03-20 00:30:57 +03:00
SPI works, may say it's over
This commit is contained in:
@@ -1,20 +1,406 @@
|
||||
USB-CAN adapter
|
||||
===============
|
||||
# USB-CAN-GPIO Adapter Command Protocol
|
||||
|
||||
Based on [USB-CAN](https://github.com/eddyem/stm32samples/tree/master/F0%3AF030%2CF042%2CF072/usbcan_ringbuffer).
|
||||
|
||||
Unlike original (only USB-CAN) adapter, in spite of it have GPIO contacts on board, this firmware allows to use both
|
||||
USB-CAN and GPIO.
|
||||
|
||||
The old USB-CAN is available as earlier by /dev/USB-CANx, also you can see new device: /dev/USB-GPIOx.
|
||||
New interface allows you to configure GPIO and use it's base functions: in/out/ADC.
|
||||
The old USB-CAN is available as earlier by /dev/USB-CANx, also you can see new device: /dev/USB-GPIOx.
|
||||
New interface allows you to configure GPIO and use it's base functions.
|
||||
|
||||
---
|
||||
|
||||
## DMA channels
|
||||
# GPIO Interface Protocol for `/dev/USB-CANx`
|
||||
|
||||
DMA1 channel 1: ADC
|
||||
---
|
||||
|
||||
DMA1 channel 4: USART[1,2]_TX
|
||||
## Overview
|
||||
This firmware implements a versatile USB device with two CDC ACM interfaces:
|
||||
- **ICAN** (interface 0) — for CAN bus communication
|
||||
- **IGPIO** (interface 1) — for GPIO, USART, I2C, SPI, PWM control and monitoring
|
||||
|
||||
DMA1 channel 5: USART[1,2]_RX
|
||||
The device can be configured dynamically via text commands sent over the USB virtual COM ports. All settings can be saved to internal flash and restored on startup.
|
||||
|
||||
---
|
||||
|
||||
## General Command Format
|
||||
- Commands are case-sensitive, up to 15 characters long.
|
||||
- Parameters are separated by spaces or commas.
|
||||
- Numeric values can be decimal, hexadecimal (`0x...`), octal (`0...`), or binary (`b...`).
|
||||
- For `USART=...` command the part after `=` is interpreted according to the current `hexinput` mode (see `hexinput` command).
|
||||
Specific numeric values (as in `SPI` and `I2C` send) are hex-only without `0x` prefix.
|
||||
|
||||
---
|
||||
|
||||
## Global Commands
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `canspeed [=value]` | Get or set CAN bus speed (kBaud, 10─1000). |
|
||||
| `curcanspeed` | Show the actual CAN speed currently used by the interface. |
|
||||
| `curpinconf` | Dump the *current* (working) pin configuration ─ may differ from saved config. |
|
||||
| `dumpconf` | Dump the global configuration stored in RAM (including CAN speed, interface names, pin settings, USART/I2C/SPI parameters). |
|
||||
| `eraseflash` | Erase the entire user configuration storage area. |
|
||||
| `help` | Show help message. |
|
||||
| `hexinput [=0/1]` | Set input mode: `0` ─ plain text, `1` ─ hex bytes + quoted text. Affects commands like `USART` and `sendcan`. |
|
||||
| `iic=addr data...` | Write data over I2C. Address and data bytes are given in hex. Example: `iic=50 01 02` |
|
||||
| `iicread=addr nbytes` | Read `nbytes` from I2C device at `addr` and display as hex. Both addr and nbytes are hex numbers. |
|
||||
| `iicreadreg=addr reg nbytes` | Read `nbytes` from I2C device register `reg`. Also hex. |
|
||||
| `iicscan` | Start scanning I2C bus; found addresses are printed asynchronously. |
|
||||
| `mcutemp` | Show MCU internal temperature in ℃*10. |
|
||||
| `mcureset` | Reset the microcontroller. |
|
||||
| `PAx [= ...]` | Configure or read GPIOA pin `x` (0-15). See **Pin Configuration** below. |
|
||||
| `PBx [= ...]` | Configure or read GPIOB pin `x`. |
|
||||
| `pinout [=function1,function2,...]` | List all pins and their available alternate functions. If a space- or comma-separated list of functions is given, only pins supporting any of them are shown. |
|
||||
| `pwmmap` | Show pins capable of PWM and indicate collisions (pins sharing the same timer channel). |
|
||||
| `readconf` | Re-read configuration from flash into RAM. |
|
||||
| `reinit` | Apply the current pin configuration to hardware. Must be issued after any pin changes. |
|
||||
| `saveconf` | Save current RAM configuration to flash. |
|
||||
| `sendcan=...` | Send data over the CAN USB interface. If `hexinput=1`, the argument is parsed as hex bytes with plain text in quotes; otherwise as plain text (a newline is appended). |
|
||||
| `setiface=N [=name]` | Set or get the name of interface `N` (0 = CAN, 1 = GPIO). |
|
||||
| `SPI=data...` | Perform SPI transfer: send hex bytes, receive and display the received bytes. |
|
||||
| `time` | Show system time in milliseconds since start. |
|
||||
| `USART[=...]` | If `=...` is given, send data over USART (text or hex depending on `hexinput`). Without argument, read and display any received USART data (as text or hex, according to USART's `TEXT`/`HEX` setting). |
|
||||
| `vdd` | Show approximate supply voltage (Vdd, V*100). |
|
||||
|
||||
---
|
||||
|
||||
## Pin Configuration (Commands `PAx`, `PBx`)
|
||||
|
||||
Pins are configured with the syntax:
|
||||
```
|
||||
PAx = MODE PULL OTYPE FUNC MISC ...
|
||||
```
|
||||
or for a simple digital output:
|
||||
```
|
||||
PAx = 0 # set pin low
|
||||
PAx = 1 # set pin high
|
||||
```
|
||||
or for PWM output:
|
||||
```
|
||||
PAx = value # set PWM duty cycle (0-255)
|
||||
```
|
||||
|
||||
### Available Keywords (in any order)
|
||||
|
||||
| Group | Keyword | Meaning |
|
||||
|---------|----------|---------|
|
||||
| **MODE** | `AIN` | Analog input (ADC) |
|
||||
| | `IN` | Digital input |
|
||||
| | `OUT` | Digital output |
|
||||
| | `AF` | Alternate function ─ automatically set when a function like `USART`, `SPI`, `I2C`, `PWM` is selected. |
|
||||
| **PULL** | `PU` | Pull-up enabled (GPIO-only group, don't affect on functions) |
|
||||
| | `PD` | Pull-down enabled |
|
||||
| | `FL` | Floating (no pull) |
|
||||
| **OTYPE**| `PP` | Push-pull output |
|
||||
| | `OD` | Open-drain output |
|
||||
| **FUNC** | `USART` | Enable USART alternate function |
|
||||
| | `SPI` | Enable SPI alternate function |
|
||||
| | `I2C` | Enable I2C alternate function |
|
||||
| | `PWM` | Enable PWM alternate function |
|
||||
| **MISC** | `MONITOR`| Enable asynchronous monitoring of pin changes (for GPIO input or ADC) or USART incoming message. When the pin changes, its new value is sent over USB automatically. |
|
||||
| | `THRESHOLD n` | Set ADC threshold (0-4095). Only meaningful with `AIN`. A change larger than this triggers an async report (if `MONITOR` enabled on this pin). |
|
||||
| | `SPEED n` | Set interface speed/frequency (baud for USART, Hz for SPI, speed index for I2C). |
|
||||
| | `TEXT` | USART operates in text mode (lines terminated by `\n`). |
|
||||
| | `HEX` | USART operates in binary mode (data output as hex dump, data input by IDLE-separated portions if `MONITOR` enabled). |
|
||||
|
||||
### Notes
|
||||
- `AF` is automatically set when any `FUNC` is selected; you do not need to type it explicitly.
|
||||
- For `PWM`, the duty cycle can be set by assigning a number (0-255) directly, e.g. `PA1=128`.
|
||||
- For `OUT`, assigning `0` or `1` sets/clears the pin.
|
||||
- For `ADC` (`AIN`), `MONITOR` uses the `THRESHOLD` value; if not given, any change triggers a report.
|
||||
- Conflicting configurations (e.g., two different USARTs on the same pins, or missing SCL/SDA for I2C) are detected and rejected with an error message.
|
||||
|
||||
After changing pin settings, you must issue the `reinit` command for the changes to take effect.
|
||||
|
||||
---
|
||||
|
||||
## USART
|
||||
|
||||
### Pin Assignment
|
||||
USART1 can be used on:
|
||||
- PA9 (TX), PA10 (RX)
|
||||
- PB6 (TX), PB7 (RX)
|
||||
|
||||
USART2 on:
|
||||
- PA2 (TX), PA3 (RX)
|
||||
|
||||
Both USARTs share the same DMA channels, so only one USART can be active at a time.
|
||||
|
||||
### Configuration via Pin Commands
|
||||
When you set a pin to `FUNC_USART`, you can also specify:
|
||||
- `SPEED n` ─ baud rate (default 9600)
|
||||
- `TEXT` ─ enable line-oriented mode (data is buffered until `\n`)
|
||||
- `HEX` ─ binary mode (data is sent/received as raw bytes; asynchronous output appears as hex dump)
|
||||
- `MONITOR` ─ enable asynchronous reception; received data will be sent over USB automatically (as text lines or hex dump depending on mode)
|
||||
|
||||
Example:
|
||||
```
|
||||
PA9 = USART SPEED 115200 TEXT MONITOR
|
||||
PA10 = USART
|
||||
reinit
|
||||
```
|
||||
Now USART1 is active at 115200 baud, text mode, with monitoring. Any incoming line will be printed as `USART = ...`.
|
||||
|
||||
### Sending Data
|
||||
```
|
||||
USART=Hello, world! # if hexinput=0, sends plain text
|
||||
USART=48 65 6c 6c 6f # if hexinput=1, sends 5 bytes
|
||||
```
|
||||
If `TEXT` mode is enabled, a newline is automatically appended to the transmitted string.
|
||||
|
||||
### Receiving Data
|
||||
Without `=`, the command reads and displays any data waiting in the ring buffer, like:
|
||||
```
|
||||
USART = 48 65 6c 6c 6f
|
||||
```
|
||||
If in `TEXT` mode, only complete lines are returned. In `HEX` mode, all received bytes are shown as a hex dump.
|
||||
|
||||
If `MONITOR` disabled, but incoming data flow is too large for buffering between consequent `USART` calls,
|
||||
some "old" data would be printed asynchroneously.
|
||||
|
||||
---
|
||||
|
||||
## I2C
|
||||
|
||||
### Pin Assignment
|
||||
|
||||
I2C1 is available on any mix of:
|
||||
- PB6 (SCL), PB7 (SDA)
|
||||
- PB10 (SCL), PB11 (SDA)
|
||||
|
||||
You must configure both SCL and SDA pins for I2C.
|
||||
|
||||
### Configuration via Pin Commands
|
||||
- `I2C` ─ selects I2C alternate function
|
||||
- `SPEED n` ─ speed index: 0=10kHz, 1=100kHz, 2=400kHz, 3=1MHz (default 100kHz if omitted)
|
||||
- `MONITOR` is not used for I2C.
|
||||
|
||||
Example:
|
||||
```
|
||||
PB6 = I2C SPEED 2
|
||||
PB7 = I2C
|
||||
reinit
|
||||
```
|
||||
This sets up I2C at 400 kHz.
|
||||
|
||||
### Commands
|
||||
- `iic=addr data...` ─ write bytes to device at 7-bit address `addr`. Address and data are given in hex, e.g. `iic=50 01 02 03`.
|
||||
- `iicread=addr nbytes` ─ read `nbytes` (both numbers are hex) from device and display as hex dump.
|
||||
- `iicreadreg=addr reg nbytes` ─ read `nbytes` from register `reg` (all numbers are hex).
|
||||
- `iicscan` ─ start scanning all possible 7-bit addresses (1─127). As devices respond, messages like `foundaddr = 0x50` are printed asynchronously.
|
||||
|
||||
---
|
||||
|
||||
## SPI
|
||||
|
||||
### Pin Assignment
|
||||
|
||||
SPI1 can be used on any mix of:
|
||||
- **PA5** (SCK), **PA6** (MISO), **PA7** (MOSI)
|
||||
- **PB3** (SCK), **PB4** (MISO), **PB5** (MOSI)
|
||||
|
||||
All three pins are not strictly required; you may configure only SCK+MOSI (TX only) or SCK+MISO (RX only). The SPI peripheral will be set to the appropriate mode automatically.
|
||||
|
||||
### Configuration via Pin Commands
|
||||
- `SPI` ─ selects SPI alternate function
|
||||
- `SPEED n` ─ desired frequency in Hz (actual nearest prescaler will be chosen automatically)
|
||||
- `CPOL` ─ clock polarity (1 = idle high)
|
||||
- `CPHA` ─ clock phase (1 = second edge)
|
||||
- `LSBFIRST` ─ LSB first transmission.
|
||||
|
||||
If `CPOL`/`CPHA` are not given, they default to 0 (mode 0). `LSBFIRST` defaults to MSB first.
|
||||
|
||||
Example:
|
||||
```
|
||||
PA5 = SPI SPEED 2000000 CPOL CPHA
|
||||
PA6 = SPI
|
||||
PA7 = SPI
|
||||
reinit
|
||||
```
|
||||
Configures SPI at ~2 MHz, mode 3 (CPOL=1, CPHA=1), full duplex.
|
||||
|
||||
### Commands
|
||||
- `SPI=data...` ─ send the given hex bytes, and display the received bytes. Works in full-duplex or write-only modes. Example:
|
||||
```
|
||||
SPI=01 02 03
|
||||
```
|
||||
will send three bytes and output the three bytes received simultaneously.
|
||||
|
||||
- `SPI=n` — reads `n` bytest of data (n should be decimal, binary or hex with prefixes `0b` or `0x`).
|
||||
|
||||
---
|
||||
|
||||
## PWM
|
||||
|
||||
### Pin Assignment
|
||||
PWM is available on many pins; see the output of `pwmmap` (or `pinout=PWM`) for a complete list with possible conflicts (pins sharing the same timer channel).
|
||||
Conflicts are automatically detected ─ if you try to use two conflicting pins, one will be reset to default.
|
||||
|
||||
### Configuration via Pin Commands
|
||||
- `PWM` ─ selects PWM alternate function
|
||||
- No additional parameters are needed; the duty cycle is set by writing a number directly to the pin.
|
||||
|
||||
Example:
|
||||
```
|
||||
PA1 = PWM
|
||||
reinit
|
||||
PA1=128 # set 50% duty cycle
|
||||
```
|
||||
|
||||
### Reading PWM Value
|
||||
```
|
||||
PA1
|
||||
```
|
||||
returns the current duty cycle (0─255).
|
||||
|
||||
---
|
||||
|
||||
## Monitoring and Asynchronous Messages
|
||||
|
||||
When a pin is configured with `MONITOR` and is not in AF mode (i.e., GPIO input or ADC), any change in its state triggers an automatic USB message. The format is the same as the pin getter: `PAx = value`. For ADC, the value is the ADC reading; the message is sent only when the change exceeds the programmed `THRESHOLD` (if any).
|
||||
|
||||
USART monitoring (if enabled with `MONITOR`) sends received data asynchronously, using the same output format as the `USART` command.
|
||||
|
||||
I2C scan results are also printed asynchronously while scan mode is active.
|
||||
|
||||
---
|
||||
|
||||
## Saving Configuration
|
||||
|
||||
The entire configuration (interface names, CAN speed, pin settings, USART/I2C/SPI parameters) can be saved to flash with `saveconf`.
|
||||
On startup, the last saved configuration is automatically loaded. The flash storage uses a simple rotating scheme, so many previous configurations are preserved until the storage area is erased with `eraseflash`.
|
||||
|
||||
---
|
||||
|
||||
## Error Codes
|
||||
Most commands return one of the following status strings (if not silent):
|
||||
|
||||
| Code | Meaning |
|
||||
|-------------|---------|
|
||||
| `OK` | Command executed successfully. |
|
||||
| `BADCMD` | Unknown command. |
|
||||
| `BADPAR` | Invalid parameter. |
|
||||
| `BADVAL` | Value out of range or unacceptable. |
|
||||
| `WRONGLEN` | Message length incorrect. |
|
||||
| `CANTRUN` | Cannot execute due to current state (e.g., peripheral not configured). |
|
||||
| `BUSY` | Resource busy (e.g., USART TX busy). |
|
||||
| `OVERFLOW` | Input string too long. |
|
||||
|
||||
Commands that produce no direct output (e.g., getters) return nothing (silent) and only print the requested value.
|
||||
|
||||
---
|
||||
|
||||
# CAN Interface Protocol for `/dev/USB-CANx`
|
||||
|
||||
---
|
||||
|
||||
This part describes the simple text‑based protocol used to communicate with the CAN interface of the USB‑CAN‑GPIO adapter.
|
||||
The interface appears as a standard CDC ACM virtual COM port (e.g. `/dev/ttyACM0` or `/dev/USB-CANx`).
|
||||
All commands are terminated by a newline character (`\n`). Numeric parameters can be entered in decimal, hexadecimal (prefix `0x`), octal (prefix `0`), or binary (prefix `b`).
|
||||
|
||||
---
|
||||
|
||||
## Received CAN Message Format
|
||||
|
||||
When a CAN message is received (and display is not paused), it is printed as:
|
||||
|
||||
```
|
||||
<timestamp_ms> #<ID> <data0> <data1> ... <dataN-1>
|
||||
```
|
||||
|
||||
- **`<timestamp_ms>`** – system time in milliseconds since start.
|
||||
- **`<ID>`** – 11‑bit CAN identifier in hexadecimal (e.g. `0x123`).
|
||||
- **`<dataX>`** – data bytes in hexadecimal, separated by spaces. If the message has no data, only the timestamp and ID are printed.
|
||||
|
||||
Example:
|
||||
```
|
||||
12345 #0x123 0xDE 0xAD 0xBE 0xEF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### General
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `?` | Show a brief help message. | `?` |
|
||||
|
||||
### CAN Configuration & Control
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `b [speed]` | Set CAN bus speed in kBaud (10–1000). Without argument, display the current speed. | `b 250` |
|
||||
| `I` | Re‑initialise the CAN controller with the last set speed. | `I` |
|
||||
| `c` | Show CAN status registers (MSR, TSR, RF0R, RF1R). | `c` |
|
||||
| `e` | Show the CAN error register (ESR) with a human‑readable description. | `e` |
|
||||
|
||||
### Sending Messages
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `s ID [byte0 ... byte7]` | Send a CAN message with the given ID and up to 8 data bytes. Returns immediately after queuing. | `s 0x123 0xAA 0xBB` |
|
||||
| `S ID [byte0 ... byte7]` | Same as `s`. | `S 0x100 0x01` |
|
||||
|
||||
### Flood (Periodic Transmission)
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `F ID [byte0 ... byte7]` | Set a message to be transmitted repeatedly. The message is stored and sent every `t` milliseconds. | `F 0x200 0x55 0xAA` |
|
||||
| `i` | Enable *incremental* flood mode. Sends a 4‑byte counter (increasing by 1 each time) using the ID from the last `F` command. | `i` |
|
||||
| `t <ms>` | Set the flood period in milliseconds (default 5 ms). | `t 10` |
|
||||
|
||||
### Filtering
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `f bank fifo mode num0 [num1 [num2 [num3]]]` | Configure a hardware filter. `bank` — filter bank number (0–27). `fifo` — FIFO assignment (0 or 1). `mode` — `I` for ID list mode, `M` for mask mode. `numX` — IDs or ID/mask pairs (for mask mode). | `f 0 1 I 0x123 0x456` – two IDs in list mode. `f 1 0 M 0x123 0x7FF` – ID 0x123 with mask 0x7FF (accept all). |
|
||||
| `l` | List all active filters with their configuration. | `l` |
|
||||
| `a <ID>` | Add an ID to the software ignore list (up to 10 IDs). Messages with this ID will not be printed. | `a 0x321` |
|
||||
| `p` | Print the current ignore list. | `p` |
|
||||
| `d` | Clear the entire ignore list. | `d` |
|
||||
| `P` | Pause/resume printing of incoming CAN messages. Toggles between paused and running. | `P` |
|
||||
|
||||
### LEDs & Misc
|
||||
| Command | Description | Example |
|
||||
|---------|-------------|---------|
|
||||
| `o` | Turn both LEDs off. | `o` |
|
||||
| `O` | Turn both LEDs on. | `O` |
|
||||
| `T` | Show the system time in milliseconds since start. | `T` |
|
||||
| `R` | Perform a software reset of the microcontroller. | `R` |
|
||||
|
||||
---
|
||||
|
||||
## Error Reporting
|
||||
|
||||
When an error occurs, the device may print one of the following messages:
|
||||
|
||||
| Message | Meaning |
|
||||
|-----------------|---------|
|
||||
| `ERROR: ...` | General error with a descriptive text. |
|
||||
| `NAK` | A transmitted message was not acknowledged (e.g., no receiver on the bus). |
|
||||
| `FIFO overrun` | A CAN FIFO overrun occurred – messages were lost. |
|
||||
| `CAN bus is off`| The controller entered the bus‑off state; try to restart it with `I`. |
|
||||
|
||||
Additionally, the `e` command gives a detailed breakdown of the error register.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- All settings (baud rate, filters, ignore list, flood message) are stored in RAM only and are lost after a reset or power‑off.
|
||||
To make changes (only speed available) permanent, use the **GPIO interface** commands `saveconf`/`storeconf` after configuring CAN.
|
||||
- The device can only handle one active USART at a time, but the CAN interface operates independently.
|
||||
- The command parser is case‑sensitive for the single‑letter commands (they are expected in lower case, except where noted).
|
||||
|
||||
---
|
||||
|
||||
## How to distinguish between identical device
|
||||
|
||||
In the **GPIO interface** you can setup custom interface name (`setiface=...`) for both USB-CAN and USB-GPIO interfaces.
|
||||
After storing them in flash, reconnect and `lsusb -v` for given device will show your saved names on `iInterface` fields.
|
||||
These fields could be used to create human-readable symlinks in `/dev`.
|
||||
|
||||
To see symlink in `/dev` to your `/dev/ttyACMx` device based on `iInterface` field, add this udev-script to `/etc/udev/rules.d/usbids.rules`
|
||||
|
||||
```
|
||||
ACTION=="add", DRIVERS=="usb", ENV{USB_IDS}="%s{idVendor}:%s{idProduct}"
|
||||
ACTION=="add", ENV{USB_IDS}=="067b:2303", ATTRS{interface}=="?*", PROGRAM="/bin/bash -c \"ls /dev | grep $attr{interface} | wc -l \"", SYMLINK+="$attr{interface}%c", MODE="0666", GROUP="tty"
|
||||
ACTION=="add", ENV{USB_IDS}=="0483:5740", ATTRS{interface}=="?*", PROGRAM="/bin/bash -c \"ls /dev | grep $attr{interface} | wc -l \"", SYMLINK+="$attr{interface}%c", MODE="0666", GROUP="tty"
|
||||
```
|
||||
|
||||
@@ -44,6 +44,7 @@ const user_conf *Flash_Data = (const user_conf *)(&__varsstart);
|
||||
#define PINEN {.enable = 1}
|
||||
// USART1 @9600 with monitoring
|
||||
#define U1 {.enable = 1, .mode = MODE_AF, .speed = SPEED_HIGH, .afno = 1, .af = FUNC_USART, .monitor = 1}
|
||||
#define S1 {.enable = 1, .mode = MODE_AF, .speed = SPEED_HIGH, .afno = 0, .af = FUNC_SPI}
|
||||
// GPIOA, enabled: PA0-PA3, PA5-PA7, PA9, PA10
|
||||
#define PACONF \
|
||||
[0] = PINEN, [1] = PINEN, [2] = PINEN, [3] = PINEN, [5] = PINEN, \
|
||||
@@ -51,8 +52,8 @@ const user_conf *Flash_Data = (const user_conf *)(&__varsstart);
|
||||
|
||||
// GPIOB, enabled: PB0-PB7, PB10, PB11
|
||||
#define PBCONF \
|
||||
[0] = PINEN, [1] = PINEN, [2] = PINEN, [3] = PINEN, [4] = PINEN, \
|
||||
[5] = PINEN, [6] = PINEN, [7] = PINEN, [10] = PINEN, [11] = PINEN
|
||||
[0] = PINEN, [1] = PINEN, [2] = PINEN, [3] = S1, [4] = S1, \
|
||||
[5] = S1, [6] = PINEN, [7] = PINEN, [10] = PINEN, [11] = PINEN
|
||||
|
||||
user_conf the_conf = {
|
||||
.userconf_sz = sizeof(user_conf),
|
||||
@@ -64,6 +65,7 @@ user_conf the_conf = {
|
||||
.iIlengths = {14, 16},
|
||||
.pinconfig = {[0] = {PACONF}, [1] = {PBCONF}},
|
||||
.usartconfig = {.speed = 9600, .idx = 0, .RXen = 1, .TXen = 1, .textproto = 1, .monitor = 1},
|
||||
.spiconfig = {.speed = 1000000, .cpol = 1, .cpha = 1},
|
||||
};
|
||||
|
||||
int currentconfidx = -1; // index of current configuration
|
||||
@@ -196,3 +198,7 @@ static int erase_flash(const void *start, const void *end){
|
||||
int erase_storage(){
|
||||
return erase_flash(Flash_Data, NULL);
|
||||
}
|
||||
|
||||
uint32_t storage_capacity(){
|
||||
return maxCnum;
|
||||
}
|
||||
|
||||
@@ -38,15 +38,25 @@
|
||||
// maximal size (in letters, ASCII, no ending \0) of iInterface for settings
|
||||
#define MAX_IINTERFACE_SZ (16)
|
||||
|
||||
typedef struct {
|
||||
uint32_t speed;
|
||||
uint8_t cpol : 1;
|
||||
uint8_t cpha : 1;
|
||||
uint8_t lsbfirst : 1;
|
||||
// these flags - only for data in/out formatting
|
||||
uint8_t rxonly : 1; // use format SPI=len instead of SPI=data
|
||||
uint8_t txonly : 1; // don't receive data
|
||||
} spiconfig_t;
|
||||
|
||||
/*
|
||||
* struct to save user configurations
|
||||
*/
|
||||
typedef struct __attribute__((packed, aligned(4))){
|
||||
uint16_t userconf_sz; // "magick number"
|
||||
uint16_t CANspeed; // default CAN speed (in kBaud!!!)
|
||||
uint32_t SPIspeed; // SPI speed, baud
|
||||
uint16_t iInterface[InterfacesAmount][MAX_IINTERFACE_SZ]; // we store Interface name here in UTF!
|
||||
spiconfig_t spiconfig;
|
||||
uint8_t iIlengths[InterfacesAmount]; // length in BYTES (symbols amount x2)!
|
||||
uint16_t iInterface[InterfacesAmount][MAX_IINTERFACE_SZ]; // we store Interface name here in UTF!
|
||||
// gpio settings
|
||||
pinconfig_t pinconfig[2][16]; // GPIOA, GPIOB
|
||||
usartconf_t usartconfig;
|
||||
@@ -62,3 +72,4 @@ void flashstorage_init();
|
||||
int store_userconf();
|
||||
void dump_userconf();
|
||||
int erase_storage();
|
||||
uint32_t storage_capacity();
|
||||
|
||||
@@ -398,6 +398,10 @@ int chkpinconf(){
|
||||
if(active_spi != -1){
|
||||
if(spiprops.issck && (spiprops.ismiso || spiprops.ismosi)){
|
||||
haveSPI = 1;
|
||||
if(!spiprops.ismosi) the_conf.spiconfig.rxonly = 1;
|
||||
else the_conf.spiconfig.rxonly = 0;
|
||||
if(!spiprops.ismiso) the_conf.spiconfig.txonly = 1;
|
||||
else the_conf.spiconfig.txonly = 0;
|
||||
}else{
|
||||
DBG("SPI needs SCK and MOSI or MISO\n");
|
||||
ret = FALSE;
|
||||
@@ -551,7 +555,7 @@ int gpio_reinit(){
|
||||
}else usart_stop();
|
||||
if(haveI2C) i2c_setup((i2c_speed_t) the_conf.I2Cspeed);
|
||||
else i2c_stop();
|
||||
if(haveSPI) spi_setup(the_conf.SPIspeed);
|
||||
if(haveSPI) spi_setup();
|
||||
else spi_stop();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -69,8 +69,7 @@ static uint8_t hex_input_mode = 0; // ==0 for text input, 1 for HEX + text in qu
|
||||
COMMAND(saveconf, "save current user configuration into flash") \
|
||||
COMMAND(sendcan, "send all after '=' to CAN USB interface") \
|
||||
COMMAND(setiface, "set/get name of interface x (0 - CAN, 1 - GPIO)") \
|
||||
COMMAND(SPI, "transfer SPI data: SPI=data (hex)") \
|
||||
COMMAND(storeconf, "save config to flash") \
|
||||
COMMAND(SPI, "transfer SPI data: SPI=data (hex); if RXONLY: SPI = size (size to read, bytes)") \
|
||||
COMMAND(time, "show current time (ms)") \
|
||||
COMMAND(USART, "Read USART data or send (USART=hex)") \
|
||||
COMMAND(vdd, "get approx Vdd value (V*100)")
|
||||
@@ -114,7 +113,10 @@ enum MiscValues{
|
||||
MISC_THRESHOLD,
|
||||
MISC_SPEED,
|
||||
MISC_TEXT,
|
||||
MISC_HEX
|
||||
MISC_HEX,
|
||||
MISC_LSBFIRST,
|
||||
MISC_CPOL,
|
||||
MISC_CPHA,
|
||||
};
|
||||
|
||||
// TODO: add HEX input?
|
||||
@@ -138,6 +140,9 @@ enum MiscValues{
|
||||
KW(TEXT) \
|
||||
KW(HEX) \
|
||||
KW(PWM) \
|
||||
KW(LSBFIRST) \
|
||||
KW(CPOL) \
|
||||
KW(CPHA)
|
||||
|
||||
|
||||
typedef enum{ // indexes of string keywords
|
||||
@@ -173,6 +178,9 @@ static const Keyword keywords[] = {
|
||||
KEY(SPEED, GROUP_MISC, MISC_SPEED)
|
||||
KEY(TEXT, GROUP_MISC, MISC_TEXT)
|
||||
KEY(HEX, GROUP_MISC, MISC_HEX)
|
||||
KEY(LSBFIRST, GROUP_MISC, MISC_LSBFIRST)
|
||||
KEY(CPOL, GROUP_MISC, MISC_CPOL)
|
||||
KEY(CPHA, GROUP_MISC, MISC_CPHA)
|
||||
#undef K
|
||||
};
|
||||
#define NUM_KEYWORDS (sizeof(keywords)/sizeof(keywords[0]))
|
||||
@@ -195,10 +203,13 @@ static const char *pinhelp =
|
||||
" OTYPE: PP or OD (push-pull or open-drain)\n"
|
||||
" FUNC: USART, SPI, I2C or PWM (enable alternate function and configure peripheal)\n"
|
||||
" MISC: MONITOR - send data by USB as only state changed\n"
|
||||
" THRESHOLD (ADC only) - monitoring threshold, ADU\n"
|
||||
" SPEED - interface speed/frequency\n"
|
||||
" THRESHOLD val (ADC only) - monitoring threshold, ADU\n"
|
||||
" SPEED val - interface speed/frequency\n"
|
||||
" TEXT - USART means data as text ('\\n'-separated strings)\n"
|
||||
" HEX - USART means data as binary (output: HEX)\n"
|
||||
" CPHA - set SPI CPHA to 1\n"
|
||||
" CPOL - set SPI CPOL to 1\n"
|
||||
" LCBFIRST - SPI use lsb-first proto\n"
|
||||
"\n"
|
||||
;
|
||||
|
||||
@@ -338,6 +349,7 @@ static errcodes_t pin_setter(uint8_t port, uint8_t pin, char *setter){
|
||||
uint32_t *pending_u32 = NULL; // -//- for uint32_t
|
||||
uint32_t wU32 = UINT32_MAX; // for pending
|
||||
usartconf_t UsartConf;
|
||||
spiconfig_t spiconf = {}; // for flags CPHA/CPOL/LSBFIRST
|
||||
if(!get_curusartconf(&UsartConf)) return ERR_CANTRUN;
|
||||
char *saveptr, *token = strtok_r(setter, DELIM_, &saveptr);
|
||||
while(token){
|
||||
@@ -401,6 +413,15 @@ static errcodes_t pin_setter(uint8_t port, uint8_t pin, char *setter){
|
||||
case MISC_HEX: // clear text flag
|
||||
UsartConf.textproto = 0;
|
||||
break;
|
||||
case MISC_CPHA:
|
||||
spiconf.cpha = 1;
|
||||
break;
|
||||
case MISC_CPOL:
|
||||
spiconf.cpol = 1;
|
||||
break;
|
||||
case MISC_LSBFIRST:
|
||||
spiconf.lsbfirst = 1;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -419,11 +440,15 @@ static errcodes_t pin_setter(uint8_t port, uint8_t pin, char *setter){
|
||||
if(!chkusartconf(&UsartConf)) return ERR_BADVAL;
|
||||
}else if(func_set == FUNC_I2C){ // check speed
|
||||
if(wU32 != UINT32_MAX){
|
||||
i2c_speed_t s = (wU32 > I2C_SPEED_1M) ? I2C_SPEED_10K : static_cast <i2c_speed_t> (wU32);
|
||||
if(wU32 >= I2C_SPEED_AMOUNT) return ERR_BADVAL;
|
||||
i2c_speed_t s = static_cast <i2c_speed_t> (wU32);
|
||||
the_conf.I2Cspeed = static_cast <uint8_t> (s);
|
||||
}
|
||||
}else if(func_set == FUNC_SPI){
|
||||
if(wU32 != UINT32_MAX) the_conf.SPIspeed = wU32;
|
||||
if(wU32 != UINT32_MAX) the_conf.spiconfig.speed = wU32;
|
||||
the_conf.spiconfig.cpha = spiconf.cpha;
|
||||
the_conf.spiconfig.cpol = spiconf.cpol;
|
||||
the_conf.spiconfig.lsbfirst = spiconf.lsbfirst;
|
||||
}
|
||||
if(func_set != 0xFF) mode_set = MODE_AF;
|
||||
if(mode_set == 0xFF) return ERR_BADVAL; // user forgot to set mode
|
||||
@@ -480,11 +505,6 @@ static errcodes_t cmd_reinit(const char _U_ *cmd, char _U_ *args){
|
||||
return ERR_AMOUNT;
|
||||
}
|
||||
|
||||
static errcodes_t cmd_storeconf(const char _U_ *cmd, char _U_ *args){
|
||||
if(!store_userconf()) return ERR_CANTRUN;
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
// canspeed = baudrate (kBaud)
|
||||
static errcodes_t cmd_canspeed(const char *cmd, char *args){
|
||||
int32_t S;
|
||||
@@ -575,6 +595,7 @@ static errcodes_t cmd_curpinconf(const char _U_ *cmd, char _U_ *args){
|
||||
|
||||
static errcodes_t cmd_dumpconf(const char _U_ *cmd, char _U_ *args){
|
||||
SEND("userconf_sz="); SEND(u2str(the_conf.userconf_sz));
|
||||
SEND("\nstorage_capacity="); SEND(u2str(storage_capacity()));
|
||||
SEND("\ncurrentconfidx="); SENDn(i2str(currentconfidx));
|
||||
for(int i = 0; i < InterfacesAmount; ++i){
|
||||
SEND("interface"); PUTCHAR('0' + i);
|
||||
@@ -602,7 +623,7 @@ static errcodes_t cmd_dumpconf(const char _U_ *cmd, char _U_ *args){
|
||||
NL();
|
||||
}
|
||||
if(I2C1->CR1 & I2C_CR1_PE){ // I2C active, show its speed
|
||||
SEND("iicspeed=");
|
||||
S(I2C); SEND(EQ); S(SPEED); PUTCHAR(' ');
|
||||
switch(the_conf.I2Cspeed){
|
||||
case 0: SEND("10kHz"); break;
|
||||
case 1: SEND("100kHz"); break;
|
||||
@@ -613,8 +634,14 @@ static errcodes_t cmd_dumpconf(const char _U_ *cmd, char _U_ *args){
|
||||
NL();
|
||||
}
|
||||
if(SPI1->CR1 & SPI_CR1_SPE){
|
||||
SEND("spispeed=");
|
||||
SENDn(u2str(the_conf.SPIspeed));
|
||||
S(SPI); SEND(EQ);
|
||||
S(SPEED); PUTCHAR(' '); SEND(u2str(the_conf.spiconfig.speed));
|
||||
if(the_conf.spiconfig.cpol) SP(CPOL);
|
||||
if(the_conf.spiconfig.cpha) SP(CPHA);
|
||||
if(the_conf.spiconfig.lsbfirst) SP(LSBFIRST);
|
||||
if(the_conf.spiconfig.rxonly) SEND(" RXONLY");
|
||||
else if(the_conf.spiconfig.txonly) SEND(" TXONLY");
|
||||
NL();
|
||||
}
|
||||
#undef S
|
||||
#undef SP
|
||||
@@ -817,7 +844,7 @@ static errcodes_t cmd_iicread(const char *cmd, char *args){
|
||||
addr <<= 1;
|
||||
if(!i2c_read(addr, curbuf, nbytes)) return ERR_CANTRUN;
|
||||
CMDEQ();
|
||||
if(nbytes < 9) NL();
|
||||
if(nbytes > 8) NL();
|
||||
hexdump(sendfun, curbuf, nbytes);
|
||||
return ERR_AMOUNT;
|
||||
}
|
||||
@@ -836,7 +863,7 @@ static errcodes_t cmd_iicreadreg(const char *cmd, char *args){
|
||||
addr <<= 1;
|
||||
if(!i2c_read_reg(addr, nreg, curbuf, nbytes)) return ERR_CANTRUN;
|
||||
CMDEQ();
|
||||
if(nbytes < 9) NL();
|
||||
if(nbytes > 8) NL();
|
||||
hexdump(sendfun, curbuf, nbytes);
|
||||
return ERR_AMOUNT;
|
||||
}
|
||||
@@ -891,16 +918,21 @@ static errcodes_t cmd_pinout(const char _U_ *cmd, char *args){
|
||||
SEND((port == 0) ? "PA" : "PB");
|
||||
SEND(u2str(pin));
|
||||
SEND(": ");
|
||||
if(listmask == 0xff) SEND("GPIO"); // don't send "GPIO" for specific choice
|
||||
if(mask & (1 << FUNC_AIN)){ SEND(COMMA); SEND(str_keywords[STR_AIN]); }
|
||||
int needcomma = FALSE;
|
||||
#define COMMA() do{if(needcomma) SEND(COMMA); needcomma = TRUE;}while(0)
|
||||
if(listmask == 0xff){ // don't send "GPIO" for specific choice
|
||||
SEND("GPIO");
|
||||
needcomma = TRUE;
|
||||
}
|
||||
if(mask & (1 << FUNC_AIN)){ COMMA(); SEND(str_keywords[STR_AIN]); }
|
||||
if(mask & (1 << FUNC_USART)){ // USARTn_aX (n - 1/2, a - R/T)
|
||||
int idx = get_usart_index(port, pin, &up);
|
||||
SEND(COMMA); SEND(str_keywords[STR_USART]); PUTCHAR('1' + idx);
|
||||
COMMA(); SEND(str_keywords[STR_USART]); PUTCHAR('1' + idx);
|
||||
PUTCHAR('_'); PUTCHAR(up.isrx ? 'R' : 'T'); PUTCHAR('X');
|
||||
}
|
||||
if(mask & (1 << FUNC_SPI)){
|
||||
int idx = get_spi_index(port, pin, &sp);
|
||||
SEND(COMMA); SEND(str_keywords[STR_SPI]); PUTCHAR('1' + idx);
|
||||
COMMA(); SEND(str_keywords[STR_SPI]); PUTCHAR('1' + idx);
|
||||
PUTCHAR('_');
|
||||
if(sp.ismiso) SEND("MISO");
|
||||
else if(sp.ismosi) SEND("MOSI");
|
||||
@@ -908,13 +940,13 @@ static errcodes_t cmd_pinout(const char _U_ *cmd, char *args){
|
||||
}
|
||||
if(mask & (1 << FUNC_I2C)){
|
||||
int idx = get_i2c_index(port, pin, &ip);
|
||||
SEND(COMMA); SEND(str_keywords[STR_I2C]); PUTCHAR('1' + idx);
|
||||
COMMA(); SEND(str_keywords[STR_I2C]); PUTCHAR('1' + idx);
|
||||
PUTCHAR('_');
|
||||
SEND(ip.isscl ? "SCL" : "SDA");
|
||||
}
|
||||
if(mask & (1 << FUNC_PWM)){
|
||||
canPWM(port, pin, &tp);
|
||||
SEND(COMMA); SEND(str_keywords[STR_PWM]);
|
||||
COMMA(); SEND(str_keywords[STR_PWM]);
|
||||
SEND(u2str(tp.timidx)); // timidx == TIMNO!
|
||||
PUTCHAR('_');
|
||||
PUTCHAR('1' + tp.chidx);
|
||||
@@ -923,6 +955,7 @@ static errcodes_t cmd_pinout(const char _U_ *cmd, char *args){
|
||||
}
|
||||
}
|
||||
return ERR_AMOUNT;
|
||||
#undef COMMA
|
||||
}
|
||||
|
||||
static errcodes_t cmd_SPI(const char *cmd, char *args){
|
||||
@@ -930,14 +963,27 @@ static errcodes_t cmd_SPI(const char *cmd, char *args){
|
||||
if(!(SPI1->CR1 & SPI_CR1_SPE)) return ERR_CANTRUN;
|
||||
char *setter = splitargs(args, NULL);
|
||||
if(!setter) return ERR_BADVAL;
|
||||
int len = parse_hex_data(setter, curbuf, MAXSTRLEN);
|
||||
int len;
|
||||
uint8_t *txbuf = curbuf, *rxbuf = curbuf;
|
||||
if(the_conf.spiconfig.rxonly){
|
||||
uint32_t L;
|
||||
char *nxt = getnum(setter, &L);
|
||||
if(nxt == setter || L > MAXSTRLEN) return ERR_BADVAL;
|
||||
len = static_cast <int> (L);
|
||||
txbuf = NULL;
|
||||
}else len = parse_hex_data(setter, curbuf, MAXSTRLEN);
|
||||
if(len <= 0) return ERR_BADVAL;
|
||||
int got = spi_transfer(curbuf, curbuf, len);
|
||||
if(the_conf.spiconfig.txonly) rxbuf = NULL;
|
||||
int got = spi_transfer(txbuf, rxbuf, len);
|
||||
if(-1 == got) return ERR_CANTRUN;
|
||||
if(0 == got) return ERR_BUSY;
|
||||
CMDEQ();
|
||||
hexdump(sendfun, curbuf, got);
|
||||
return ERR_AMOUNT;
|
||||
if(!the_conf.spiconfig.txonly){
|
||||
CMDEQ();
|
||||
if(got > 8) NL();
|
||||
hexdump(sendfun, curbuf, got);
|
||||
return ERR_AMOUNT;
|
||||
}
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
constexpr uint32_t hash(const char* str, uint32_t h = 0){
|
||||
|
||||
@@ -126,6 +126,8 @@ int startPWM(uint8_t port, uint8_t pin){
|
||||
volatile TIM_TypeDef *timer = timers[idx];
|
||||
uint8_t chidx = timer_map[port][pin].chidx;
|
||||
uint32_t chen = TIM_CCER_CC1E << (chidx<<2);
|
||||
volatile uint32_t *CCR = &timers[idx]->CCR1 + timer_map[port][pin].chidx;
|
||||
*CCR = 0; // set initial value to zero
|
||||
if(0 == (timer->CCER & chen)){
|
||||
if(0 == channel_counter[idx]++) timer->CR1 |= TIM_CR1_CEN; // start timer if need
|
||||
timer->CCER |= chen; // enable channel
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <stm32f0.h>
|
||||
|
||||
#include "flash.h"
|
||||
#include "hardware.h"
|
||||
#include "spi.h"
|
||||
|
||||
@@ -38,14 +39,20 @@ static uint16_t get_baudrate_prescaler(uint32_t speed_hz){
|
||||
}
|
||||
|
||||
|
||||
// Master, 8bit, CPOL=0, CPHA=0, MSB first
|
||||
void spi_setup(uint32_t speed){ // speed in Hz
|
||||
void spi_setup(){
|
||||
RCC->APB2RSTR |= RCC_APB2RSTR_SPI1RST;
|
||||
RCC->APB2RSTR = 0;
|
||||
SPI1->CR1 = 0;
|
||||
uint16_t br = get_baudrate_prescaler(speed);
|
||||
SPI1->CR1 = SPI_CR1_MSTR | (br << 3) | SPI_CR1_SSM | SPI_CR1_SSI;
|
||||
SPI1->CR2 = SPI_CR2_SSOE;
|
||||
uint16_t br = get_baudrate_prescaler(the_conf.spiconfig.speed);
|
||||
uint32_t cr1 = SPI_CR1_MSTR | (br << 3) | SPI_CR1_SSM | SPI_CR1_SSI;
|
||||
if(the_conf.spiconfig.cpol) cr1 |= SPI_CR1_CPOL;
|
||||
if(the_conf.spiconfig.cpha) cr1 |= SPI_CR1_CPHA;
|
||||
if(the_conf.spiconfig.lsbfirst) cr1 |= SPI_CR1_LSBFIRST;
|
||||
// there would be a lot of problem to set rxonly!
|
||||
//if(the_conf.spiconfig.rxonly) cr1 |= SPI_CR1_RXONLY;
|
||||
SPI1->CR1 = cr1;
|
||||
// rxne after 8bits, ds 8bit
|
||||
SPI1->CR2 = /*SPI_CR2_SSOE | */ SPI_CR2_FRXTH| SPI_CR2_DS_2|SPI_CR2_DS_1|SPI_CR2_DS_0;
|
||||
SPI1->CR1 |= SPI_CR1_SPE;
|
||||
}
|
||||
|
||||
@@ -63,12 +70,12 @@ int spi_transfer(const uint8_t *tx, uint8_t *rx, int len){
|
||||
if (--timeout == 0) return -1; // error by timeout: TX isn't ready
|
||||
}
|
||||
uint8_t out = (tx) ? tx[i] : 0;
|
||||
*(uint8_t*)&SPI1->DR = out; // ÚÁÐÉÓØ × DR
|
||||
*((volatile uint8_t*)&SPI1->DR) = out;
|
||||
timeout = 1000000;
|
||||
while(!(SPI1->SR & SPI_SR_RXNE)){
|
||||
if(--timeout == 0) return 0;
|
||||
}
|
||||
uint8_t in = *(uint8_t*)&SPI1->DR; // ÞÔÅÎÉÅ ÉÚ DR
|
||||
uint8_t in = *((volatile uint8_t*)&SPI1->DR);
|
||||
if(rx) rx[i] = in;
|
||||
}
|
||||
//while(SPI1->SR & SPI_SR_BSY){ }
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
void spi_setup(uint32_t speed); // speed in Hz
|
||||
void spi_setup();
|
||||
void spi_stop();
|
||||
int spi_transfer(const uint8_t *tx, uint8_t *rx, int len);
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE QtCreatorProject>
|
||||
<!-- Written by QtCreator 18.0.2, 2026-03-17T23:56:25. -->
|
||||
<!-- Written by QtCreator 18.0.2, 2026-03-18T23:46:41. -->
|
||||
<qtcreator>
|
||||
<data>
|
||||
<variable>EnvironmentId</variable>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
Readme.md
|
||||
adc.c
|
||||
adc.h
|
||||
can.c
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#define BUILD_NUMBER "221"
|
||||
#define BUILD_DATE "2026-03-17"
|
||||
#define BUILD_NUMBER "226"
|
||||
#define BUILD_DATE "2026-03-18"
|
||||
|
||||
Reference in New Issue
Block a user