From e2a20a046a681fd45c1dffdb462e2c8b801be8c2 Mon Sep 17 00:00:00 2001 From: Edward Emelianov Date: Wed, 18 Mar 2026 23:49:29 +0300 Subject: [PATCH] SPI works, may say it's over --- F0:F030,F042,F072/usbcan_gpio/Readme.md | 402 +++++++++++++++++- F0:F030,F042,F072/usbcan_gpio/flash.c | 10 +- F0:F030,F042,F072/usbcan_gpio/flash.h | 15 +- F0:F030,F042,F072/usbcan_gpio/gpio.c | 6 +- F0:F030,F042,F072/usbcan_gpio/gpioproto.cpp | 102 +++-- F0:F030,F042,F072/usbcan_gpio/pwm.c | 2 + F0:F030,F042,F072/usbcan_gpio/spi.c | 21 +- F0:F030,F042,F072/usbcan_gpio/spi.h | 2 +- F0:F030,F042,F072/usbcan_gpio/usbcangpio.bin | Bin 29884 -> 30456 bytes .../usbcan_gpio/usbcangpio.creator.user | 2 +- .../usbcan_gpio/usbcangpio.files | 1 + F0:F030,F042,F072/usbcan_gpio/version.inc | 4 +- 12 files changed, 515 insertions(+), 52 deletions(-) diff --git a/F0:F030,F042,F072/usbcan_gpio/Readme.md b/F0:F030,F042,F072/usbcan_gpio/Readme.md index 5a88ded..ca166d9 100644 --- a/F0:F030,F042,F072/usbcan_gpio/Readme.md +++ b/F0:F030,F042,F072/usbcan_gpio/Readme.md @@ -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: + +``` + # ... +``` + +- **``** – system time in milliseconds since start. +- **``** – 11‑bit CAN identifier in hexadecimal (e.g. `0x123`). +- **``** – 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 ` | 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 ` | 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" +``` diff --git a/F0:F030,F042,F072/usbcan_gpio/flash.c b/F0:F030,F042,F072/usbcan_gpio/flash.c index f1e5b73..ee1d11f 100644 --- a/F0:F030,F042,F072/usbcan_gpio/flash.c +++ b/F0:F030,F042,F072/usbcan_gpio/flash.c @@ -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; +} diff --git a/F0:F030,F042,F072/usbcan_gpio/flash.h b/F0:F030,F042,F072/usbcan_gpio/flash.h index a7aa4c8..8aab2e8 100644 --- a/F0:F030,F042,F072/usbcan_gpio/flash.h +++ b/F0:F030,F042,F072/usbcan_gpio/flash.h @@ -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(); diff --git a/F0:F030,F042,F072/usbcan_gpio/gpio.c b/F0:F030,F042,F072/usbcan_gpio/gpio.c index 9866c07..7d32769 100644 --- a/F0:F030,F042,F072/usbcan_gpio/gpio.c +++ b/F0:F030,F042,F072/usbcan_gpio/gpio.c @@ -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; } diff --git a/F0:F030,F042,F072/usbcan_gpio/gpioproto.cpp b/F0:F030,F042,F072/usbcan_gpio/gpioproto.cpp index 2bb344f..f902421 100644 --- a/F0:F030,F042,F072/usbcan_gpio/gpioproto.cpp +++ b/F0:F030,F042,F072/usbcan_gpio/gpioproto.cpp @@ -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 (wU32); + if(wU32 >= I2C_SPEED_AMOUNT) return ERR_BADVAL; + i2c_speed_t s = static_cast (wU32); the_conf.I2Cspeed = static_cast (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 (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){ diff --git a/F0:F030,F042,F072/usbcan_gpio/pwm.c b/F0:F030,F042,F072/usbcan_gpio/pwm.c index 2b0d208..79c9aa4 100644 --- a/F0:F030,F042,F072/usbcan_gpio/pwm.c +++ b/F0:F030,F042,F072/usbcan_gpio/pwm.c @@ -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 diff --git a/F0:F030,F042,F072/usbcan_gpio/spi.c b/F0:F030,F042,F072/usbcan_gpio/spi.c index 0e09c83..f6a390f 100644 --- a/F0:F030,F042,F072/usbcan_gpio/spi.c +++ b/F0:F030,F042,F072/usbcan_gpio/spi.c @@ -18,6 +18,7 @@ #include +#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){ } diff --git a/F0:F030,F042,F072/usbcan_gpio/spi.h b/F0:F030,F042,F072/usbcan_gpio/spi.h index 50bbb93..81e9dad 100644 --- a/F0:F030,F042,F072/usbcan_gpio/spi.h +++ b/F0:F030,F042,F072/usbcan_gpio/spi.h @@ -20,6 +20,6 @@ #include -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); diff --git a/F0:F030,F042,F072/usbcan_gpio/usbcangpio.bin b/F0:F030,F042,F072/usbcan_gpio/usbcangpio.bin index 21b1057526b1c838249cc5c1d048e8b149be843b..172a78e06cb0469278bb01fd5edc7d7041945db6 100755 GIT binary patch delta 15272 zcmb7r3tUr2*7)2!AOsWytt22Mxq!$+LD2f5h6HbuTvP;G5wHz{8hli$Xz{g)RVzN) z3fFFt(AKTpt&eW&Vr^|}TU*<2wYA&CM;Arg1R-g6`$%7V^S=KxH?fcJ|NZ~J-^cHF zGBamp&Yd}P&Y3f3hO=K0-fxH`q(+MSW+KUbLL^xL&^aotGyieH_lccv&LA#y{(MF7 zT!8tA_<&(d2qW%(@~cTjL79@BhYqj=I2%lA7Z%Y2yQ!{2^)PC|nX4 zQcOfiim~-x+~%V6pTvbze~Ql}fZ%%xyGQC8k)$0uYy~hrz}!FL{uqpcY~P57B#|tV zFp|Wbf}iS{Clc$%9HQhU-BjGnUaHBYFwLbjBZw{o6H1&d)KZPi?4&VcwA6z1i+#R@ zHOQJOH&{^@RY%n@ElCq3S_-NB3!5mWCn;kLp6|(JNS)9OTnBcgHXzAQ%qK}}2^~{5 zN+H`{rW1KC1qnoD%z#{3aE4)qKE-6($!r|;8KFBhH+cn-dsd1RMW=V19D#^qBE+#Z z{+*gpHp0r~kL*W<$d+#`Q;Iy-{LIaibmA+;4q zf`7a5xGylz+;t+3?*xJ{nprS*0yNz+b{nwRH?|D&cc)H@qkJg$+X0%AGiy^@pyqli zDH3_=yz>HsbQyb5p>zFr9)91IZ6-*m@HH}89{+wP) zK3CwV&3IHZ@@6U8A+JxerZ#9N$O`KF8D)Bk{;d_>w=HCvxR)BK>sOkz@pIK_#tNb5 zlW^?(_7zv$Z>>Q2-wgDtVs%2#_VAbPODva}F%Ni)X8=<~?MQ+|2zi)o>ExJ|VM3AJ z_=CIkbR4}C#6~jLg0h&?>6yUCYw5G6WlcUnmB_59yCRS(Uv+)u9%`?qV4zw)$%e5# z)SDGGRQ)O|h4ORk)f7>prfQnPY^9&69GADat_kHzZWX2yWqGZyyR9qh_Ocd?Xh;FH=Fst@+xzFEHfaGoA2!&enjJt-iuDqOj$&Ei2NUdvf z0;Z0tV?G_17GKx&DOObweBG&^$LR?{s-ID3W(mZ8W_f0Tpdi2;%xoTW4MZi@US=qx zky36H6wN?k_G4y*Vw|8i$P_4wK%$l?9x-FH!>^+esWHhX-{m;7ndg_{y)j5~{5Wu~ z2&re!hNK9%cX}T6IkkwILpe^rQ~B<^Bb%9%C_mQrw%(#DLi37+o{S(OkE}~N8fFs% zC>PnAop-55o=DrmXh$ATS-+AhP$IHAHKlbWmGa2nm>T6;;wk35a{QvJ4nh7$_8Kbj z_*Z@XeS)qqI}k#-;|9dmx^!euaCU2uJnLv@Pdx2n@V06!X5eS4BD2QUMj~Z} z!M+f85L=0oJliUYPzn^HtTeNQ9db7n<9>~Xh|r2eCY@4(Xk%9D2B3A&+o$G|)VHsFb^z^$PK*0gpx@aGcq3^{(z zCoV*eV*W@wBq9jJ>>GQ93|BGNCa>lRO;$5J}J!ks{-5hHOeeEU`bD_=MzEo1x zo}!lIFR~@&KVZXjIJVh)FW)-XcLCbXfp(;?sRQM|7p-6DE$8b~eUbK+USmg?_4y*B z8^Qc3XIxU)`z%P`40{RRlJzqG$jLIN!_abpoWpzPv&h!&ju08?Rq;(Pd)GxVVs9po zx#(?i6C}apy3_gcd)`L3oX?&0Hn?Sc?se}XH)dIw{TC+&i{Sth{58OxWyd0LrVNZd z?46P%05w3xdL)rRnhKB(kPA=*Fd6`Cy+j0108k2Waw+rj_^HGKW_bLZLY-Fu%TXAe zaJg5(kD<(41XxuRJJl=Y$Fq`Il)Jz@Hh9Y|QFObjfD{;JT_vRarUmI8@x0@TcuvpZ z&CWb9J9FCOQ2{ESHP}Zen3F_a%=O=E5ur*&)f(wBDVUJiA?b55C~*?HzRS^+(OZ7I zuZQ}C&LX<0+h$pEi4|!pteeD&f!G^~s0-!au%U^_E_I5)mVKxfdUCwjf_WY1=|hxt zGs;J{?;N*f=L}ZLimucap@d`cOo&~5ljUY&bdpReAh8Evnp7Nb*ieX3bqlGff3+n5NGvA^VarE(97TRaeBO)M?8qK{MM z#)GiJNk{87Bj6K9!MGv&Ax9DVi+<#Bf#=VhI3vx5K*1zTEEB_UB|=8Th#3KcNR)Gg zA{ac89gbX-cr3(@MJqkt)ge{ml(EoJHYYo8 z|FS#-u`gZ|6e4?>QwYC>PE;6Ylb%T1WQ`GDO;zC>1AS8-mY$GrB1lXp<>C0AT6#$K z0!X-g!S^n);yV|fP_`|?j|1Ud8f3O_braUa9%y2$=6Mhd!(0%-Yz2Gpu|L5*Pfrwu z*)!pY(0(0~C@#6GC|@7P=7uGT|8k+6dn@nNOC7}2F~IEm+;a@1=^#i(Q-y4$q%;=^ zJS_0)JGMgHi`1*^xvb0#jSmg5?ak1uy&^VkFxcfP>zs6iei2cZ-&KamK!q^ zoAyYpi22TqnT>2@6eDI_ZUK*xM==8C7M~+#4)n$5kLu3`pZN-W2w721Ec2-wr*X{3 zZVBd?88~J>p7IFFIqwFxnI$~iP)^DtZ@EXDKsf~SI^drj_bT5lhB?Wn66Pf^BY4NC z{f&yoOhZVdKicuMrRn%r}xi*2WPPI1_mtTZEYmv*E8moF% zw!S@9FIS%#dF5!G)%UM7I|u*XpRT2-_Ab zD!sc~9Y{svGmR-Lg&M29mF}e)E8@)$rYAmv@`ZLim12t5ra&!GnyO7&npCjgF0!s* z$|sFA3!Jf_MX@%c&Y~(t^NIwX4}vk)SVjV7;WRjVF*XTkV^3^-Y>F0L6FV2_-Z#~x zkF{0Nov1#wcNIY@O*L~5X_ZEc(vlTNOjxi$zc;l_xu79aB~>%(`~?JAuQ_jQBh`j0 z4Pb8V70%DdIfly(f;@Enzs!yblF}zBK3a&Bb7+}yjy`^sP#fR0Rnbn)*6&IExFN0~ zO&k{+ubr*$XxNh~)JitRZAPLv0kqOi)5kXwK<1QVx?wsT0pryv#u9p(;YK5&k{dUW zWb~6_r$sf9t|qRVh%Bl0h7qZ57$?zL^W;WJY<8<@BHgTEj790WC7ZuNH$Yy#alEJM zLSwW&vBWh(HAY(xsf8S;J)=@9(}51NwjF4uoo80>aqfx(J}FgTF| zE6fFGj~2qQHc6m1J_zUMFN+lBJ#>6EHf2e61&S>|w;phX*IEohny?g!qnOpI`x81R>lHAfgi;5vZpEM1&bL48n0E#s8!zv^DamPwn z=f$ayuOgJWCQD9-HrHrTET21-m{_%Z-ruM;=Dp$xqv~}6&*{LI#;;6^A$Kw`mkAV) zn>6j3mP(5?T{{d1yj8}z1~pmiP%A4T>;T@jG0#2QkYfCoOQKlm))~fVM=|A-vlq1~ z7dK?WCflRNQlqL07!euEJmEr$5If}CmW(yywZ(?<&YGgtHw7X! zta1^dju9eMfdp#aph9YawLcgSE;$&CMZC#yoEEn4IVnffjLUr$=@avW{a5r|Xm9P) zl%^f!#o8;t))(Xp`#-&7>lL5q!|Yst+?`W}nLk!soVVVDE!(_7?4o_^7iWtyzOgyR zo5iP0Gc_?b1zNgCxjDp^hXlD!J5h!Tf3V+ju#sT+!tay!$|Q2u;n~Y z{AiTSS>Ils_!(adkO{Flejz{9bv!XND10^zc7V5>Ay(!WwqsLt)maR`UMEs@QbnK< zN1Q}ah?V*kDq%aLoeOn8IQ8&r9o)cE-shX;He=F0UxW*Dc+yWhMZFKv59!mFcDm<~ z%jSODfIT(QZIp!+D`A+ezLxpADY#!vbf_zQ(|LY0`Eol#><#dx@%$TnL}`Q*PkIGY zUeyc_h~2qxeylejg?*t#RYxZadd!UnNQ-JfT#Xj#dm6*6#`mLZH%WoGT&=~{;cHhl zO=R~_)ilaEL@l7ZDY2tjLQGwQaP2ME?O68O0t$aF+gAibl17J=0jDjDmHT#)CA{#B zg9ZM^i4^59g+yODUmNc;ZzuDAslE->Goc#!a%dLyQU}Nt6%gI1?d}_hA?#tc$5%=t zg$!9CBJ!>G0IZ_77A2hL>-PH=^CSnooq+$fk0px#jK}}lzAYjpW>(;R+D)k9=}cG? z`Gzq2zIQ!X%uju-t~tQ07&vRM0)=e_pQHfPv~(qZ%28uejm@tGe3|_KHh{wb(@c@0 ziKGQdf&jg6oVsJauR^&4-~hm3fNlWuQTXYAMpcmBs{EW6jO|{dDa3lck#*ecz0PAd zdJhBkn<#dz_Zp8~<#hmdFp6#V+IXzdt7r+a|Ma%I5U=sI-b-|4SDn#c zMiNuJDNW93VlKZq_pQnTW2EH2f>dYdH}oRr4O(t|gO)YEp}%hISt@V5VSJq?Cd-ZQ z{y6{1cYj>_q*-JY^(31f{ZZ0UZ|c7H_2IwqpCI59^+DX|W%^~q4ZeBU{a2_9u|IMz z44>e0-*V3lALnylaXrIN(MR>^EB(VsjR%MS!ec+@o*e!&pS#ZO9X`V6u5!DEpXYO* za+cx4WnrQU-ywQ{Yd%0QvKc{a)PNJ#@FwQakyMu+Qr zpnd^;K-EkA2d$%*S9GCMRieqKs)U8DHc?@wO;9-A_M|FVB(f5UdN@`cP|bn;;Z&7+ z#=hDe+I>~XtXQTGu{4(p^Dm0dzk+MzXQ1Z3ABwdSOr1CeesT9s@MBI@iF>NWpSicI zN`O>Htq{n(?cSxlGn|**HGFR+mjuGM0H~*PxpXEB@c_xf83R=4(bq)`5N-A z08gSP(K*0f1?a`?0WJVs0x$t?G|@R(CGklV%kcIR^b5`ZHNr~)kLHql)rimZrX8LZgT6STDh^#^vOwqy3Qy!4e11G zIi$Dww9YgLX`-Uecw^YaCZy^y)~(vBt!uhzNGcu${Rf!D(wQkA($eBO zlTA3Q5u1*$FvtHTU6b2atwSA@&}m(Q&P-N(gzH8dWmhFCPLe3 z9A%Pdeul`$9=a>)(8nro7R6F&_B8e%SBhX5XYxJ)w3@zu7 zN^58oDsPwyVQx2!TlL!voyKi?yK%L00j8==f?uU}lu@MY;l~h8VmrCfu%~oW`>8fV z8@++~aZ0v%H5~)tD5Oj;vQmWS-cZ8PAX^+nxnf%k9K)9z(qab^nMc%Qv%#WF*=|ua z!KCV{EGjLXEbea+Xm2#iVM*jo7S-Zan9UYtUDF;xy+Pn0W}rNQt#*|}(o)^LVDbEz-ux7dP}khCIn|511)bxD)}-!eU2h#qWpl} zqMT#6h(`hwpDr-SjTi)~ zI%B?}$EYwYV)S=EI0oO$;j2hgK&U}$5SD!%^Y-1DX6!X%wZ)ecmo6c6-+c%TiV%vh z<+=$?@Y&DPAHcam%)C#(2ejX(2TayjWZN+KE}hnbbM1p?X>pI-_!ez#sfY5G!8d6z z58&)FgZ0!ILz=1UA*&pAu$8Eb(V)v6fb9b;v-}k^2&03n7t1w5n`RHOImq4)pPSr8zy95xgV*zK&h(->ZnCa=BOYrr{B}iklx&F2rr*%ojIylB(PVGW<6_=klK%oi9zGv_$9a0^lbVVTGlLbY4HRjkG!Vc{m zuN@q<8zBdYuDBJd$MrO|e<7Mk*x-Zr<*F7_dmB)m&}LEYHpInrHr7$eCKym2VJ9Yc zQCSdJCmLj#?RI#NQ5mR31dsiZungko1kemr|)SqmEp3$1` zZNmIPxV=I75IK6>k&9vrzr2ZeSk!X@U9KzX0KdO~uj9Pj3Iqo(Ybjz1scl9@mTu|} z!GGGZogHgKSrnYI63fI6qI3-!>icV7QbD|JiiWi*5m7n~VO*GP_GA2y_BUZ#0+_1M zs(`5?tJ++G7?@vviwy!!)Gl{Z#dD~h`dn6mN+J9*-q20eQxNwwrkaB6$pCisLhJ|r zT;AB9^Lw5MvUf+ZZ}=kvs;3G{zL3pC5LJXzTRKuGai2lo;Uz&<86c)cXgu%#vF|yJ zr{7#bMckOnJ~-yO6=b5}Dy%%RXSnOAxRQS^l)F(O1ZO?LAOU;p{q8u$Dt8+2Wxe~( zl^)&@?>M@6=fW`vHR#v5HAr%UzG-SoFSA`Y906ghhNzNtRhmhzDYc!;xklHU{$rM3 zqtjM*Q$NemwR2No`?j|_(ak1j8Tn~6jxyb8At#}F^o$1G%0v$q3UAg^1DjBOifvBC zxkBO=@>~smNY8)>ybOJyBRp+z)~uthJp3-5(L4?OmE(Rw@eXY3DzKJIZCLDjf^X9D zb~QvQ%A96IzOEaa9dnE*B1-hKj}{8}iV|mH{*U(Y*@%oerkBOH#+=CfL&9GXIXwf} zXE?tgh$-{c6WRx4XoA20_h7z)6 zI1`Q$uqaO7GiJEn2(h<(y>z@n-T@Mk+CZesg6x}NkY3;=;hFCbvZvv^gxg(&;|NYe z-iOKE5YED(xiiD7K>_zSSPJkEv_a9Kru2Pq<+u#R9RH>-Wn(TPR0-fuZs%`N|1^wX zoslvZ1z8bb%*ft7{0DvzRyZFA+2rV_4n{w9!TSi^4py%{V)g8MhLIx3UJtfKY@hwH zVFJqb;6uCxd?;c8?K_6o@FM!KFT|StE&7%8UK4_S+M?QLG|^omSqtp+jq;Y|X1w_~ z2H8EKWf7BUTQ$6tPTPiE81ryFHShm2o^62D^TxC07u$*KEyESGR@YD>Qr7KRT@+;J z1ckY9a2dXbzQ-`|5MqduE6Xb*cGXrpO!I>yeFSN~*Pw?&%6f1>eU-xs)g(Q(e8~3L z@b2LU3(?KPw?$TYOD~1G7G)%;mR2O8PAbSI_-D)C)+FE5QuVTP`NU1+*%H)J=f-n# zGAfEXXmu)l23+`+w=F7BcZNQps2cdYo{P;uw_bI<=BlS|J$%aSLS(8gH3!a+fBmcM ztA#ZgnGmYJ=+d+kML6%j{u|M=ec-~^b`S{hBKJ5DCDso2_E3J;n_ii&&qf!fOm|=v z#G#^EBv9kCCk_SSw;Np->+YTOm?WPegJp^}v5KEi|xJHwt6c1vX*Uk@SlUHfNS)47yrkb|Phvy$}+CV(qy7A)U544dR~2^As0} zmTbBmVBh1;L2wzi)0^vVpM@51%!d{;xie8Bqnu~vpMkwW06PMab4vdj@AP2_5;^yp zMuXGW1g08mM|HS<34|S6U#3Oi7Gj(kc@*4U(_LV%A#&J0(1#|${(T=Y<)~)B{-@~2 zj_9?nh{h&(3B{klm`J>mLg6i}j6rq=cND@yq<5Oj%4_#TH-Qr1*@D0un~Sa&x}K&j z5T!6yl()bZY|C(Exfp1Nhch9x#eBUJ3M=fLFqZ*v*k5vBew5?ed3$g$!UOMMfK_t> zkmnx<|3Ig<5o9)WDs2iZPO zQb1fso^9M=X!9vIG`ultuvT+sbiK#X7A?jHQNPdeFbrsw9S-FNv z`xegETZij#3$ty#vzl*GbPa#)yn|rq2I6Rvl*b*21+7m&uC}V_8 zdMv=Ua62^;o76hZAVbpWoe=lCXHaFRu|!yz%w>UW2icL_IB-b=><{puY3A@mOv?WG zAnMmU3wW~AcyVqDig><_2w}>zfHLMyOi0=bLGA-|)*{?XVn z9O^f%EqwmIq3<6~?0RJvwgGk0X=RE3Ql{W5a;ZMOqE0}kbs z7;v;2ZT~bN>p`%K_*xwH`%T~V4a9mNE??T7u7*1^`xqxG_!91;u!7!Y9{hx#ds8?U z1y~)h6|=bkVk#HlleJ-*B4IFcr+|die%(18RZ+3W!ISX`4xHnAQenX%LQ!0N5oEhP z^dPU2ZJ`*rIj1R^TCX{zU0951t_;=kb<0B{Ov@H@kPyo@i=4ONaYv35>oPGV$Sw-u zSv&(*)<*Eq2z+sEpFf1(NbGjwogLXU!x5Q~hM(sL5EGUH`LFzkX<3Ivz5}JDcB9nP zXJ|E%)^SmeJcuhS;)(+7@i2kNp>CdrVi=3xf&emuv?n-Q$%DVB*N4G~dU5QSAaEok z0QCwuuzGQ9LU12!RzY@22%^rpgpFf^fev`|;R|%|WPcvotqZU_{aD)fxwb>i*MS|l z#=hD4D5N9pk)i)J)W#3}SjhYb2u=mq$6;*nJTI_?Cv$lS+iz!pW3L9<`42t?H;dE! z*s7@jTjH0%Q6< z*p z>iiIvl~NcoF6lx#>K^!PMTk&TL8=a6nTfUGS2#g-I$$dyof6_D$mVwNt8W*$u_VPE zHwV}!IIUiA0E-A2dd-2+lH&pPL++j^HZ@en&vSI>S3#qK5kb=g4HO)}H&4~$XH!%7 zb|?fAb+zAxDM^8n281~uhVWBxB46(b#+*pqs*Y7B2H4ByP(d5vBPTzA2G~*1K_e4DTi-=)D~x;x-=Va8)SNChLT zhuMb(<^%8Z9ajW@W$vLsgt<=z1S(+e7P&@d7pdfM!*qskyC9hDdQI;hevK~~f*Cxk zV}aFjt}nW1vx7elpXBRj1V_3;?2G)v7ww?*Xn!(q z;C2PrNqCkw2VENs8|Y-%Ijj5Nv~c5wivWxBwv(8)R{wrqEix0u1bMD6!2W@EIW`O} z2jZXkB4*;*Kr_$6o3e zw7exbMtH5>P-nzYYao4UGyQ%)WX}WS=hEdo-T5w0P?Os|3b@ zx4nrXRB0_!^bAjrV;}YriZWpI^Z+6i;}20Zl{F7X-42Q#yBFL+=G7n z3SuI>?s4{E&20f^Sho?B;k~7rBv#BhvFE!_esSeN&pV*sL4Ys#0iE%GPN#zN(iORkZG)KfDyw+43wkBKETTk^WlgZ!Sl}NAXgtlT;*7!_ZfcfN`Eh(j`LgjROVOk1D)%8 z9AXy{ezE*{ImpKQJK(f{A9B4YN-&@J-r<>~zF|5xI0;1^DBELv>;Qro-uvADAT@?<9ark^gWiRraoQmJhz~!3g_E$4imdmvdg*cG+3XX;$+CH9+Ps9RxoDamWg*W&u$3ciM5!3g59UQfrG_I*BG2t|k!&UM z?1E~Kogj~>^MFLA7I|uYT%S1O3t;Om7#w!M?t(U$&A0jc;UG(SNqiZ~CH^8qpZU;u z4z7ta0AIpcRMp7f_D0oL0;gxJw&@Qcv!np#|S9pGvK8lclr&|$on zmKy}(^bgl>b#d-1$^#4?uoo9|0N?fiOTdKqY_}0U@j^fzkdiVC(;)?QiK0LBEBizyDTGsrmPa z-*)(a(-E8SoE8>%!5r3a1c+{~dwf68rzJO>v4y}^1zQX(p7=dJwk}vi z3*Z76JN8&~u_$9PPD@8fBod9su)X zA*3$=yaa&x^Z}$_0({+eYT|=~#9hf?r$sRcrQkFcq0u;%oEq8KEEH~RT+KY(Je5!X z()^TJ5&hFv>B<#_iuKFd6g5?P1z&7it0pK7 zkk3lMRDx#w_U~-m|A1o_tWTM9HU?@0zikUE1w{y9bCzt3mqZf+%@V?;?|-8V|Fr;b o_7APUH`xNYe)v9)vO%B>jZ9)Z>($KUwo2)TNeD%N>1ZqcKXKBxsQ>@~ delta 14694 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 - + EnvironmentId diff --git a/F0:F030,F042,F072/usbcan_gpio/usbcangpio.files b/F0:F030,F042,F072/usbcan_gpio/usbcangpio.files index 73f5be2..40808f5 100644 --- a/F0:F030,F042,F072/usbcan_gpio/usbcangpio.files +++ b/F0:F030,F042,F072/usbcan_gpio/usbcangpio.files @@ -1,3 +1,4 @@ +Readme.md adc.c adc.h can.c diff --git a/F0:F030,F042,F072/usbcan_gpio/version.inc b/F0:F030,F042,F072/usbcan_gpio/version.inc index 6226929..a6d9cf3 100644 --- a/F0:F030,F042,F072/usbcan_gpio/version.inc +++ b/F0:F030,F042,F072/usbcan_gpio/version.inc @@ -1,2 +1,2 @@ -#define BUILD_NUMBER "221" -#define BUILD_DATE "2026-03-17" +#define BUILD_NUMBER "226" +#define BUILD_DATE "2026-03-18"