summaryrefslogtreecommitdiff
path: root/firmware/s1224/src/common/afsk_soft.c
blob: 3e5d966e5adb34ac78a963619d6ea99c82dc1529 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
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();
}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com