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