Projeto de Osciloscópio e Analisador de Espectro Utilizando
Micro-Controlador PIC16F877


Projeto apresentado às disciplinas de Arquitetura de Computadores e Teoria das Comunicações III do Curso de Engenharia Elétrica - Ênfase em Telecomunicações, do Centro de Ciências Exatas e de Tecnologia da Pontifícia Universidade Católica do Paraná.

Orientadores:
Altair Olivo Santin
Marcelo Eduardo Pellenz


OBJETIVO

O objetivo do projeto é desenvolver, com o uso do micro-controlador PIC16F877, um osciloscópio digital e um analisador de espectro digital, para visualização em um computador.


DESCRIÇÃO

Utilizamos as entradas dos sinais analógicos do conversor A/D do micro-controlador PIC16F877, na verdade apenas uma AN0/RA0, o sinal é digitalizado e transmitido para o computador, onde é tratado por um software para a sua implementação gráfica tanto no osciloscópio como para o analisador de espectro.

Para o osciloscópio, o software somente plotará as coordenadas do sinal quantizado. Para o analisador de espectro, o sinal digital deverá ser tratado pelo software com a utilização da FFT (Fast Fourier Transform - Transformada Rápida de Fourier), para ser depois plotado.

LISTA DE COMPONENTES

  • 1 DIODO 1N4007;

  • CAPACITORES:

    • 1 - 470 F 35V;

    • 7 - 1 F;

    • 1 - 47 F 35V;

    • 1 - 150 pF ;

    • 2 - 22 pF;

    • 1 - 100 F;

  • RESISTORES:

    • 1 - 220 ;

    • 1 - 330 ;

    • 1 - 10 k;

    • 1 - 2,2 k;

    • 8 - 4,7 k;

    • 4 - 1 k;

    • 1 - 470 ;

    • 1 - 22 ;

  • 1 MAX 232;

  • 1 LM317;

  • 1 LM7805;

  • 1 SN7407N;

  • 4 BC557;

  • 1 CRYSTAL 11.0592 MHz;

  • 2 CONNECTORES DB9 (FÊMEA 90º);

  • 1 LEDS VERDE E AMARELO;

  • 1 SOQUETE 40 PINOS TORNEADO;

  • 1 SOQUETE 14 PINOS TORNEADO;

  • 1 SOQUETE 16 PINOS TORNEADO;

  • 2 POSTE DE CONECTOR MACHO;

  • 1 JACK DE ALIMENTAÇÃO;

  • 1 CHAVE DE RESET;

  • 1 CHAVE H;

  • 1 PLACA DE FENOLITE 15X15;

  • 1 PIC16F877;

  • 1 CONECTOR DB25 MACHO 1 COM CAPA;

  • 1 CONECTOR DB9 MACHO 1 COM CAPA.


CONSTRUÇÃO DO HARDWARE

Para a construção do hardware, foi utilizado o esquemático apresentado na figura 1, de design por Valter Klein, técnico dos laboratórios de Engenharia Elétrica da PUC. O hardware terá função de receber o sinal analógico, realizar a conversão AD e fazer a comunicação via serial. Através desse hardware é possível fazer a gravação do PIC.

Esquemático do circuito impresso da placa
Figura 1 - Layout do circuito impresso.

Observações:
A gravação do PIC utilizando o hardware descrito anteriormente não foi possível devido a um erro não identificado no esquemático.

Placa do microcontrolador
Figura 2 - Placa montada

Outro problema identificado no hardware foi na alimentação do PIC que deveria ser 5V, mas a tensão vista neste ponto não era correta, então foi feito um jumper direto da saída do transistor LM7805 para o pino 11 do PIC. O fio branco representa o jumper comentado acima, os demais fios foram necessários pois a trilha se soltou da placa.


Figura 3 - Circuito com jumper.


PROGRAMAÇÃO PIC - PROGRAMA DE CONVERSÃO A/D E TRANSMISSÃO SERIAL

Para programar o micro-controlador, foi utilizado o MPLAB IDE v7.10 com o plug-in PIC C LITE. Na figura 4, descreveremos os passos para executar a conversão analógica - digital e transmissão dos dados do PIC para o PC e em seguida os códigos fonte que descreve todos os passos que foram seguidos.


