From a7362dc14bca1233eb34b41aefebe9cfb22684ac Mon Sep 17 00:00:00 2001 From: "info@mode42.com" Date: Fri, 7 Aug 2026 18:09:58 +0000 Subject: Initial push --- firmware/s1224/CMakeLists.txt | 67 +++++ firmware/s1224/README.md | 95 +++++++ firmware/s1224/config/default.ini | 22 ++ firmware/s1224/include/afsk_profile.h | 35 +++ firmware/s1224/include/afsk_soft.h | 32 +++ firmware/s1224/include/banner.h | 12 + firmware/s1224/include/baud_config.h | 10 + firmware/s1224/include/board_pins.h | 39 +++ firmware/s1224/include/config.h | 58 ++++ firmware/s1224/include/crystal_steer.h | 10 + firmware/s1224/include/display.h | 17 ++ firmware/s1224/include/hdlc.h | 49 ++++ firmware/s1224/include/kiss.h | 34 +++ firmware/s1224/include/modem_probe.h | 14 + firmware/s1224/include/ptt.h | 10 + firmware/s1224/include/sense.h | 19 ++ firmware/s1224/include/status.h | 23 ++ firmware/s1224/include/usb_cmd.h | 11 + firmware/s1224/include/version.h | 9 + firmware/s1224/src/common/afsk_soft.c | 224 ++++++++++++++++ firmware/s1224/src/common/banner.c | 82 ++++++ firmware/s1224/src/common/baud_config.c | 19 ++ firmware/s1224/src/common/board_pins.c | 47 ++++ firmware/s1224/src/common/config.c | 422 ++++++++++++++++++++++++++++++ firmware/s1224/src/common/crystal_steer.c | 17 ++ firmware/s1224/src/common/display.c | 187 +++++++++++++ firmware/s1224/src/common/hdlc.c | 192 ++++++++++++++ firmware/s1224/src/common/kiss.c | 98 +++++++ firmware/s1224/src/common/main.c | 61 +++++ firmware/s1224/src/common/modem_probe.c | 22 ++ firmware/s1224/src/common/ptt.c | 31 +++ firmware/s1224/src/common/sense.c | 72 +++++ firmware/s1224/src/common/status.c | 78 ++++++ firmware/s1224/src/common/usb_cmd.c | 267 +++++++++++++++++++ 34 files changed, 2385 insertions(+) create mode 100644 firmware/s1224/CMakeLists.txt create mode 100644 firmware/s1224/README.md create mode 100644 firmware/s1224/config/default.ini create mode 100644 firmware/s1224/include/afsk_profile.h create mode 100644 firmware/s1224/include/afsk_soft.h create mode 100644 firmware/s1224/include/banner.h create mode 100644 firmware/s1224/include/baud_config.h create mode 100644 firmware/s1224/include/board_pins.h create mode 100644 firmware/s1224/include/config.h create mode 100644 firmware/s1224/include/crystal_steer.h create mode 100644 firmware/s1224/include/display.h create mode 100644 firmware/s1224/include/hdlc.h create mode 100644 firmware/s1224/include/kiss.h create mode 100644 firmware/s1224/include/modem_probe.h create mode 100644 firmware/s1224/include/ptt.h create mode 100644 firmware/s1224/include/sense.h create mode 100644 firmware/s1224/include/status.h create mode 100644 firmware/s1224/include/usb_cmd.h create mode 100644 firmware/s1224/include/version.h create mode 100644 firmware/s1224/src/common/afsk_soft.c create mode 100644 firmware/s1224/src/common/banner.c create mode 100644 firmware/s1224/src/common/baud_config.c create mode 100644 firmware/s1224/src/common/board_pins.c create mode 100644 firmware/s1224/src/common/config.c create mode 100644 firmware/s1224/src/common/crystal_steer.c create mode 100644 firmware/s1224/src/common/display.c create mode 100644 firmware/s1224/src/common/hdlc.c create mode 100644 firmware/s1224/src/common/kiss.c create mode 100644 firmware/s1224/src/common/main.c create mode 100644 firmware/s1224/src/common/modem_probe.c create mode 100644 firmware/s1224/src/common/ptt.c create mode 100644 firmware/s1224/src/common/sense.c create mode 100644 firmware/s1224/src/common/status.c create mode 100644 firmware/s1224/src/common/usb_cmd.c (limited to 'firmware/s1224') diff --git a/firmware/s1224/CMakeLists.txt b/firmware/s1224/CMakeLists.txt new file mode 100644 index 0000000..a255afb --- /dev/null +++ b/firmware/s1224/CMakeLists.txt @@ -0,0 +1,67 @@ +# T-Modem-s1224 Pico firmware — softmodem · three UF2 images (build separately) +cmake_minimum_required(VERSION 3.13) + +if(NOT DEFINED ENV{PICO_SDK_PATH} AND NOT DEFINED PICO_SDK_PATH) + message(FATAL_ERROR "Set PICO_SDK_PATH to the Raspberry Pi Pico SDK root") +endif() +if(DEFINED ENV{PICO_SDK_PATH} AND NOT DEFINED PICO_SDK_PATH) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) +endif() + +include(${PICO_SDK_PATH}/external/pico_sdk_import.cmake) + +project(s1224_firmware C CXX ASM) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +pico_sdk_init() + +if(NOT DEFINED S1224_BITRATE) + set(S1224_BITRATE 1200) +endif() + +if(S1224_BITRATE EQUAL 300) + set(S1224_TARGET s1224-300) +elseif(S1224_BITRATE EQUAL 1200) + set(S1224_TARGET s1224-1200) +elseif(S1224_BITRATE EQUAL 2400) + set(S1224_TARGET s1224-2400) +else() + message(FATAL_ERROR "S1224_BITRATE must be 300, 1200, or 2400") +endif() + +add_executable(${S1224_TARGET} + src/common/main.c + src/common/status.c + src/common/ptt.c + src/common/modem_probe.c + src/common/usb_cmd.c + src/common/display.c + src/common/board_pins.c + src/common/crystal_steer.c + src/common/baud_config.c + src/common/config.c + src/common/sense.c + src/common/hdlc.c + src/common/kiss.c + src/common/afsk_soft.c + src/common/banner.c +) + +target_include_directories(${S1224_TARGET} PRIVATE include) +target_compile_definitions(${S1224_TARGET} PRIVATE S1224_BITRATE=${S1224_BITRATE}) +target_link_libraries(${S1224_TARGET} + pico_stdlib + hardware_gpio + hardware_i2c + hardware_flash + hardware_sync + hardware_pwm + hardware_adc + m +) + +pico_enable_stdio_usb(${S1224_TARGET} 1) +pico_enable_stdio_uart(${S1224_TARGET} 0) +pico_add_extra_outputs(${S1224_TARGET}) +pico_set_binary_type(${S1224_TARGET} default) diff --git a/firmware/s1224/README.md b/firmware/s1224/README.md new file mode 100644 index 0000000..976947c --- /dev/null +++ b/firmware/s1224/README.md @@ -0,0 +1,95 @@ +# T-Modem-s1224 — Pico USB KISS half-TNC + softmodem + +**AS-IS** · GPLv3 (repo `LICENSE`) · no support, no warranty · **v1.0-prototype**. + +**Three** UF2 images for Raspberry Pi Pico (RP2040). Softmodem does air AFSK; Pico is **USB KISS half-TNC** (HDLC) plus display / PTT / sense. **No** TCM3105 chip path. + +| Owns | Does **not** | +|------|----------------| +| HDLC TX/RX (flags, bit-stuff, FCS-CCITT) | Chip bitbang / TCM3105 TXD-RXD | +| Soft AFSK: PWM TX (GP6) · Goertzel RX (GP26 ADC) | Runtime baud switch | +| USB CDC: KISS `0xC0` + ASCII service | XTAL-rate crystal steering | +| Auto PTT GP9 during KISS TX · `PTT ON`/`OFF` | Modem IC / socket | +| Display / INI texts · carrier sense · fixed boot banner | INI/CDC boot-banner toggle | + +## Tone profiles + +| `-DS1224_BITRATE=` | UF2 | Mark / Space Hz | +|--------------------|-----|-----------------| +| `300` | `s1224-300.uf2` | 1600 / 1800 | +| `1200` (default) | `s1224-1200.uf2` | 1200 / 2200 (Bell 202) | +| `2400` | `s1224-2400.uf2` | ~1775 / 3250 (c1224 Y2-class) | + +## Build + +Requires [Pico SDK](https://github.com/raspberrypi/pico-sdk) and `arm-none-eabi-gcc`. + +```bash +export PICO_SDK_PATH=/path/to/pico-sdk # e.g. T-Modem-c1224/.deps/pico-sdk +cd firmware/s1224 +mkdir -p build && cd build +cmake .. -DS1224_BITRATE=1200 # or 300 / 2400 +make -j$(nproc) +# → s1224-1200.uf2 s1224-1200.elf +``` + +Rebuild separately for each bitrate (clean build dir or out-of-tree dirs recommended). + +### Flash + +1. Hold **BOOTSEL**, plug USB (board power = **12 V** barrel). +2. Copy `s1224-*.uf2` to RPI-RP2 (or `picotool load -f …`). +3. Host opens `/dev/ttyACM*` (USB CDC). + +## Any KISS terminal (standalone — no MAX25) + +**MAX25-Stack is not required.** **All terminals should work insofar as applicable** — flash UF2 → plug USB → open `/dev/ttyACM*` → KISS `0xC0` or ASCII `HELP`. + +| Step | Detail | +|------|--------| +| Device | `/dev/ttyACM*` | +| Framing | Raw **8N1** · binary **KISS** (`0xC0` … `0xC0`) · DATA cmd `0x00` | +| Boot | Fixed source banner **always** once · `quiet=1` damps extra STATUS/CONFIG ASCII only | + +## Pins + +| GPIO / rail | Role | +|-------------|------| +| GP0 / GP1 | I2C LCD | +| GP6 | **AF_TX** — PWM AFSK out (bypass DNI modem footprint) | +| GP26 | **AF_RX** — ADC0 soft demod in | +| GP7 / GP8 | Legacy chip pads — leave open on softmodem boards | +| GP9 | **PTT** (auto on TX + manual CDC) | +| GP10–12 | Optional LEDs | +| — | LED_PWR on **5 V rail** | + +## Boot banner + +Fixed source text in `src/common/banner.c` / `include/version.h`. **Always** printed once after USB CDC ready. + +## USB CDC ASCII + +| Command | Effect | +|---------|--------| +| `HELP` | Command list | +| `STATUS` | state, PTT, signal, packet, carrier, bitrate | +| `ID` | `device_id` / `callsign` / bitrate / quiet | +| `PTT ON` / `PTT OFF` | Key GP9 | +| `QUIET ON` / `OFF` | Suppress extra unsolicited CDC ASCII | +| `INI` / `GET` / `SET` / `SAVE` / `RELOAD` / `DEFAULTS` | Config | + +**`SAVE`:** writes a config **blob** into the **last 4 KiB** of Pico flash — **not** a filesystem. + +## INI (`[terminal]`) + +| Key | Default | Role | +|-----|---------|------| +| `callsign` | empty | Operator call / station string for `ID` | +| `device_id` | `s1224` | Short device mark | +| `quiet` | `1` | Suppress extra unsolicited CDC ASCII (not boot banner) | + +See [`share/s1224.ini.example`](../../share/s1224.ini.example). + +## Rate + +Firmware does not switch baud at runtime. Flash the UF2 that matches the intended tone profile. diff --git a/firmware/s1224/config/default.ini b/firmware/s1224/config/default.ini new file mode 100644 index 0000000..6f24e7e --- /dev/null +++ b/firmware/s1224/config/default.ini @@ -0,0 +1,22 @@ +# T-Modem-s1224 built-in defaults (flash override via CDC SAVE) + +[display] +starting=s1224 is starting +ready=s1224 is ready +fault=s1224 is fault +sending=s1224 is sending +signal=s1224 signal +packet=s1224 packet +power=s1224 power +idle=s1224 idle + +[terminal] +callsign= +device_id=s1224 +quiet=1 + +[sense] +cdt_active_high=0 +packet_hold_ms=500 +packet_edge_min=4 +packet_window_ms=50 diff --git a/firmware/s1224/include/afsk_profile.h b/firmware/s1224/include/afsk_profile.h new file mode 100644 index 0000000..da0fc29 --- /dev/null +++ b/firmware/s1224/include/afsk_profile.h @@ -0,0 +1,35 @@ +/** + * Mark/space tone pairs per build (Hz) — see product README. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_AFSK_PROFILE_H +#define S1224_AFSK_PROFILE_H + +#include + +#ifndef S1224_BITRATE +#define S1224_BITRATE 1200 +#endif + +typedef struct { + uint32_t baud; + uint32_t mark_hz; + uint32_t space_hz; + const char *profile_name; +} afsk_profile_t; + +static inline afsk_profile_t afsk_profile_get(void) +{ +#if S1224_BITRATE == 300 + return (afsk_profile_t){300, 1600, 1800, "HF-300 (1600/1800)"}; +#elif S1224_BITRATE == 1200 + return (afsk_profile_t){1200, 1200, 2200, "Bell202-1200"}; +#elif S1224_BITRATE == 2400 + /* c1224 Y2 / 6.5536 MHz field overclock class — NOT Dire Wolf 2400 */ + return (afsk_profile_t){2400, 1775, 3250, "Y2-class-2400"}; +#else +#error "S1224_BITRATE must be 300, 1200, or 2400" +#endif +} + +#endif /* S1224_AFSK_PROFILE_H */ diff --git a/firmware/s1224/include/afsk_soft.h b/firmware/s1224/include/afsk_soft.h new file mode 100644 index 0000000..5a23141 --- /dev/null +++ b/firmware/s1224/include/afsk_soft.h @@ -0,0 +1,32 @@ +/** + * Softmodem AFSK — PWM/DDS TX + Goertzel RX (ADC when wired). + * Profile tones selected at compile time via S1224_BITRATE=300|1200|2400. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_AFSK_SOFT_H +#define S1224_AFSK_SOFT_H + +#include +#include +#include + +#ifndef S1224_BITRATE +#define S1224_BITRATE 1200 +#endif + +void afsk_soft_init(void); + +/** Transmit HDLC info payload (AX.25 frame without flags/FCS). Auto-PTT. */ +bool afsk_tx_frame(const uint8_t *info, size_t info_len); + +typedef void (*afsk_rx_frame_fn)(const uint8_t *info, size_t len); +void afsk_rx_set_callback(afsk_rx_frame_fn fn); +void afsk_rx_poll(void); + +bool afsk_tx_busy(void); +unsigned afsk_bitrate(void); + +/** Carrier/energy sense for UI (softmodem — not chip CDT). */ +bool afsk_rx_carrier_active(void); + +#endif /* S1224_AFSK_SOFT_H */ diff --git a/firmware/s1224/include/banner.h b/firmware/s1224/include/banner.h new file mode 100644 index 0000000..d9bc1a2 --- /dev/null +++ b/firmware/s1224/include/banner.h @@ -0,0 +1,12 @@ +/** + * Fixed CDC boot banner — source text only (not INI / CDC keys). + * Always printed once after USB ready; quiet does not suppress it. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_BANNER_H +#define S1224_BANNER_H + +/** Print fixed boot banner (call once after CDC ready). */ +void banner_print(void); + +#endif /* S1224_BANNER_H */ diff --git a/firmware/s1224/include/baud_config.h b/firmware/s1224/include/baud_config.h new file mode 100644 index 0000000..28e07ee --- /dev/null +++ b/firmware/s1224/include/baud_config.h @@ -0,0 +1,10 @@ +#ifndef S1224_BAUD_CONFIG_H +#define S1224_BAUD_CONFIG_H + +typedef struct { + const char *label; +} baud_profile_config_t; + +const baud_profile_config_t *baud_config_get(void); + +#endif diff --git a/firmware/s1224/include/board_pins.h b/firmware/s1224/include/board_pins.h new file mode 100644 index 0000000..0fb1a21 --- /dev/null +++ b/firmware/s1224/include/board_pins.h @@ -0,0 +1,39 @@ +/** + * T-Modem-s1224 pin map — softmodem service Pico (same PCB base as c1224). + * No TCM3105: PWM AF out bypasses chip socket (prototype jumper to AF path). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_BOARD_PINS_H +#define S1224_BOARD_PINS_H + +#include +#include + +#define S1224_HAS_CHIP_MODEM 0 + +/* I2C LCD (shared-kit class; level-shifted on board) */ +#define PIN_I2C_SDA 0 +#define PIN_I2C_SCL 1 +#define I2C_LCD_BAUDRATE 100000 +#define I2C_LCD_ADDR_DEFAULT 0x27 + +/* GP6 · PCB net AF_TX · PWM AFSK out */ +#define PIN_AF_TX 6 + +/* GP26 · PCB net AF_RX when routed · ADC channel 0 */ +#define PIN_AF_RX_ADC 26 + +/* GP7/GP8 · NC on s1224 PCB · optional FW sense if wired */ +#define PIN_GP7_SENSE 7 +#define PIN_MODEM_RXD PIN_GP7_SENSE /* alias · was chip RXD name */ +#define PIN_MODEM_CDT 8 /* unused · NC on board */ + +#define PIN_PTT_DRV 9 /* GP9 · PTT keying */ + +#define PIN_LED_PTT 10 +#define PIN_LED_DCD 11 +#define PIN_LED_TXRX 12 + +void board_pins_init(void); + +#endif /* S1224_BOARD_PINS_H */ diff --git a/firmware/s1224/include/config.h b/firmware/s1224/include/config.h new file mode 100644 index 0000000..9792aed --- /dev/null +++ b/firmware/s1224/include/config.h @@ -0,0 +1,58 @@ +/** + * Operator INI texts + sense tuning — defaults in FW, overlay from flash. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_CONFIG_H +#define S1224_CONFIG_H + +#include +#include +#include + +#define S1224_TEXT_MAX 48 + +typedef struct { + char starting[S1224_TEXT_MAX]; + char ready[S1224_TEXT_MAX]; + char fault[S1224_TEXT_MAX]; + char sending[S1224_TEXT_MAX]; + char signal[S1224_TEXT_MAX]; + char packet[S1224_TEXT_MAX]; + char power[S1224_TEXT_MAX]; + char idle[S1224_TEXT_MAX]; + /* Terminal-facing identity (CDC ID) */ + char callsign[S1224_TEXT_MAX]; + char device_id[S1224_TEXT_MAX]; + /* Extra CDC ASCII — quiet=1 default; never suppresses fixed boot banner */ + bool quiet; /* true = no unsolicited STATUS/PROBE/CONFIG ASCII */ + /* Sense tuning */ + bool cdt_active_high; /* false = active-low (default pull-up idle) */ + uint16_t packet_hold_ms; + uint16_t packet_edge_min; + uint16_t packet_window_ms; +} s1224_config_t; + +void config_init(void); +const s1224_config_t *config_get(void); +s1224_config_t *config_mutable(void); +/** True when extra unsolicited CDC ASCII (STATUS/PROBE/CONFIG) is allowed. */ +bool config_unsolicited_ok(void); + +/** Apply defaults (RAM only). */ +void config_load_defaults(void); +/** Load overlay from flash INI region if valid. */ +bool config_load_flash(void); +/** Persist current config as INI text to flash. */ +bool config_save_flash(void); +/** Erase flash overlay (next boot = defaults only until SAVE). */ +bool config_erase_flash(void); + +/** SET KEY=value (case-insensitive key). Returns false if unknown key. */ +bool config_set_kv(const char *key, const char *value); +/** GET KEY → copy value into buf. */ +bool config_get_kv(const char *key, char *buf, size_t buflen); + +/** Dump INI-like text to USB (printf). */ +void config_dump_ini(void); + +#endif diff --git a/firmware/s1224/include/crystal_steer.h b/firmware/s1224/include/crystal_steer.h new file mode 100644 index 0000000..43f0dd7 --- /dev/null +++ b/firmware/s1224/include/crystal_steer.h @@ -0,0 +1,10 @@ +#ifndef S1224_CRYSTAL_STEER_H +#define S1224_CRYSTAL_STEER_H + +#include + +/** Softmodem — no-op (rate = compile-time S1224_BITRATE). */ +void crystal_steer_apply(void); +bool crystal_steer_applied(void); + +#endif diff --git a/firmware/s1224/include/display.h b/firmware/s1224/include/display.h new file mode 100644 index 0000000..67b32ce --- /dev/null +++ b/firmware/s1224/include/display.h @@ -0,0 +1,17 @@ +#ifndef S1224_DISPLAY_H +#define S1224_DISPLAY_H + +#include + +/** + * I2C HD44780 via PCF8574 backpack (0x27/0x3F). USB always logs. + * Shows Power/ready, Signal, Packet from INI texts. + */ +void display_init(void); +bool display_present(void); +/** Refresh LCD from current status + sense (uses INI texts). */ +void display_update(void); +/** Legacy single-line push (also used before full UI ready). */ +void display_show(const char *line); + +#endif diff --git a/firmware/s1224/include/hdlc.h b/firmware/s1224/include/hdlc.h new file mode 100644 index 0000000..9b4cb22 --- /dev/null +++ b/firmware/s1224/include/hdlc.h @@ -0,0 +1,49 @@ +/** + * HDLC frame helpers — flags, bit-stuff, FCS-CCITT (AX.25). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_HDLC_H +#define S1224_HDLC_H + +#include +#include +#include + +#define HDLC_FLAG 0x7Eu +#define HDLC_FCS_INIT 0xFFFFu +#define HDLC_FCS_GOOD 0xF0B8u +#define HDLC_MAX_INFO 512 + +uint16_t hdlc_fcs_update(uint16_t fcs, uint8_t byte); +uint16_t hdlc_fcs_final(uint16_t fcs); + +/** + * Build on-wire bit stream (NRZI already applied by caller after this): + * FLAG + stuffed(info + FCS_lo + FCS_hi) + FLAG(+optional trailing flags). + * Returns bit count written to bits[] (MSB-first within each stuffed byte stream + * is not used — output is a raw bit array, LSB-first per AX.25 bit order). + * + * bits_cap is capacity in bits. Returns 0 on overflow / bad args. + */ +size_t hdlc_encode_bits(const uint8_t *info, size_t info_len, + uint8_t *bits, size_t bits_cap, + unsigned trailing_flags); + +/** + * Feed one NRZ bit (after NRZI decode). Returns true when a complete good + * frame is available in out[] (info only, FCS stripped). + */ +typedef struct { + uint8_t buf[HDLC_MAX_INFO + 2]; + size_t len; + uint8_t ones; + bool in_frame; + bool stuffing; + uint8_t byte_acc; + uint8_t bit_count; +} hdlc_rx_t; + +void hdlc_rx_init(hdlc_rx_t *rx); +bool hdlc_rx_bit(hdlc_rx_t *rx, bool bit, uint8_t *out, size_t *out_len); + +#endif /* S1224_HDLC_H */ diff --git a/firmware/s1224/include/kiss.h b/firmware/s1224/include/kiss.h new file mode 100644 index 0000000..66a890b --- /dev/null +++ b/firmware/s1224/include/kiss.h @@ -0,0 +1,34 @@ +/** + * KISS frame encode/decode (FEND 0xC0). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_KISS_H +#define S1224_KISS_H + +#include +#include +#include + +#define KISS_FEND 0xC0u +#define KISS_FESC 0xDBu +#define KISS_TFEND 0xDCu +#define KISS_TFESC 0xDDu +#define KISS_CMD_DATA 0x00u +#define KISS_MAX_PAYLOAD 512 + +typedef struct { + uint8_t buf[KISS_MAX_PAYLOAD + 4]; + size_t len; + bool in_frame; + bool esc; +} kiss_rx_t; + +void kiss_rx_init(kiss_rx_t *kx); +/** Feed one byte. Returns true when a DATA payload is ready in out. */ +bool kiss_rx_byte(kiss_rx_t *kx, uint8_t b, uint8_t *out, size_t *out_len); + +/** Write KISS DATA frame to dst; returns bytes written (0 on overflow). */ +size_t kiss_encode_data(const uint8_t *payload, size_t len, + uint8_t *dst, size_t dst_cap); + +#endif /* S1224_KISS_H */ diff --git a/firmware/s1224/include/modem_probe.h b/firmware/s1224/include/modem_probe.h new file mode 100644 index 0000000..7e63904 --- /dev/null +++ b/firmware/s1224/include/modem_probe.h @@ -0,0 +1,14 @@ +#ifndef S1224_MODEM_PROBE_H +#define S1224_MODEM_PROBE_H + +#include + +typedef struct { + bool softmodem_ok; + bool af_tx_pwm; + bool ready; +} modem_probe_result_t; + +modem_probe_result_t modem_probe_run(void); + +#endif diff --git a/firmware/s1224/include/ptt.h b/firmware/s1224/include/ptt.h new file mode 100644 index 0000000..501c558 --- /dev/null +++ b/firmware/s1224/include/ptt.h @@ -0,0 +1,10 @@ +#ifndef S1224_PTT_H +#define S1224_PTT_H + +#include + +void ptt_init(void); +void ptt_set(bool on); +bool ptt_get(void); + +#endif diff --git a/firmware/s1224/include/sense.h b/firmware/s1224/include/sense.h new file mode 100644 index 0000000..50933f3 --- /dev/null +++ b/firmware/s1224/include/sense.h @@ -0,0 +1,19 @@ +/** + * Dual sense: CDT→Signal/RX, RXD→Packet (HDLC 0x7E / edge activity). + * Status only — not a Host↔Chip data bridge. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#ifndef S1224_SENSE_H +#define S1224_SENSE_H + +#include + +void sense_init(void); +/** Poll GPIO / heuristics; call from main loop (~10 ms). */ +void sense_poll(void); + +bool sense_signal_active(void); /* CDT carrier/tone */ +bool sense_packet_active(void); /* RXD activity or flag seen (held) */ +bool sense_packet_flag(void); /* last window saw 0x7E */ + +#endif diff --git a/firmware/s1224/include/status.h b/firmware/s1224/include/status.h new file mode 100644 index 0000000..3c62390 --- /dev/null +++ b/firmware/s1224/include/status.h @@ -0,0 +1,23 @@ +#ifndef S1224_STATUS_H +#define S1224_STATUS_H + +#include + +typedef enum { + S1224_STATE_STARTING = 0, + S1224_STATE_READY, + S1224_STATE_FAULT, + S1224_STATE_SENDING, + S1224_STATE_SIGNAL, /* CDT active (Signal/RX) */ + S1224_STATE_PACKET /* RXD packet sense (may coexist with signal) */ +} s1224_state_t; + +/** Operator-facing text for state (from INI). */ +const char *s1224_state_string(s1224_state_t st); +void status_set(s1224_state_t st); +s1224_state_t status_get(void); +/** Recompute composite state from PTT + sense; publish if changed. */ +void status_tick(void); +void status_publish(void); + +#endif diff --git a/firmware/s1224/include/usb_cmd.h b/firmware/s1224/include/usb_cmd.h new file mode 100644 index 0000000..8f0ded6 --- /dev/null +++ b/firmware/s1224/include/usb_cmd.h @@ -0,0 +1,11 @@ +#ifndef S1224_USB_CMD_H +#define S1224_USB_CMD_H + +#include +#include + +void usb_cmd_init(void); +void usb_cmd_poll(void); +void usb_cmd_on_rx_frame(const uint8_t *info, size_t len); + +#endif diff --git a/firmware/s1224/include/version.h b/firmware/s1224/include/version.h new file mode 100644 index 0000000..d1a288f --- /dev/null +++ b/firmware/s1224/include/version.h @@ -0,0 +1,9 @@ +#ifndef S1224_VERSION_H +#define S1224_VERSION_H +/** + * Firmware version SSoT — bump here; banner and docs follow. + * Matches product tag style without leading 'v' (tag: v1.0-prototype). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#define S1224_FW_VERSION "1.0-prototype" +#endif diff --git a/firmware/s1224/src/common/afsk_soft.c b/firmware/s1224/src/common/afsk_soft.c new file mode 100644 index 0000000..3e5d966 --- /dev/null +++ b/firmware/s1224/src/common/afsk_soft.c @@ -0,0 +1,224 @@ +/* + * Continuous-phase AFSK TX via PWM frequency (mark/space) + NRZI/HDLC. + * RX: per-bit Goertzel mark/space on ADC (GP26) when wired. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "afsk_soft.h" +#include "afsk_profile.h" +#include "hdlc.h" +#include "board_pins.h" +#include "ptt.h" +#include "status.h" +#include "hardware/gpio.h" +#include "hardware/pwm.h" +#include "hardware/clocks.h" +#include "pico/stdlib.h" +#include +#include + +#define BIT_US (1000000u / (unsigned)S1224_BITRATE) +#define TX_BITS_MAX ((HDLC_MAX_INFO + 4) * 10 + 64) +#define PWM_CLKDIV 4.0f +#define RX_SAMPLES_BIT 8 +#define RX_FS_HZ ((unsigned)S1224_BITRATE * RX_SAMPLES_BIT) +#define CARRIER_THRESH 50.0f + +static uint8_t g_tx_bits[TX_BITS_MAX]; +static volatile bool g_tx_busy; +static bool g_nrzi_mark; +static hdlc_rx_t g_rx; +static afsk_rx_frame_fn g_rx_cb; +static uint8_t g_rx_out[HDLC_MAX_INFO]; +static uint g_pwm_slice; +static uint g_pwm_chan; +static afsk_profile_t g_prof; +static bool g_carrier; + +#if PIN_AF_RX_ADC >= 0 +#include "hardware/adc.h" +#define RX_GOERTZEL_ENABLE 1 +static float g_rx_samp[RX_SAMPLES_BIT]; +static unsigned g_rx_n; +static absolute_time_t g_next_adc; +static bool g_rx_nrzi_mark; +static float g_coeff_mark; +static float g_coeff_space; +#else +#define RX_GOERTZEL_ENABLE 0 +#endif + +static void pwm_tone_hz(uint32_t freq_hz) +{ + if (freq_hz < 100) { + pwm_set_enabled(g_pwm_slice, false); + return; + } + uint32_t sys = clock_get_hz(clk_sys); + uint32_t wrap = (uint32_t)((float)sys / (freq_hz * PWM_CLKDIV)) - 1u; + if (wrap < 10) { + wrap = 10; + } + pwm_set_clkdiv(g_pwm_slice, PWM_CLKDIV); + pwm_set_wrap(g_pwm_slice, wrap); + pwm_set_chan_level(g_pwm_slice, g_pwm_chan, wrap / 2u); + pwm_set_enabled(g_pwm_slice, true); +} + +static void nrzi_play_bit(bool data_bit) +{ + if (!data_bit) { + g_nrzi_mark = !g_nrzi_mark; + } + pwm_tone_hz(g_nrzi_mark ? g_prof.mark_hz : g_prof.space_hz); + busy_wait_us_32(BIT_US); +} + +#if RX_GOERTZEL_ENABLE +static float goertzel_mag(const float *x, unsigned n, float coeff) +{ + float s0 = 0.0f, s1 = 0.0f, s2 = 0.0f; + for (unsigned i = 0; i < n; i++) { + s0 = x[i] + coeff * s1 - s2; + s2 = s1; + s1 = s0; + } + float power = s1 * s1 + s2 * s2 - coeff * s1 * s2; + return power < 0.0f ? 0.0f : power; +} + +static float goertzel_coeff(uint32_t tone_hz) +{ + float w = 2.0f * (float)M_PI * ((float)tone_hz / (float)RX_FS_HZ); + return 2.0f * cosf(w); +} +#endif + +void afsk_soft_init(void) +{ + g_prof = afsk_profile_get(); + gpio_set_function(PIN_AF_TX, GPIO_FUNC_PWM); + g_pwm_slice = pwm_gpio_to_slice_num(PIN_AF_TX); + g_pwm_chan = pwm_gpio_to_channel(PIN_AF_TX); + pwm_tone_hz(0); + g_tx_busy = false; + g_nrzi_mark = true; + hdlc_rx_init(&g_rx); + g_rx_cb = NULL; + g_carrier = false; + +#if RX_GOERTZEL_ENABLE + adc_init(); + adc_gpio_init(PIN_AF_RX_ADC); + adc_select_input((uint)(PIN_AF_RX_ADC - 26)); + g_rx_n = 0; + g_rx_nrzi_mark = true; + g_coeff_mark = goertzel_coeff(g_prof.mark_hz); + g_coeff_space = goertzel_coeff(g_prof.space_hz); + g_next_adc = get_absolute_time(); +#endif +} + +unsigned afsk_bitrate(void) +{ + return (unsigned)S1224_BITRATE; +} + +bool afsk_tx_busy(void) +{ + return g_tx_busy; +} + +bool afsk_rx_carrier_active(void) +{ + return g_carrier; +} + +void afsk_rx_set_callback(afsk_rx_frame_fn fn) +{ + g_rx_cb = fn; +} + +bool afsk_tx_frame(const uint8_t *info, size_t info_len) +{ + if (g_tx_busy || !info || info_len == 0 || info_len > HDLC_MAX_INFO) { + return false; + } + size_t nbits = hdlc_encode_bits(info, info_len, g_tx_bits, TX_BITS_MAX, 2); + if (nbits == 0) { + return false; + } + + g_tx_busy = true; + status_set(S1224_STATE_SENDING); + status_publish(); + ptt_set(true); + sleep_ms(40); + + g_nrzi_mark = true; + pwm_tone_hz(g_prof.mark_hz); + + for (size_t i = 0; i < nbits; i++) { + nrzi_play_bit(g_tx_bits[i] != 0); + } + + pwm_tone_hz(0); + sleep_ms(20); + ptt_set(false); + status_set(S1224_STATE_READY); + status_publish(); + g_tx_busy = false; + return true; +} + +#if RX_GOERTZEL_ENABLE +static void rx_process_bit_window(void) +{ + float m = goertzel_mag(g_rx_samp, RX_SAMPLES_BIT, g_coeff_mark); + float s = goertzel_mag(g_rx_samp, RX_SAMPLES_BIT, g_coeff_space); + float peak = m > s ? m : s; + g_carrier = peak > CARRIER_THRESH; + + bool tone_mark = (m >= s); + bool data_bit = (tone_mark == g_rx_nrzi_mark); /* NRZI: 1 = no transition */ + if (tone_mark != g_rx_nrzi_mark) { + g_rx_nrzi_mark = tone_mark; + } + + size_t out_len = 0; + if (hdlc_rx_bit(&g_rx, data_bit, g_rx_out, &out_len) && g_rx_cb && out_len > 0) { + g_rx_cb(g_rx_out, out_len); + } +} + +static void rx_goertzel_sample(void) +{ + absolute_time_t now = get_absolute_time(); + if (absolute_time_diff_us(g_next_adc, now) < 0) { + return; + } + g_next_adc = delayed_by_us(now, 1000000u / RX_FS_HZ); + + uint16_t raw = adc_read(); + /* Center around mid-scale (12-bit ADC). */ + g_rx_samp[g_rx_n++] = ((float)raw - 2048.0f) / 2048.0f; + + if (g_rx_n >= RX_SAMPLES_BIT) { + g_rx_n = 0; + rx_process_bit_window(); + } +} +#else +static void rx_goertzel_sample(void) +{ + /* TX-only boards: leave PIN_AF_RX_ADC as -1. */ + g_carrier = false; +} +#endif + +void afsk_rx_poll(void) +{ + if (g_tx_busy) { + return; + } + rx_goertzel_sample(); +} diff --git a/firmware/s1224/src/common/banner.c b/firmware/s1224/src/common/banner.c new file mode 100644 index 0000000..21d1f3c --- /dev/null +++ b/firmware/s1224/src/common/banner.c @@ -0,0 +1,82 @@ +/* + * Fixed boot banner — edit source only; not INI / CDC. + * Always printed once after USB CDC ready (main); quiet never suppresses it. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "banner.h" +#include "version.h" +#include "afsk_soft.h" +#include +#include + +/* Fixed CDC column width for centered serial look (40–48). */ +#define BANNER_WIDTH 44 + +/** Display columns (UTF-8 code points ≈ 1 col; ASCII-safe). */ +static size_t banner_cols(const char *s) +{ + size_t cols = 0; + while (*s) { + unsigned char c = (unsigned char)*s++; + if ((c & 0x80u) == 0u) { + cols++; + } else if ((c & 0xE0u) == 0xC0u) { + if (*s) { + s++; + } + cols++; + } else if ((c & 0xF0u) == 0xE0u) { + if (*s) { + s++; + } + if (*s) { + s++; + } + cols++; + } else if ((c & 0xF8u) == 0xF0u) { + if (*s) { + s++; + } + if (*s) { + s++; + } + if (*s) { + s++; + } + cols++; + } else { + cols++; + } + } + return cols; +} + +static void banner_line(const char *text) +{ + size_t cols = banner_cols(text); + size_t pad = 0; + if (cols < BANNER_WIDTH) { + pad = (BANNER_WIDTH - cols) / 2u; + } + for (size_t i = 0; i < pad; i++) { + putchar(' '); + } + fputs(text, stdout); + fputs("\r\n", stdout); +} + +void banner_print(void) +{ + /* Fixed boot banner — edit source only */ + char line[BANNER_WIDTH + 8]; + + snprintf(line, sizeof(line), "T-Modem-s1224 %s", S1224_FW_VERSION); + banner_line(line); + + snprintf(line, sizeof(line), "USB KISS Half-TNC · AFSK %u", + (unsigned)afsk_bitrate()); + banner_line(line); + + banner_line("Non-Profit · GNU GPLv3 · AS-IS"); + banner_line("https://github.com/ngteq"); +} diff --git a/firmware/s1224/src/common/baud_config.c b/firmware/s1224/src/common/baud_config.c new file mode 100644 index 0000000..8a94a19 --- /dev/null +++ b/firmware/s1224/src/common/baud_config.c @@ -0,0 +1,19 @@ +#include "baud_config.h" +#include "afsk_soft.h" + +static const baud_profile_config_t cfg = { +#if S1224_BITRATE == 300 + .label = "s1224-300", +#elif S1224_BITRATE == 1200 + .label = "s1224-1200", +#elif S1224_BITRATE == 2400 + .label = "s1224-2400", +#else + .label = "s1224", +#endif +}; + +const baud_profile_config_t *baud_config_get(void) +{ + return &cfg; +} diff --git a/firmware/s1224/src/common/board_pins.c b/firmware/s1224/src/common/board_pins.c new file mode 100644 index 0000000..d02ebd0 --- /dev/null +++ b/firmware/s1224/src/common/board_pins.c @@ -0,0 +1,47 @@ +/* + * Board GPIO setup — T-Modem-s1224 softmodem Pico. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "board_pins.h" +#include "hardware/gpio.h" +#include "hardware/i2c.h" + +void board_pins_init(void) +{ + gpio_init(PIN_AF_TX); + gpio_set_dir(PIN_AF_TX, GPIO_OUT); + gpio_put(PIN_AF_TX, 0); + +#if PIN_AF_RX_ADC >= 0 + gpio_init(PIN_AF_RX_ADC); + gpio_set_dir(PIN_AF_RX_ADC, GPIO_IN); +#endif + + gpio_init(PIN_MODEM_RXD); + gpio_set_dir(PIN_MODEM_RXD, GPIO_IN); + gpio_pull_up(PIN_MODEM_RXD); + + gpio_init(PIN_MODEM_CDT); + gpio_set_dir(PIN_MODEM_CDT, GPIO_IN); + gpio_pull_up(PIN_MODEM_CDT); + + gpio_init(PIN_PTT_DRV); + gpio_set_dir(PIN_PTT_DRV, GPIO_OUT); + gpio_put(PIN_PTT_DRV, 0); + + gpio_init(PIN_LED_PTT); + gpio_set_dir(PIN_LED_PTT, GPIO_OUT); + gpio_put(PIN_LED_PTT, 0); + gpio_init(PIN_LED_DCD); + gpio_set_dir(PIN_LED_DCD, GPIO_OUT); + gpio_put(PIN_LED_DCD, 0); + gpio_init(PIN_LED_TXRX); + gpio_set_dir(PIN_LED_TXRX, GPIO_OUT); + gpio_put(PIN_LED_TXRX, 0); + + i2c_init(i2c0, I2C_LCD_BAUDRATE); + gpio_set_function(PIN_I2C_SDA, GPIO_FUNC_I2C); + gpio_set_function(PIN_I2C_SCL, GPIO_FUNC_I2C); + gpio_pull_up(PIN_I2C_SDA); + gpio_pull_up(PIN_I2C_SCL); +} diff --git a/firmware/s1224/src/common/config.c b/firmware/s1224/src/common/config.c new file mode 100644 index 0000000..7c8f33c --- /dev/null +++ b/firmware/s1224/src/common/config.c @@ -0,0 +1,422 @@ +/* + * INI defaults + flash overlay (last 4 KiB of Pico flash). + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "config.h" +#include "hardware/flash.h" +#include "hardware/sync.h" +#include "pico/stdlib.h" +#include +#include +#include +#include + +#define CFG_MAGIC0 'S' +#define CFG_MAGIC1 '1' +#define CFG_MAGIC2 '2' +#define CFG_MAGIC3 '4' +#define CFG_VERSION 1 +#define CFG_SECTOR_SIZE 4096 +/* Store config in the last flash sector */ +#define CFG_FLASH_OFFSET (PICO_FLASH_SIZE_BYTES - CFG_SECTOR_SIZE) + +typedef struct __attribute__((packed)) { + uint8_t magic[4]; + uint8_t version; + uint8_t reserved; + uint16_t length; + uint16_t crc; + /* uint8_t data[length] follows */ +} cfg_hdr_t; + +static s1224_config_t g_cfg; + +static uint16_t crc16_ccitt(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= (uint16_t)data[i] << 8; + for (int b = 0; b < 8; b++) { + if (crc & 0x8000) { + crc = (uint16_t)((crc << 1) ^ 0x1021); + } else { + crc <<= 1; + } + } + } + return crc; +} + +static void copy_text(char *dst, const char *src) +{ + size_t n = strlen(src); + if (n >= S1224_TEXT_MAX) { + n = S1224_TEXT_MAX - 1; + } + memcpy(dst, src, n); + dst[n] = '\0'; +} + +void config_load_defaults(void) +{ + copy_text(g_cfg.starting, "s1224 is starting"); + copy_text(g_cfg.ready, "s1224 is ready"); + copy_text(g_cfg.fault, "s1224 is fault"); + copy_text(g_cfg.sending, "s1224 is sending"); + copy_text(g_cfg.signal, "s1224 signal"); + copy_text(g_cfg.packet, "s1224 packet"); + copy_text(g_cfg.power, "s1224 power"); + copy_text(g_cfg.idle, "s1224 idle"); + copy_text(g_cfg.callsign, ""); + copy_text(g_cfg.device_id, "s1224"); + g_cfg.quiet = true; + g_cfg.cdt_active_high = false; + g_cfg.packet_hold_ms = 500; + g_cfg.packet_edge_min = 4; + g_cfg.packet_window_ms = 50; +} + +static char *trim(char *s) +{ + while (*s && isspace((unsigned char)*s)) { + s++; + } + char *e = s + strlen(s); + while (e > s && isspace((unsigned char)e[-1])) { + *--e = '\0'; + } + return s; +} + +static bool parse_bool(const char *v, bool *out) +{ + if (!v || !*v) { + return false; + } + if (v[0] == '1' || v[0] == 'y' || v[0] == 'Y' || v[0] == 't' || v[0] == 'T') { + *out = true; + return true; + } + if (v[0] == '0' || v[0] == 'n' || v[0] == 'N' || v[0] == 'f' || v[0] == 'F') { + *out = false; + return true; + } + return false; +} + +bool config_set_kv(const char *key, const char *value) +{ + if (!key || !value) { + return false; + } + char kbuf[32]; + size_t n = strlen(key); + if (n >= sizeof(kbuf)) { + return false; + } + for (size_t i = 0; i <= n; i++) { + kbuf[i] = (char)tolower((unsigned char)key[i]); + } + + if (strcmp(kbuf, "starting") == 0) { + copy_text(g_cfg.starting, value); + return true; + } + if (strcmp(kbuf, "ready") == 0) { + copy_text(g_cfg.ready, value); + return true; + } + if (strcmp(kbuf, "fault") == 0) { + copy_text(g_cfg.fault, value); + return true; + } + if (strcmp(kbuf, "sending") == 0) { + copy_text(g_cfg.sending, value); + return true; + } + if (strcmp(kbuf, "signal") == 0) { + copy_text(g_cfg.signal, value); + return true; + } + if (strcmp(kbuf, "packet") == 0) { + copy_text(g_cfg.packet, value); + return true; + } + if (strcmp(kbuf, "power") == 0) { + copy_text(g_cfg.power, value); + return true; + } + if (strcmp(kbuf, "idle") == 0) { + copy_text(g_cfg.idle, value); + return true; + } + if (strcmp(kbuf, "callsign") == 0) { + copy_text(g_cfg.callsign, value); + return true; + } + if (strcmp(kbuf, "device_id") == 0) { + copy_text(g_cfg.device_id, value); + return true; + } + if (strcmp(kbuf, "quiet") == 0) { + bool b; + if (!parse_bool(value, &b)) { + return false; + } + g_cfg.quiet = b; + return true; + } + if (strcmp(kbuf, "cdt_active_high") == 0) { + bool b; + if (!parse_bool(value, &b)) { + return false; + } + g_cfg.cdt_active_high = b; + return true; + } + if (strcmp(kbuf, "packet_hold_ms") == 0) { + g_cfg.packet_hold_ms = (uint16_t)atoi(value); + return true; + } + if (strcmp(kbuf, "packet_edge_min") == 0) { + g_cfg.packet_edge_min = (uint16_t)atoi(value); + return true; + } + if (strcmp(kbuf, "packet_window_ms") == 0) { + g_cfg.packet_window_ms = (uint16_t)atoi(value); + return true; + } + return false; +} + +bool config_get_kv(const char *key, char *buf, size_t buflen) +{ + if (!key || !buf || buflen == 0) { + return false; + } + char kbuf[32]; + size_t n = strlen(key); + if (n >= sizeof(kbuf)) { + return false; + } + for (size_t i = 0; i <= n; i++) { + kbuf[i] = (char)tolower((unsigned char)key[i]); + } + + const char *src = NULL; + char num[16]; + if (strcmp(kbuf, "starting") == 0) { + src = g_cfg.starting; + } else if (strcmp(kbuf, "ready") == 0) { + src = g_cfg.ready; + } else if (strcmp(kbuf, "fault") == 0) { + src = g_cfg.fault; + } else if (strcmp(kbuf, "sending") == 0) { + src = g_cfg.sending; + } else if (strcmp(kbuf, "signal") == 0) { + src = g_cfg.signal; + } else if (strcmp(kbuf, "packet") == 0) { + src = g_cfg.packet; + } else if (strcmp(kbuf, "power") == 0) { + src = g_cfg.power; + } else if (strcmp(kbuf, "idle") == 0) { + src = g_cfg.idle; + } else if (strcmp(kbuf, "callsign") == 0) { + src = g_cfg.callsign; + } else if (strcmp(kbuf, "device_id") == 0) { + src = g_cfg.device_id; + } else if (strcmp(kbuf, "quiet") == 0) { + snprintf(num, sizeof(num), "%d", (int)g_cfg.quiet); + src = num; + } else if (strcmp(kbuf, "cdt_active_high") == 0) { + snprintf(num, sizeof(num), "%d", (int)g_cfg.cdt_active_high); + src = num; + } else if (strcmp(kbuf, "packet_hold_ms") == 0) { + snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_hold_ms); + src = num; + } else if (strcmp(kbuf, "packet_edge_min") == 0) { + snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_edge_min); + src = num; + } else if (strcmp(kbuf, "packet_window_ms") == 0) { + snprintf(num, sizeof(num), "%u", (unsigned)g_cfg.packet_window_ms); + src = num; + } else { + return false; + } + snprintf(buf, buflen, "%s", src); + return true; +} + +static void parse_ini_text(char *text) +{ + char *save = NULL; + for (char *line = strtok_r(text, "\r\n", &save); line; + line = strtok_r(NULL, "\r\n", &save)) { + line = trim(line); + if (line[0] == '\0' || line[0] == '#' || line[0] == ';') { + continue; + } + if (line[0] == '[') { + continue; /* section headers ignored — flat keys */ + } + char *eq = strchr(line, '='); + if (!eq) { + continue; + } + *eq = '\0'; + char *k = trim(line); + char *v = trim(eq + 1); + (void)config_set_kv(k, v); + } +} + +bool config_load_flash(void) +{ + const uint8_t *base = (const uint8_t *)(XIP_BASE + CFG_FLASH_OFFSET); + const cfg_hdr_t *hdr = (const cfg_hdr_t *)base; + if (hdr->magic[0] != CFG_MAGIC0 || hdr->magic[1] != CFG_MAGIC1 || + hdr->magic[2] != CFG_MAGIC2 || hdr->magic[3] != CFG_MAGIC3) { + return false; + } + if (hdr->version != CFG_VERSION) { + return false; + } + if (hdr->length == 0 || hdr->length > (CFG_SECTOR_SIZE - sizeof(cfg_hdr_t) - 1)) { + return false; + } + const uint8_t *data = base + sizeof(cfg_hdr_t); + if (crc16_ccitt(data, hdr->length) != hdr->crc) { + return false; + } + static char tmp[CFG_SECTOR_SIZE]; + if (hdr->length + 1 > sizeof(tmp)) { + return false; + } + memcpy(tmp, data, hdr->length); + tmp[hdr->length] = '\0'; + parse_ini_text(tmp); + return true; +} + +static size_t build_ini(char *out, size_t outsz) +{ + int n = snprintf(out, outsz, + "# s1224.ini — operator texts + terminal + sense\n" + "[texts]\n" + "starting=%s\n" + "ready=%s\n" + "fault=%s\n" + "sending=%s\n" + "signal=%s\n" + "packet=%s\n" + "power=%s\n" + "idle=%s\n" + "[terminal]\n" + "callsign=%s\n" + "device_id=%s\n" + "quiet=%d\n" + "[sense]\n" + "cdt_active_high=%d\n" + "packet_hold_ms=%u\n" + "packet_edge_min=%u\n" + "packet_window_ms=%u\n", + g_cfg.starting, g_cfg.ready, g_cfg.fault, g_cfg.sending, + g_cfg.signal, g_cfg.packet, g_cfg.power, g_cfg.idle, + g_cfg.callsign, g_cfg.device_id, + (int)g_cfg.quiet, + (int)g_cfg.cdt_active_high, + (unsigned)g_cfg.packet_hold_ms, + (unsigned)g_cfg.packet_edge_min, + (unsigned)g_cfg.packet_window_ms); + if (n < 0) { + return 0; + } + if ((size_t)n >= outsz) { + return outsz - 1; + } + return (size_t)n; +} + +bool config_save_flash(void) +{ + char body[CFG_SECTOR_SIZE - sizeof(cfg_hdr_t)]; + size_t len = build_ini(body, sizeof(body)); + if (len == 0) { + return false; + } + uint8_t sector[CFG_SECTOR_SIZE]; + memset(sector, 0xFF, sizeof(sector)); + cfg_hdr_t *hdr = (cfg_hdr_t *)sector; + hdr->magic[0] = CFG_MAGIC0; + hdr->magic[1] = CFG_MAGIC1; + hdr->magic[2] = CFG_MAGIC2; + hdr->magic[3] = CFG_MAGIC3; + hdr->version = CFG_VERSION; + hdr->reserved = 0; + hdr->length = (uint16_t)len; + hdr->crc = crc16_ccitt((const uint8_t *)body, len); + memcpy(sector + sizeof(cfg_hdr_t), body, len); + + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(CFG_FLASH_OFFSET, CFG_SECTOR_SIZE); + flash_range_program(CFG_FLASH_OFFSET, sector, CFG_SECTOR_SIZE); + restore_interrupts(ints); + return true; +} + +bool config_erase_flash(void) +{ + uint32_t ints = save_and_disable_interrupts(); + flash_range_erase(CFG_FLASH_OFFSET, CFG_SECTOR_SIZE); + restore_interrupts(ints); + return true; +} + +void config_dump_ini(void) +{ + char body[CFG_SECTOR_SIZE - sizeof(cfg_hdr_t)]; + size_t len = build_ini(body, sizeof(body)); + if (len == 0) { + printf("(empty)\r\n"); + return; + } + /* print line by line for CDC */ + for (size_t i = 0; i < len; i++) { + putchar(body[i]); + if (body[i] == '\n') { + putchar('\r'); + } + } + if (len == 0 || body[len - 1] != '\n') { + printf("\r\n"); + } +} + +void config_init(void) +{ + config_load_defaults(); + bool flash_ok = config_load_flash(); + /* Extra chatter only when quiet=0 (fixed boot banner is separate / always on). */ + if (config_unsolicited_ok()) { + if (flash_ok) { + printf("CONFIG: flash INI loaded\r\n"); + } else { + printf("CONFIG: defaults (no flash overlay)\r\n"); + } + } +} + +const s1224_config_t *config_get(void) +{ + return &g_cfg; +} + +s1224_config_t *config_mutable(void) +{ + return &g_cfg; +} + +bool config_unsolicited_ok(void) +{ + return !g_cfg.quiet; +} diff --git a/firmware/s1224/src/common/crystal_steer.c b/firmware/s1224/src/common/crystal_steer.c new file mode 100644 index 0000000..863b7b1 --- /dev/null +++ b/firmware/s1224/src/common/crystal_steer.c @@ -0,0 +1,17 @@ +/* + * Softmodem product — no crystal mux / no TCM3105 XTAL steering. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "crystal_steer.h" + +static bool g_applied; + +void crystal_steer_apply(void) +{ + g_applied = true; +} + +bool crystal_steer_applied(void) +{ + return g_applied; +} diff --git a/firmware/s1224/src/common/display.c b/firmware/s1224/src/common/display.c new file mode 100644 index 0000000..e2875be --- /dev/null +++ b/firmware/s1224/src/common/display.c @@ -0,0 +1,187 @@ +/* + * HD44780 LCD via PCF8574 I2C backpack + USB mirror. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "display.h" +#include "board_pins.h" +#include "config.h" +#include "status.h" +#include "sense.h" +#include "ptt.h" +#include "hardware/i2c.h" +#include "pico/stdlib.h" +#include +#include +#include + +/* PCF8574 bit map (common backpack): P0=RS P1=RW P2=E P3=BL P4-7=D4-D7 */ +#define LCD_RS 0x01 +#define LCD_RW 0x02 +#define LCD_EN 0x04 +#define LCD_BL 0x08 + +static bool g_lcd; +static uint8_t g_addr = I2C_LCD_ADDR_DEFAULT; +static char g_last_l0[17]; +static char g_last_l1[17]; + +static bool i2c_write_u8(uint8_t addr, uint8_t v) +{ + return i2c_write_blocking(i2c0, addr, &v, 1, false) >= 0; +} + +static bool i2c_probe(uint8_t addr) +{ + uint8_t b = 0; + return i2c_write_blocking(i2c0, addr, &b, 1, false) >= 0; +} + +static void lcd_write4(uint8_t nibble, uint8_t mode) +{ + uint8_t data = (uint8_t)((nibble & 0xF0) | LCD_BL | mode); + i2c_write_u8(g_addr, (uint8_t)(data | LCD_EN)); + sleep_us(1); + i2c_write_u8(g_addr, (uint8_t)(data & (uint8_t)~LCD_EN)); + sleep_us(50); +} + +static void lcd_send(uint8_t value, uint8_t mode) +{ + lcd_write4((uint8_t)(value & 0xF0), mode); + lcd_write4((uint8_t)((value << 4) & 0xF0), mode); +} + +static void lcd_cmd(uint8_t c) +{ + lcd_send(c, 0); +} + +static void lcd_data(uint8_t c) +{ + lcd_send(c, LCD_RS); +} + +static void lcd_set_cursor(uint8_t col, uint8_t row) +{ + static const uint8_t row_addr[] = {0x00, 0x40, 0x14, 0x54}; + if (row > 1) { + row = 1; + } + lcd_cmd((uint8_t)(0x80 | (row_addr[row] + col))); +} + +static void lcd_print_line(uint8_t row, const char *s) +{ + char buf[17]; + size_t n = strlen(s); + if (n > 16) { + n = 16; + } + memset(buf, ' ', 16); + memcpy(buf, s, n); + buf[16] = '\0'; + lcd_set_cursor(0, row); + for (int i = 0; i < 16; i++) { + lcd_data((uint8_t)buf[i]); + } +} + +static bool lcd_init_seq(uint8_t addr) +{ + g_addr = addr; + sleep_ms(50); + lcd_write4(0x30, 0); + sleep_ms(5); + lcd_write4(0x30, 0); + sleep_us(150); + lcd_write4(0x30, 0); + sleep_us(150); + lcd_write4(0x20, 0); /* 4-bit */ + lcd_cmd(0x28); /* 2 line, 5x8 */ + lcd_cmd(0x0C); /* display on, cursor off */ + lcd_cmd(0x06); /* entry mode */ + lcd_cmd(0x01); /* clear */ + sleep_ms(2); + return true; +} + +void display_init(void) +{ + g_lcd = false; + g_last_l0[0] = '\0'; + g_last_l1[0] = '\0'; + + uint8_t addrs[] = {0x27, 0x3F}; + for (unsigned i = 0; i < sizeof(addrs); i++) { + if (i2c_probe(addrs[i])) { + if (lcd_init_seq(addrs[i])) { + g_lcd = true; + if (config_unsolicited_ok()) { + printf("LCD: HD44780 @ 0x%02X\r\n", addrs[i]); + } + return; + } + } + } + if (config_unsolicited_ok()) { + printf("LCD: none — USB status only\r\n"); + } +} + +bool display_present(void) +{ + return g_lcd; +} + +void display_show(const char *line) +{ + if (!line) { + return; + } + if (g_lcd) { + lcd_print_line(0, line); + } +} + +void display_update(void) +{ + const s1224_config_t *cfg = config_get(); + char l0[17]; + char l1[17]; + + s1224_state_t st = status_get(); + if (st == S1224_STATE_FAULT) { + snprintf(l0, sizeof(l0), "%.16s", cfg->fault); + } else if (ptt_get()) { + snprintf(l0, sizeof(l0), "%.16s", cfg->sending); + } else if (st == S1224_STATE_STARTING) { + snprintf(l0, sizeof(l0), "%.16s", cfg->starting); + } else { + /* Power/ready on line 0 */ + snprintf(l0, sizeof(l0), "%.16s", cfg->ready); + } + + /* Line 1: Signal + Packet from sense */ + bool sig = sense_signal_active(); + bool pkt = sense_packet_active(); + if (sig && pkt) { + snprintf(l1, sizeof(l1), "%.7s %.7s", cfg->signal, cfg->packet); + } else if (sig) { + snprintf(l1, sizeof(l1), "%.16s", cfg->signal); + } else if (pkt) { + snprintf(l1, sizeof(l1), "%.16s", cfg->packet); + } else { + snprintf(l1, sizeof(l1), "%.16s", cfg->power); + } + + if (strcmp(l0, g_last_l0) != 0 || strcmp(l1, g_last_l1) != 0) { + strncpy(g_last_l0, l0, sizeof(g_last_l0)); + strncpy(g_last_l1, l1, sizeof(g_last_l1)); + g_last_l0[16] = '\0'; + g_last_l1[16] = '\0'; + if (g_lcd) { + lcd_print_line(0, l0); + lcd_print_line(1, l1); + } + } +} diff --git a/firmware/s1224/src/common/hdlc.c b/firmware/s1224/src/common/hdlc.c new file mode 100644 index 0000000..3cee0da --- /dev/null +++ b/firmware/s1224/src/common/hdlc.c @@ -0,0 +1,192 @@ +/* + * HDLC encode/decode — AX.25 bit order (LSB first), FCS-CCITT. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "hdlc.h" + +uint16_t hdlc_fcs_update(uint16_t fcs, uint8_t byte) +{ + fcs ^= (uint16_t)byte; + for (int i = 0; i < 8; i++) { + if (fcs & 1u) { + fcs = (uint16_t)((fcs >> 1) ^ 0x8408u); + } else { + fcs >>= 1; + } + } + return fcs; +} + +uint16_t hdlc_fcs_final(uint16_t fcs) +{ + return (uint16_t)~fcs; +} + +static bool push_bit(uint8_t *bits, size_t bits_cap, size_t *n, bool v) +{ + if (*n >= bits_cap) { + return false; + } + bits[*n] = v ? 1u : 0u; + (*n)++; + return true; +} + +static bool stuff_bit(uint8_t *bits, size_t bits_cap, size_t *n, + unsigned *ones, bool bit) +{ + if (!push_bit(bits, bits_cap, n, bit)) { + return false; + } + if (bit) { + (*ones)++; + if (*ones == 5u) { + if (!push_bit(bits, bits_cap, n, false)) { + return false; + } + *ones = 0; + } + } else { + *ones = 0; + } + return true; +} + +static bool emit_raw_byte(uint8_t *bits, size_t bits_cap, size_t *n, uint8_t b) +{ + for (int i = 0; i < 8; i++) { + if (!push_bit(bits, bits_cap, n, (b >> i) & 1u)) { + return false; + } + } + return true; +} + +static bool emit_stuffed_byte(uint8_t *bits, size_t bits_cap, size_t *n, + unsigned *ones, uint8_t b) +{ + for (int i = 0; i < 8; i++) { + if (!stuff_bit(bits, bits_cap, n, ones, (b >> i) & 1u)) { + return false; + } + } + return true; +} + +size_t hdlc_encode_bits(const uint8_t *info, size_t info_len, + uint8_t *bits, size_t bits_cap, + unsigned trailing_flags) +{ + if (!info || !bits || info_len > HDLC_MAX_INFO) { + return 0; + } + size_t n = 0; + unsigned ones = 0; + + if (!emit_raw_byte(bits, bits_cap, &n, HDLC_FLAG)) { + return 0; + } + + uint16_t fcs = HDLC_FCS_INIT; + for (size_t i = 0; i < info_len; i++) { + fcs = hdlc_fcs_update(fcs, info[i]); + if (!emit_stuffed_byte(bits, bits_cap, &n, &ones, info[i])) { + return 0; + } + } + fcs = hdlc_fcs_final(fcs); + if (!emit_stuffed_byte(bits, bits_cap, &n, &ones, (uint8_t)(fcs & 0xFFu))) { + return 0; + } + if (!emit_stuffed_byte(bits, bits_cap, &n, &ones, (uint8_t)((fcs >> 8) & 0xFFu))) { + return 0; + } + + ones = 0; + unsigned flags = trailing_flags ? trailing_flags : 1u; + for (unsigned f = 0; f < flags; f++) { + if (!emit_raw_byte(bits, bits_cap, &n, HDLC_FLAG)) { + return 0; + } + } + return n; +} + +void hdlc_rx_init(hdlc_rx_t *rx) +{ + rx->len = 0; + rx->ones = 0; + rx->in_frame = false; + rx->stuffing = false; + rx->byte_acc = 0; + rx->bit_count = 0; +} + +bool hdlc_rx_bit(hdlc_rx_t *rx, bool bit, uint8_t *out, size_t *out_len) +{ + if (bit) { + rx->ones++; + } else { + if (rx->ones == 5u) { + /* stuffed 0 — discard */ + rx->ones = 0; + return false; + } + if (rx->ones == 6u) { + /* flag */ + if (rx->in_frame && rx->len >= 3) { + uint16_t fcs = HDLC_FCS_INIT; + for (size_t i = 0; i < rx->len; i++) { + fcs = hdlc_fcs_update(fcs, rx->buf[i]); + } + if (fcs == HDLC_FCS_GOOD) { + size_t info_len = rx->len - 2; + for (size_t i = 0; i < info_len; i++) { + out[i] = rx->buf[i]; + } + *out_len = info_len; + rx->len = 0; + rx->bit_count = 0; + rx->byte_acc = 0; + rx->ones = 0; + rx->in_frame = true; /* abutting frames */ + return true; + } + } + rx->len = 0; + rx->bit_count = 0; + rx->byte_acc = 0; + rx->ones = 0; + rx->in_frame = true; + return false; + } + if (rx->ones > 6u) { + /* abort */ + rx->in_frame = false; + rx->len = 0; + rx->bit_count = 0; + rx->byte_acc = 0; + rx->ones = 0; + return false; + } + rx->ones = 0; + } + + if (!rx->in_frame) { + return false; + } + + rx->byte_acc |= (uint8_t)((bit ? 1u : 0u) << rx->bit_count); + rx->bit_count++; + if (rx->bit_count >= 8) { + if (rx->len < sizeof(rx->buf)) { + rx->buf[rx->len++] = rx->byte_acc; + } else { + rx->in_frame = false; + rx->len = 0; + } + rx->byte_acc = 0; + rx->bit_count = 0; + } + return false; +} diff --git a/firmware/s1224/src/common/kiss.c b/firmware/s1224/src/common/kiss.c new file mode 100644 index 0000000..1d5d0e7 --- /dev/null +++ b/firmware/s1224/src/common/kiss.c @@ -0,0 +1,98 @@ +/* + * KISS encode/decode. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "kiss.h" + +void kiss_rx_init(kiss_rx_t *kx) +{ + kx->len = 0; + kx->in_frame = false; + kx->esc = false; +} + +bool kiss_rx_byte(kiss_rx_t *kx, uint8_t b, uint8_t *out, size_t *out_len) +{ + if (b == KISS_FEND) { + if (kx->in_frame && kx->len >= 1) { + uint8_t cmd = kx->buf[0]; + if ((cmd & 0x0Fu) == KISS_CMD_DATA && kx->len > 1) { + size_t n = kx->len - 1; + for (size_t i = 0; i < n; i++) { + out[i] = kx->buf[i + 1]; + } + *out_len = n; + kx->len = 0; + kx->esc = false; + kx->in_frame = true; + return true; + } + } + kx->len = 0; + kx->esc = false; + kx->in_frame = true; + return false; + } + if (!kx->in_frame) { + return false; + } + if (kx->esc) { + if (b == KISS_TFEND) { + b = KISS_FEND; + } else if (b == KISS_TFESC) { + b = KISS_FESC; + } + kx->esc = false; + } else if (b == KISS_FESC) { + kx->esc = true; + return false; + } + if (kx->len < sizeof(kx->buf)) { + kx->buf[kx->len++] = b; + } else { + kx->in_frame = false; + kx->len = 0; + } + return false; +} + +static bool push(uint8_t *dst, size_t cap, size_t *n, uint8_t b) +{ + if (*n >= cap) { + return false; + } + dst[(*n)++] = b; + return true; +} + +static bool push_esc(uint8_t *dst, size_t cap, size_t *n, uint8_t b) +{ + if (b == KISS_FEND) { + return push(dst, cap, n, KISS_FESC) && push(dst, cap, n, KISS_TFEND); + } + if (b == KISS_FESC) { + return push(dst, cap, n, KISS_FESC) && push(dst, cap, n, KISS_TFESC); + } + return push(dst, cap, n, b); +} + +size_t kiss_encode_data(const uint8_t *payload, size_t len, + uint8_t *dst, size_t dst_cap) +{ + size_t n = 0; + if (!push(dst, dst_cap, &n, KISS_FEND)) { + return 0; + } + if (!push_esc(dst, dst_cap, &n, KISS_CMD_DATA)) { + return 0; + } + for (size_t i = 0; i < len; i++) { + if (!push_esc(dst, dst_cap, &n, payload[i])) { + return 0; + } + } + if (!push(dst, dst_cap, &n, KISS_FEND)) { + return 0; + } + return n; +} diff --git a/firmware/s1224/src/common/main.c b/firmware/s1224/src/common/main.c new file mode 100644 index 0000000..ae17585 --- /dev/null +++ b/firmware/s1224/src/common/main.c @@ -0,0 +1,61 @@ +/* + * T-Modem-s1224 — USB KISS half-TNC + softmodem AFSK (PWM TX / Goertzel RX). + * Three UF2 builds (300 / 1200 / 2400). No TCM3105 chip path. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "pico/stdlib.h" +#include "board_pins.h" +#include "status.h" +#include "ptt.h" +#include "modem_probe.h" +#include "usb_cmd.h" +#include "display.h" +#include "crystal_steer.h" +#include "baud_config.h" +#include "config.h" +#include "sense.h" +#include "afsk_soft.h" +#include "banner.h" +#include "hardware/gpio.h" +#include + +int main(void) +{ + stdio_init_all(); + sleep_ms(800); + + board_pins_init(); + ptt_init(); + config_init(); + display_init(); + sense_init(); + afsk_soft_init(); + usb_cmd_init(); + afsk_rx_set_callback(usb_cmd_on_rx_frame); + + status_set(S1224_STATE_STARTING); + status_publish(); + /* Fixed boot banner — always once after CDC ready; source-only (banner.c). */ + banner_print(); + + modem_probe_result_t probe = modem_probe_run(); + if (probe.ready) { + status_set(S1224_STATE_READY); + } else { + status_set(S1224_STATE_FAULT); + } + status_publish(); + + while (true) { + usb_cmd_poll(); + afsk_rx_poll(); + sense_poll(); + + gpio_put(PIN_LED_DCD, sense_signal_active() ? 1 : 0); + gpio_put(PIN_LED_PTT, ptt_get() ? 1 : 0); + gpio_put(PIN_LED_TXRX, sense_packet_active() ? 1 : 0); + + status_tick(); + sleep_us(50); + } +} diff --git a/firmware/s1224/src/common/modem_probe.c b/firmware/s1224/src/common/modem_probe.c new file mode 100644 index 0000000..9624d32 --- /dev/null +++ b/firmware/s1224/src/common/modem_probe.c @@ -0,0 +1,22 @@ +/* + * Softmodem health — no chip/crystal checks. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "modem_probe.h" +#include "config.h" +#include "afsk_soft.h" +#include + +modem_probe_result_t modem_probe_run(void) +{ + modem_probe_result_t r = {0}; + r.softmodem_ok = true; + r.af_tx_pwm = true; + r.ready = r.softmodem_ok && (afsk_bitrate() == 300u || afsk_bitrate() == 1200u || afsk_bitrate() == 2400u); + + if (config_unsolicited_ok()) { + printf("PROBE softmodem=%d bitrate=%u -> %s\r\n", + (int)r.softmodem_ok, afsk_bitrate(), r.ready ? "READY" : "FAULT"); + } + return r; +} diff --git a/firmware/s1224/src/common/ptt.c b/firmware/s1224/src/common/ptt.c new file mode 100644 index 0000000..f54f221 --- /dev/null +++ b/firmware/s1224/src/common/ptt.c @@ -0,0 +1,31 @@ +/* + * PTT via Pico GPIO — host asserts over USB CDC (Type-A Path B), not softmodem. + * PIN_PTT_DRV = keying (required). PIN_LED_PTT = optional/spare TX LED mirror. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "ptt.h" +#include "board_pins.h" +#include "hardware/gpio.h" + +static bool g_ptt; + +void ptt_init(void) +{ + gpio_init(PIN_PTT_DRV); + gpio_set_dir(PIN_PTT_DRV, GPIO_OUT); + gpio_put(PIN_PTT_DRV, 0); + g_ptt = false; + gpio_put(PIN_LED_PTT, 0); +} + +void ptt_set(bool on) +{ + g_ptt = on; + gpio_put(PIN_PTT_DRV, on ? 1 : 0); + gpio_put(PIN_LED_PTT, on ? 1 : 0); +} + +bool ptt_get(void) +{ + return g_ptt; +} diff --git a/firmware/s1224/src/common/sense.c b/firmware/s1224/src/common/sense.c new file mode 100644 index 0000000..376004d --- /dev/null +++ b/firmware/s1224/src/common/sense.c @@ -0,0 +1,72 @@ +/* + * Sense for softmodem: carrier from demod stub + optional GP7 heuristic. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "sense.h" +#include "board_pins.h" +#include "config.h" +#include "afsk_soft.h" +#include "hardware/gpio.h" +#include "pico/stdlib.h" +#include + +static bool g_signal; +static bool g_packet; +static bool g_flag; +static absolute_time_t g_packet_until; +static absolute_time_t g_window_start; +static uint16_t g_edges; +static bool g_last_rxd; +static bool g_hold_valid; + +void sense_init(void) +{ + g_signal = false; + g_packet = false; + g_flag = false; + g_hold_valid = false; + g_window_start = get_absolute_time(); + g_edges = 0; + g_last_rxd = gpio_get(PIN_MODEM_RXD); +} + +void sense_poll(void) +{ + const s1224_config_t *cfg = config_get(); + + g_signal = afsk_rx_carrier_active(); + + bool rxd = gpio_get(PIN_MODEM_RXD); + if (rxd != g_last_rxd) { + g_edges++; + g_last_rxd = rxd; + } + + uint32_t win_ms = cfg->packet_window_ms ? cfg->packet_window_ms : 50; + if (absolute_time_diff_us(g_window_start, get_absolute_time()) >= + (int64_t)win_ms * 1000) { + if (g_edges >= cfg->packet_edge_min) { + g_packet_until = make_timeout_time_ms( + cfg->packet_hold_ms ? cfg->packet_hold_ms : 500); + g_hold_valid = true; + } + g_edges = 0; + g_window_start = get_absolute_time(); + } + + if (g_hold_valid) { + if (time_reached(g_packet_until)) { + g_hold_valid = false; + g_packet = false; + g_flag = false; + } else { + g_packet = true; + } + } else { + g_packet = false; + } +} + +bool sense_signal_active(void) { return g_signal; } +bool sense_packet_active(void) { return g_packet; } +bool sense_packet_flag(void) { return g_flag; } diff --git a/firmware/s1224/src/common/status.c b/firmware/s1224/src/common/status.c new file mode 100644 index 0000000..ca4df5c --- /dev/null +++ b/firmware/s1224/src/common/status.c @@ -0,0 +1,78 @@ +/* + * Status from PTT + dual sense; texts from INI. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "status.h" +#include "config.h" +#include "display.h" +#include "ptt.h" +#include "sense.h" +#include + +static s1224_state_t g_state = S1224_STATE_STARTING; + +const char *s1224_state_string(s1224_state_t st) +{ + const s1224_config_t *cfg = config_get(); + switch (st) { + case S1224_STATE_STARTING: + return cfg->starting; + case S1224_STATE_READY: + return cfg->ready; + case S1224_STATE_FAULT: + return cfg->fault; + case S1224_STATE_SENDING: + return cfg->sending; + case S1224_STATE_SIGNAL: + return cfg->signal; + case S1224_STATE_PACKET: + return cfg->packet; + default: + return cfg->fault; + } +} + +void status_set(s1224_state_t st) +{ + g_state = st; +} + +s1224_state_t status_get(void) +{ + return g_state; +} + +void status_publish(void) +{ + const char *s = s1224_state_string(g_state); + if (config_unsolicited_ok()) { + printf("STATUS %s\r\n", s); + } + display_update(); +} + +void status_tick(void) +{ + if (g_state == S1224_STATE_FAULT || g_state == S1224_STATE_STARTING) { + display_update(); + return; + } + + s1224_state_t next; + if (ptt_get()) { + next = S1224_STATE_SENDING; + } else if (sense_packet_active()) { + next = S1224_STATE_PACKET; + } else if (sense_signal_active()) { + next = S1224_STATE_SIGNAL; + } else { + next = S1224_STATE_READY; + } + + if (next != g_state) { + g_state = next; + status_publish(); + } else { + display_update(); + } +} diff --git a/firmware/s1224/src/common/usb_cmd.c b/firmware/s1224/src/common/usb_cmd.c new file mode 100644 index 0000000..e4a5fc7 --- /dev/null +++ b/firmware/s1224/src/common/usb_cmd.c @@ -0,0 +1,267 @@ +/* + * USB CDC multiplexer: KISS binary (0xC0 frames) + ASCII service commands. + * Transparent Host↔Chip bit bridge stays abandoned — KISS half-TNC is the exception. + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#include "usb_cmd.h" +#include "pico/stdlib.h" +#include "ptt.h" +#include "status.h" +#include "config.h" +#include "sense.h" +#include "board_pins.h" +#include "afsk_soft.h" +#include "kiss.h" +#include "hardware/gpio.h" +#include +#include +#include + +#define LINE_MAX 96 + +static char g_line[LINE_MAX]; +static unsigned g_len; +static kiss_rx_t g_kiss; +static uint8_t g_kiss_payload[KISS_MAX_PAYLOAD]; + +static void trim_copy_upper_cmd(char *dst, size_t dstsz, const char *src) +{ + while (*src && isspace((unsigned char)*src)) { + src++; + } + size_t i = 0; + while (src[i] && i + 1 < dstsz) { + dst[i] = (char)toupper((unsigned char)src[i]); + i++; + } + dst[i] = '\0'; + while (i > 0 && isspace((unsigned char)dst[i - 1])) { + dst[--i] = '\0'; + } +} + +static void handle_line(void) +{ + char cmd[LINE_MAX]; + trim_copy_upper_cmd(cmd, sizeof(cmd), g_line); + if (cmd[0] == '\0') { + return; + } + + if (strcmp(cmd, "PTT ON") == 0 || strcmp(cmd, "PTT 1") == 0) { + ptt_set(true); + status_tick(); + printf("OK PTT ON\r\n"); + return; + } + if (strcmp(cmd, "PTT OFF") == 0 || strcmp(cmd, "PTT 0") == 0) { + ptt_set(false); + status_tick(); + printf("OK PTT OFF\r\n"); + return; + } + if (strcmp(cmd, "STATUS") == 0) { + printf("STATUS state=%s ptt=%d signal=%d packet=%d flag=%d " + "carrier=%d bitrate=%u softmodem=1 kiss_half_tnc=1\r\n", + s1224_state_string(status_get()), + (int)ptt_get(), + (int)sense_signal_active(), + (int)sense_packet_active(), + (int)sense_packet_flag(), + (int)afsk_rx_carrier_active(), + afsk_bitrate()); + return; + } + if (strcmp(cmd, "HELP") == 0) { + printf("Commands:\r\n"); + printf(" PTT ON | PTT OFF\r\n"); + printf(" STATUS | HELP | ID\r\n"); + printf(" QUIET ON|OFF | VERBOSE ON|OFF\r\n"); + printf(" INI | GET | SET =\r\n"); + printf(" SAVE | RELOAD | DEFAULTS\r\n"); + printf("Dual protocol: byte 0xC0 → KISS; line ASCII → service.\r\n"); + printf("Packet: use KISS-only (any host). Service cmds optional.\r\n"); + printf("Boot banner always on (source). quiet=1 damps extra ASCII.\r\n"); + printf("PTT = GP9 (auto during KISS TX; manual PTT ON/OFF kept).\r\n"); + printf("Softmodem AFSK · S1224_BITRATE=%u (build: 300/1200/2400).\r\n", + afsk_bitrate()); + printf("PWM AF_TX GP6 · ADC AF_RX GP26 · no chip modem · no baud switch.\r\n"); + printf("INI: texts + callsign device_id quiet + sense keys\r\n"); + return; + } + if (strcmp(cmd, "ID") == 0) { + const s1224_config_t *cfg = config_get(); + printf("ID device_id=%s callsign=%s bitrate=%u quiet=%d\r\n", + cfg->device_id[0] ? cfg->device_id : "s1224", + cfg->callsign, + afsk_bitrate(), + (int)cfg->quiet); + return; + } + if (strcmp(cmd, "QUIET ON") == 0 || strcmp(cmd, "QUIET 1") == 0) { + config_mutable()->quiet = true; + printf("OK QUIET ON\r\n"); + return; + } + if (strcmp(cmd, "QUIET OFF") == 0 || strcmp(cmd, "QUIET 0") == 0) { + config_mutable()->quiet = false; + printf("OK QUIET OFF\r\n"); + return; + } + if (strcmp(cmd, "VERBOSE ON") == 0 || strcmp(cmd, "VERBOSE 1") == 0) { + config_mutable()->quiet = false; + printf("OK VERBOSE ON\r\n"); + return; + } + if (strcmp(cmd, "VERBOSE OFF") == 0 || strcmp(cmd, "VERBOSE 0") == 0) { + config_mutable()->quiet = true; + printf("OK VERBOSE OFF\r\n"); + return; + } + if (strcmp(cmd, "INI") == 0) { + config_dump_ini(); + return; + } + if (strcmp(cmd, "SAVE") == 0) { + printf(config_save_flash() ? "OK SAVE\r\n" : "ERR SAVE\r\n"); + return; + } + if (strcmp(cmd, "RELOAD") == 0) { + config_load_defaults(); + if (config_load_flash()) { + printf("OK RELOAD flash\r\n"); + } else { + printf("OK RELOAD defaults (no flash)\r\n"); + } + status_publish(); + return; + } + if (strcmp(cmd, "DEFAULTS") == 0) { + config_load_defaults(); + printf("OK DEFAULTS (RAM; SAVE to persist)\r\n"); + status_publish(); + return; + } + if (strncmp(cmd, "GET ", 4) == 0) { + char key[32]; + const char *p = g_line; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (strncasecmp(p, "GET", 3) == 0) { + p += 3; + } + while (*p && isspace((unsigned char)*p)) { + p++; + } + size_t i = 0; + while (*p && !isspace((unsigned char)*p) && i + 1 < sizeof(key)) { + key[i++] = *p++; + } + key[i] = '\0'; + char val[S1224_TEXT_MAX]; + if (config_get_kv(key, val, sizeof(val))) { + printf("OK %s=%s\r\n", key, val); + } else { + printf("ERR unknown key\r\n"); + } + return; + } + if (strncmp(cmd, "SET ", 4) == 0) { + const char *p = g_line; + while (*p && isspace((unsigned char)*p)) { + p++; + } + if (strncasecmp(p, "SET", 3) == 0) { + p += 3; + } + while (*p && isspace((unsigned char)*p)) { + p++; + } + char tmp[LINE_MAX]; + snprintf(tmp, sizeof(tmp), "%s", p); + char *eq = strchr(tmp, '='); + if (!eq) { + printf("ERR SET key=value\r\n"); + return; + } + *eq = '\0'; + char *k = tmp; + char *v = eq + 1; + while (*k && isspace((unsigned char)*k)) { + k++; + } + char *ke = k + strlen(k); + while (ke > k && isspace((unsigned char)ke[-1])) { + *--ke = '\0'; + } + while (*v && isspace((unsigned char)*v)) { + v++; + } + char *ve = v + strlen(v); + while (ve > v && isspace((unsigned char)ve[-1])) { + *--ve = '\0'; + } + if (config_set_kv(k, v)) { + printf("OK SET %s=%s (SAVE to persist)\r\n", k, v); + status_publish(); + } else { + printf("ERR unknown key or bad value\r\n"); + } + return; + } + + printf("ERR unknown (try HELP)\r\n"); +} + +void usb_cmd_on_rx_frame(const uint8_t *info, size_t len) +{ + uint8_t frame[KISS_MAX_PAYLOAD + 16]; + size_t n = kiss_encode_data(info, len, frame, sizeof(frame)); + for (size_t i = 0; i < n; i++) { + putchar_raw((int)frame[i]); + } +} + +void usb_cmd_poll(void) +{ + int c; + while ((c = getchar_timeout_us(0)) != PICO_ERROR_TIMEOUT) { + uint8_t b = (uint8_t)c; + + /* KISS path: FEND or mid-frame */ + if (b == KISS_FEND || g_kiss.in_frame) { + size_t plen = 0; + if (kiss_rx_byte(&g_kiss, b, g_kiss_payload, &plen)) { + if (plen > 0 && !afsk_tx_busy()) { + afsk_tx_frame(g_kiss_payload, plen); + } + } + continue; + } + + if (c == '\r' || c == '\n') { + if (g_len > 0) { + g_line[g_len] = '\0'; + handle_line(); + g_len = 0; + } + continue; + } + if (c == 0x08 || c == 0x7f) { + if (g_len > 0) { + g_len--; + } + continue; + } + if (g_len + 1 < LINE_MAX && c >= 32 && c < 127) { + g_line[g_len++] = (char)c; + } + } +} + +void usb_cmd_init(void) +{ + g_len = 0; + kiss_rx_init(&g_kiss); +} -- cgit v1.3.1