Bell 202 is a quite old modem standard that is still used today in amateur radio for data transmission over VHF (Packet Radio and APRS) and industrial automation (HART). It is a very simple FSK modem. The speed is limited to 1200 baud, which makes it very easy to implement on any small microcontroller. My implementation is built on top of sinewave generator code for XMEGA described in a previous post.
Bell 202 uses two frequencies – 1200 Hz and 2200 Hz for mark and space. Mark and space are not bits 0 and 1. The modem (in amateur radio and APRS) is governed by the following rules:
- The input data are bytes (8-bits).
- Bits are transmitted LSB first.
- A zero bit is sent as a change of frequency (from mark to space or the other way around).
- A one bit is sent as a no change of frequency.
- If more than 5 one bits have been sent – there is always a change in frequency (bit stuffing).
- Bit stuffing is not applied if the byte is 0x7E (it is the start flag of an AX.25 frame, specific to ham radio).
- Symbols are sent at 1200 baud (ie. the bit clock runs at 1200Hz, but some cycles can do bit stuffing, so a data bit is not necessarily sent every time).
Bit stuffing is needed by the receiver for correct data reception as there is no separate clock sent. Imagine a person with a flashlight at night, far away. You have agreed to transmit 1 as light and 0 as darkness, with a speed of 1 bit per second. You need a watch to count time as you look at the flashlight afar. If alternative zeros and ones are sent – the reception is easy. If several identical bits are sent in a row followed by the opposite bit – it is also manageable, but if 100 identical bits are sent – are you sure that 100 were sent? Or perhaps 99? or? 102? Bit stuffing helps to solve this problem by assuring that there will always be a change in the transmitted signal, even if the data stays the same for a long period.
The code
The transmission code depends on sinewave generator functions described in a previous post. Core functionality is executed within function modem_bit_clock_tick_from_ISR that is called from a timer interrupt 1200 times a second. This function handles the whole transmit state machine. At the end of transmission it stops the timer and sets a flag, that is checked from a periodic task. The periodic task delivers callbacks to application code.
Setup of a new transmission requires a pointer to the data, its length and optionally a function to be called when the transmission is done. modem_send function is responsible for setting all state variables and starting the timer. While the modem is transmitting the CPU can be in sleep mode as everything happens in interrupts.
I have successfully tested this code with Xastir and Direwolf 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#ifndef AFSK_H_ #define AFSK_H_ #include <stdint.h> #include <stdbool.h> typedef void(*modem_tx_complete_callback_t)(void); extern bool modem_tx_busy_flag; void modem_task(void); void modem_send(uint8_t *data, uint16_t length, modem_tx_complete_callback_t cb); #endif |
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 |
#include "dds.h" #include "modem.h" #include <avr/io.h> #include <avr/interrupt.h> //timer used by the modem to interrupt at 1200 baud #define TIMER_MODEM TCD0 #define TIMER_MODEM_OVF_vect TCD0_OVF_vect /* --------- public data ---------------- */ bool modem_tx_busy_flag = false; /* --------- private data --------------- */ static volatile uint16_t _data_length; static volatile uint8_t* _data_ptr; static volatile uint8_t _bit_index; static volatile uint8_t _bit_one_counter; static volatile uint8_t _current_byte; static volatile bool _tx_done_flag = false; static modem_tx_complete_callback_t _complete_cb; /* --------- private prototypes --------- */ static void modem_bit_clock_tick_from_ISR(void); /* ----------- implementation ----------- */ #define dds_send_bit_one() //'1' bit is no change in frequency #define dds_send_bit_zero() dds_shift()//'0' bit is a change of the frequency void modem_send(uint8_t *data, uint16_t length, modem_tx_complete_callback_t cb){ _complete_cb = cb; _data_length = length; _data_ptr = data; _current_byte = *_data_ptr; _bit_one_counter = 0; modem_tx_busy_flag = true; _tx_done_flag = false; dds_start(true/*use Vcc as output reference*/); //start the generator //start the bit clock TIMER_MODEM.CNT = 0; //counter value TIMER_MODEM.CTRLA = TC_CLKSEL_DIV1_gc; //no prescaling, timer clk = 32MHz TIMER_MODEM.CTRLB = TC_WGMODE_NORMAL_gc; //count up to PER TIMER_MODEM.PER = 26667; //this gives 1199,985 interrupts per second (baud) TIMER_MODEM.INTCTRLA = TC_OVFINTLVL_HI_gc; //overflow interrupt enable } ISR(TIMER_MODEM_OVF_vect){ modem_bit_clock_tick_from_ISR(); } //this is fired from timer ISR at 1200bps static void modem_bit_clock_tick_from_ISR(void){ //bit stuffing - check if 5 consecutive ones apperaed - add extra zero //but don't stuff the flag if (_bit_one_counter == 5 && *_data_ptr != 0x7E/*AX.25 flag byte*/){ dds_send_bit_zero(); _bit_one_counter = 0; return; //don't move to next bit, do it at the next invocation } uint8_t current_bit = _current_byte & 0x01; if (current_bit == 0){ //zero bit - shift frequency dds_send_bit_zero(); _bit_one_counter = 0; } else { //one bit - do not change frequency dds_send_bit_one(); _bit_one_counter++; } //move on to the next bit _current_byte = _current_byte >> 1; _bit_index++; if (_bit_index == 8){ //whole byte was sent _bit_index = 0; _data_ptr++; _data_length--; _current_byte = *_data_ptr; if (_data_length == 0){ modem_tx_busy_flag = false; _tx_done_flag = true; TIMER_MODEM.CTRLA = TC_CLKSEL_OFF_gc; //disable bit clock dds_off(); //disable signal generator } } } void modem_task(void){ if (_tx_done_flag){ _tx_done_flag = false; if (_complete_cb){ _complete_cb(); } } } |