Figura 4 - Fluxograma da programação do PIC

Código fonte arquivo *.c:

#include <pic.h>
#include <stdio.h>
#include "usart.h"
unsigned char read(unsigned char channel){
unsigned char x,y;
ADGO=1;
while(ADGO)continue;
x = ADRESL;
x = x >> 2;
y = ADRESH;
y = y << 6;
x = x | y
return( x );
}
void transmite(unsigned char byte)
{
while(!TXIF)
continue;
TXREG = byte
}
void main(void)
{
unsigned char x;
ADCON0=0x01;
ADCON1=0x80;
TRISB=0xF0;
INTCON=0;
init_comms();
for(;;)
{
x=read(0);
transmite (x);
}
}

Código fonte arquivo usart.h:

#ifndef _SERIAL_H_
#define _SERIAL_H_
#define BAUD 19200
#define FOSC 11059200L
#define NINE 0 /* Use 9bit communication? FALSE=8bit */
#define DIVIDER ((int)(FOSC/(16UL * BAUD) -1))
#define HIGH_SPEED 1
#if NINE == 1
#define NINE_BITS 0x40
#else
#define NINE_BITS 0
#endif
#if HIGH_SPEED == 1
#define SPEED 0x4
#else
#define SPEED 0
#endif
#if defined(_16F87) || defined(_16F88)
#define RX_PIN TRISB2
#define TX_PIN TRISB5
#else
#define RX_PIN TRISC7
#define TX_PIN TRISC6
#endif
/* Inicializa Serial */
#define init_comms()\
SPBRG = DIVIDER; \
RX_PIN = 1; \
TX_PIN = 1; \
RCSTA = (NINE_BITS|0x90); \
TXSTA = (SPEED|NINE_BITS|0x20)
void transmite(unsigned char);
#endif

O primeiro passo seguido para a conversão AD, foi setar o registrador ADCON0. Este registrador tem a função de controlar algumas operações do módulo AD, como a freqüência de clock do AD, o canal a ser utilizado (canal 0, no pino RA0/AN0), controla a inicialização da conversão (GO/DONE) e que conversões AD serão realizadas (ADON). O segundo passo foi setar o registrador ADCON1. Este registrador tem como função nos dizer o formato do resultado, que pode ser justificado para direita quanto para esquerda, configura o controle das entradas dos ports, no nosso caso todas as 8 entradas são analógicas.

Para iniciar a comunicação serial foi necessário determinar a taxa de transmissão e a freqüência de oscilação que vamos trabalhar, no nosso caso escolhemos um BAUD RATE de 19200 bits por segundo e uma Fosc de 11.0592 MHz. Outro valor importante é o SPBRG que é calculado da seguinte forma: (FOSC/(16UL * BAUD) -1). Após termos inicializado a comunicação serial, entramos em um loop infinito que chama duas funções que serão utilizadas para dar um ajuste no byte a ser transmitido e realizar a transmissão pela serial.

O ajuste feito no valor do byte a ser transmitido foi o seguinte: primeiro fizemos um shift do registrador ADRESL em duas posições para a direita (x = x >> 2;), pois tínhamos o objetivo de eliminar os dois bits menos significativos do ADRESL, após esse passo foi feito um shift do registrador ADRESH em 6 posições para a esquerda (y = y << 6; ), realizamos após esse passo um OU lógico (x = x | y;) da parte alta com a parte baixa, sobrando apenas os 8 bits de informação que nos importa. Esses procedimentos descritos acima diminuem a precisão, mas aumenta a velocidade, pois ele demora apenas um ciclo de máquina para transmitir um byte que antes a cada informação demoraria 2 ciclos.

A função transmite envia dados pela serial. Antes, ela verifica se ainda existe algum dado para ser transmitido, e caso não tenha, um novo byte é colocado para a transmissão.


CONEXÃO SERIAL

A conexão serial entre o hardware e o PC foi feita com um cabo utilizando conectores DB9 e da seguinte forma:


Figura 5 - Diagrama de conexão dos conectores DB9

