target/fw/layer1: Add initial version of TCH primitives
authorSylvain Munaut <tnt@246tNt.com>
Sun, 19 Sep 2010 13:49:48 +0000 (15:49 +0200)
committerSylvain Munaut <tnt@246tNt.com>
Mon, 25 Oct 2010 18:58:32 +0000 (20:58 +0200)
The initial bringup is mainly Dieter Spaar's work. I took the
logic and rewrote it, adapting to later scheduler changes and
adding support for several other things (tch_mode, initial HR
support, various cleanup, ...).

Initially-Written-by: Dieter Spaar <spaar@mirider.augusta.de>
Signed-off-by: Sylvain Munaut <tnt@246tNt.com>
src/target/firmware/include/layer1/prim.h
src/target/firmware/include/layer1/sync.h
src/target/firmware/layer1/Makefile
src/target/firmware/layer1/prim_tch.c [new file with mode: 0644]

index e9823de..c8e49a6 100644 (file)
@@ -26,4 +26,7 @@ void l1a_rach_req(uint8_t fn51, uint8_t mf_off, uint8_t ra);
 extern const struct tdma_sched_item nb_sched_set[];
 extern const struct tdma_sched_item nb_sched_set_ul[];
 
+extern const struct tdma_sched_item tch_sched_set[];
+extern const struct tdma_sched_item tch_a_sched_set[];
+
 #endif /* _L1_PRIM_H */
index f60966f..06ce6a0 100644 (file)
@@ -34,6 +34,7 @@ enum l1_compl {
        L1_COMPL_FB,
        L1_COMPL_RACH,
        L1_COMPL_TX_NB,
+       L1_COMPL_TX_TCH,
 };
 
 typedef void l1_compl_cb(enum l1_compl c);
