First the block diagrams of my set-up...
Transmit chain
Receive chain
The transmit AD9850 is set to 7076kHz and sends the JT65 symbols on this and 64 higher frequencies spaced 2.69Hz above. The SDR is set to USB on a dial frequency of 7074.725 to give a received audio sync tone at 1000Hz. These tones are sent in 126 time slots of 327ms, a total message transmission time of 47.8sec.
JT65 comms
JT65 requires text input. But there are a few special messages which are not plain text, but cunningly encoded to send more than the limit of 13 characters. Messages such as a reply to a CQ "VK4EF M0IFA R-13" replying to VK4EF with a Roger, and report of 13dB signal strength. But here is a minimum QSO
Simple JT65 QSO Messages
I have not yet puzzled out how to encode these special messages on the Arduino, so today I can only send max 13 character normal text messages (Upper Case only plus 0-9). My code has been updated to do this, by entering the message on a serial communication app called iSerialTerm (see Mac App store).
Here's a few photos showing what I am doing...
My Desk Top, SDR Concept RX, iMac running HDSDR and JT65-HF programs, AD9850 synthesiser TX
Close up of the RX and TX. Note the tiny piece of yellow wire which is the TX antenna!
iSerialTerm app running on my MacBook to communicate via Serial with the Arduino UNO. Note 9600 baud, and line end '\n'.
The message entered on the MacBook, TX waiting for an on-the-minute timing to start sending
The TX sending the message
The SDR receiver tuning, centre frequency 7050kHz
The HDSDR signal received
The JT65-HF signal received
... and finally the text message received. Voila.
Code
// JT65_ADS_TEXT
// V1.3 29-3-17 added message input/output using iSerialTerm app
// Code based on Feld Hell beacon for Arduino by K6HX
// Timer setup code by LA3PNA.
// AD9850
// W_CLK 8
// FQ_UD 9
// DATA 10
// RESET 11
// LCD I2C bus (16x2)
// SDA = A4
// SCL = A5
// RTC I2C bus
// SDA = A4
// SCL = A5
#include "ADS9850.h"
#include "LiquidCrystal_I2C.h"
#include "Wire.h"
// chose address of 0x27 or 0x3F for LCD
//#define LCDADDR 0x27
#define LCDADDR 0x3F
#define LCDCOLS 16
#define LCDROWS 2
// RTC address
#define RTCADDR 0x68
// Stuff specific to the general (integer) version of the Reed-Solomon codecs
#define MODNN(x) modnn(rs,x)
#define MM (rs->mm)
#define NN (rs->nn)
#define ALPHA_TO (rs->alpha_to)
#define INDEX_OF (rs->index_of)
#define GENPOLY (rs->genpoly)
#define NROOTS (rs->nroots)
#define FCR (rs->fcr)
#define PRIM (rs->prim)
#define IPRIM (rs->iprim)
#define PAD (rs->pad)
#define A0 (NN)
#define TONE_SPACING 269 // ~2.6917 Hz
#define SUBMODE_A 5812 // CTC value for JT65A
#define SYMBOL_COUNT 126
// send indicator
#define LED_PIN 13
// AD9850 pins
#define W_CLK 8
#define FQ_UD 9
#define DATA 10
#define RESET 11
typedef unsigned int data_t;
/* Reed-Solomon codec control block */
struct rs {
int mm; /* Bits per symbol */
int nn; /* Symbols per block (= (1<= rs->nn) {
x -= rs->nn;
x = (x >> rs->mm) + (x & rs->nn);
}
return x;
}
uint8_t jt_code(char c)
{
/* Validate the input then return the proper integer code */
// Return 255 as an error code if the char is not allowed
if (isdigit(c))
{
return (uint8_t)(c - 48);
}
else if (c >= 'A' && c <= 'Z')
{
return (uint8_t)(c - 55);
}
else if (c == ' ')
{
return 36;
}
else if (c == '+')
{
return 37;
}
else if (c == '-')
{
return 38;
}
else if (c == '.')
{
return 39;
}
else if (c == '/')
{
return 40;
}
else if (c == '?')
{
return 41;
}
else
{
return 255;
}
}
// timer interrupt veector
ISR(TIMER1_COMPA_vect)
{
proceed = true;
}
// Reed Soloman encoder by KA9Q
void encode_rs_int(void *p, data_t *data, data_t *parity)
{
struct rs *rs = (struct rs *)p;
#undef A0
#define A0 (NN) /* Special reserved value encoding zero in index form */
int i, j;
data_t feedback;
memset(parity, 0, NROOTS * sizeof(data_t));
for (i = 0; i < NN - NROOTS - PAD; i++) {
feedback = INDEX_OF[data[i] ^ parity[0]];
if (feedback != A0) { /* feedback term is non-zero */
#ifdef UNNORMALIZED
/* This line is unnecessary when GENPOLY[NROOTS] is unity, as it must
always be for the polynomials constructed by init_rs()
*/
feedback = MODNN(NN - GENPOLY[NROOTS] + feedback);
#endif
for (j = 1; j < NROOTS; j++)
parity[j] ^= ALPHA_TO[MODNN(feedback + GENPOLY[NROOTS - j])];
}
/* Shift */
memmove(&parity[0], &parity[1], sizeof(data_t) * (NROOTS - 1));
if (feedback != A0)
parity[NROOTS - 1] = ALPHA_TO[MODNN(feedback + GENPOLY[0])];
else
parity[NROOTS - 1] = 0;
}
}
void free_rs_int(void *p)
{
struct rs *rs = (struct rs *)p;
free(rs->alpha_to);
free(rs->index_of);
free(rs->genpoly);
free(rs);
}
// init RS enc, symb size, poly coeff, first root, primative, no roots, padding
void *init_rs_int(int symsize, int gfpoly, int fcr, int prim, int nroots, int pad) {
struct rs *rs;
//#undef NULL
//#define NULL ((void *)0)
int i, j, sr, root, iprim;
rs = ((struct rs *)0);
/* Check parameter ranges */
if (symsize < 0 || symsize > 8 * sizeof(data_t)) {
goto done;
}
if (fcr < 0 || fcr >= (1 << symsize))
goto done;
if (prim <= 0 || prim >= (1 << symsize))
goto done;
if (nroots < 0 || nroots >= (1 << symsize))
goto done; /* Can't have more roots than symbol values! */
if (pad < 0 || pad >= ((1 << symsize) - 1 - nroots))
goto done; /* Too much padding */
rs = (struct rs *)calloc(1, sizeof(struct rs));
if (rs == NULL)
goto done;
rs->mm = symsize;
rs->nn = (1 << symsize) - 1;
rs->pad = pad;
rs->alpha_to = (data_t *)malloc(sizeof(data_t) * (rs->nn + 1));
if (rs->alpha_to == NULL) {
free(rs);
rs = ((struct rs *)0);
goto done;
}
rs->index_of = (data_t *)malloc(sizeof(data_t) * (rs->nn + 1));
if (rs->index_of == NULL) {
free(rs->alpha_to);
free(rs);
rs = ((struct rs *)0);
goto done;
}
/* Generate Galois field lookup tables */
rs->index_of[0] = A0; /* log(zero) = -inf */
rs->alpha_to[A0] = 0; /* alpha**-inf = 0 */
sr = 1;
for (i = 0; i < rs->nn; i++) {
rs->index_of[sr] = i;
rs->alpha_to[i] = sr;
sr <<= 1;
if (sr & (1 << symsize))
sr ^= gfpoly;
sr &= rs->nn;
}
if (sr != 1) {
/* field generator polynomial is not primitive! */
free(rs->alpha_to);
free(rs->index_of);
free(rs);
rs = ((struct rs *)0);
goto done;
}
/* Form RS code generator polynomial from its roots */
rs->genpoly = (data_t *)malloc(sizeof(data_t) * (nroots + 1));
if (rs->genpoly == NULL) {
free(rs->alpha_to);
free(rs->index_of);
free(rs);
rs = ((struct rs *)0);
goto done;
}
rs->fcr = fcr;
rs->prim = prim;
rs->nroots = nroots;
/* Find prim-th root of 1, used in decoding */
for (iprim = 1; (iprim % prim) != 0; iprim += rs->nn)
;
rs->iprim = iprim / prim;
rs->genpoly[0] = 1;
for (i = 0, root = fcr * prim; i < nroots; i++, root += prim) {
rs->genpoly[i + 1] = 1;
/* Multiply rs->genpoly[] by @**(root + x) */
for (j = i; j > 0; j--) {
if (rs->genpoly[j] != 0)
rs->genpoly[j] = rs->genpoly[j - 1] ^ rs->alpha_to[modnn(rs, rs->index_of[rs->genpoly[j]] + root)];
else
rs->genpoly[j] = rs->genpoly[j - 1];
}
/* rs->genpoly[0] can never be zero */
rs->genpoly[0] = rs->alpha_to[modnn(rs, rs->index_of[rs->genpoly[0]] + root)];
}
/* convert rs->genpoly[] to index form for quicker encoding */
for (i = 0; i <= nroots; i++)
rs->genpoly[i] = rs->index_of[rs->genpoly[i]];
done:;
return rs;
}
uint8_t gray_code(uint8_t c)
{
return (c >> 1) ^ c;
}
void rs_encode(uint8_t * data, uint8_t * symbols)
{
unsigned int dat1[12];
unsigned int b[51];
unsigned int i;
// Reverse data order for the Karn codec.
for (i = 0; i < 12; i++)
{
dat1[i] = data[11 - i];
}
// Compute the parity symbols
encode_rs_int(rs, dat1, b);
// Move parity symbols and data into symbols array, in reverse order.
for (i = 0; i < 51; i++)
{
symbols[50 - i] = b[i];
}
for (i = 0; i < 12; i++)
{
symbols[i + 51] = dat1[11 - i];
}
}
void jt65_encode(char * message, uint8_t symbols[SYMBOL_COUNT])
{
uint8_t i, j, k;
// Convert all chars to uppercase
for (i = 0; i < 13; i++)
{
if (islower(message[i]))
{
message[i] = toupper(message[i]);
}
}
// Collapse multiple spaces down to one
// Pad the message with trailing spaces
uint8_t len = strlen(message);
if (len < 13)
{
for (i = len; i < 13; i++)
{
message[i] = ' ';
}
}
// Bit packing
// -----------
uint8_t c[12];
uint32_t n1, n2, n3;
// Find the N values
n1 = jt_code(message[0]);
n1 = n1 * 42 + jt_code(message[1]);
n1 = n1 * 42 + jt_code(message[2]);
n1 = n1 * 42 + jt_code(message[3]);
n1 = n1 * 42 + jt_code(message[4]);
n2 = jt_code(message[5]);
n2 = n2 * 42 + jt_code(message[6]);
n2 = n2 * 42 + jt_code(message[7]);
n2 = n2 * 42 + jt_code(message[8]);
n2 = n2 * 42 + jt_code(message[9]);
n3 = jt_code(message[10]);
n3 = n3 * 42 + jt_code(message[11]);
n3 = n3 * 42 + jt_code(message[12]);
// Pack bits 15 and 16 of N3 into N1 and N2,
// then mask reset of N3 bits
n1 = (n1 << 1) + ((n3 >> 15) & 1);
n2 = (n2 << 1) + ((n3 >> 16) & 1);
n3 = n3 & 0x7fff;
// Set the freeform message flag
n3 += 32768;
c[0] = (n1 >> 22) & 0x003f;
c[1] = (n1 >> 16) & 0x003f;
c[2] = (n1 >> 10) & 0x003f;
c[3] = (n1 >> 4) & 0x003f;
c[4] = ((n1 & 0x000f) << 2) + ((n2 >> 26) & 0x0003);
c[5] = (n2 >> 20) & 0x003f;
c[6] = (n2 >> 14) & 0x003f;
c[7] = (n2 >> 8) & 0x003f;
c[8] = (n2 >> 2) & 0x003f;
c[9] = ((n2 & 0x0003) << 4) + ((n3 >> 12) & 0x000f);
c[10] = (n3 >> 6) & 0x003f;
c[11] = n3 & 0x003f;
// Reed-Solomon encoding
// ---------------------
uint8_t s[63];
k = 0;
rs_encode(c, s);
// Interleaving
// ------------
uint8_t d[63];
uint8_t d1[7][9];
// Fill temp d1 array
for (i = 0; i < 9; i++)
{
for (j = 0; j < 7; j++)
{
d1[i][j] = s[(i * 7) + j];
}
}
// Interleave and translate back to 1D destination array
for (i = 0; i < 7; i++)
{
for (j = 0; j < 9; j++)
{
d[(i * 9) + j] = d1[j][i];
}
}
// Gray Code
// ---------
uint8_t g[63];
for (i = 0; i < 63; i++)
{
g[i] = gray_code(d[i]);
}
// Merge with sync vector
// ----------------------
const uint8_t sync_vector[126] =
{ 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0,
0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1,
0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1,
0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1,
0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1,
1, 1, 1, 1, 1, 1
};
j = 0;
for (i = 0; i < 126; i++)
{
if (sync_vector[i])
{
symbols[i] = 0;
}
else
{
symbols[i] = g[j] + 2;
j++;
}
}
}
// get time from RTC, convert bcd to decimal
void getRTC()
{
// Reset the register pointer
Wire.beginTransmission(RTCADDR);
byte zero = 0x00;
Wire.write(zero);
Wire.endTransmission();
// request 1st 3 bytes from the RTC address
Wire.requestFrom(RTCADDR, 3);
// get the s/m/h time data
sec = bcdToDec(Wire.read());
mns = bcdToDec(Wire.read());
hrs = bcdToDec(Wire.read() & 0b111111); // mask 24 hour time bit
}
// Convert binary coded decimal to normal decimal numbers
byte bcdToDec(byte val)
{
return ( (val / 16 * 10) + (val % 16) );
}
// get input msg[] U.C.
bool getMsg(char *m)
{
char ch;
int n;
n = 0;
if (Serial.available() > 0) { // if input
while (Serial.available() > 0) { // get input
ch = Serial.read(); // use upper case as input
if (ch == '\n') ch = '\0'; // end of text
m[n++] = ch;
delay(20); // let USB catch up
}
return true; // got input
}
return false; // no input
}
// clear msg and buffer
void clearBuf(char *m) {
m[0] = '\0';
while (Serial.available() > 0) Serial.read();
}
// display msg *m at c)ol, r)ow
void dispMsg(uint8_t c, uint8_t r, char *m)
{
lcd.setCursor(c, r);
lcd.print(m);
}
// display freq at c)ol, r)ow, f (cHz), d decimal places
void dispFreq(uint8_t c, uint8_t r, float f, float cf, uint8_t d) {
lcd.setCursor(c, r);
lcd.print((f + cf / 100.0), d);
lcd.print("Hz ");
}
// display time at col, row
void dispTime(byte c, byte r) {
lcd.setCursor(c, r);
if (hrs < 10)
lcd.print("0");
lcd.print(hrs);
lcd.print(":");
if (mns < 10)
lcd.print("0");
lcd.print(mns);
lcd.print(":");
if (sec < 10)
lcd.print("0");
lcd.print(sec);
}
No comments:
Post a Comment