quarta-feira, abril 27, 2016

Módulo de Rádio FM com CI RDA5807M - Parte 3

Agora que já conseguimos colocar o módulo funcionar, vamos começar a explorar um recurso interessante: o RDS (Radio Data System).


O objetivo do RDS é permitir às estações de rádio transmitir pequenas quantidades de dados. Os usos típicos são o envio da identificação da estação, informações sobre o que está sendo transmitido (como nome e interprete da música) e informações sobre o transito.

Onde estes dados são colocados no sinal? Originalmente a transmissão FM enviava somente um canal de som, com frequências de 30Hz a 15KHz. O sistema foi expandido para envio de dois canais (som estéreo) colocando informação adicional em torno dos 38KHz. Um sinal contínuo de 19KHz indica a presença desta informação adicional. Os dados do RDS são enviados em torno dos 57KHz (3*19KHz). A figura abaixo (by Arthur Murray - http://en.wikipedia.org/wiki/File:RDS_vs_DirectBand_FM-spectrum2.png) mostra as várias informações que podem ser transmitidas por uma estação FM.


Os dados são transmitidos a uma taxa ligeiramente inferior a 1200bps (57000/48 bps para ser preciso). A transmissão é feita por grupos de 104 bits, divididos em quatro blocos de 26 bits. Estes 26 bits correspondem a 16 bits de informação mais 10 bits para detecção e correção de erros.

O primeiro bloco (bloco A) contém sempre o "código de identificação do programa" (PI code) que, nos EUA,  informa o prefixo da emissora. O segundo bloco (bloco B) informa o tipo de grupo, que determina o que está codificado nos blocos C e D. Daí em diante a coisa fica ainda mais complicada, a norma americana pode ser vista aqui.

O RDA5807M possui toda a lógica para detectar, decodificar e conferir um grupo, disponibilizando os dados dos quatro blocos em quatro registradores de 16 bits. Uma limitação é que ele não efetua as correções, nem disponibiliza os bits necessários para a correção. Estranhamente, são indicados os erros encontrados apenas nos blocos A e B. Portanto não temos como saber se os dados nos blocos C e D estão corretos.

Uma outra dificuldade é saber quando um novo grupo está disponível. Existe um bit no registrador de status para isto, mas não encontrei uma forma de desligá-lo após ler os registradores. Este artigo sugere desligar e ligar a função de RDS, mas fiquei com a impressão que a recepção de um novo grupo demora bastante quando isto é feito. Outra forma, usada pela biblioteca que vou usar em testes futuros, é simplesmente ver se mudou em relação à leitura anterior.

Como primeiro teste, vamos experimentar detectar a recepção de grupos através da comparação e mostrar os blocos em hexadecimal. O código abaixo usa a mesma biblioteca do post anterior.
#include <Wire.h>
#include <RDA5807M.h>

// Estação para teste, escolha uma estação
// que você sabe que envia RDS
const word ESTACAO = 9690;  // 96,9MHz

RDA5807M radio;
word bloco[4] = { 0, 0, 0, 0 };
byte regblc[4] =
{
  RDA5807M_REG_RDSA, RDA5807M_REG_RDSB,
  RDA5807M_REG_RDSC, RDA5807M_REG_RDSD
};

void setup()
{
  // Inicia a serial
  Serial.begin(9600);

  // Inicia o rádio
  radio.begin(RDA5807M_BAND_WEST);
  radio.mute();
  sintoniza (ESTACAO);
  radio.unMute(true);

  // Tempinho para começar a receber RDS
  delay (500);

  // Informa o status
  word frequency = radio.getFrequency();
  Serial.print(F("Sintonizado em "));
  Serial.print(frequency / 100);
  Serial.print(".");
  Serial.print(frequency % 100);
  Serial.println(F("MHz FM"));
  Serial.print(F("RSSI = "));
  Serial.print(radio.getRSSI());
  Serial.println("dBuV");
  word status = radio.getRegister(RDA5807M_REG_STATUS);
  if(status & RDA5807M_STATUS_RDSR)
    Serial.println(F("* RDS Group Ready"));
  if(status & RDA5807M_STATUS_STC)
    Serial.println(F("* Seek/Tune Complete"));
  if(status & RDA5807M_STATUS_RDSS)
    Serial.println(F("* RDS Decoder Synchronized"));
  if(status & RDA5807M_STATUS_ST)
    Serial.println(F("* Stereo Reception"));
}

void loop()
{
  if (atlBlocos())
  {
    for (int i = 0; i < 4; i++)
    {
      printReg(bloco[i]);
    }
    Serial.println();
  }
  delay (10);
}

// Atualiza os blocos
// Retorna true se houve mudança
byte atlBlocos()
{
  byte mudou = false;
  word status = radio.getRegister(RDA5807M_REG_STATUS);
  word erros = radio.getRegister(RDA5807M_REG_RSSI) &
                (RDA5807M_BLERA_MASK | RDA5807M_BLERB_MASK);
  
  if((status & RDA5807M_STATUS_RDSR) && (erros == 0))
  {
    for (int i = 0; i < 4; i++)
    {
      word aux = radio.getRegister(regblc[i]);
      if (aux != bloco[i])
      {
        bloco[i] = aux;
        mudou = true;
      }
    }
  }
  return mudou;
}

// Imprime um tegistrador em hexa
const char hexa[] = "0123456789ABCDEF";
void printReg (word val)
{
    char aux[6];
    aux[0] = hexa[(val >> 12) & 0x0F];
    aux[1] = hexa[(val >> 8) & 0x0F];
    aux[2] = hexa[(val >> 4) & 0x0F];
    aux[3] = hexa[val & 0x0F];
    aux[4] = ' ';
    aux[5] = 0;
    Serial.print(aux);
}

// Sintoniza em uma frequência
// (esta rotina está faltando na biblioteca)
#define RDA5807M_TUNE_BIT word(0x0010)
void sintoniza (word freq)
{
  // verifica a programação de banda e espaçamento
  word band = radio.getRegister(RDA5807M_REG_TUNING) & 
                (RDA5807M_BAND_MASK | RDA5807M_SPACE_MASK);
  const byte space = band & RDA5807M_SPACE_MASK;
  band = (band & RDA5807M_BAND_MASK) >> 2;
  
  // calcula o valor do canal
  freq -= pgm_read_word(&RDA5807M_BandLowerLimits[band]);
  word channel = (freq*10)/pgm_read_byte(&RDA5807M_ChannelSpacings[space]);
  
  // efetua a sintonia
  radio.updateRegister(RDA5807M_REG_TUNING, RDA5807M_TUNE_BIT, 0);
  radio.updateRegister(RDA5807M_REG_TUNING, 
      RDA5807M_CHAN_MASK | RDA5807M_TUNE_BIT, 
      (channel << RDA5807M_CHAN_SHIFT) | RDA5807M_TUNE_BIT);
  
  // aguarda sintonizar
  for (int i = 0; i < 10; i++)
  {
    word status = radio.getRegister(RDA5807M_REG_STATUS);
    if(status & RDA5807M_STATUS_STC)
       break;
    delay (100);
  }
}
Vamos agora alterar a rotina loop() para decodificar a identificação da emissora enviada através de grupos do tipo A0 ou B0 (código adaptado do artigo mencionado acima). Nestes grupos o bloco D possui dois caracteres e o bloco B informa a posição destes caracteres em um buffer de 8 posições.
#define RDS_GROUP     0xF800
#define RDS_GROUP_A0  0x0000
#define RDS_GROUP_B0  0x0800

char nome[9] = "????????";

void loop()
{
  if (atlBlocos())
  {
    if (((bloco[1] & RDS_GROUP) == RDS_GROUP_A0) ||
        ((bloco[1] & RDS_GROUP) == RDS_GROUP_B0))
    {
      byte offset = (bloco[1] & 0x03) << 1;
      char c1 = (char)(bloco[3] >> 8);
      char c2 = (char)(bloco[3] & 0xFF);
      nome[offset] = c1;
      nome[offset+1] = c2;
      Serial.println (nome);
    }
  }
  delay (10);
}
Executando este código com a emissora que estou usando para testes, deu para perceber dois problemas:
  • Ocorrem com frequência erros no bloco D. Uma solução é só aceitar o bloco se for recebido igual mais de uma vez consecutiva.
  • Apesar da norma recomendar não mudar o texto em intervalo inferior a 1 minuto, esta emissora (e outras que eu testei) muda o texto a cada poucos segundos para enviar o seu slogan.
A versão abaixo inclui alguns filtros adicionais para tentar fornecer textos mais limpos.
char nome_a[9] = "????????";
char nome_b[9] = "????????";

void loop()
{
  if (atlBlocos())
  {
    if (((bloco[1] & RDS_GROUP) == RDS_GROUP_A0) ||
        ((bloco[1] & RDS_GROUP) == RDS_GROUP_B0))
    {
      if (atlNome())
      {
        Serial.println (nome);
      }
    }
  }
}

// Atualiza o nome
// Retorna true se tem um novo nome
byte atlNome()
{
  // extrai a posição e os caracteres
  byte offset = (bloco[1] & 0x03) << 1;
  char c1 = (char)(bloco[3] >> 8);
  char c2 = (char)(bloco[3] & 0xFF);
  byte igual;
  
  // despreza caracteres absurdos
  if ((c1 < 0x20) || (c1 > 0x7E))
    return false;
  if ((c2 < 0x20) || (c2 > 0x7E))
    return false;

  // só atualizar nome_a depois de receber
  // duas vezes igual
  if (nome_b[offset] == c1)
  {
    nome_a[offset] = c1;
    nome_b[offset] = '?';
  }
  else
  {
    nome_b[offset] = c1;
  }
  if (nome_b[offset+1] == c2)
  {
    nome_a[offset+1] = c2;
    nome_b[offset+1] = '?';
  }
  else
  {
    nome_b[offset+1] = c2;
  }

  // verifica se recebeu um nome completo
  for (int i = 0; i < 8; i++)
  {
    if (nome_a[i] == '?')
      return false;
  }
  
  // atualizar o nome e preparar para receber o
  // próximo
  igual = true;
  for (int i = 0; i < 8; i++)
  {
    if (nome[i] != nome_a[i])
      igual = false;
    nome[i] = nome_a[i];
    nome_a[i] = nome_b[i] = '?';
  }
  
  return !igual;
}
No próximo post vamos experimentar uma biblioteca mais sofisticada, que decodifica vários tipos de grupos. De qualquer forma, está claro que o suporte a RDS no RDA5807M está longe de ser perfeito.

Nenhum comentário: