diff options
Diffstat (limited to 'firmware/s1224/src/common/afsk_soft.c')
| -rw-r--r-- | firmware/s1224/src/common/afsk_soft.c | 224 |
1 files changed, 224 insertions, 0 deletions
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 <string.h> +#include <math.h> + +#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(); +} |
