In a previous post I pondered briefly about BPSK (used in PSK31) as compared to FSK. Instead of traditional frequency-shift keying, in BPSK information is transmitted by patterns of polarity-reversals (sometimes called 180-degree phase shifts). One way to think about this would be to swap antenna terminals on each phase reversal.
BPSK uses a 180 degree phase shift when encoding a zero bit in a varicode character. By way of review, BPSK uses a sinusoid of constant amplitude and a 180 degree phase shift to represent a binary 1 as opposed to a binary 0.
Now, the problem with this in radio circuits is the phase reversal if hard keyed will result in a lot of splatter and the accompanying bandwidth. So, in radio circuits as discussed in my series about PSK31 on the arduino, we typically ramp the sinusoid level down to zero at the phase change points to eliminate this splatter and shift the phase reversal to the middle of the bit time. PSK31 encodes a zero bit as a phase reversal and a one bit as no phase change. In other aspects however PSK31 is BPSK.
In thinking about BPSK vs. FSK or more accurately, MSK (Minimum Shift Keying) I found myself pondering whether they are functionally equivalent.
MSK is basically FSK with the shift set to ½ the baud rate. Realistically this is the smallest shift you can use without trading off transmission speed. PSK31 uses a 31.25 baud transmission speed. So, if we were going to encode a 31.25 baud data transmission using MSK, our keying shift would be 1/4 of the data rate either side of the transmit center frequency.
31.25 baud / 2 = 15.625 / 2 = 7.8125 Hz
If you think of the phase of a carrier that is 7.8125 Hz below the transmit center frequency, it will lag by 90 degrees after 32 mS, and at 7.8125 Hz above, it will lead by 90 degrees after 32 mS for a difference of 180 degrees.
So MSK appears to be functionally equivalent to BPSK while using +90 and -90 degree shifts instead of 0 and 180 degree shifts.
An advantage to this approach is that the resultant signal has no amplitude modulated component and so non-linear amplification techniques may be utilized greatly simplifying transmitter design. It does require the ability to do phase continuous frequency changes however.
Hmm...
Ok, so to give this concept a play around, I decided to use all the recent work I did on the Minima to implement 1 Hz tuning on the Si570 and see if I could implement a decode-able PSK31 varicode message using MSK techniques with the Si570.
The Si570 is technically capable of 0.42 Hz frequency precision, but for this experiment, I will use 8 Hz as the frequency shift above and below the transmit frequency instead of 7.8125 Hz.
At 7.8125 Hz, 360 degrees of phase change takes 128 ms. 90 degrees of phase change requires 128/4 = 32 ms.
At 8 Hz, 360 degrees of phase change takes 125 ms. 90 degrees of phase change requires 125/4 = 31.25 ms.
I suspect that most PSK31 decoding applications will be able to deal with this difference.
Having modified my recent PSK31 code to drive the Si570 oscillator rather than generate audio, I have run into a number of problems. I suspected that the i2c communications library that comes with the Arduino IDE would require interrupts to be enabled, which is true. If you try to call the Si570 code from within a timer interrupt, it will hang the device.
Enabling interrupts around the Si570 call within the timer ISR cures the hanging problem, but fails to communicate with the Si570 to set the frequency. I suspect that while the library is returning quickly, the i2c communications doesn't complete until sometime later.
Moving the setFrequency() call out of the ISR generates MSK, but it appears that there are bits being lost. The ISR is completing in 1.040 us, but is not accurately generating all of the frequency shifts. My suspicion is that the wire library is returning quickly, but that its own interrupt are not completing in a timely fashion to allow precise control over when frequency shifts are happening. I will have to do some measurements on the i2c communications and put some debugging pin toggles in the wire library to measure how long things are taking to complete.
More to come...
Showing posts with label BPSK. Show all posts
Showing posts with label BPSK. Show all posts
Sunday, December 28, 2014
Saturday, December 27, 2014
Generating Audio PSK31 with an Arduino (Part 2)
I have completed my initial pass on PSK31 audio generation using an Arduino. While I have not yet fixed the PSK31 character timing, fldigi is able to decode what I am generating, so there is a bit of character timing flexibility at least in the implementation of fldigi. I have not tested with any other PSK decoder as it is still my intention to fix this part of the implementation. However, I wanted to publish an update with what I have working regardless.
Here is the top part of the code listing where we describe the functionality and define the table that translates between 7 bit ASCII characters and the variable bit length PSK31 character set.
// PSK31 audio generation
// Jeff Whitlatch - ko7m
// We are going to generate a 1 kHz centre frequency tone.
// Each 1 kHz cycle of the sinusoid will be generated from
// 32 eight bit amplitude samples. The period of a 1 kHz tone
// is 1 ms. Each of the 32 samples per cycle has a period
// of 31.25 us. We will construct each sinusoid from a 32 byte
// per cycle lookup table of amplitude values ranging from
// 0x00 to 0xff where the zero crossing value is 0x80.
// The PSK31 character bit time is 31.25 ms constructed of 1024
// samples. A binary zero is represented by a phase reversal
// while a binary 1 is represented by the lack of a phase reversal.
// Characters are encoded with a variable bit length code (varicode)
// where the length of each character is inversely
// proportional to the frequency of use in the english language
// of that character. Characters are encoded with a bit
// pattern where there are no sequential zero bits. Two zero bits
// in a row signify the end of a character.
// Varicode lookup table
//
// This table defines the PKS31 varicode. There are 128 entries,
// corresponding to ASCII characters 0-127 with two bytes for each entry.
// The bits for the varicode are to be shifted out LSB-first.
//
// More than one zero in sequence signifies the end of the character.
// For modulation, a 0 represents a phase reversal while a 1
// represents a steady-state carrier.
uint16_t varicode[] = {
0x0355, // 0 NUL
0x036d, // 1 SOH
0x02dd, // 2 STX
0x03bb, // 3 ETX
0x035d, // 4 EOT
0x03eb, // 5 ENQ
0x03dd, // 6 ACK
0x02fd, // 7 BEL
0x03fd, // 8 BS
0x00f7, // 9 HT
0x0017, // 10 LF
0x03db, // 11 VT
0x02ed, // 12 FF
0x001f, // 13 CR
0x02bb, // 14 SO
0x0357, // 15 SI
0x03bd, // 16 DLE
0x02bd, // 17 DC1
0x02d7, // 18 DC2
0x03d7, // 19 DC3
0x036b, // 20 DC4
0x035b, // 21 NAK
0x02db, // 22 SYN
0x03ab, // 23 ETB
0x037b, // 24 CAN
0x02fb, // 25 EM
0x03b7, // 26 SUB
0x02ab, // 27 ESC
0x02eb, // 28 FS
0x0377, // 29 GS
0x037d, // 30 RS
0x03fb, // 31 US
0x0001, // 32 SP
0x01ff, // 33 !
0x01f5, // 34 @
0x015f, // 35 #
0x01b7, // 36 $
0x02ad, // 37 %
0x0375, // 38 &
0x01fd, // 39 '
0x00df, // 40 (
0x00ef, // 41 )
0x01ed, // 42 *
0x01f7, // 43 +
0x0057, // 44 ,
0x002b, // 45 -
0x0075, // 46 .
0x01eb, // 47 /
0x00ed, // 48 0
0x00bd, // 49 1
0x00b7, // 50 2
0x00ff, // 51 3
0x01dd, // 52 4
0x01b5, // 53 5
0x01ad, // 54 6
0x016b, // 55 7
0x01ab, // 56 8
0x01db, // 57 9
0x00af, // 58 :
0x017b, // 59 ;
0x016f, // 60 <
0x0055, // 61 =
0x01d7, // 62 >
0x03d5, // 63 ?
0x02f5, // 64 @
0x005f, // 65 A
0x00d7, // 66 B
0x00b5, // 67 C
0x00ad, // 68 D
0x0077, // 69 E
0x00db, // 70 F
0x00bf, // 71 G
0x0155, // 72 H
0x007f, // 73 I
0x017f, // 74 J
0x017d, // 75 K
0x00eb, // 76 L
0x00dd, // 77 M
0x00bb, // 78 N
0x00d5, // 79 O
0x00ab, // 80 P
0x0177, // 81 Q
0x00f5, // 82 R
0x007b, // 83 S
0x005b, // 84 T
0x01d5, // 85 U
0x015b, // 86 V
0x0175, // 87 W
0x015d, // 88 X
0x01bd, // 89 Y
0x02d5, // 90 Z
0x01df, // 91 [
0x01ef, // 92
0x01bf, // 93 ]
0x03f5, // 94 ^
0x016d, // 95 _
0x03ed, // 96 `
0x000d, // 97 a
0x007d, // 98 b
0x003d, // 99 c
0x002d, // 100 d
0x0003, // 101 e
0x002f, // 102 f
0x006d, // 103 g
0x0035, // 104 h
0x000b, // 105 i
0x01af, // 106 j
0x00fd, // 107 k
0x001b, // 108 l
0x0037, // 109 m
0x000f, // 110 n
0x0007, // 111 o
0x003f, // 112 p
0x01fb, // 113 q
0x0015, // 114 r
0x001d, // 115 s
0x0005, // 116 t
0x003b, // 117 u
0x006f, // 118 v
0x006b, // 119 w
0x00fb, // 120 x
0x005d, // 121 y
0x0157, // 122 z
0x03b5, // 123 {
0x01bb, // 124 |
0x02b5, // 125 }
0x03ad, // 126 ~
0x02b7 // 127 (del)
};
Now we define the sinusoid data table of 513 bytes. The first 512 bytes are 16 cycles of sine data ramping up from zero to full volume.
// 16 cycles of 32 samples each (512 bytes) of ramp-up
// sinusoid information. There is an extra byte at the
// end of the table with the value 0x80 which allows the
// first byte to always be at the zero crossing point
// whether ramping up or down.
char data[] = {
0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC,
0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
0x80
};
// The last 32 bytes (33 with the extra on the end)
// define a single cycle of full amplitude sinusoid.
#define one (&data[15*32]) // Sine table pointer for a one bit
#define zero (&data[16*32]) // Sine table pointer for a zero bit
// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
The following variables are used in the interrupt service routine. These variables define the 7 bit ASCII buffer of the text to be sent. The idea is to maintain a head and tail index into the buffer where the head is the next character to be sent. The tail is the place where new text will be inserted. The buffer is circular and when head == tail, the buffer is empty and we stop the PSK31 transmission.
// Variables used by the timer ISR to generate sinusoidal information.
volatile char rgchBuf[256]; // Buffer of text to send
volatile uint8_t head = 0; // Buffer head (next character to send)
volatile uint8_t tail = 0; // Buffer tail (next insert point)
The following variable holds the variable bit length character currently being sent. Bits are sent least significant bit (LSB) first. When two zero bits have been sent, the character is finished and the next character is fetched from the buffer above.
volatile uint16_t vcChar = 0; // Current varicode char being sent
The following variable should be a constant as there is a constant number of phase points in 1/2 the PSK31 bit time of 1024 phase points. I will fix this in the final version.
volatile int cbHalfBit = 512;
The following variables are keeping track of the current phase point (index into the sine table) and the number of phase points that remain in the direction we are scanning the table. Lastly, we keep track of if we are currently sending a PSK31 one bit or a zero bit.
volatile char *pbSine = zero;
volatile int cbDirection = 512;
volatile char fSendOne = false;
The IX variable is the increment (+1 or -1) to add to the phase index to get to the next phase point to be processed. The sign of the variable indicates whether we are processing the table in the forward or reverse direction. Phase is the current phase of the sinusoid and is either +1 for no phase shift or -1 for 180 degree phase shift. The fFullBit variable tells us if we are processing the first or second half of the PSK31 bit.
volatile char ix = -1;
volatile char phase = 1;
volatile char fFullBit = 0;
The cZeroBits counts the number of consecutive zero bits that have been sent in order to detect the end of a character. The maxZeroBits variable tells us how many zero bits indicate the end of a character. This is also used to send a few zero bits at the end of a transmission before turning off the tone.
volatile char cZeroBits = 0;
volatile char maxZeroBits = 2;
The following code sets up timer 2 to process our phase point generation. This still needs to be adjusted to be 31.25 microseconds rather than the current 32 microseconds. I will fix this, I promise. It just has not been a priority.
// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See th ATMega datasheet for all the gory details
// This is not quite right for PSK31 as it is 32 us vs. 31.25 us
void timer2Setup()
{
// Clock prescaler = 1
sbi (TCCR2B, CS20); // 001 = no prescaling
cbi (TCCR2B, CS21);
cbi (TCCR2B, CS22);
// Phase Correct PWM
cbi (TCCR2A, COM2A0); // 10 = clear OC2A on compare match when up counting
sbi (TCCR2A, COM2A1); // set OC2A on compare match when down counting
// Mode 1
sbi (TCCR2A, WGM20); // 101 = Mode 5 uses OCR2A as top value rather than 0xff
cbi (TCCR2A, WGM21);
}
Now we have the main waveform generation state machine. This code is still very rough and will need to be optimized once I get all functionality implemented.
// Timer 2 interrupt service routine (ISR).
//
// Grab the next phase point from the table and
// set the amplitude value of the sinusoid being
// constructed. For a one bit, set 512 phase points
// (16 amplitudes of 32 samples each) to ramp
// down to zero and then immediately back up to full
// amplitude for a total of 1024 phase points.
//
// For a zero bit, there is no amplitude or phase
// change, so we just play 32 phase points of
// full amplitude data 32 times for a total of 1024
// phase points.
//
// Each end of the ramp-up table starts with a zero
// crossing byte, so there is one extra byte in
// the table (513 entries). Ramping up plays bytes
// 0 -> 511 and ramping down plays bytes 512 -> 1
// allowing each direction to start at the zero
// crossing point.
ISR(TIMER2_OVF_vect)
{
// Set current amplitude value for the sine wave
// being constructed taking care to invert the
// phase when processing the table in reverse order.
OCR2A = *pbSine * ix * phase;
pbSine += ix;
// At the half bit time, we need to change phase
// if generating a zero bit
if (0 == --cbHalfBit)
{
cbHalfBit = 512;
// Get the next varichar bit to send
if (fFullBit)
{
// Count the number of sequential zero bits
if (fSendOne = vcChar & 1) cZeroBits = 0; else cZeroBits++;
// Shift off the least significant bit.
vcChar >>= 1;
// If we have sent two zero bits, end of character has occurred
if (cZeroBits > maxZeroBits)
{
cZeroBits = 0;
// If send buffer not empty, get next varicode character
if (head != tail)
{
// Assumes a 256 byte buffer as index increments modulo 256
vcChar = varicode[rgchBuf[head++]];
}
else
if (maxZeroBits > 2) cbi (TIMSK2,TOIE2); else maxZeroBits = 75;
}
}
fFullBit = !fFullBit; // Toggle end of full bit flag
// When we get done ramping down, phase needs to
// change unless we are sending a one bit
if (ix < 0 &&!fSendOne) phase = -phase;
}
// At the end of the table for the bit being
// generated, we need to change direction
// and process the table in the other direction.
if (0 == --cbDirection)
{
cbDirection = fSendOne ? 32 : 512;
ix = -ix;
}
}
Setup is going to set our pin modes, set up timer 2 and put some test text into the send buffer to be processed. Once the timer is enabled, PSK generation is automagic.
void setup()
{
// PWM output for timer2 is pin 10 on the ATMega2560
// If you use an ATMega328 (such as the UNO) you need
// to make this pin 11
// See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
pinMode(10, OUTPUT); // Timer 2 PWM output on mega256 is pin 10
// Set up timer2 to a phase correct 32kHz clock
timer2Setup();
// Put something in the buffer to be sent
strcpy((char *) &rgchBuf[0], "\ncq cq cq de ko7m ko7m ko7m"
"\ncq cq cq de ko7m ko7m ko7m pse k\n");
tail = strlen((const char *) rgchBuf);
head = 0;
sbi (TIMSK2,TOIE2); // Enable timer 2.
}
Nothing to do (yet) in the main loop
void loop()
{
}
Here is a screen shot of fldigi decoding the test message I have hard coded.
As always, your mileage may vary. If you have any questions or comments I would love to hear from you by posting here or dropping me a line at ko7m at arrl dot org and I will do my best to help you out.
Here is the top part of the code listing where we describe the functionality and define the table that translates between 7 bit ASCII characters and the variable bit length PSK31 character set.
// PSK31 audio generation
// Jeff Whitlatch - ko7m
// We are going to generate a 1 kHz centre frequency tone.
// Each 1 kHz cycle of the sinusoid will be generated from
// 32 eight bit amplitude samples. The period of a 1 kHz tone
// is 1 ms. Each of the 32 samples per cycle has a period
// of 31.25 us. We will construct each sinusoid from a 32 byte
// per cycle lookup table of amplitude values ranging from
// 0x00 to 0xff where the zero crossing value is 0x80.
// The PSK31 character bit time is 31.25 ms constructed of 1024
// samples. A binary zero is represented by a phase reversal
// while a binary 1 is represented by the lack of a phase reversal.
// Characters are encoded with a variable bit length code (varicode)
// where the length of each character is inversely
// proportional to the frequency of use in the english language
// of that character. Characters are encoded with a bit
// pattern where there are no sequential zero bits. Two zero bits
// in a row signify the end of a character.
// Varicode lookup table
//
// This table defines the PKS31 varicode. There are 128 entries,
// corresponding to ASCII characters 0-127 with two bytes for each entry.
// The bits for the varicode are to be shifted out LSB-first.
//
// More than one zero in sequence signifies the end of the character.
// For modulation, a 0 represents a phase reversal while a 1
// represents a steady-state carrier.
uint16_t varicode[] = {
0x0355, // 0 NUL
0x036d, // 1 SOH
0x02dd, // 2 STX
0x03bb, // 3 ETX
0x035d, // 4 EOT
0x03eb, // 5 ENQ
0x03dd, // 6 ACK
0x02fd, // 7 BEL
0x03fd, // 8 BS
0x00f7, // 9 HT
0x0017, // 10 LF
0x03db, // 11 VT
0x02ed, // 12 FF
0x001f, // 13 CR
0x02bb, // 14 SO
0x0357, // 15 SI
0x03bd, // 16 DLE
0x02bd, // 17 DC1
0x02d7, // 18 DC2
0x03d7, // 19 DC3
0x036b, // 20 DC4
0x035b, // 21 NAK
0x02db, // 22 SYN
0x03ab, // 23 ETB
0x037b, // 24 CAN
0x02fb, // 25 EM
0x03b7, // 26 SUB
0x02ab, // 27 ESC
0x02eb, // 28 FS
0x0377, // 29 GS
0x037d, // 30 RS
0x03fb, // 31 US
0x0001, // 32 SP
0x01ff, // 33 !
0x01f5, // 34 @
0x015f, // 35 #
0x01b7, // 36 $
0x02ad, // 37 %
0x0375, // 38 &
0x01fd, // 39 '
0x00df, // 40 (
0x00ef, // 41 )
0x01ed, // 42 *
0x01f7, // 43 +
0x0057, // 44 ,
0x002b, // 45 -
0x0075, // 46 .
0x01eb, // 47 /
0x00ed, // 48 0
0x00bd, // 49 1
0x00b7, // 50 2
0x00ff, // 51 3
0x01dd, // 52 4
0x01b5, // 53 5
0x01ad, // 54 6
0x016b, // 55 7
0x01ab, // 56 8
0x01db, // 57 9
0x00af, // 58 :
0x017b, // 59 ;
0x016f, // 60 <
0x0055, // 61 =
0x01d7, // 62 >
0x03d5, // 63 ?
0x02f5, // 64 @
0x005f, // 65 A
0x00d7, // 66 B
0x00b5, // 67 C
0x00ad, // 68 D
0x0077, // 69 E
0x00db, // 70 F
0x00bf, // 71 G
0x0155, // 72 H
0x007f, // 73 I
0x017f, // 74 J
0x017d, // 75 K
0x00eb, // 76 L
0x00dd, // 77 M
0x00bb, // 78 N
0x00d5, // 79 O
0x00ab, // 80 P
0x0177, // 81 Q
0x00f5, // 82 R
0x007b, // 83 S
0x005b, // 84 T
0x01d5, // 85 U
0x015b, // 86 V
0x0175, // 87 W
0x015d, // 88 X
0x01bd, // 89 Y
0x02d5, // 90 Z
0x01df, // 91 [
0x01ef, // 92
0x01bf, // 93 ]
0x03f5, // 94 ^
0x016d, // 95 _
0x03ed, // 96 `
0x000d, // 97 a
0x007d, // 98 b
0x003d, // 99 c
0x002d, // 100 d
0x0003, // 101 e
0x002f, // 102 f
0x006d, // 103 g
0x0035, // 104 h
0x000b, // 105 i
0x01af, // 106 j
0x00fd, // 107 k
0x001b, // 108 l
0x0037, // 109 m
0x000f, // 110 n
0x0007, // 111 o
0x003f, // 112 p
0x01fb, // 113 q
0x0015, // 114 r
0x001d, // 115 s
0x0005, // 116 t
0x003b, // 117 u
0x006f, // 118 v
0x006b, // 119 w
0x00fb, // 120 x
0x005d, // 121 y
0x0157, // 122 z
0x03b5, // 123 {
0x01bb, // 124 |
0x02b5, // 125 }
0x03ad, // 126 ~
0x02b7 // 127 (del)
};
Now we define the sinusoid data table of 513 bytes. The first 512 bytes are 16 cycles of sine data ramping up from zero to full volume.
// 16 cycles of 32 samples each (512 bytes) of ramp-up
// sinusoid information. There is an extra byte at the
// end of the table with the value 0x80 which allows the
// first byte to always be at the zero crossing point
// whether ramping up or down.
char data[] = {
0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC,
0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
0x80
};
// The last 32 bytes (33 with the extra on the end)
// define a single cycle of full amplitude sinusoid.
#define one (&data[15*32]) // Sine table pointer for a one bit
#define zero (&data[16*32]) // Sine table pointer for a zero bit
// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
The following variables are used in the interrupt service routine. These variables define the 7 bit ASCII buffer of the text to be sent. The idea is to maintain a head and tail index into the buffer where the head is the next character to be sent. The tail is the place where new text will be inserted. The buffer is circular and when head == tail, the buffer is empty and we stop the PSK31 transmission.
// Variables used by the timer ISR to generate sinusoidal information.
volatile char rgchBuf[256]; // Buffer of text to send
volatile uint8_t head = 0; // Buffer head (next character to send)
volatile uint8_t tail = 0; // Buffer tail (next insert point)
The following variable holds the variable bit length character currently being sent. Bits are sent least significant bit (LSB) first. When two zero bits have been sent, the character is finished and the next character is fetched from the buffer above.
volatile uint16_t vcChar = 0; // Current varicode char being sent
The following variable should be a constant as there is a constant number of phase points in 1/2 the PSK31 bit time of 1024 phase points. I will fix this in the final version.
volatile int cbHalfBit = 512;
The following variables are keeping track of the current phase point (index into the sine table) and the number of phase points that remain in the direction we are scanning the table. Lastly, we keep track of if we are currently sending a PSK31 one bit or a zero bit.
volatile char *pbSine = zero;
volatile int cbDirection = 512;
volatile char fSendOne = false;
The IX variable is the increment (+1 or -1) to add to the phase index to get to the next phase point to be processed. The sign of the variable indicates whether we are processing the table in the forward or reverse direction. Phase is the current phase of the sinusoid and is either +1 for no phase shift or -1 for 180 degree phase shift. The fFullBit variable tells us if we are processing the first or second half of the PSK31 bit.
volatile char ix = -1;
volatile char phase = 1;
volatile char fFullBit = 0;
The cZeroBits counts the number of consecutive zero bits that have been sent in order to detect the end of a character. The maxZeroBits variable tells us how many zero bits indicate the end of a character. This is also used to send a few zero bits at the end of a transmission before turning off the tone.
volatile char cZeroBits = 0;
volatile char maxZeroBits = 2;
The following code sets up timer 2 to process our phase point generation. This still needs to be adjusted to be 31.25 microseconds rather than the current 32 microseconds. I will fix this, I promise. It just has not been a priority.
// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See th ATMega datasheet for all the gory details
// This is not quite right for PSK31 as it is 32 us vs. 31.25 us
void timer2Setup()
{
// Clock prescaler = 1
sbi (TCCR2B, CS20); // 001 = no prescaling
cbi (TCCR2B, CS21);
cbi (TCCR2B, CS22);
// Phase Correct PWM
cbi (TCCR2A, COM2A0); // 10 = clear OC2A on compare match when up counting
sbi (TCCR2A, COM2A1); // set OC2A on compare match when down counting
// Mode 1
sbi (TCCR2A, WGM20); // 101 = Mode 5 uses OCR2A as top value rather than 0xff
cbi (TCCR2A, WGM21);
}
Now we have the main waveform generation state machine. This code is still very rough and will need to be optimized once I get all functionality implemented.
// Timer 2 interrupt service routine (ISR).
//
// Grab the next phase point from the table and
// set the amplitude value of the sinusoid being
// constructed. For a one bit, set 512 phase points
// (16 amplitudes of 32 samples each) to ramp
// down to zero and then immediately back up to full
// amplitude for a total of 1024 phase points.
//
// For a zero bit, there is no amplitude or phase
// change, so we just play 32 phase points of
// full amplitude data 32 times for a total of 1024
// phase points.
//
// Each end of the ramp-up table starts with a zero
// crossing byte, so there is one extra byte in
// the table (513 entries). Ramping up plays bytes
// 0 -> 511 and ramping down plays bytes 512 -> 1
// allowing each direction to start at the zero
// crossing point.
ISR(TIMER2_OVF_vect)
{
// Set current amplitude value for the sine wave
// being constructed taking care to invert the
// phase when processing the table in reverse order.
OCR2A = *pbSine * ix * phase;
pbSine += ix;
// At the half bit time, we need to change phase
// if generating a zero bit
if (0 == --cbHalfBit)
{
cbHalfBit = 512;
// Get the next varichar bit to send
if (fFullBit)
{
// Count the number of sequential zero bits
if (fSendOne = vcChar & 1) cZeroBits = 0; else cZeroBits++;
// Shift off the least significant bit.
vcChar >>= 1;
// If we have sent two zero bits, end of character has occurred
if (cZeroBits > maxZeroBits)
{
cZeroBits = 0;
// If send buffer not empty, get next varicode character
if (head != tail)
{
// Assumes a 256 byte buffer as index increments modulo 256
vcChar = varicode[rgchBuf[head++]];
}
else
if (maxZeroBits > 2) cbi (TIMSK2,TOIE2); else maxZeroBits = 75;
}
}
fFullBit = !fFullBit; // Toggle end of full bit flag
// When we get done ramping down, phase needs to
// change unless we are sending a one bit
if (ix < 0 &&!fSendOne) phase = -phase;
}
// At the end of the table for the bit being
// generated, we need to change direction
// and process the table in the other direction.
if (0 == --cbDirection)
{
cbDirection = fSendOne ? 32 : 512;
ix = -ix;
}
}
Setup is going to set our pin modes, set up timer 2 and put some test text into the send buffer to be processed. Once the timer is enabled, PSK generation is automagic.
void setup()
{
// PWM output for timer2 is pin 10 on the ATMega2560
// If you use an ATMega328 (such as the UNO) you need
// to make this pin 11
// See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
pinMode(10, OUTPUT); // Timer 2 PWM output on mega256 is pin 10
// Set up timer2 to a phase correct 32kHz clock
timer2Setup();
// Put something in the buffer to be sent
strcpy((char *) &rgchBuf[0], "\ncq cq cq de ko7m ko7m ko7m"
"\ncq cq cq de ko7m ko7m ko7m pse k\n");
tail = strlen((const char *) rgchBuf);
head = 0;
sbi (TIMSK2,TOIE2); // Enable timer 2.
}
Nothing to do (yet) in the main loop
void loop()
{
}
Here is a screen shot of fldigi decoding the test message I have hard coded.
As always, your mileage may vary. If you have any questions or comments I would love to hear from you by posting here or dropping me a line at ko7m at arrl dot org and I will do my best to help you out.
Wednesday, December 24, 2014
Generating Audio PSK31 with an Arduino (update)
This is really just an update to my previous post rather than a complete "part 2" of my discussion. I have been tending a some rather tedious medical issues and have not been writing code for a while. However, I have a quick update to the start of my PSK code that fixes the anomalous waveform seen here.
The updated waveform now looks phase continuous without the glitch previously seen.
The signal sounds nice when played through amplified speakers. Here is a screen shot from fldigi of the continuous idle signal. Looks good and the signal is pretty clean on the waterfall display. I still have to fix the clock timer speed to get the character time correct, but that can wait for now.
Next up is to implement character transmission. For those following along at home, here is the updated code in its entirety. Remember that as written this is for the ATMega2560 board. You will need to change the PWM output pin to pin 11 for the UNO and other ATMega328 devices.
// PSK31 audio generation
// Jeff Whitlatch - ko7m
// 16 cycles of 32 samples each (512 bytes) of ramp-up sinusoid information.
// There is an extra byte at the end of the table with the value 0x80 which
// allows the first byte to always be at the zero crossing point whether
// ramping up or down.
char zero[] = {
0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC,
0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
0x80
};
// The last 32 bytes (33 with the extra on the end) define a single cycle of
// full amplitude sinusoid.
#define one (&zero[15*32])
// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
// Variables used by the timer ISR to generate sinusoidal information.
volatile char *pbSine = &zero[0];
volatile int cbSine = 512;
volatile char ix = 1;
volatile char phase = 1;
// PSK31 engine
// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See the ATMega datasheet for all the gory details
// This is not quite right for PSK31 as it is 32 us vs. 31.25 us
void timer2Setup()
{
// Clock prescaler = 1
sbi (TCCR2B, CS20); // 001 = no prescaling
cbi (TCCR2B, CS21);
cbi (TCCR2B, CS22);
// Phase Correct PWM
cbi (TCCR2A, COM2A0); // 10 = clear OC2A on compare match when up counting
sbi (TCCR2A, COM2A1); // set OC2A on compare match when down counting
// Mode 1
sbi (TCCR2A, WGM20); // 101 = Mode 5 uses OCR2A as top value rather than 0xff
cbi (TCCR2A, WGM21);
}
// Timer 2 interrupt service routine (ISR).
//
// Grab the next phase point from the table and set the amplitude value
// of the sinusoid being constructed. For a one bit, set 512 phase points
// (16 amplitudes of 32 samples each) to ramp down to zero and then immediately
// back up to full amplitude for a total of 1024 phase points.
//
// For a zero bit, there is not amplitude or phase change, so we just
// play 32 phase points of full amplitude data 32 times for a total of
// 1024 phase points.
//
// Each end of the ramp-up table starts with a zero crossing byte, so there
// is one extra byte in the table (513 entries). Ramping up plays bytes 0 -> 511
// and ramping down plays bytes 512 -> 1 allowing each direction to start at the
// zero crossing point.
ISR(TIMER2_OVF_vect)
{
// Set current amplitude value for the sine wave being constructed taking
// care to invert the phase when processing the table in reverse order.
OCR2A = *(pbSine)* ix * phase;
pbSine += ix;
if (0 == --cbSine)
{
cbSine = 512;
// When we get done ramping down, phase needs to change
if (ix < 0) phase = -phase;
ix = -ix;
}
}
void setup()
{
// PWM output for timer2 is pin 10 on the ATMega2560
// If you use an ATMega328 (such as the UNO) you need to make this pin 11
// See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
pinMode(10, OUTPUT); // Timer 2 PWM output on mega256 is pin 10
// Set up timer2 to a phase correct 32kHz clock
timer2Setup();
sbi (TIMSK2,TOIE2); // Enable timer 2.
}
void loop()
{
}
The updated waveform now looks phase continuous without the glitch previously seen.
The signal sounds nice when played through amplified speakers. Here is a screen shot from fldigi of the continuous idle signal. Looks good and the signal is pretty clean on the waterfall display. I still have to fix the clock timer speed to get the character time correct, but that can wait for now.
Next up is to implement character transmission. For those following along at home, here is the updated code in its entirety. Remember that as written this is for the ATMega2560 board. You will need to change the PWM output pin to pin 11 for the UNO and other ATMega328 devices.
// PSK31 audio generation
// Jeff Whitlatch - ko7m
// 16 cycles of 32 samples each (512 bytes) of ramp-up sinusoid information.
// There is an extra byte at the end of the table with the value 0x80 which
// allows the first byte to always be at the zero crossing point whether
// ramping up or down.
char zero[] = {
0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC,
0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
0x80
};
// The last 32 bytes (33 with the extra on the end) define a single cycle of
// full amplitude sinusoid.
#define one (&zero[15*32])
// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
// Variables used by the timer ISR to generate sinusoidal information.
volatile char *pbSine = &zero[0];
volatile int cbSine = 512;
volatile char ix = 1;
volatile char phase = 1;
// PSK31 engine
// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See the ATMega datasheet for all the gory details
// This is not quite right for PSK31 as it is 32 us vs. 31.25 us
void timer2Setup()
{
// Clock prescaler = 1
sbi (TCCR2B, CS20); // 001 = no prescaling
cbi (TCCR2B, CS21);
cbi (TCCR2B, CS22);
// Phase Correct PWM
cbi (TCCR2A, COM2A0); // 10 = clear OC2A on compare match when up counting
sbi (TCCR2A, COM2A1); // set OC2A on compare match when down counting
// Mode 1
sbi (TCCR2A, WGM20); // 101 = Mode 5 uses OCR2A as top value rather than 0xff
cbi (TCCR2A, WGM21);
}
// Timer 2 interrupt service routine (ISR).
//
// Grab the next phase point from the table and set the amplitude value
// of the sinusoid being constructed. For a one bit, set 512 phase points
// (16 amplitudes of 32 samples each) to ramp down to zero and then immediately
// back up to full amplitude for a total of 1024 phase points.
//
// For a zero bit, there is not amplitude or phase change, so we just
// play 32 phase points of full amplitude data 32 times for a total of
// 1024 phase points.
//
// Each end of the ramp-up table starts with a zero crossing byte, so there
// is one extra byte in the table (513 entries). Ramping up plays bytes 0 -> 511
// and ramping down plays bytes 512 -> 1 allowing each direction to start at the
// zero crossing point.
ISR(TIMER2_OVF_vect)
{
// Set current amplitude value for the sine wave being constructed taking
// care to invert the phase when processing the table in reverse order.
OCR2A = *(pbSine)* ix * phase;
pbSine += ix;
if (0 == --cbSine)
{
cbSine = 512;
// When we get done ramping down, phase needs to change
if (ix < 0) phase = -phase;
ix = -ix;
}
}
void setup()
{
// PWM output for timer2 is pin 10 on the ATMega2560
// If you use an ATMega328 (such as the UNO) you need to make this pin 11
// See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
pinMode(10, OUTPUT); // Timer 2 PWM output on mega256 is pin 10
// Set up timer2 to a phase correct 32kHz clock
timer2Setup();
sbi (TIMSK2,TOIE2); // Enable timer 2.
}
void loop()
{
}
Thursday, December 18, 2014
Generating Audio PSK31 with an Arduino
I have been playing around with the idea of generating PSK31 audio with an Arduino. In a previous post a few years ago, I implemented a similar approach using the Parallax Propeller processor. I never took that implementation beyond a proof of concept, but hope that others will find this interesting and perhaps useful. Some of this information is copied from this previous post for ease of understanding.
PSK31 is a digital communications mode which is intended for live keyboard-to-keyboard conversations, similar to radioteletype. The data rate is 31.25 baud (about 50 word-per-minute). PSK31's ITU emission designator is 60H0J2B. It uses BPSK modulation without error correction.
Instead of traditional frequency-shift keying, the information is transmitted by patterns of polarity-reversals (sometimes called 180-degree phase shifts). One way to think about this would be to swap antenna terminals on each phase reversal.
The 31.25 baud data rate was chosen so that the system will handle hand-sent typed text easily. There is a problem with PSK keying, namely the effect of key-clicks. If hard keying of phase reversals were done, the result would be a very broad emission. The solution is to filter the output or to shape the envelope amplitude of each bit, which amounts to the same thing.
In PSK31 a cosine shape is used. Phase reversals are done at the minimum amplitude points. The spectrum during a continuous sequence of polarity reversals at 31 baud will consist of two pure tones at +15/-15 Hz from the center frequency and no splatter. A binary 0 is represented by a phase reversal and a binary 1 by no phase reversal.
Once I get up to full amplitude, I use a separate 32 eight-byte table consisting of a single sinusoid. For a binary zero, I ramp down (512 samples) by processing the above table in reverse, reverse the phase and immediately ramp back up again (another 512 samples), this time processing the table in the forward direction. For a binary one, I use the 32 eight bit sample table below and repeat it 32 times for 1024 total samples.
I considered using my software DDS example for generating PSK, but decided instead to do a very similar implementation that was not quite so general purpose in nature in order to gain some performance enhancements. I certainly don't need 32 bits of frequency setting resolution when generating audio tones and I think I need more phase points per cycle of the tone generated. The example below is not at odds with my DDS example, it is just a special case of the more generic implementation previously described.
So far, I just have it generating a sequence of PSK31 zero bits, which represents an idle state with no data to transmit. I want to see how this signal looks before I get busy implementing a full PSK31 encoder.
Firstly, we need our sinusoidal ramp up information. This table could be compressed down to about 16 bytes if you are willing to do more calculations during interrupt processing. Only 1/4 of one cycle (90 degrees) of data needs to be stored at full amplitude values. The upper two bits could indicate which 90 degrees we are generating (0-89 degrees, 90-179 degrees, 180-269 degrees and 270-359 degrees). Furthermore, we could calculate any number of amplitude values between zero and full amplitude at the cost of more CPU time during interrupt processing. For now, I am storing the entire table and just processing one table entry per interrupt. I have one extra byte at the end that is technically not necessary representing the final phase point at the zero crossing.
// 16 cycles of 32 samples each (512 bytes) of ramp-up sinusoid information
char zero[] = { 0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC
,0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
0x80
};
The last 32 bytes of this table are a single sinusoid cycle that is at full amplitude, so rather than create a separate table for this data, I will just index into the above array and use the last 32 bytes.
#define one (&zero[15*32])
// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
The following variables are marked as volatile as they are used in an interrupt service routine. We keep track of which phase point (index into above table) we are processing and a count of the remaining phase points in the table. When ramping down from full volume to zero, we process the phase table in reverse order. Reversing the processing order has the effect of reversing the phase, so in order to be phase continuous for an entire PSK31 bit, we must reverse the phase by negating the table entry when ramping down. Additionally, every time we cross through zero, we have to also reverse the phase.
volatile char *pbSine = &zero[0]; // Index into sinusoid table
volatile int cbSine = 512; // Length of sinusoid table
volatile char ix = 1; // Increment to get to next phase point
volatile char phase = 1; // PSK31 phase reversal
The following is not quite correct for PSK31, though it is close. We need to process a phase point every 31.25 us which is 32 kHz. Currently I am dividing the system clock (16 MHz) by 512 for 31.25 kHz or 32 us for every phase point, so I am a little slow. I will tend to this issue a little later. Right now I am just trying to see if I can generate the waveforms.
// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See th ATMega datasheet for all the gory details
void timer2Setup()
{
// Clock prescaler = 1
sbi (TCCR2B, CS20); // 001 = no prescaling
cbi (TCCR2B, CS21);
cbi (TCCR2B, CS22);
// Phase Correct PWM
cbi (TCCR2A, COM2A0); // 10 = clear OC2A on compare match when up counting
sbi (TCCR2A, COM2A1); // set OC2A on compare match when down counting
// Mode 1
sbi (TCCR2A, WGM20); // 101 = Mode 5 uses OCR2A as top value rather than 0xff
cbi (TCCR2A, WGM21);
}
Now for the interrupt service routine of the timer, I just fetch the next phase point out of the table and set it as the amplitude value. If we are ramping down we negate the value to reverse the phase and when starting to ramp up again, we reverse the phase again. When this is all figured out, I would likely rewrite all this in assembly.
// Timer 2 interrupt service routine (ISR).
ISR(TIMER2_OVF_vect)
{
// Set current amplitude value for the sine wave being constructed
// taking care to invert the phase when processing the table in
// reverse order.
OCR2A = *(pbSine) * ix * phase;
pbSine += ix;
if (0 == cbSine--)
{
cbSine = 512;
// When we get done ramping down, phase needs to change
if (ix < 0) phase = -phase;
ix = -ix;
}
}
Setup is assuming an ATMega2560 board. I use pin 10 for Timer 2 PWM output. There is nothing (yet) to do in the main loop. This code just generates a slightly too slow string of PSK31 zero bits.
void setup()
{
// PWM output for timer2 is pin 10 on the ATMega2560
// If you use an ATMega328 (such as the UNO) you need to make this pin 11
// See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
pinMode(10, OUTPUT); // Timer 2 PWM output on mega256 is pin 10
// Set up timer2 to a phase correct 32kHz clock
timer2Setup();
sbi (TIMSK2,TOIE2); // Enable timer 2.
}
void loop()
{
}
I have placed a simple RC low pass filter on the output, so the integration is not very good, but looking at this on the scope, we see the following:
This looks pretty good, but I have some concerns about what I see here. One concern is at the point where the ramp up table is processed in reverse order with a phase change. There is a bit of a glitch here, so I may need to clean up my phase points in this area slightly.
Additionally, there is a lot of noise as the amplitude is ramping down that I need to investigate as can be seen here.
This appears to just be an artifact of my simple RC filter integrator that I am using to remove high frequency components of the PWM output from the Arduino.
So, I am a long way from generating PSK31 signals, but wanted to share the progress as I head in that direction. As always, comments and questions are welcome by posting here or by emailing ko7m at arrl dot net.
PSK31 is a digital communications mode which is intended for live keyboard-to-keyboard conversations, similar to radioteletype. The data rate is 31.25 baud (about 50 word-per-minute). PSK31's ITU emission designator is 60H0J2B. It uses BPSK modulation without error correction.
Instead of traditional frequency-shift keying, the information is transmitted by patterns of polarity-reversals (sometimes called 180-degree phase shifts). One way to think about this would be to swap antenna terminals on each phase reversal.
The 31.25 baud data rate was chosen so that the system will handle hand-sent typed text easily. There is a problem with PSK keying, namely the effect of key-clicks. If hard keying of phase reversals were done, the result would be a very broad emission. The solution is to filter the output or to shape the envelope amplitude of each bit, which amounts to the same thing.
In PSK31 a cosine shape is used. Phase reversals are done at the minimum amplitude points. The spectrum during a continuous sequence of polarity reversals at 31 baud will consist of two pure tones at +15/-15 Hz from the center frequency and no splatter. A binary 0 is represented by a phase reversal and a binary 1 by no phase reversal.
The audio tone chosen is 1 KHz constructed from 32 eight-bit samples at full amplitude per cycle. The period of a 1 KHz tone cycle is 1 millisecond. Each of the 32 samples per cycle has a period of 31.25 microseconds. The audio samples are eight bit values from 0x00 - 0xFF with the zero crossing value at the midpoint 0x80.
The PSK31 character bit time is 32 ms constructed of 1024 samples. A binary zero is represented by a phase reversal while a binary one is presented by the absence of a phase reversal. Characters are encoded with a variable bit length code (varicode) where the length of each character is inversely proportional to the frequency of use. Characters are encoded with a bit pattern where there are no sequential zero bits. Two zero bits in a row signify the end of a character.
In order to implement the ramp up/down scenarios at phase reversals, I have constructed a couple of tables of sinusoid information. The first consists of 512 table entries defining the 16 cycles of ramping up from zero to full amplitude. I have plotted the data in Excel as follows:
I considered using my software DDS example for generating PSK, but decided instead to do a very similar implementation that was not quite so general purpose in nature in order to gain some performance enhancements. I certainly don't need 32 bits of frequency setting resolution when generating audio tones and I think I need more phase points per cycle of the tone generated. The example below is not at odds with my DDS example, it is just a special case of the more generic implementation previously described.
So far, I just have it generating a sequence of PSK31 zero bits, which represents an idle state with no data to transmit. I want to see how this signal looks before I get busy implementing a full PSK31 encoder.
Firstly, we need our sinusoidal ramp up information. This table could be compressed down to about 16 bytes if you are willing to do more calculations during interrupt processing. Only 1/4 of one cycle (90 degrees) of data needs to be stored at full amplitude values. The upper two bits could indicate which 90 degrees we are generating (0-89 degrees, 90-179 degrees, 180-269 degrees and 270-359 degrees). Furthermore, we could calculate any number of amplitude values between zero and full amplitude at the cost of more CPU time during interrupt processing. For now, I am storing the entire table and just processing one table entry per interrupt. I have one extra byte at the end that is technically not necessary representing the final phase point at the zero crossing.
// 16 cycles of 32 samples each (512 bytes) of ramp-up sinusoid information
char zero[] = { 0x80,0x80,0x80,0x80,0x81,0x81,0x82,0x82,
0x83,0x83,0x83,0x83,0x83,0x82,0x82,0x81,
0x7F,0x7E,0x7D,0x7B,0x7A,0x79,0x78,0x77,
0x76,0x76,0x76,0x77,0x78,0x79,0x7B,0x7D,
0x80,0x82,0x85,0x87,0x89,0x8B,0x8D,0x8E,
0x8F,0x8F,0x8F,0x8D,0x8C,0x89,0x86,0x83,
0x7F,0x7C,0x78,0x75,0x71,0x6E,0x6C,0x6B,
0x6A,0x6A,0x6B,0x6C,0x6F,0x72,0x76,0x7B,
0x80,0x84,0x89,0x8E,0x92,0x96,0x99,0x9A,
0x9B,0x9B,0x9A,0x98,0x94,0x90,0x8B,0x85,
0x7F,0x79,0x73,0x6E,0x69,0x64,0x61,0x5F,
0x5E,0x5E,0x60,0x62,0x66,0x6C,0x72,0x78,
0x80,0x87,0x8E,0x95,0x9B,0xA0,0xA4,0xA6,
0xA7,0xA7,0xA5,0xA2,0x9D,0x97,0x90,0x88,
0x7F,0x77,0x6F,0x67,0x60,0x5A,0x56,0x53,
0x52,0x52,0x55,0x59,0x5E,0x65,0x6D,0x76,
0x80,0x89,0x92,0x9B,0xA3,0xA9,0xAE,0xB2,
0xB3,0xB2,0xB0,0xAB,0xA5,0x9D,0x94,0x8A,
0x7F,0x75,0x6A,0x61,0x58,0x51,0x4B,0x48,
0x46,0x47,0x4A,0x4F,0x56,0x5F,0x69,0x74,
0x80,0x8B,0x97,0xA1,0xAB,0xB3,0xB9,0xBD,
0xBE,0xBD,0xBA,0xB4,0xAD,0xA3,0x98,0x8C,
0x7F,0x73,0x66,0x5B,0x50,0x48,0x41,0x3D,
0x3C,0x3D,0x40,0x46,0x4F,0x59,0x65,0x72,
0x80,0x8D,0x9B,0xA7,0xB2,0xBC,0xC2,0xC7,
0xC9,0xC8,0xC4,0xBD,0xB4,0xA9,0x9C,0x8E,
0x7F,0x71,0x62,0x55,0x49,0x3F,0x38,0x33,
0x31,0x33,0x37,0x3E,0x47,0x53,0x61,0x70,
0x80,0x8F,0x9F,0xAD,0xB9,0xC4,0xCC,0xD1,
0xD2,0xD1,0xCD,0xC5,0xBB,0xAE,0xA0,0x90,
0x7F,0x6F,0x5F,0x50,0x42,0x37,0x2F,0x2A,
0x28,0x29,0x2E,0x36,0x41,0x4E,0x5D,0x6E,
0x80,0x91,0xA2,0xB2,0xC0,0xCB,0xD4,0xD9,
0xDB,0xDA,0xD5,0xCD,0xC1,0xB3,0xA3,0x92,
0x7F,0x6D,0x5B,0x4B,0x3C,0x30,0x27,0x21,
0x1F,0x21,0x26,0x2F,0x3B,0x49,0x5A,0x6C,
0x80,0x93,0xA5,0xB6,0xC6,0xD2,0xDC,0xE1,
0xE4,0xE2,0xDC,0xD3,0xC7,0xB8,0xA6,0x93,
0x7F,0x6C,0x58,0x46,0x37,0x2A,0x20,0x1A,
0x18,0x19,0x1F,0x29,0x35,0x45,0x57,0x6B,
0x80,0x94,0xA8,0xBB,0xCB,0xD8,0xE2,0xE9,
0xEB,0xE9,0xE3,0xD9,0xCC,0xBC,0xA9,0x95,
0x7F,0x6A,0x56,0x43,0x32,0x24,0x1A,0x13,
0x11,0x13,0x19,0x23,0x31,0x42,0x55,0x6A,
0x80,0x95,0xAB,0xBE,0xCF,0xDD,0xE8,0xEF,
0xF1,0xEF,0xE9,0xDE,0xD0,0xBF,0xAB,0x96,
0x7F,0x69,0x53,0x3F,0x2E,0x1F,0x15,0x0E,
0x0B,0x0D,0x14,0x1F,0x2D,0x3F,0x53,0x69,
0x80,0x96,0xAD,0xC1,0xD3,0xE2,0xED,0xF4,
0xF6,0xF4,0xED,0xE2,0xD4,0xC2,0xAD,0x97,
0x7F,0x68,0x52,0x3D,0x2B,0x1C,0x10,0x09,
0x07,0x09,0x10,0x1B,0x2A,0x3C,0x51,0x68,
0x80,0x97,0xAE,0xC3,0xD6,0xE5,0xF0,0xF7,
0xFA,0xF8,0xF1,0xE6,0xD6,0xC4,0xAF,0x98,
0x7F,0x67,0x50,0x3B,0x28,0x19,0x0D,0x06,
0x04,0x06,0x0D,0x18,0x28,0x3A,0x50,0x67,
0x80,0x98,0xAF,0xC5,0xD8,0xE7,0xF3,0xFA,
0xFD,0xFA,0xF3,0xE8,0xD8,0xC5,0xB0,0x98,
0x7F,0x67,0x4F,0x3A,0x27,0x17,0x0B,0x04,
0x01,0x04,0x0B,0x17,0x26,0x39,0x4F,0x67,
0x80,0x98,0xB0,0xC6,0xD9,0xE9,0xF4,0xFC
,0xFE,0xFC,0xF5,0xE9,0xD9,0xC6,0xB0,0x98,
0x7F,0x67,0x4F,0x39,0x26,0x16,0x0A,0x03,
0x01,0x03,0x0A,0x16,0x26,0x39,0x4F,0x67,
0x80
};
The last 32 bytes of this table are a single sinusoid cycle that is at full amplitude, so rather than create a separate table for this data, I will just index into the above array and use the last 32 bytes.
#define one (&zero[15*32])
// Useful macros for setting and resetting bits
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
The following variables are marked as volatile as they are used in an interrupt service routine. We keep track of which phase point (index into above table) we are processing and a count of the remaining phase points in the table. When ramping down from full volume to zero, we process the phase table in reverse order. Reversing the processing order has the effect of reversing the phase, so in order to be phase continuous for an entire PSK31 bit, we must reverse the phase by negating the table entry when ramping down. Additionally, every time we cross through zero, we have to also reverse the phase.
volatile char *pbSine = &zero[0]; // Index into sinusoid table
volatile int cbSine = 512; // Length of sinusoid table
volatile char ix = 1; // Increment to get to next phase point
volatile char phase = 1; // PSK31 phase reversal
The following is not quite correct for PSK31, though it is close. We need to process a phase point every 31.25 us which is 32 kHz. Currently I am dividing the system clock (16 MHz) by 512 for 31.25 kHz or 32 us for every phase point, so I am a little slow. I will tend to this issue a little later. Right now I am just trying to see if I can generate the waveforms.
// Setup timer2 with prescaler = 1, PWM mode to phase correct PWM
// See th ATMega datasheet for all the gory details
void timer2Setup()
{
// Clock prescaler = 1
sbi (TCCR2B, CS20); // 001 = no prescaling
cbi (TCCR2B, CS21);
cbi (TCCR2B, CS22);
// Phase Correct PWM
cbi (TCCR2A, COM2A0); // 10 = clear OC2A on compare match when up counting
sbi (TCCR2A, COM2A1); // set OC2A on compare match when down counting
// Mode 1
sbi (TCCR2A, WGM20); // 101 = Mode 5 uses OCR2A as top value rather than 0xff
cbi (TCCR2A, WGM21);
}
Now for the interrupt service routine of the timer, I just fetch the next phase point out of the table and set it as the amplitude value. If we are ramping down we negate the value to reverse the phase and when starting to ramp up again, we reverse the phase again. When this is all figured out, I would likely rewrite all this in assembly.
// Timer 2 interrupt service routine (ISR).
ISR(TIMER2_OVF_vect)
{
// Set current amplitude value for the sine wave being constructed
// taking care to invert the phase when processing the table in
// reverse order.
OCR2A = *(pbSine) * ix * phase;
pbSine += ix;
if (0 == cbSine--)
{
cbSine = 512;
// When we get done ramping down, phase needs to change
if (ix < 0) phase = -phase;
ix = -ix;
}
}
Setup is assuming an ATMega2560 board. I use pin 10 for Timer 2 PWM output. There is nothing (yet) to do in the main loop. This code just generates a slightly too slow string of PSK31 zero bits.
void setup()
{
// PWM output for timer2 is pin 10 on the ATMega2560
// If you use an ATMega328 (such as the UNO) you need to make this pin 11
// See https://spreadsheets.google.com/pub?key=rtHw_R6eVL140KS9_G8GPkA&gid=0
pinMode(10, OUTPUT); // Timer 2 PWM output on mega256 is pin 10
// Set up timer2 to a phase correct 32kHz clock
timer2Setup();
sbi (TIMSK2,TOIE2); // Enable timer 2.
}
void loop()
{
}
I have placed a simple RC low pass filter on the output, so the integration is not very good, but looking at this on the scope, we see the following:
This looks pretty good, but I have some concerns about what I see here. One concern is at the point where the ramp up table is processed in reverse order with a phase change. There is a bit of a glitch here, so I may need to clean up my phase points in this area slightly.
Additionally, there is a lot of noise as the amplitude is ramping down that I need to investigate as can be seen here.
This appears to just be an artifact of my simple RC filter integrator that I am using to remove high frequency components of the PWM output from the Arduino.
So, I am a long way from generating PSK31 signals, but wanted to share the progress as I head in that direction. As always, comments and questions are welcome by posting here or by emailing ko7m at arrl dot net.
Subscribe to:
Posts (Atom)