A razão para ligar ponto-a-ponto os pinos de TX e RX é que na placa do PIC, as ligações do conector DB-9 e a entrada/saída para a serial do PC do MAX 232 já é trocada.


PROGRAMA OSCILOSCÓPIO

Para o programa do osciloscópio foi utilizada a biblioteca PlotLab 2.1 Beta para o Borland C++ Builder 5, desenvolvido por Mitov Software. Esta biblioteca possibilita uma implementação fácil e uma plotagem rápida de dados. A biblioteca PlotLab possui várias opções, como zoom, auto-escala, salvar como imagem o que é mostrado na tela, entre outras.

Uma vez colocada o componente do PlotLab, basta criar um vetor de números reais, onde cada valor é uma amostra da conversão analógica/digital, e passá-lo para o canal da biblioteca. O programa plotará então os dados deste vetor no eixo y pelo seu índice, que é o eixo x.

O programa ainda utiliza o componente Timer da tabela de System de componentes do Builder para realizar a repetição da aquisição e transferência de dados para o gráfico. Na figura 6 temos o fluxograma do osciloscópio.


Figura 6 - Fluxograma do programa osciloscópio

Para a aquisição de dados da serial, foram utilizadas as funções CreateFile (para obtenção da porta serial), GetCommState e SetCommState (para configurar a porta), SetCommTimeouts (para configurar a utilização de timeouts na leitura da serial) e ReadFile (para a leitura da serial).

A configuração da recepção serial é a seguinte, conforme programado também no PIC:

  • Porta = COM2;
  • Taxa de transferência (BAUD) = 19200;
  • 8 Bits de dados por vez;
  • Sem paridade;
  • Um bit de parada;

Também foi utilizado timeouts para a leitura serial, para cancelar a recepção caso algo der errado na obtenção dos valores. Após a aquisição pela serial, os dados recebidos são devidamente escalados para os valores de tensão lidos.

A seguir, segue o código fonte para o arquivo Unit1.cpp, o único arquivo modificado dentre todos criados pelo projeto do Builder:

Código fonte arquivo Unit1.cpp:

#include <vcl.h>
#pragma hdrstop

#include "Unit1.h"

#pragma package(smart_init)
#pragma link "SLScope"
#pragma resource "*.dfm"
TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
float Buffer[ 128 ]; // Buffer de números reais para guardar os dados para o osciloscópio

BOOL m_bPortReady; // variáveis necessárias para as funções
HANDLE m_hComm;
DCB m_dcb;
COMMTIMEOUTS m_CommTimeouts;
BOOL bReadRC;
DWORD iBytesRead;
DWORD dwSize;
DWORD dwRead;
unsigned char RxChar[ 128 ]; // Buffer guardar os dados da serial

m_hComm = CreateFile("Com2", // Abrir porta COM2
GENERIC_READ, // acesso somente de leitura
0, // acesso exclusivo
NULL, // sem segurança
OPEN_EXISTING, // necessário para comunicação serial
0, // no overlapped I/O
NULL); // null template

m_bPortReady = SetupComm(m_hComm, 128, 128); // tamanho do buffer de recepção da serial

m_bPortReady = GetCommState(m_hComm, &m_dcb); // obtém os dados de configuração da serial
m_dcb.BaudRate = 19200; // dados de configuração da serial
m_dcb.ByteSize = 8;
m_dcb.Parity = NOPARITY;
m_dcb.StopBits = ONESTOPBIT;
m_dcb.fAbortOnError = FALSE;

m_bPortReady = SetCommState(m_hComm, &m_dcb); // aplica os dados de configuração

m_bPortReady = GetCommTimeouts (m_hComm, &m_CommTimeouts); // obtém os dados de configuração de timeouts da serial

m_CommTimeouts.ReadIntervalTimeout = 50; // dados de configuração de timeouts
m_CommTimeouts.ReadTotalTimeoutConstant = 50;
m_CommTimeouts.ReadTotalTimeoutMultiplier = 10;
m_CommTimeouts.WriteTotalTimeoutConstant = 0;
m_CommTimeouts.WriteTotalTimeoutMultiplier = 0;

m_bPortReady = SetCommTimeouts (m_hComm, &m_CommTimeouts); // aplica a configuração de timeouts

