Schéma générateur balise autonome WSPR à MSP430

Balise WSPR autonome avec MSP430 – le code source

[GTranslate]

Un kit pour ce circuit de générateur de balise WSPR sera bientôt disponible (http://xv4y NULL.radioclub NULL.asia/boutique/?slug=product_info NULL.php&products_id=30).

Ca m’a pris plus de temps que prévu pour publier le code source de mon générateur WSPR à MSP430 pour la balise QRSS de QRP Labs. Le programme “en production” est stable et fonctionne plusieurs jours d’affilée sans problèmes avec de bons reports (spots) sur WSPR. Le problème vient d’un bogue dans le mode debugging justement, les sorties sur port série sensées aidées à la mise au point ne fonctionne pas bien. Ce bogue résulte à la fois d’un problème de l’IDE Energia (au niveau du compilateur certainement) et d’une utilisation trop importante de la SRAM quand je compile le programme en incluant la librairie Serial. Difficile à résoudre, j’ai passé plusieurs jours à essayer d’optimiser les variables et l’utilisation de la RAM mais sans succès. Je vous livre donc le code “tel que” en sachant que le bogue n’est pas gênant en production.

Pour modifier votre balise QRSS en balise autonome WSPR rien de bien compliqué. Il faut construire un petit circuit selon le schéma ci-dessous. En utilisant un LaunchPad les circuits des LED, contacteurs et quartz sont déjà présents. Il faut y ajouter de quoi :

  • alimenter le MSP430G2553 du LaunchPad en 3,3V
  • créer une tension analogique à 4 niveaux grâce à une échelle R-2R à partir des sorties P2.1 et P2.2
  • alimenter en +5V l’étage PA de la balise uniquement quand c’est nécessaire grâce à un transistor BC548 (ou équivalent) connecté sur la sortie P2.0

Schéma générateur balise autonome WSPR à MSP430 (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2012/09/WSPR_balise_schéma NULL.png)

Sur le circuit de la balise par elle-même, rien de compliqué. Les modifications sont résumées sur le schéma suivant et consiste à :

  • Retirer le micro-contrôleur ATTiny8
  • Récupérer le +5V, la masse et le contrôle de la LED “varicap” sur le connecteur DIL8
  • Dessouder la patte +5V du potentiomètre de polarisation (bias) du transistor FET du PA pour la connecter sur le transistor BC548
  • Procéder à un nouvel alignement de la puissance de sortie du PA,
  • Procéder à un nouvel alignement de l’excursion en fréquence (shift) pour qu’elle soit au total de 6Hz
  • Procéder à un nouveau réglage de la fréquence pour être dans la bande WSPR (autour de 10 140 200 Hz sur 30 mètres)

Modifications de la balise QRSS QRP Labs pour WSPR - XV4Y (http://xv4y NULL.radioclub NULL.asia/wp-content/uploads/2012/10/Kit-WSPR-Mod-balise NULL.jpg)

Pour compiler le code il vous faut bien entendu un LaunchPad avec un MSP430G2253 (http://xv4y NULL.radioclub NULL.asia/2012/04/27/les-micro-controleurs-msp430-de-texas-instruments/). L’environnement de développement Energia correctement installé sur votre micro-ordinateur. La bibliothèque RTC que j’ai développée (http://xv4y NULL.radioclub NULL.asia/2012/05/22/bibliotheque-rtc-pour-le-msp430/) doit aussi être installée dans le répertoire de vos documents Energia. Il vous faut ensuite modifier les paramètre d’indicatif (callsign), de carré Locator et de puissance. Ni la compilation ni le téléchargement ne doivent produire d’erreurs. Une fois le LaunchPad programmé, je vous conseille de placer en position ouverte tous les cavaliers (jumpers) reliant le circuit de programmation-émulation au micro-contrôleur.

L’utilisation du programme par lui même est simple. Vous le mettez sous tension, les deux LEDs rouges et jaunes du LaunchPad vont clignoter alternativement assez rapidement. Le générateur est mode “attente de synchro” et transmet une séquence de test durant au total 30 secondes permettant de vérifier les différentes fréquences symboles et l’extinction du PA.

Ensuite, en utilisant une horloge précise (celle de votre GPS, horloge DCF-77 ou ordinateur synchronisé par NTP), vous appuyez sur le bouton S2 du LaunchPad juste quand l’horloge passe à la seconde zéro d’une minute paire.

Le générateur se met alors en mode “balise” et va transmettre tout de suite une première séquence WSPR. La séquence finie la LED jaune se met à clignoter très rapidement. Si vous appuyez sur le bouton S2 à ce moment, la balise passera à nouveau en émission à la prochaine minute paire. Sinon, elle passera en émission aléatoirement suivant le paramètre que vous avez donné, par défaut le taux est de 20% (tx_rate = 5).

Chez moi, l’horloge reste bonne pendant au moins 72 heures. Après cela elle prend environ une seconde de retard, ce qui suffit à faire que les stations les plus lointaines ne décodent plus la séquence WSPR. La remettre à l’heure ne prend que quelques minutes. L’expérience du GPS montre que sauf à partir dans des solutions complexes et coûteuses (http://www NULL.oe1ifm NULL.at/index NULL.php?option=com_content&view=article&id=49&Itemid=56), atteindre une meilleure stabilité effective reste difficile.

/* WSPR Static Generator for MSP430G2553
 * Code for Energia 008
 
 * By Yannick DEVOS - XV4Y - May-October 2012
    http://xv4y.radioclub.asia/

    Copyright 2012 Yannick DEVOS under GPL 3.0 license
    Any commercial use or inclusion in a kit is subject to author approval
 
 * Based on code by DH3JO Martin Nawrath for WSPR sequence encoding

====
 * Ouput on 2 bits PinA and PinB
 * PTT_key output allows to turn on/off the TX PA while sending
 * Mirror on LED1 and LED2 du LaunchPad for testing
 * Output to Serial for testing
 * Using an R-2R ladder it makes the G0UPL/G0XAR beacon frequency shift

====
Revision history :
v1.00    2012-10-18
         First release

====
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You can download a copy of the GNU General Public License at <http://www.gnu.org/licenses/>
*/

// Here modify to your taste or needs

#define PTT_key 8
#define wsprPinA 10
#define wsprPinB 9
#define LEDPinA 14
#define LEDPinB 2
#define StartBtn 5

const char call[] = "XV4Y ";
const char locator[] = "OK20";
const byte power = 20;
const char tx_rate = 5;

// Be carefull, this is only for debugging.
// If you enable this in production, the clock will drift to much (5 seconds in 10 minutes)
//#define SERIAL_OUT_INFO

// This is in case you use a MSP430 that does not have Hardwart UART. TimerSerial works great but bring a lot of clock drift. Replace each "Serial" object call in the code by mySerial.
/*
#ifdef SERIAL_OUT_INFO
#include <TimerSerial.h> 
TimerSerial mySerial;
#endif
*/

// Here under don't touch anything unless you know what you do

#include <legacymsp430.h>
#include <sRTC.h> // Library for RTC clock with MSP430

const char SyncVec[162] = {
  1,1,0,0,0,0,0,0,1,0,0,0,1,1,1,0,0,0,1,0,0,1,0,1,1,1,1,0,0,0,0,0,0,0,1,0,0,1,0,1,0,0,0,0,0,0,1,0,
  1,1,0,0,1,1,0,1,0,0,0,1,1,0,1,0,0,0,0,1,1,0,1,0,1,0,1,0,1,0,0,1,0,0,1,0,1,1,0,0,0,1,1,0,1,0,1,0,
  0,0,1,0,0,0,0,0,1,0,0,1,0,0,1,1,1,0,1,1,0,0,1,1,0,1,0,0,0,1,1,1,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,0,
  0,0,0,1,1,0,1,0,1,1,0,0,0,1,1,0,0,0
};

boolean force_send = 1;  // The first time the beacon is turned on it will send a sequence
byte symbol, bb, begin_sec;
int begin_chunk;

byte c[11];                // encoded message

byte sym[170];             // symbol table 162
byte symt[170];            // symbol table temp
unsigned long n1;          // encoded callsign
unsigned long m1;          // encodes locator

#ifdef SERIAL_OUT_INFO
#endif
RealTimeClock myClock;

//******************************************************************
// Defining pins mode
// Encoding the WSPR sequence

void setup() {
  pinMode(PTT_key, OUTPUT);
  pinMode(wsprPinA, OUTPUT);
  pinMode(wsprPinB, OUTPUT);
  pinMode(LEDPinA, OUTPUT);
  pinMode(LEDPinB, OUTPUT);
  pinMode(StartBtn, INPUT_PULLUP);

  #ifdef SERIAL_OUT_INFO
  Serial.begin(9600);
  Serial.println("\n* MSP430 WPSR beacon"); 
  #endif

  digitalWrite( PTT_key, LOW );
  digitalWrite( wsprPinA, LOW );
  digitalWrite( wsprPinB, LOW );

  digitalWrite( LEDPinA, HIGH );
  digitalWrite( LEDPinB, HIGH );

  prepare_sequence();

  #ifdef SERIAL_OUT_INFO
  Serial.println("* Wait clock is set. Sending test sequence.");
  #endif
  begin_chunk = myClock.RTC_chunk;
  begin_sec = myClock.RTC_sec;
  while(digitalRead(StartBtn)) {
    // This is for blinking LED fast
    if ((myClock.RTC_chunk-(begin_chunk+15))%60==0)
      digitalWrite( LEDPinA, HIGH );
    if ((myClock.RTC_chunk-(begin_chunk+30))%60==0)
      digitalWrite( LEDPinB, HIGH );
    if ((myClock.RTC_chunk-(begin_chunk+45))%60==0)
      digitalWrite( LEDPinA, LOW );
    if ((myClock.RTC_chunk-(begin_chunk+60))%60==0)
      digitalWrite( LEDPinB, LOW );
    
    // We also send a test sequence to help calibrate the transmitter
    if ((myClock.RTC_chunk-(begin_chunk+0))%240==0) 
      send_test(begin_sec);

   };
  randomSeed(myClock.RTC_chunk);
  myClock.Set_Time(0,0,0);
};

//******************************************************************
// Here starts the actual sequence sending

void loop() {
  #ifdef SERIAL_OUT_INFO
  Serial.print("Time:");
  Serial.print(myClock.RTC_min, DEC);
  Serial.print(":");
  Serial.print(myClock.RTC_sec, DEC);
  Serial.print(".");
  Serial.println(myClock.RTC_chunk, DEC);
  #endif

  digitalWrite( PTT_key, LOW );

  digitalWrite( wsprPinB, LOW );
  digitalWrite( LEDPinB, LOW );

  digitalWrite( wsprPinA, LOW );
  digitalWrite( LEDPinA, LOW );

  begin_chunk = myClock.RTC_chunk;
  
  #ifdef SERIAL_OUT_INFO
  Serial.println("* Waiting 1st sec of even mins.");
  #endif

  while (!(myClock.RTC_sec==1 && (myClock.RTC_min%2)==0)) {  // We start each first second of even minutes
    if ((myClock.RTC_chunk-(begin_chunk+10))%20==0) digitalWrite( LEDPinA, HIGH );
    if ((myClock.RTC_chunk-(begin_chunk+20))%20==0) digitalWrite( LEDPinA, LOW );
    if (!digitalRead(StartBtn)) force_send = 1;  // Pressing the button while in wait mode force the sequence to start next time
  };
  
  if (random(tx_rate+1)==tx_rate || force_send) {  // In order to minimize band occupation, randomly start the transmission depending on tx_rate
    force_send = 0;
    digitalWrite( PTT_key, HIGH );
    send_sequence();
    #ifdef SERIAL_OUT_INFO
    Serial.println("\n* End");
    #endif
  } else {
    // If not, we wait around 1 sec so we don't go through this too early...
    digitalWrite( PTT_key, LOW );
    begin_chunk = myClock.RTC_chunk;
    while ( ((256+myClock.RTC_chunk-begin_chunk)%256)<220 ) {
      delay(0);
    };
  };
};

/* Functions declaration
 * This code by DH3JO
 * KHM 2009 /  Martin Nawrath
 * Kunsthochschule fuer Medien Koeln
 * Academy of Media Arts Cologne
*/


//******************************************************************
// normalize characters 0..9 A..Z Space in order 0..36
char chr_normf(char bc ) {
  char cc=36;
  if (bc >= '0' && bc <= '9') cc=bc-'0';
  if (bc >= 'A' && bc <= 'Z') cc=bc-'A'+10;
  if (bc == ' ' ) cc=36;

  return(cc);
}

//******************************************************************
void encode_call(){
  unsigned long t1;

  // coding of callsign
  n1=chr_normf(call[0]);
  n1=n1*36+chr_normf(call[1]);
  n1=n1*10+chr_normf(call[2]);
  n1=n1*27+chr_normf(call[3])-10;
  n1=n1*27+chr_normf(call[4])-10;
  n1=n1*27+chr_normf(call[5])-10;

  // merge coded callsign into message array c[]
  t1=n1;
  c[0]= t1 >> 20;
//  t1=n1;
  c[1]= t1 >> 12;
//  t1=n1;
  c[2]= t1 >> 4;
//  t1=n1;
  c[3]= t1 << 4;
}

//******************************************************************
void encode_locator(){

  unsigned long t1;
  // coding of locator
  m1=179-10*(chr_normf(locator[0])-10)-chr_normf(locator[2]);
  m1=m1*180+10*(chr_normf(locator[1])-10)+chr_normf(locator[3]);
  m1=m1*128+power+64;

  // merge coded locator and power into message array c[]
  t1=m1;
  c[3]= c[3] + ( 0x0f & t1 >> 18);
//  t1=m1;
  c[4]= t1 >> 10;
//  t1=m1;
  c[5]= t1 >> 2;
//  t1=m1;
  c[6]= t1 << 6;

}
//******************************************************************
// convolutional encoding of message array c[] into a 162 bit stream
void encode_conv(){
  int bc=0;
  int cnt=0;
  int cc;
  unsigned long sh1=0;

  cc=c[0];

  for (int i=0; i < 81;i++) {
    if (i % 8 == 0 ) {
      cc=c[bc];
      bc++;
    }
    if (cc & 0x80) sh1=sh1 | 1;

    symt[cnt++]=parity(sh1 & 0xF2D05351);
    symt[cnt++]=parity(sh1 & 0xE4613C47);

    cc=cc << 1;
    sh1=sh1 << 1;
  }

}

//******************************************************************
byte parity(unsigned long li)
{
  byte po = 0;
  while(li != 0)
  {
    po++;
    li&= (li-1);
  }
  return (po & 1);
}

//******************************************************************
// interleave reorder the 162 data bits and and merge table with the sync vector
void interleave_sync(){
  int ii,ij,b2,bis,ip;
  ip=0;

  for (ii=0;ii<=255;ii++) {
    bis=1;
    ij=0;
    for (b2=0;b2 < 8 ;b2++) {
      if (ii & bis) ij= ij | (0x80 >> b2);
      bis=bis << 1;
    }
    if (ij < 162 ) {
      sym[ij]= SyncVec[ij] +2*symt[ip];
      ip++;
    }
  }
}

// This code by Yannick DEVOS XV4Y 2012

//******************************************************************
// Compute and initialize the sequence
void prepare_sequence() {

  encode_call();
  #ifdef SERIAL_OUT_INFO
  Serial.print("call: ");
  Serial.print(call);
  Serial.print(" ");
  Serial.print(n1,HEX);
  Serial.println(" ");
  #endif

  encode_locator();
  #ifdef SERIAL_OUT_INFO
  Serial.print("locator: ");
  Serial.print(locator);
  Serial.print(" ");
  Serial.print(m1 << 2,HEX);
  Serial.println(" ");
  for (bb=0;bb<=10;bb++){
    Serial.print(c[bb],HEX);
    Serial.print(",");
  }
  Serial.println("");
  #endif

  encode_conv();
  #ifdef SERIAL_OUT_INFO
  Serial.println("");
  for (bb=0;bb<162 ;bb++){
    Serial.print(symt[bb],DEC);
    Serial.print(".");
    if ( (bb+1) %32 == 0) Serial.println("");
  }
  Serial.println("");
  #endif

  interleave_sync();
  #ifdef SERIAL_OUT_INFO
  Serial.println("Channel symbols :");
  for (bb=0;bb<162 ;bb++){
    Serial.print(sym[bb],DEC);
    Serial.print(".");
    if ( (bb+1) %32 == 0) Serial.println("");
  }
  Serial.println("\nEnd of init.");
  #endif

};

//******************************************************************
// Function to send the test sequence
void send_test(char begin_sec) {
    if ((myClock.RTC_sec-(begin_sec+0))%30==0) {
      digitalWrite( PTT_key, LOW );
      digitalWrite( wsprPinA, LOW );
      digitalWrite( wsprPinB, LOW );
      #ifdef SERIAL_OUT_INFO
      Serial.println("* TX Low, Send 00 *");
      #endif
    }

    if ((myClock.RTC_sec-(begin_sec+6))%30==0) {
      digitalWrite( PTT_key, HIGH );
      digitalWrite( wsprPinA, LOW );
      digitalWrite( wsprPinB, LOW );
      #ifdef SERIAL_OUT_INFO
      Serial.println("* TX High, Send 00 *");
      #endif
    }

    if ((myClock.RTC_sec-(begin_sec+12))%30==0) {
      digitalWrite( PTT_key, HIGH );
      digitalWrite( wsprPinA, HIGH );
      digitalWrite( wsprPinB, LOW );
      #ifdef SERIAL_OUT_INFO
      Serial.println("* TX High, Send 01 *");
      #endif
    }

    if ((myClock.RTC_sec-(begin_sec+18))%30==0) {
      digitalWrite( PTT_key, HIGH );
      digitalWrite( wsprPinA, LOW );
      digitalWrite( wsprPinB, HIGH );
      #ifdef SERIAL_OUT_INFO
      Serial.println("* TX High, Send 10 *");
      #endif
    }
    
    if ((myClock.RTC_sec-(begin_sec+24))%30==0) {
      digitalWrite( PTT_key, HIGH );
      digitalWrite( wsprPinA, HIGH );
      digitalWrite( wsprPinB, HIGH );
      #ifdef SERIAL_OUT_INFO
      Serial.println("* TX High, Send 11 *");
      #endif
    }

};

  
//******************************************************************
// Function to send the sequence
void send_sequence() {
  for (bb = 0; bb < 162; bb++) {
    symbol = sym[bb];

    #ifdef SERIAL_OUT_INFO
    Serial.print(" ");
    Serial.print(symbol%10, DEC);
    #endif

    digitalWrite( wsprPinB, (symbol >> 1) );
    digitalWrite( LEDPinB, (symbol >> 1) );

    digitalWrite( wsprPinA, (symbol & 1) );
    digitalWrite( LEDPinA, (symbol & 1) );
    begin_chunk = myClock.RTC_chunk;
    while ( ((256+myClock.RTC_chunk-begin_chunk)%256)<175 ) {
      delay(0);
    };
  };
};


//******************************************************************
// Interruption for the RTC clock
interrupt(TIMER1_A0_VECTOR) Tic_Tac(void) {
  myClock.Inc_chunk();		      // Update chunks
};