@@ -75,8 +76,9 @@ struct l1s_state {
        int8_t          ta;
        uint8_t         tx_power;
 
-       /* TCH mode */
+       /* TCH */
        uint8_t         tch_mode;
+       uint8_t         tch_sync;
 
        /* Transmit queues of pending packets for main DCCH and ACCH */
        struct llist_head tx_queue[_NUM_L1S_CHAN];
index d6bcbff..4f834ea 100644 (file)
@@ -5,5 +5,5 @@ layer1_SRCS=avg.c agc.c afc.c sync.c tdma_sched.c tpu_window.c init.c l23_api.c
                mframe_sched.c sched_gsmtime.c async.c rfch.c apc.c
 
 layer1_SRCS += prim_pm.c prim_rach.c prim_tx_nb.c prim_rx_nb.c prim_fbsb.c \
-               prim_freq.c prim_utils.c
+               prim_freq.c prim_utils.c prim_tch.c
 
diff --git a/src/target/firmware/layer1/prim_tch.c b/src/target/firmware/layer1/prim_tch.c
new file mode 100644 (file)
index 0000000..4a3b363
--- /dev/null
@@ -0,0 +1,598 @@
+/* Layer 1 - TCH */
+
+/* (C) 2010 by Dieter Spaar <spaar@mirider.augusta.de>
+ * (C) 2010 by Sylvain Munaut <tnt@246tnt.com>
+ *
+ * All Rights Reserved
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <defines.h>
+#include <debug.h>
+#include <memory.h>
+#include <byteorder.h>
+#include <osmocore/gsm_utils.h>
+#include <osmocore/protocol/gsm_04_08.h>
+#include <osmocore/msgb.h>
+#include <calypso/dsp_api.h>
+#include <calypso/irq.h>
+#include <calypso/tpu.h>
+#include <calypso/tsp.h>
+#include <calypso/dsp.h>
+#include <calypso/timer.h>
+#include <comm/sercomm.h>
+
+#include <rffe.h>
+#include <layer1/sync.h>
+#include <layer1/afc.h>
+#include <layer1/agc.h>
+#include <layer1/tdma_sched.h>
+#include <layer1/mframe_sched.h>
+#include <layer1/tpu_window.h>
+#include <layer1/l23_api.h>
+#include <layer1/rfch.h>
+#include <layer1/prim.h>
+
+#include <l1ctl_proto.h>
+
+
+/* This computes various parameters both for the DSP and for
+ * our logic. Not all are used all the time, but it's easier
+ * to build all in one place */
+static void tch_get_params(struct gsm_time *time, uint8_t chan_nr,
+                           uint32_t *fn_report, uint8_t *tch_f_hn,
+                           uint8_t *tch_sub, uint8_t *tch_mode)
+{
+       uint8_t tn = chan_nr & 0x07;
+       uint8_t cbits = chan_nr >> 3;
+
+       *tch_f_hn = (cbits & 2) ? 0 : 1;
+
+       if (*tch_f_hn) {
+               *fn_report = (time->fn - (tn * 13) + 104) % 104;
+               *tch_sub = 0;
+       } else {
+               uint8_t chan_sub = cbits & 1;
+               uint8_t tn_report = (tn & ~1) | chan_sub;
+               *fn_report = (time->fn - (tn_report * 13) + 104) % 104;
+               *tch_sub = chan_sub;
+       }
+
+       if (tch_mode) {
+               switch (l1s.tch_mode) {
+               case GSM48_CMODE_SPEECH_V1:
+                       *tch_mode = *tch_f_hn ? TCH_FS_MODE : TCH_HS_MODE;
+                       break;
+               case GSM48_CMODE_SPEECH_EFR:
+                       *tch_mode = *tch_f_hn ? TCH_EFR_MODE : SIG_ONLY_MODE;
+                       break;
+               default:
+                       *tch_mode = SIG_ONLY_MODE;
+               }
+       }
+}
+
+
+/* -------------------------------------------------------------------------
+ * Shared completion handler
+ * ------------------------------------------------------------------------- */
+
+/*
+ * FIXME We really need a better way to handle completion, where we can
+ *       pass arguments and such ...
+ *
+ *       Right now, we just 'hope' it gets processed before the next one ...
+ */
+
+static uint16_t last_tx_tch_fn;
+
+static void l1a_tx_tch_compl(__unused enum l1_compl c)
+{
+       struct msgb *msg;
+
+       msg = l1_create_l2_msg(L1CTL_DATA_CONF, last_tx_tch_fn, 0, 0);
+       l1_queue_for_l2(msg);
+}
+
+static __attribute__ ((constructor)) void prim_tch_init(void)
+{
+       l1s.completion[L1_COMPL_TX_TCH]  = &l1a_tx_tch_compl;
+}
+
+
+/* -------------------------------------------------------------------------
+ * TCH: Voice & FACCH
+ * ------------------------------------------------------------------------- */
+
+/*
+ * Voice and FACCH data are spread in various ways depending on a lot of
+ * factors. Trying to handle that with the mframe scheduler is just a mess,
+ * so we schedule it burst by burst and handle the complex logic inside the
+ * primitive task code itself.
+ */
+
+
+#define FACCH_MEAS_HIST        8       /* Up to 8 bursts history */
+struct l1s_rx_tch_state {
+       struct l1s_meas_hdr meas[FACCH_MEAS_HIST];
+};
+
+static struct l1s_rx_tch_state rx_tch;
+
+
+static int l1s_tch_resp(__unused uint8_t p1, __unused uint8_t p2, uint16_t p3)
+{
+       static uint8_t meas_id = 0;
+       uint8_t mf_task_id = p3 & 0xff;
+       struct gsm_time rx_time;
+       uint8_t chan_nr;
+       uint16_t arfcn;
+       uint8_t tsc, tn;
+       uint8_t tch_f_hn, tch_sub;
+       uint32_t fn_report;
+       int facch_rx_now, traffic_rx_now;
+
+       /* Get/compute various parameters */
+       gsm_fn2gsmtime(&rx_time, (l1s.current_time.fn - 1 + GSM_MAX_FN) % GSM_MAX_FN);
+       rfch_get_params(&rx_time, &arfcn, &tsc, &tn);
+       chan_nr = mframe_task2chan_nr(mf_task_id, tn);
+       tch_get_params(&rx_time, chan_nr, &fn_report, &tch_f_hn, &tch_sub, NULL);
+
+       meas_id = (meas_id + 1) % FACCH_MEAS_HIST; /* absolute value doesn't matter */
+
+       /* Collect measurements */
+       rx_tch.meas[meas_id].toa_qbit = dsp_api.db_r->a_serv_demod[D_TOA];
+       rx_tch.meas[meas_id].pm_dbm8 =
+               agc_inp_dbm8_by_pm(dsp_api.db_r->a_serv_demod[D_PM] >> 3);
+       rx_tch.meas[meas_id].freq_err =
+               ANGLE_TO_FREQ(dsp_api.db_r->a_serv_demod[D_ANGLE]);
+       rx_tch.meas[meas_id].snr = dsp_api.db_r->a_serv_demod[D_SNR];
+
+       /* feed computed frequency error into AFC loop */
+       if (rx_tch.meas[meas_id].snr > AFC_SNR_THRESHOLD)
+               afc_input(rx_tch.meas[meas_id].freq_err, arfcn, 1);
+       else
+               afc_input(rx_tch.meas[meas_id].freq_err, arfcn, 0);
+
+       /* Tell the RF frontend to set the gain appropriately */
+       rffe_set_gain(rx_tch.meas[meas_id].pm_dbm8 / 8, CAL_DSP_TGT_BB_LVL);
+
+       /* FACCH Block end ? */
+       if (tch_f_hn) {
+               /* FACCH/F: B0(0...7),B1(4...11),B2(8...11,0...3) (mod 13) */
+               facch_rx_now = ((rx_time.fn % 13) % 4) == 3;
+       } else {
+               /* FAACH/H: See GSM 05.02 Clause 7 Table 1of9 */
+               uint8_t t2_norm = rx_time.t2 - tch_sub;
+               facch_rx_now = (t2_norm == 15) ||
+                              (t2_norm == 23) ||
+                              (t2_norm ==  6);
+       }
+
+       if (facch_rx_now && (dsp_api.ndb->a_fd[0] & (1<<B_BLUD))) {
+               struct msgb *msg;
+               struct l1ctl_info_dl *dl;
+               struct l1ctl_data_ind *di;
+               uint16_t num_biterr;
+               uint32_t avg_snr = 0;
+               int32_t avg_dbm8 = 0;
+               int i, n;
+
+               /* Allocate msgb */
+                       /* FIXME: we actually want all allocation out of L1S! */
+               msg = l1ctl_msgb_alloc(L1CTL_DATA_IND);
+               if(!msg) {
+                       printf("TCH FACCH: unable to allocate msgb\n");
+                       goto skip;
+               }
+
+               dl = (struct l1ctl_info_dl *) msgb_put(msg, sizeof(*dl));
+               di = (struct l1ctl_data_ind *) msgb_put(msg, sizeof(*di));
+
+               /* Fill DL header (should be about the first burst ... here is the last) */
+               dl->chan_nr = chan_nr;
+               dl->link_id = 0x00;     /* FACCH */
+               dl->band_arfcn = htons(arfcn);
+               dl->frame_nr = htonl(rx_time.fn);
+
+               /* Average SNR & RX level */
+               n = tch_f_hn ? 8 : 6;
+               for (i=0; i<n; i++) {
+                       int j = (meas_id + FACCH_MEAS_HIST - i) % FACCH_MEAS_HIST;
+                       avg_snr += rx_tch.meas[j].snr;
+                       avg_dbm8 += rx_tch.meas[j].pm_dbm8;
+               }
+
+               dl->snr = avg_snr / n;
+               dl->rx_level = (avg_dbm8 / (8*n)) + 110;
+
+               /* Errors & CRC status */
+               num_biterr = dsp_api.ndb->a_fd[2] & 0xffff;
+               if (num_biterr > 0xff)
+                       dl->num_biterr = 0xff;
+               else
+                       dl->num_biterr = num_biterr;
+
+               dl->fire_crc = ((dsp_api.ndb->a_fd[0] & 0xffff) & ((1 << B_FIRE1) | (1 << B_FIRE0))) >> B_FIRE0;
+
+               /* Update rx level for pm report */
+               pu_update_rx_level(dl->rx_level);
+
+               /* Copy actual data, skipping the information block [0,1,2] */
+               dsp_memcpy_from_api(di->data, &dsp_api.ndb->a_fd[3], 23, 0);
+
+               /* Give message to up layer */
+               l1_queue_for_l2(msg);
+
+       skip:
+               /* Reset A_FD header (needed by DSP) */
+               /* B_FIRE1 =1, B_FIRE0 =0 , BLUD =0 */
+               dsp_api.ndb->a_fd[0] = (1<<B_FIRE1);
+               dsp_api.ndb->a_fd[2] = 0xffff;
+
+               /* Reset A_DD_0 header in NDB (needed by DSP) */
+               dsp_api.ndb->a_dd_0[0] = 0;
+               dsp_api.ndb->a_dd_0[2] = 0xffff;
+
+               /* Reset A_DD_1 header in NDB (needed by DSP) */
+               dsp_api.ndb->a_dd_1[0] = 0;
+               dsp_api.ndb->a_dd_1[2] = 0xffff;
+       }
+
+       /* Traffic now ? */
+       if (tch_f_hn) {
+               /* TCH/F: B0(0...7),B1(4...11),B2(8...11,0...3) (mod 13)*/
+               traffic_rx_now = ((rx_time.fn % 13) % 4) == 3;
+       } else {
+               /* TCH/H0: B0(0,2,4,6),B1(4,6,8,10),B2(8,10,0,2) (mod 13) */
+               /*     H1: B0(1,3,5,7),B1(5,7,9,11),B2(9,11,1,3) (mod 13) */
+               traffic_rx_now = (((rx_time.fn - tch_sub + 13) % 13) % 4) == 2;
+       }
+
+       if (traffic_rx_now) {
+               volatile uint16_t *traffic_buf;
+
+               traffic_buf = tch_sub ? dsp_api.ndb->a_dd_1 : dsp_api.ndb->a_dd_0;
+
+               if (traffic_buf[0] & (1<<B_BLUD)) {
+                       /* Reset traffic buffer header in NDB (needed by DSP) */
+                       traffic_buf[0] = 0;
+                       traffic_buf[2] = 0xffff;
+               }
+       }
+
+       /* mark READ page as being used */
+       dsp_api.r_page_used = 1;
+
+       return 0;
+}
+
+static int l1s_tch_cmd(__unused uint8_t p1, __unused uint8_t p2, uint16_t p3)
+{
+       uint8_t mf_task_id = p3 & 0xff;
+       uint8_t chan_nr;
+       uint16_t arfcn;
+       uint8_t tsc, tn;
+       uint8_t tch_f_hn, tch_sub, tch_mode;
+       uint32_t fn_report;
+       uint8_t sync = 0;
+       static int icnt;
+       int facch_tx_now;
+
+       /* Get/compute various parameters */
+       rfch_get_params(&l1s.next_time, &arfcn, &tsc, &tn);
+       chan_nr = mframe_task2chan_nr(mf_task_id, tn);
+       tch_get_params(&l1s.next_time, chan_nr, &fn_report, &tch_f_hn, &tch_sub, &tch_mode);
+
+       /* Sync & FACCH delay */
+       if (l1s.tch_sync) {
+               l1s.tch_sync = 0;
+               sync = 1;
+               icnt = 0;
+       } else if (icnt <= 26)
+               icnt++;
+
+       /* Load FACCH data if we start a new burst */
+       /* (the DSP wants the data on the CMD of the burst _preceding_ the
+        * first burst) */
+       if (tch_f_hn) {
+               /* FACCH/F: B0(0...7),B1(4...11),B2(8...11,0...3) */
+               facch_tx_now = ((l1s.next_time.fn % 13) % 4) == 3;
+       } else {
+               /* FAACH/H: See GSM 05.02 Clause 7 Table 1of9 */
+               uint8_t t2_norm = l1s.next_time.t2 - tch_sub;
+               facch_tx_now = (t2_norm == 23) ||
+                              (t2_norm ==  6) ||
+                              (t2_norm == 15);
+       }
+
+       if (facch_tx_now) {
+               uint16_t *info_ptr = dsp_api.ndb->a_fu;
+               struct msgb *msg;
+               const uint8_t *data;
+
+               /* Pull FACCH data (if ready) */
+               if (icnt > 26)
+                       msg = msgb_dequeue(&l1s.tx_queue[L1S_CHAN_MAIN]);
+               else
+                       msg = NULL;
+
+               /* If TX is empty and we're signalling only, use dummy frame */
+               if (msg)
+                       data = msg->l3h;
+               else if (tch_mode == SIG_ONLY_MODE)
+                       data = pu_get_idle_frame();
+               else
+                       data = NULL;
+
+               /* Do we really send something ? */
+               if (data) {
+                       /* Fill data block header */
+                       info_ptr[0] = (1 << B_BLUD);    /* 1st word: Set B_BLU bit. */
+                       info_ptr[1] = 0;                /* 2nd word: cleared. */
+                       info_ptr[2] = 0;                /* 3nd word: cleared. */
+
+                       /* Copy the actual data after the header */
+                       dsp_memcpy_to_api(&info_ptr[3], data, 23, 0);
+               }
+
+               /* Indicate completion (FIXME: early but easier this way for now) */
+               if (msg) {
+                       last_tx_tch_fn = l1s.next_time.fn;
+                       l1s_compl_sched(L1_COMPL_TX_TCH);
+               }
+
+               /* Free msg now that we're done with it */
+               if (msg)
+                       msgb_free(msg);
+       }
+
+       /* Configure DSP for TX/RX */
+       l1s_tx_apc_helper(arfcn);
+
+       dsp_load_tch_param(
+               &l1s.next_time,
+               tch_mode, tch_f_hn ? TCH_F : TCH_H, tch_sub,
+               0, sync, tn
+       );
+
+       dsp_load_rx_task(TCHT_DSP_TASK, 0, tsc); /* burst_id unused for TCH */
+       l1s_rx_win_ctrl(arfcn, L1_RXWIN_NB, 0);
+
+       dsp_load_tx_task(TCHT_DSP_TASK, 0, tsc); /* burst_id unused for TCH */
+       l1s_tx_win_ctrl(arfcn, L1_TXWIN_NB, 0, 3);
+
+       return 0;
+}
+
+
+const struct tdma_sched_item tch_sched_set[] = {
+       SCHED_ITEM_DT(l1s_tch_cmd, 0, 0, 0),    SCHED_END_FRAME(),
+                                               SCHED_END_FRAME(),
+       SCHED_ITEM(l1s_tch_resp, 0, 0, -4),     SCHED_END_FRAME(),
+       SCHED_END_SET()
+};
+
+
+/* -------------------------------------------------------------------------
+ * TCH: SACCH
+ * ------------------------------------------------------------------------- */
+
+/*
+ * SACCH data are spread over 4 bursts, however they are so far appart that
+ * we can't use the normal scheduler to schedule all them at once in a single
+ * set.
+ * Therefore, the task code itself decides in which burst it is, if it's the
+ * start/end, and act appropriately.
+ */
+
+
+struct l1s_rx_tch_a_state {
+       struct l1s_meas_hdr meas[4];
+
+       struct msgb *msg;
+       struct l1ctl_info_dl *dl;
+       struct l1ctl_data_ind *di;
+};
+
+static struct l1s_rx_tch_a_state rx_tch_a;
+
+
+static int l1s_tch_a_resp(__unused uint8_t p1, __unused uint8_t p2, uint16_t p3)
+{
+       uint8_t mf_task_id = p3 & 0xff;
+       struct gsm_time rx_time;
+       uint8_t chan_nr;
+       uint16_t arfcn;
+       uint8_t tsc, tn;
+       uint8_t tch_f_hn, tch_sub;
+       uint32_t fn_report;
+       uint8_t burst_id;
+
+       /* It may happen we've never gone through cmd(0) yet, skip until then */
+       if (!rx_tch_a.msg)
+               goto skip;
+
+       /* Get/compute various parameters */
+       gsm_fn2gsmtime(&rx_time, (l1s.current_time.fn - 1 + GSM_MAX_FN) % GSM_MAX_FN);
+       rfch_get_params(&rx_time, &arfcn, &tsc, &tn);
+       chan_nr = mframe_task2chan_nr(mf_task_id, tn);
+       tch_get_params(&rx_time, chan_nr, &fn_report, &tch_f_hn, &tch_sub, NULL);
+       burst_id = (fn_report - 12) / 26;
+
+       /* Collect measurements */
+       rx_tch_a.meas[burst_id].toa_qbit = dsp_api.db_r->a_serv_demod[D_TOA];
+       rx_tch_a.meas[burst_id].pm_dbm8 =
+               agc_inp_dbm8_by_pm(dsp_api.db_r->a_serv_demod[D_PM] >> 3);
+       rx_tch_a.meas[burst_id].freq_err =
+               ANGLE_TO_FREQ(dsp_api.db_r->a_serv_demod[D_ANGLE]);
+       rx_tch_a.meas[burst_id].snr = dsp_api.db_r->a_serv_demod[D_SNR];
+
+       /* feed computed frequency error into AFC loop */
+       if (rx_tch_a.meas[burst_id].snr > AFC_SNR_THRESHOLD)
+               afc_input(rx_tch_a.meas[burst_id].freq_err, arfcn, 1);
+       else
+               afc_input(rx_tch_a.meas[burst_id].freq_err, arfcn, 0);
+
+       /* Tell the RF frontend to set the gain appropriately */
+       rffe_set_gain(rx_tch_a.meas[burst_id].pm_dbm8 / 8, CAL_DSP_TGT_BB_LVL);
+
+       /* Last burst, read data & send to the up layer */
+       if ((burst_id == 3) && (dsp_api.ndb->a_cd[0] & (1<<B_BLUD))) {
+               unsigned int i;
+               uint16_t num_biterr;
+               uint32_t avg_snr = 0;
+               int32_t avg_dbm8 = 0;
+
+               /* Average SNR & RX level + error & crc status */
+               for (i=0; i<4; i++) {
+                       avg_snr += rx_tch_a.meas[i].snr;
+                       avg_dbm8 += rx_tch_a.meas[i].pm_dbm8;
+               }
+               rx_tch_a.dl->snr = avg_snr / 4;
+               rx_tch_a.dl->rx_level = (avg_dbm8 / (8*4)) + 110;
+
+               num_biterr = dsp_api.ndb->a_cd[2];
+               if (num_biterr > 0xff)
+                       rx_tch_a.dl->num_biterr = 0xff;
+               else
+                       rx_tch_a.dl->num_biterr = num_biterr;
+
+               rx_tch_a.dl->fire_crc = ((dsp_api.ndb->a_cd[0] & 0xffff) & ((1 << B_FIRE1) | (1 << B_FIRE0))) >> B_FIRE0;
+
+               /* Update rx level for pm report */
+               pu_update_rx_level(rx_tch_a.dl->rx_level);
+
+               /* Copy actual data, skipping the information block [0,1,2] */
+               dsp_memcpy_from_api(rx_tch_a.di->data, &dsp_api.ndb->a_cd[3], 23, 0);
+
+               /* Give message to up layer */
+               l1_queue_for_l2(rx_tch_a.msg);
+               rx_tch_a.msg = NULL; rx_tch_a.dl = NULL; rx_tch_a.di = NULL;
+
+               /* Reset header */
+               dsp_api.ndb->a_cd[0] = (1<<B_FIRE1);
+               dsp_api.ndb->a_cd[2] = 0xffff;
+       }
+
+skip:
+       /* mark READ page as being used */
+       dsp_api.r_page_used = 1;
+
+       return 0;
+}
+
+static int l1s_tch_a_cmd(__unused uint8_t p1, __unused uint8_t p2, uint16_t p3)
+{
+       uint8_t mf_task_id = p3 & 0xff;
+       uint8_t chan_nr;
+       uint16_t arfcn;
+       uint8_t tsc, tn;
+       uint8_t tch_f_hn, tch_sub;
+       uint32_t fn_report;
+       uint8_t burst_id;
+
+       /* Get/compute various parameters */
+       rfch_get_params(&l1s.next_time, &arfcn, &tsc, &tn);
+       chan_nr = mframe_task2chan_nr(mf_task_id, tn);
+       tch_get_params(&l1s.next_time, chan_nr, &fn_report, &tch_f_hn, &tch_sub, NULL);
+       burst_id = (fn_report - 12) / 26;
+
+       /* Load SACCH data if we start a new burst */
+       if (burst_id == 0) {
+               uint16_t *info_ptr = dsp_api.ndb->a_cu;
+               struct msgb *msg;
+               const uint8_t *data;
+
+               /* If the TX queue is empty, send dummy measurement */
+               msg = msgb_dequeue(&l1s.tx_queue[L1S_CHAN_SACCH]);
+               data = msg ? msg->l3h : pu_get_meas_frame();
+
+               /* Fill data block header */
+               info_ptr[0] = (1 << B_BLUD);    /* 1st word: Set B_BLU bit. */
+               info_ptr[1] = 0;                /* 2nd word: cleared. */
+               info_ptr[2] = 0;                /* 3nd word: cleared. */
+
+               /* Copy the actual data after the header */
+               dsp_memcpy_to_api(&info_ptr[3], data, 23, 0);
+
+               /* Indicate completion (FIXME: early but easier this way for now) */
+               if (msg) {
+                       last_tx_tch_fn = l1s.next_time.fn;
+                       l1s_compl_sched(L1_COMPL_TX_TCH);
+               }
+
+               /* Free msg now that we're done with it */
+               if (msg)
+                       msgb_free(msg);
+       }
+
+       /* Allocate RX burst */
+       if (burst_id == 0) {
+               /* Clear 'dangling' msgb */
+               if (rx_tch_a.msg) {
+                       /* Can happen if the task was shutdown in the middle of
+                        * 4 bursts ... */
+                       msgb_free(rx_tch_a.msg);
+               }
+
+               /* Allocate burst */
+                       /* FIXME: we actually want all allocation out of L1S! */
+               rx_tch_a.msg = l1ctl_msgb_alloc(L1CTL_DATA_IND);
+               if (!rx_tch_a.msg)
+                       printf("tch_a_cmd(0): unable to allocate msgb\n");
+
+               rx_tch_a.dl = (struct l1ctl_info_dl *) msgb_put(rx_tch_a.msg, sizeof(*rx_tch_a.dl));
+               rx_tch_a.di = (struct l1ctl_data_ind *) msgb_put(rx_tch_a.msg, sizeof(*rx_tch_a.di));
+
+               /* Pre-fill DL header with some info about burst(0) */
+               rx_tch_a.dl->chan_nr = chan_nr;
+               rx_tch_a.dl->link_id = 0x40;    /* SACCH */
+               rx_tch_a.dl->band_arfcn = htons(arfcn);
+               rx_tch_a.dl->frame_nr = htonl(l1s.next_time.fn);
+       }
+
+       /* Configure DSP for TX/RX */
+       l1s_tx_apc_helper(arfcn);
+
+       dsp_load_tch_param(
+               &l1s.next_time,
+               SIG_ONLY_MODE, tch_f_hn ? TCH_F : TCH_H, tch_sub,
+               0, 0, tn
+       );
+
+       dsp_load_rx_task(TCHA_DSP_TASK, 0, tsc); /* burst_id unused for TCHA */
+       l1s_rx_win_ctrl(arfcn, L1_RXWIN_NB, 0);
+
+       dsp_load_tx_task(TCHA_DSP_TASK, 0, tsc); /* burst_id unused for TCHA */
+       l1s_tx_win_ctrl(arfcn, L1_TXWIN_NB, 0, 3);
+
+       return 0;
+}
+
+
+const struct tdma_sched_item tch_a_sched_set[] = {
+       SCHED_ITEM_DT(l1s_tch_a_cmd, 0, 0, 0),  SCHED_END_FRAME(),
+                                               SCHED_END_FRAME(),
+       SCHED_ITEM(l1s_tch_a_resp, 0, 0, -4),   SCHED_END_FRAME(),
+       SCHED_END_SET()
+};