bReadRC = ReadFile(m_hComm, &RxChar, 128, &iBytesRead, NULL); // lê da serial 128 bytes
CloseHandle(m_hComm); // fecha a porta serial

for( int i = 0; i < 128; i ++ )
{
Buffer[ i ] = RxChar[ i ] / 51.2; // transfere os dados lidos da serial para o buffer, fazendo a escala
}
SLScope1->Channels->Channels[ 0 ]->Data->SetYData( Buffer, 128 ); // transfere os valores do buffer para o osciloscópio
}


PROGRAMA ANALISADOR DE ESPECTRO

O programa do analisador de espectro é praticamente igual ao do osciloscópio, tendo apenas adicionalmente a função para a realização da FFT (Fast Fourier Transform - Transformada Rápida de Fourier) e algumas outras variáveis e estruturas necessárias para a realização da função. A figura 7 mostra o fluxograma da FFT.


Figura 7 - Fluxograma do Analisador de Espectro

A função em linguagem C que realiza a FFT foi obtida do livro C Algorithms for Digital Signal Processing. Após a recepção de todos os dados da serial, é chamada a FFT, que realiza todos os cálculos e volta com um vetor de números complexos (o vetor de números complexos é uma estrutura previamente criada que possui os campos de números reais e números imaginários).

Antes de passar os valores para serem plotados, é calculado o módulo destes números complexos, pois o analisador de espectro irá mostrar o módulo do sinal de entrada.

A seguir segue o código fonte do arquivo Unit1.cpp:

Código fonte arquivo Unit1.cpp:

#include <vcl.h>
#include <math.h>
#pragma hdrstop

#include "Unit1.h"

#pragma package(smart_init)
#pragma link "SLScope"
#pragma resource "*.dfm"

typedef struct { // estrutura que guarda números complexos
float real, imag;
}COMPLEX;

void fft(COMPLEX *x, int m); // protótipo da função fft

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}

void __fastcall TForm1::Timer1Timer(TObject *Sender)
{
float Buffer[ 512 ]; // buffer que guardará os valores do módulo do sinal

COMPLEX *samp; // buffer que guarda os valores para a realização da fft
int m = 10, fft_length;

fft_length = 1 << m; // obtenção do tamanho da fft

samp = (COMPLEX *) calloc(fft_length, sizeof(COMPLEX)); // alocação de memória para o buffer samp

BOOL m_bPortReady; // variáveis necessárias para a configuração serial
HANDLE m_hComm;
DCB m_dcb;
COMMTIMEOUTS m_CommTimeouts;
BOOL bReadRC;
DWORD iBytesRead;
DWORD dwSize;
DWORD dwRead;
unsigned char RxChar[ 1024 ]; // buffer que receberá os dados da serial

m_hComm = CreateFile("Com2", // obtenção da porta serial
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
0,
NULL);

m_bPortReady = SetupComm(m_hComm, 1024, 1024); // tamanho do buffer da serial

m_bPortReady = GetCommState(m_hComm, &m_dcb); // configuração da porta serial
m_dcb.BaudRate = 19200;
m_dcb.ByteSize = 8;
m_dcb.Parity = NOPARITY;
m_dcb.StopBits = ONESTOPBIT;
m_dcb.fAbortOnError = FALSE;

m_bPortReady = SetCommState(m_hComm, &m_dcb);

m_bPortReady = GetCommTimeouts (m_hComm, &m_CommTimeouts); // configuração de timeouts para a serial

m_CommTimeouts.ReadIntervalTimeout = 50;
m_CommTimeouts.ReadTotalTimeoutConstant = 50;
m_CommTimeouts.ReadTotalTimeoutMultiplier = 10;
m_CommTimeouts.WriteTotalTimeoutConstant = 0;
m_CommTimeouts.WriteTotalTimeoutMultiplier = 0;

m_bPortReady = SetCommTimeouts (m_hComm, &m_CommTimeouts);

bReadRC = ReadFile(m_hComm, &RxChar, 1024, &iBytesRead, NULL); // leitura dos dados da serial
CloseHandle(m_hComm); // fechamento da porta serial

for( int i = 0; i < 1024; i ++ )
{
samp[ i ].real = RxChar[ i ] / 51.2; // transferencia em escala dos dados lidos
}

fft(samp, m); // chamada da função fft

for(int i = 0; i < 512; i ++) Buffer[ i ] = pow(pow(samp[ i ].real, 2) + pow(samp[ i ].imag, 2), 0.5) / 1024;
// transferência do módulo do sinal da fft
SLScope1->Channels->Channels[ 0 ]->Data->SetYData( Buffer, 512 ); // transferência dos dados para o gráfico
}

