/* RTP based GSM BER testing for osmo-bts, implementing ideas described in * https://osmocom.org/projects/osmobts/wiki/BER_Testing * * In short: The command transmits a PRBS sequence encapsulated in RTP frames, which are sent * to the BTS, which transmits that data in the (unimpaired) downlink. The mobile station * receives the data and is instructed to loop it back in the (possibly impaired) uplink. * The BTS receives that uplink, puts in in RTP frames which end up being received back by this * very tool. By correlating the received RTP with the PRBS sequence, this tool can compute * the BER (Bit Error Rate) of the (possibly impaired) uplink. Doing this with different * RF channel model simulators in the uplink allows to establish BER at different levels and * channel conditions. */ /* (C) 2019 sysmocom - s.f.m.c. GmbH; Author: Sylvain Munaut * 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, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct app_state { struct osmo_rtp_socket *rs; enum { WAIT_CONN = 0, /* Wait for incoming connection */ WAIT_LOOP, /* Wait for a somewhat valid packet to start measuring */ RUNNING, /* Main state */ } state; int pt; int ref_len; uint8_t ref_bytes[GSM_FR_BYTES]; /* FR is the largest possible one */ ubit_t ref_bits[8*GSM_FR_BYTES]; struct osmo_timer_list rtp_timer; uint16_t rx_last_seq; uint32_t rx_last_ts; uint32_t rx_idx; uint32_t tx_idx; const int *err_tbl; /* Classification table */ int err_frames; /* Number of accumulated frames */ int err_cnt[3]; /* Bit error counter */ int err_tot[3]; /* Total # bits in that class */ }; #define FLOW_REG_TX_MAX_ADVANCE 200 #define FLOW_REG_TX_MIN_ADVANCE 50 const struct log_info log_info; static const uint8_t amr_size_by_ft[] = { [0] = 12, /* 4.75 */ [1] = 13, /* 5.15 */ [2] = 15, /* 5.9 */ [3] = 17, /* 6.7 */ [4] = 19, /* 7.4 */ [5] = 20, /* 7.95 */ [6] = 26, /* 10.2 */ [7] = 31, /* 12.2 */ [8] = 5, /* SID */ }; static const char * const amr_rate_by_ft[] = { [0] = "4.75", [1] = "5.15", [2] = "5.9", [3] = "6.7", [4] = "7.4", [5] = "7.95", [6] = "10.2", [7] = "12.2", [8] = "SID", }; static void _gsm_fr_gen_ref(struct app_state *as) { struct osmo_prbs_state pn9; /* Length */ as->ref_len = GSM_FR_BYTES; /* Marker */ as->ref_bits[0] = 1; as->ref_bits[1] = 1; as->ref_bits[2] = 0; as->ref_bits[3] = 1; /* PN */ osmo_prbs_state_init(&pn9, &osmo_prbs9); pn9.state = 31; osmo_prbs_get_ubits(&as->ref_bits[4], 260, &pn9); /* Convert to bytes */ osmo_ubit2pbit_ext(as->ref_bytes, 0, as->ref_bits, 0, 8*GSM_FR_BYTES, 0); /* Init error classes */ as->err_tot[0] = 50; as->err_tot[1] = 132; as->err_tot[2] = 78; as->err_tbl = gsm_fr_bitclass; } static void _gsm_efr_gen_ref(struct app_state *as) { struct osmo_prbs_state pn9; /* Length */ as->ref_len = GSM_EFR_BYTES; /* Marker */ as->ref_bits[0] = 1; as->ref_bits[1] = 1; as->ref_bits[2] = 0; as->ref_bits[3] = 0; /* PN */ osmo_prbs_state_init(&pn9, &osmo_prbs9); pn9.state = 31; osmo_prbs_get_ubits(&as->ref_bits[4], 244, &pn9); /* Convert to bytes */ osmo_ubit2pbit_ext(as->ref_bytes, 0, as->ref_bits, 0, 8*GSM_EFR_BYTES, 0); /* Init error classes */ as->err_tot[0] = 50; as->err_tot[1] = 125; as->err_tot[2] = 73; as->err_tbl = gsm_efr_bitclass; } static void _gsm_amr_gen_ref(struct app_state *as) { struct osmo_prbs_state pn9; uint8_t hdr[2]; /* Length */ as->ref_len = 33; /* Header */ hdr[0] = 0x70; hdr[1] = 0x3c; osmo_pbit2ubit_ext(as->ref_bits, 0, hdr, 0, 16, 0); /* PN */ osmo_prbs_state_init(&pn9, &osmo_prbs9); pn9.state = 31; osmo_prbs_get_ubits(&as->ref_bits[16], 244, &pn9); /* Unused bits */ as->ref_bits[260] = 0; as->ref_bits[261] = 0; as->ref_bits[262] = 0; as->ref_bits[263] = 0; /* Convert to bytes */ osmo_ubit2pbit_ext(as->ref_bytes, 0, as->ref_bits, 0, 264, 0); /* Init error classes */ as->err_tot[0] = 81; as->err_tot[1] = 163; as->err_tot[2] = -1; as->err_tbl = gsm_amr_12_2_bitclass; } static void _gsm_gen_ref(struct app_state *as) { switch (as->pt) { case RTP_PT_GSM_FULL: _gsm_fr_gen_ref(as); break; case RTP_PT_GSM_EFR: _gsm_efr_gen_ref(as); break; case RTP_PT_AMR: _gsm_amr_gen_ref(as); break; default: fprintf(stderr, "[!] Unsupported payload type for BER measurement\n"); } } static int _gsm_ber(struct app_state *as, const uint8_t *payload, unsigned int payload_len) { ubit_t rx_bits[8*33]; int err[3]; /* Class 1a, 1b, 2 */ int ones; int i, j; if (payload) { /* Process real-payload */ osmo_pbit2ubit_ext(rx_bits, 0, payload, 0, 8*payload_len, 0); err[0] = err[1] = err[2] = 0; ones = 0; for (i = 0; i < 8 * payload_len; i++) { j = as->err_tbl[i]; if (j >= 0) { err[j] += rx_bits[i] ^ as->ref_bits[i]; ones += rx_bits[i]; } } if (ones < 32) { // This frames is probably us underrunning Tx, don't use it fprintf(stderr, "[w] Frame ignored as probably TX underrun %d %d\n", as->tx_idx, as->rx_idx); return 1; } } else { /* No payload -> Lost frame completely */ err[0] = as->err_tot[0] / 2; err[1] = as->err_tot[1] / 2; err[2] = as->err_tot[2] / 2; } if (as->state == RUNNING) { /* Update records */ if (err[0] != 0) { /* Class 1a bits bad -> Frame error */ as->err_cnt[0]++; } as->err_cnt[1] += err[1]; /* Class 1b */ as->err_cnt[2] += err[2]; /* class 2 */ as->err_frames++; /* Enough for a read-out ? */ if (as->err_frames == 200) { printf("FBER: %4.2f C1b RBER: %5.3f C2 RBER: %5.3f\n", 100.0f * as->err_cnt[0] / as->err_frames, 100.0f * as->err_cnt[1] / (as->err_tot[1] * as->err_frames), 100.0f * as->err_cnt[2] / (as->err_tot[2] * as->err_frames) ); memset(as->err_cnt, 0, sizeof(as->err_cnt)); as->err_frames = 0; } } return err[0] != 0; } static int _rtp_check_payload_type(const uint8_t *payload, unsigned int payload_len) { uint8_t ft; int pt = -1; switch (payload_len) { case GSM_FR_BYTES: /* FR or AMR 12.2k */ /* Check for AMR */ ft = (payload[1] >> 3) & 0xf; if (ft == 7) pt = RTP_PT_AMR; /* Check for FR */ else if ((payload[0] & 0xF0) == 0xD0) pt = RTP_PT_GSM_FULL; /* None of the above */ else fprintf(stderr, "[!] FR without 0xD0 signature or AMR with unknwon Frame Type ?!?\n"); break; case GSM_EFR_BYTES: /* EFR */ if ((payload[0] & 0xF0) != 0xC0) fprintf(stderr, "[!] EFR without 0xC0 signature ?!?\n"); pt = RTP_PT_GSM_EFR; break; case GSM_HR_BYTES: /* HR */ pt = RTP_PT_GSM_HALF; break; default: /* AMR */ { uint8_t cmr, cmi, sti; cmr = payload[0] >> 4; ft = (payload[1] >> 3) & 0xf; if (payload_len != amr_size_by_ft[ft]+2) fprintf(stderr, "AMR FT %u(%s) but size %u\n", ft, amr_rate_by_ft[ft], payload_len); switch (ft) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: cmi = ft; printf("AMR SPEECH with FT/CMI %u(%s), " "CMR %u\n", cmi, amr_rate_by_ft[cmi], cmr); break; case 8: /* SID */ cmi = (payload[2+4] >> 1) & 0x7; sti = payload[2+4] & 0x10; printf("AMR SID %s with CMI %u(%s), CMR %u(%s)\n", sti ? "UPDATE" : "FIRST", cmi, amr_rate_by_ft[cmi], cmr, amr_rate_by_ft[cmr]); break; } } break; } return pt; } static void rtp_timer_cb(void *priv) { struct app_state *as = (struct app_state *)priv; /* Send at least one frame if we're not too far ahead */ if (as->tx_idx < (as->rx_idx + FLOW_REG_TX_MAX_ADVANCE)) { osmo_rtp_send_frame(as->rs, as->ref_bytes, as->ref_len, GSM_RTP_DURATION); as->tx_idx++; } else { fprintf(stderr, "Skipped\n"); } /* Then maybe a second one to try and catch up to RX */ if (as->tx_idx < (as->rx_idx + FLOW_REG_TX_MIN_ADVANCE)) { osmo_rtp_send_frame(as->rs, as->ref_bytes, as->ref_len, GSM_RTP_DURATION); as->tx_idx++; } /* Re-schedule */ osmo_timer_schedule(&as->rtp_timer, 0, 20000); } static int rtp_seq_num_diff(uint16_t new, uint16_t old) { int d = (int)new - (int)old; while (d > 49152) d -= 65536; while (d < -49152) d += 65536; return d; } static void rtp_rx_cb(struct osmo_rtp_socket *rs, const uint8_t *payload, unsigned int payload_len, uint16_t seq_number, uint32_t timestamp, bool marker) { struct app_state *as = (struct app_state *)rs->priv; int pt, rc, d; // printf("Rx(%u, %d, %d, %d): %s\n", payload_len, seq_number, timestamp, marker, osmo_hexdump(payload, payload_len)); /* Identify payload */ pt = _rtp_check_payload_type(payload, payload_len); /* First packet ? */ if (as->state == WAIT_CONN) { /* Setup for this payload type */ as->pt = pt; osmo_rtp_socket_set_pt(as->rs, pt); _gsm_gen_ref(as); /* Timer every 20 ms */ osmo_timer_setup(&as->rtp_timer, rtp_timer_cb, as); osmo_timer_add(&as->rtp_timer); osmo_timer_schedule(&as->rtp_timer, 0, 20000); /* Init our time tracking */ as->rx_last_seq = seq_number; as->rx_last_ts = timestamp; /* Now we wait for a loop */ as->state = WAIT_LOOP; } /* RX sequence & timstamp tracking */ if (rtp_seq_num_diff(seq_number, as->rx_last_seq) > 1) fprintf(stderr, "[!] RTP sequence number discontinuity (%d -> %d)\n", as->rx_last_seq, seq_number); d = (timestamp - as->rx_last_ts) / GSM_RTP_DURATION; as->rx_idx += d; as->rx_last_seq = seq_number; as->rx_last_ts = timestamp; /* Account for missing frames in BER tracking */ if (d > 1) { fprintf(stderr, "[!] RTP %d missing frames assumed lost @%d\n", d-1, seq_number); while (--d) _gsm_ber(as, NULL, 0); } /* BER analysis */ rc = _gsm_ber(as, payload, payload_len); if ((as->state == WAIT_LOOP) && (rc == 0)) as->state = RUNNING; } int main(int argc, char **argv) { struct app_state _as, *as = &_as; int rc, port; /* Args */ if (argc < 2) return -1; port = atoi(argv[1]); /* App init */ memset(as, 0x00, sizeof(struct app_state)); log_init(&log_info, NULL); osmo_rtp_init(NULL); /* Start auto-connect RTP socket */ as->rs = osmo_rtp_socket_create(NULL, 0); as->rs->priv = as; as->rs->rx_cb = rtp_rx_cb; /* Jitter buffer gets in the way, we want the raw traffic */ osmo_rtp_socket_set_param(as->rs, OSMO_RTP_P_JIT_ADAP, 0); osmo_rtp_socket_set_param(as->rs, OSMO_RTP_P_JITBUF, 0); /* Bind to requested port */ fprintf(stderr, "[+] Binding RTP socket on port %u...\n", port); rc = osmo_rtp_socket_bind(as->rs, "0.0.0.0", port); if (rc < 0) { fprintf(stderr, "[!] error binding RTP socket: %d\n", rc); return rc; } /* We 'connect' to the first source we hear from */ osmo_rtp_socket_autoconnect(as->rs); /* Main loop */ while (1) osmo_select_main(0); return 0; }