void fft(COMPLEX *x, int m) { // função fft
static COMPLEX *w;
static int mstore = 0;
static int n = 1;

COMPLEX u, temp, tm;
COMPLEX *xi, *xip, *xj, *wptr;

int i, j, k, l, le, windex;

double arg, w_real, w_imag, wrecur_real, wrecur_imag, wtemp_real;

if(m != mstore) { // libera memória alocada previamente e aloca novo m
if(mstore != 0) free(w);
mstore = m;
if(m == 0) return;

n = 1 << m; // n = 2**m = tamanho da fft
le = n/2;

w = (COMPLEX *) calloc(le-1, sizeof(COMPLEX)); // aloca a memória para o w
if(!w) {
exit(1);
}

arg = 4.0 * atan(1.0)/le; //calcula os valores de w recursivamente
wrecur_real = w_real = cos(arg);

wrecur_imag = w_imag = -sin(arg);
xj = w;
for (j=1; j<le; j++) {
xj->real = (float)wrecur_real;
xj->imag = (float)wrecur_imag;
xj++;
wtemp_real = wrecur_real * w_real - wrecur_imag * w_imag;
wrecur_imag = wrecur_real * w_imag + wrecur_imag * w_real;
wrecur_real = wtemp_real;
}
}

le = n; // começa a fft
windex = 1;
for ( l=0; l<m; l++) {
le = le / 2;

for(i=0; i<n; i = i + 2*le) { // primeira iteração sem multiplicações
xi = x + i;
xip = xi + le;
temp.real = xi->real + xip->real;
temp.imag = xi->imag + xip->imag;
xip->real = xi->real - xip->real;
xip->imag = xi->imag - xip->imag;
*xi = temp;
}

wptr = w + windex - 1; // iterações restantes usam w guardado
for(j = 1; j<le; j++) {
u = *wptr;
for(i=j; i<n; i = i + 2*le) {
xi = x + i;
xip = xi + le;
temp.real = xi->real + xip->real;
temp.imag = xi->imag + xip->imag;
tm.real = xi->real - xip->real;
tm.imag = xi->imag - xip->imag;
xip->real = tm.real * u.real - tm.imag * u.imag;
xip->imag = tm.real * u.imag + tm.imag * u.real;
*xi = temp;
}
wptr = wptr + windex;
}
windex = 2 * windex;
}

j = 0; // rearranja os dados revertendo os bits
for (i=1; i < (n-1); i++) {
k = n/2;
while(k <= j) {
j = j - k;
k = k/2;
}
j = j + k;
if (i<j) {
xi = x + i;
xj = x + j;
temp = *xj;
*xj = *xi;
*xi = temp;
}
}
}

Observações com relação ao programa do analisador de espectro: ao realizar a FFT em todo o buffer (neste caso com 1024 amostras), ocorre, no meio do vetor em diante, um espelhamento do sinal , que seria a parte negativa do espectro. Para visualizar somente a parte positiva, foi enviado para o canal do osciloscópio somente a primeira metade do vetor do sinal.


EXEMPLOS DOS PROGRAMAS

Devido à biblioteca PlotLab, alguns arquivos adicionais são necessários para executar os programas. Segue abaixo imagens dos programas. Para estes exemplos, foi colocado na entrada do canal 0 do PIC sinais variados gerados por um gerador de função.

A figura 8 apresenta um sinal senoidal, na freqüência de 200 Hz, com amplitude corretamente amostrada pelo osciloscópio:


Figura 8 - Sinal senoidal mostrado pelo osciloscópio

A figura 9 mostra um sinal triangular, na mesma freqüência:


Figura 9 - Sinal triangular

A figura 10 mostra um trem de pulsos quadrado:


Figura 10 - Sinal trem de pulsos.

A figura 11 mostra o módulo do espectro de um sinal senoidal, também na freqüência de 200 Hz, amostrado pelo programa de Analisador de Espectro:


Figura 11 - Sinal senoidal mostrado pelo analisador de espectro.

A figura 12 mostra o módulo do espectro de um sinal de trem de pulsos:


Figura 12 - Sinal de trem de pulsos mostrado pelo analisador de espectro.


PROBLEMAS E LIMITAÇÕES

Um problema encontrado com a interface serial do PIC foi quando utilizamos um oscilador de 4Mhz. Com esse valor, de acordo com a tabela encontrada no datasheet, o erro seria de 8,5%. Dessa forma, conseguimos transmitir para o HyperTerminal (programa utilizado para teste da serial) apenas com um baud rate de até 19200, sendo que valor desejado era de 57600. Pesquisando e pedindo auxílio a quem entende, postamos um tópico no Fórum da Microchip. Sem demorar muito fomos auxiliados e conseguimos a informação de que essa taxa de erro é muito alta para o PC, portanto deveriamos trocar o oscilador, o qual tivesse uma taxa de erro abaixo de 4%.

Devido à baixa taxa de transmissão da serial, a amostragem correta do sinal pelo osciloscópio e o analisador de espectro sem ocorrer aliasing (distorção que ocorre ao amostrar um sinal digitalizado em uma freqüência maior que a metade da freqüência de amostragem, como diz o Teorema de Nyquist) é muito baixa, por volta de 200 Hz.

Tanto o osciloscópio quanto o analisador de espectro mostram no eixo x apenas a posição da amostra recebida, sem mostrar qual o período do sinal ou a freqüência.

Alguns problemas ocorrem na recepção serial. No momento, os programas tentam adquirir todos os dados que são amostrador na tela de uma vez, e quando ocorrem erros, o programa fica bloqueado tentando receber estes dados. Este problema é mais perceptível no analisador de espectro, onde ele tenta obter um grande número de amostras (1024), sendo mais suscetível a erros, logo ficando bloqueado por muito tempo.

Outra limitação dos programas é a falta de opções para a configuração da recepção serial, como o port usado, a taxa de transferência de dados, etc. Para utilizar estes programas, apenas utilizando as configurações aqui mostradas. Para modificá-las, basta utilizar o código fonte com as alterações necessárias e compilá-lo no Builder.


BIBLIOGRAFIA

  1. EMBREE, Paul M. C language algorithms for digital signal processing. Englewood Cliffs: Prentice Hall, 1991. 456 p. ISBN 0-13-133406-9;
  2. Using the Communications Functions, Win32 Developer's References - Ferramenta de ajuda do Borland C++ Builder 5;
  3. Microchip - Datasheet PIC16F877 - Disponível na internet;
  4. Microsoft Developer's Network - Serial Communications in Win32 - Disponível na internet;
  5. Microchip - PICmicro Mid-Range MCU Family Reference Manual - Disponível na internet;
  6. Microchip - Tutorial - Conversores Analógicos-Digitais - Disponível em arquivo;
  7. HI-TECH Software - PIC C LITE Manual - Disponível em arquivo;
  8. Simulação de eventos no MPLAB - Disponível em arquivo;
  9. Microchip - Asynchronous Communications with the PICmicro USART - Disponível em arquivo;
  10. Microchip - 10-bit A/D Converter - Disponível em arquivo.


LINKS

Seguem a seguir links que nos ajudaram a desenvolver o projeto e a resolver alguns dos problemas encontrados:


AGRADECIMENTOS

Agradecemos a todos aqueles que nos ajudaram a realizar esse projeto.

  • Professor Altair Santin;
  • Professor Marcelo Pellenz;
  • Valter Klein;
  • Aqueles que nos ajudaram de muito longe, colocando soluções para o problema do oscilador no fórum da Microchip.


ALUNOS:

Fernando Moskalewicz
Luciano Rodrigues
Rodrigo Gorges

Engenharia Elétrica - Ênfase em Telecomunicações - 7º Período
Pontifícia Universidade Católica do Paraná - PUCPR
Curitiba - PR