1- Tradução do programa de controlo para C
Depois de ter a versão assembly do programa de controlo do voltímetro digital a funcionar, este deve ser convertido para linguagem C. Só depois de completar o debug desta última versão é que deve passar aos pontos seguintes.
Ambiente de programação em C
Como o compilador de C que vamos usar é o TurboC, o qual corre apenas em ambiente DOS, é necessário ter em atenção o seguinte:
a) Ao criar-se o ficheiro source (extensão .c) com o vim, deve executar-se no editor o comando
[ESCAPE]
:set fileformat=dos
antes de fazer o save (:wq), para que o ficheiro assuma o formato do DOS. Caso contrário a compilação subsequente termina sempre com erro. Isto tem que ver com as mudanças de linha nos ficheiros de texto, que diferem entre o DOS e o linux.
b) Para compilar o programa em C, proceder do seguinte modo:
i) Entrar no emulador de DOS do linux:
Dosemu
Poucos depois aparece o prompt tradicional do DOS (C:\>).
O vosso directório pessoal do linux está mapeado no directório seguinte dentro do "dosemu":
C:\AC2\AULAS\AC2PttGn
o que significa que os vossos ficheiros podem ser acedidos quer directamente do linux, quer dentro do emulador.
ii) Para compilar o programa escrito em C é usado um script que, por sua vez, invoca o TC seguido do utilitário que converte o ficheiro objecto no formato binário do Kit Det188:
makec <nome do ficheiro s/ extensão .c>
que cria o <ficheiro.kit> (ou ‘z’).
iii) Para sair do emulador de DOS:
exitemu
Para trabalharem de maneira eficiente recomenda-se que abram sempre três terminais no linux:
- um para editar o ficheiro fonte (para evitar de sair do vim para compilar);
- um para compilar o programa e onde vai estar sempre a correr o dosemu (do qual não precisam de sair);
- um para executar o linterm (que pode também estar sempre a correr).
2- Atendimento à ADC por interrupção
Nesta fase do trabalho vamos incluir uma rotina de interrupção que será activada sempre que a ADC termina uma conversão. Das duas entradas de interrupção presentes na ficha de expansão do kit Det188 vamos usar a INT1 que ligaremos ao sinal EOC\ da ADC, depois de invertido.
Para que a rotina de interrupção seja executada sempre que o sinal INT1 é activado, é preciso:
• Inicializar o ponteiro correspondente a esta interrupção na Tabela de Endereços de Interrupção (TEI);
• Programar no Controlador de Interrupções interno (PIC) o registo de controlo correspondente a este sinal;
• No fim da rotina de interrupção, enviar para o PIC comando de fim de interrupção.
2.1- Inicialização do ponteiro de interrupção
A localização base deste ponteiro na TEI é determinada multiplicando o vector correspondente ao INT1 (vector type, 13) por 4. Assim, os 16 bits do offset do ponteiro localizam-se nas posições de memória cujos endereços são 13x4=52=34h e 35h; o segmento está localizado logo a seguir, ou seja em (13x4)+2=54=36h e 37h (fig. 1).
A inicialização do ponteiro correspondente à interrupção é
Práticas de Arquitectura de Computadores II
Trabalho 3 - Interrupções
Vector INT1 = 13 0
3 4 7
34h 37h 4Ch 4Fh
Div. Error Exception Single Step
Interrupt
INT1
Timer2 (offset)
(seg) (seg)
(seg)
(seg) (offset)
(offset)
(offset)
Vector Timer2 = 19
Fig. 1 - Tabela de Endereços de Interrupção
realizada em C usando as funções FP_OFF() e FP_SEG(), juntamente com a função poke() de escrita em memória (16 bits).
Exemplo: Inicialização do ponteiro correspondente ao INT1 para a rotina de serviço int1_isr:
poke(0, 13*4, FP_OFF(int1_isr));
poke(0, 13*4+2, FP_SEG(int1_isr));
2.2- Programação do registo de controlo no PIC A fig. 2 representa o registo de controlo do sinal de interrupção INT1. Valores possíveis para cada um dos campos deste registo são:
SFNM=C=0: configura a interrupção em fully nested mode (o modo de funcionamento mais simples que usaremos sempre);
LTM=0: configura a entrada INT1 para ser activa ao flanco;
MSK=0: desactiva a máscara, ou seja, activa a interrupção;
PR[2:0]=111: Atribui ao INT1 a prioridade 7 (mais baixa).
O que se traduz no valor 0007.
A programação do registo de controlo do INT1 resume-se pois à instrução:
outport(0xFF3A, 0x07);
2.3- Comando de fim de interrupção
Imediatamente antes de sair, a rotina de serviço à interrupção deverá enviar para o registo EOI (fig. 3) um comando de fim de interrupção. Este destina-se a desactivar o bit correspondente ao INT1 no registo In-Service, de forma a permitir o reconhecimento de interrupções subsequentes (neste sinal de interrupção ou em outros de prioridade inferior).
Os valores a usar nos campos do comando EOI são os seguintes:
SPEC/NSPEC=0: indica que o comando é do tipo específico, ou seja, a interrupção a que se refere é identificada explicitamente;
S[4:0]=Vector do INT1=13: Identifica a interrupção pelo seu vector.
O comando EOI é enviado com a instrução:
outport(0xFF22, 13);
2.4- Organização do programa
Na sua versão sem interrupções, o programa do voltímetro digital executa sucessivamente as acções seguintes:
1- Inicialização (prog. PCS3\, etc.);
2- Comanda ADC para iniciar conversão;
3- Testa se fim de conversão;
4- Lê ADC;
5- Rotina chgscale;
6- Rotina h2d;
7- Envio resultado displays;
8- Goto 2;
Com a introdução de uma rotina de atendimento à interrupção INT1 (ADC), sugere-se a estruturação do software como indicado na fig. 4.
A rotina de atendimento à interrupção e o programa principal executam as seguintes acções:
Rotina de serviço à interrupção INT1:
1- Lê ADC;
2- Rotina chgscale;
3- Escreve resultado em VadcS;
4- adcON=0;
5- Envia comando EOI para o PIC;
Programa principal:
1- Disable das interrupções;
2- Inicialização (prog. PCS3\, inicial.
ponteiro de interrupção, prog. PIC, etc.);
3- Enable das interrupções;
4- adcON=1;
5- Comanda ADC para iniciar conversão;
6- Espera até adcON=0;
7- Lê VadcS;
8- Rotina h2d;
9- Envio resultado displays;
10- Goto 4;
A comunicação e sincronização entre os dois módulos é assegurada por duas variáveis globais: VadcS e adcON. A primeira é usada para transferir o valor da ADC (já na escala 0-5.0V) entre a rotina de interrupção e o programa principal. A variável binária adcON é usada para validar o valor em VadcS e para garantir que os comandos de início de conversão (enviados pelo main) ocorrem apenas depois da ADC terminar a conversão anterior.
Note-se que a rotina chgscale foi incluída na rotina de serviço à interrupção e a rotina h2d no programa principal.
Esta opção (que é apenas uma entre outras possíveis) justifica-se pelo facto de se entender a mudança de escala como uma adaptação da saída da ADC aos requisitos do sistema, sendo por isso um processamento que deve estar encapsulado na rotina da ADC. Por outro lado o processamento correspondente à rotina h2d (assim como a multiplexagem que é preciso efectuar para alimentar os dois displays) tem a haver com as características da saída, não devendo por isso ser incluído na rotina de interrupção.
Em linguagem C, isto traduz-se por:
SFNM PR0
0 0 . . . . C LTM MSK PR2 PR1 0 1 2 3 4 5 6 15
Fig. 2 – Registo de controlo do INT1 (FF3Ah)
SPEC/
NSPEC 0 0 . . . . . 0 S4 S3 S2 S1 S0 0 1 2 3 4 15
Fig. 3 – Registo EOI (FF22h)
INT1
0
(var globais) VadcS adcON
8 8
Programa principal(main) 1 ADCRotina interrupção
INT1 (int1_isr)
Fig. 4 – Organização do software do voltímetro
/* Programa do voltímetro digital com atendimento à ADC por interrupções */
/* Constantes*/
#define ADC ADD 0x2081
#define IO_ADD 0x2080
#define EOI_ADD 0xFF22
#define INT1_VECT 13
#define INT1_CR_ADD 0xFF3A
#define INT1_CR_VAL 0x0007
/* Variáveis globais */
char VadcS, adcON;
/* Rotina de serviço da ADC */
void interrupt int1_isr(void) {
int ValADC;
...
ValADC = inportb(ADC_ADD);
...
VadcS = .... ; adcON = 0;
/* Envio de comando EOI para o PIC */
outport(EOI_ADD , INT1_VECT);
}
/* Programa main */
void main (void) {
disable(); /* disable interrupts*/
/* Inicialização do vector de interrup. */
poke(0, INT1_VECT*4, FP_OFF(int1_isr));
poke(0, INT1_VECT*4+2, FP_SEG(int1_isr));
/* Programação do PIC */
outport(INT1_CR_ADD, INT1_CR_VAL);
enable(); /* enable interrupts*/
...
adcON = 1;
outportb(ADC ADD, 0); /* start conv*/
...
}
3- Utilização do Timer2 para controlar a actualização do display
Nesta fase do trabalho vamos utilizar um timer do 80C188 para controlar a actualização (refresh) dos dígitos do display. O timer será programado para gerar uma interrupção (a cada 20ms) sendo necessário pois incluir a rotina de serviço correspondente a esta interrupção.
Assim, o código do main será aumentado com as seguintes inicializações:
• Inicialização do ponteiro de interrupção do Timer2 na Tabela de Endereços de Interrupção (TEI);
• Programação do registo de controlo correspondente aos timers no Controlador de Interrupções interno (PIC);
• Programação dos registos do Timer2.
A rotina de interrupção do Timer2 deve terminar com o envio para o PIC do comando de fim de interrupção.
3.1- Inicialização do ponteiro de interrupção
O procedimento é idêntico ao indicado em 2.1. O vector correspondente ao Timer2 é 19 (ver fig. 1) pelo que os 16 bits do offset do ponteiro localizam-se nas posições de endereços 19x4=76=4Ch e 4Dh, e o segmento nas posições (19x4)+2=78=4Eh e 4Fh.
3.2- Programação do registo de controlo no PIC O PIC possui um único registo de controlo para os três timers do 80C188 (fig. 5).
Valores possíveis a programar em cada um dos campos deste registo são:
MSK=0: desactiva a máscara de interrupção dos timers;
PR[2:0]=111: Atribui às interrupções dos timers a prioridade 7 (mais baixa).
O valor a programar será portanto 0007.
3.3- Programação do Timer2 Consiste na
• inicialização do registo MaxCountA (endereço FF62h);
• programação do registo de controlo deste timer.
O timer2 gera uma interrupção sempre que o seu contador atinge o valor de MaxCountA. Dado que pretendemos ter uma interrupção a cada 20ms e a frequência de relógio dos timers é de 1.25 MHz (0.8 μs), o valor a programar em MaxCountA será 20000/0.8=25000.
Os valores possíveis a programar nos campos do registo de controlo do timer2 (fig. 6) são:
EN=1: activa o timer;
INH\=1: valida o valor o bit EN;
INT=1: activa interrupções do timer;
MC=X: bit maximum count;
CONT=1: funcionamento continuo.
O valor a programar será portanto E001h.
3.4- Comando de fim de interrupção
O comando de fim de interrupção a enviar para o registo EOI (fig. 3) é idêntico ao usado para o caso do INT1. A única diferença reside na identificação da interrupção no campo S[4:0]. Como o PIC tem apenas um in-service bit para as interruções dos três timers, basta que o comando EOI indique que se trata de uma destas interrupções, sem
PR0
0 0 . . . . 0 MSK PR2 PR1
0 1 2 3 4 15
Fig. 5 – Registo de controlo dos três timers (FF32h)
CONT
EN INH . . . MC 0 0 0 0
0 1 2 3 4 5 15
INT 0
14 12 13
Fig. 6 – Registo de controlo do Timer2 (FF66h)
especificar qual. O código a usar em qualquer uma das interrupções dos timers é S[4:0]=8 (correspondente ao vector do Timer0). O comando EOI tem portanto o valor 8.
3.5- Organização do programa
Com a introdução da rotina de atendimento à interrupção do Timer2 passamos a ter o software do voltímetro organizado como se indica na fig. 7.
As rotinas de interrupção e o programa principal executam as seguintes acções:
Rotina de serviço à interrupção INT1 (igual à anterior):
1- Lê ADC;
2- Rotina chgscale;
3- Escreve resultado em VadcS;
4- adcON=0;
5- Envia comando EOI para o PIC;
Rotina de serviço à interrupção do Timer2:
1- Lê VadcS;
2- Rotina h2d;
3- Envia resultado displays (Em cada activação a rotina actualiza ora um, ora outro dígito);
4- Envia comando EOI para o PIC;
Programa principal:
1- Disable das interrupções;
2- Inicialização (prog. PCS3\, inicial.
ponteiros de interrupção, prog. PIC, prog.
timer2, etc.);
3- Enable das interrupções;
4- adcON=1;
5- Comanda ADC para iniciar conversão;
6- Espera até ADC_ON=0;
7- Goto 4;
Em linguagem C, isto traduz-se por:
/* Programa do voltimetro digital com atendimento à ADC por interrupções e refresh do display*/
/* Constantes*/
#define ADC ADD 0x2081
#define IO_ADD 0x2080
/* Vectores de interrupção */
#define INT1_VECT 13
#define TIMER2_VECT 19
/* Constantes referentes ao PIC */
#define EOI_ADD 0xFF22
#define INT1_CR_ADD 0xFF3A
#define INT1_CR_VAL 0x0007
#define TIMERs_CR_ADD 0xFF32
#define TIMER2_CR_VAL 0x0007
#define TIMERs_EOI_VAL 8
/* Constantes referentes ao Timer2 */
#define TIMER2_MAXCA_ADD 0xFF62
#define TIMER2_MAXCA_VAL 25000
#define TIMER2_CTRL_ADD 0xFF66
#define TIMER2_CTRL_VAL 0xE001
/* Variáveis globais */
char VadcS, adcON;
/* Rotina de serviço da ADC */
void interrupt int1_isr(void) {
int ValADC;
...
ValADC = inportb(ADC_ADD);
...
VadcS = .... ; adcON = 0;
/* Envio de comando EOI para o PIC */
outport(EOI_ADD , INT1_VECT);
}
/* Rotina de serviço ao Timer 2 */
void interrupt timer2_isr(void) {
v=VadcS; /* lê valor VadcS */
...
/* Envio de comando EOI para o PIC */
outport(EOI_ADD , TIMERs_EOI_VAL);
}
/* Programa main */
void main (void) {
disable(); /* disable interrupts*/
/* Inicialização do vector do INT1 */
poke(0, INT1_VECT*4, FP_OFF(int1_isr));
poke(0, INT1_VECT*4+2, FP_SEG(int1_isr));
/* Inicialização do vector do TIMER 2 */
poke(0, TIMER2_VECT*4, FP_OFF(timer2_isr));
poke(0, TIMER2_VECT*4+2, FP_SEG(timer2_isr));
/* Programação do PIC (INT1 e TIMER 2) */
outport(INT1_CR_ADD, INT1_CR_VAL);
outport(TIMER2_CR_ADD, TIMER2_CR_VAL);
INT1
0
(var globais) VadcS adcON
8 8
1 ADC Programa principal (main)
Rotina interrupção
INT1 (int1_isr) INT_TIMER2
Rotina interrupção
TIMER2 (timer2_isr)
Fig. 7 – Organização do software do voltimetro
/* Programação do Timer 2 */
outport(TIMER2_CTRL_ADD, TIMER2_CTRL_VAL);
outport(TIMER2_MAXCA_ADD, TIMER2_MAXCA_VAL);
enable(); /* enable interrupts*/
...
adcON = 1;
outportb(ADC ADD, 0); /* start conv*/
...
}
4- Utilização do Timer 1 para controlar a frequência de amostragem
Nesta fase final do trabalho vamos programar o Timer1 de maneira a gerar interrupções à frequência de 8Hz (125 ms).
Estas interrupções deverão desencadear o início de conversão na ADC, pelo que esta será a frequência de amostragem do sinal de entrada.
De forma semelhante ao ponto anterior, o código do main terá de incluir as seguintes inicializações:
• Inicialização do ponteiro de interrupção do Timer1 (vector type 18) na TEI;
• Programação dos registos do Timer1.
Note-se que a programação do registo de controlo dos timers, no PIC é feita uma única vez para todos os timers.
4.1- Programação do Timer1
Esta programação é idêntica à que fizemos antes para o Timer2. Neste caso o valor a escrever no registo MaxCountA (endereço FF5Ah) seria 125ms/0.8μs = 156250, número esse que excede largamente o máximo que é possível ter nos timers (de 16bits) do 80C188. Assim sugere-se uma de entre as duas soluções descritas nos pontos 4.1.1 e 4.1.2.
4.1.1- Interrupções do Timer1 a 3x8Hz
Nesta solução o timer1 é programado para gerar interrupções à frequência de 3x8Hz mas a rotina de serviço correspondente desencadeia o início de conversão na ADC apenas de 3 em 3 invocações. Desta forma o valor a programar em MaxCountA é de 156250/3≈52083.
Os valores a programar nos campos do registo de controlo do timer1 (fig. 8) são:
EN=1: activa o timer;
INH\=1: valida o valor o bit EN;
INT=1: activa interrupções do timer;
RIU=X: este bit é read-only;
MC=X: bit maximum count ignorado;
RTG=0: timer conta desde que T1IN=1;
P=0: prescaler não usado;
EXT=0: relógio interno usado;
ALT=0: apenas MaxCountA usado;
CONT=1: funcionamento continuo.
O valor a programar será portanto E001h.
4.1.2- Utilização do relógio de 50Hz do Timer2
Outra solução para o problema será fazer funcionar o timer1, não a partir do relógio interno do processador (1.25MHz), mas sim a partir do sinal de 50Hz gerado pelo timer2. O timer2 continua a gerar as interrupções para a rotina de refresh do display e ao mesmo tempo funciona como prescaler do timer1. Neste caso o valor a escrever em MaxCountA é de 125/20=6.25≈6, ao que corresponderá uma frequência de 8.33Hz (120ms).
Os valores a programar nos campos do registo de controlo do timer1 (fig. 8) são os mesmos da solução anterior, excepto o bit P (prescaler) que neste caso é 1. O valor a programar será portanto E009h.
4.2- Organização do programa
Com a introdução da rotina de atendimento à interrupção do Timer1 passamos a ter o software do voltímetro organizado como se indica na fig. 9.
Admitindo que adoptamos a solução indicada em 4.1.2, as rotinas de interrupção e o programa principal executam as seguintes acções:
Rotina de serviço à interrupção INT1:
1- Lê ADC;
2- Rotina chgscale;
3- Escreve resultado em VadcS;
4- Envia comando EOI para o PIC;
Rotina de serviço à interrupção do Timer2 (igual à anterior):
1- Lê VadcS;
2- Rotina h2d;
3- Envia resultado displays (Em cada activação a rotina actualiza ora um, ora outro dígito);
4- Envia comando EOI para o PIC;
Rotina de serviço à interrupção do Timer1 (secção 4.1.2):
1- Comanda ADC para iniciar conversão;
2- Envia comando EOI para o PIC;
Programa principal:
1- Disable das interrupções;
2- Inicialização (prog. PCS3\, inicial.
ponteiros de interrupção, prog. PIC, prog.
timer2, prog. timer1, etc.);
3- Enable das interrupções;
4- Ciclo infinito;
INT1 VadcS
8 8
ADC Programa
principal (main)
Rotina interrupção
INT1 (int1_isr) INT_TIMER2
Rotina interrupção
TIMER2 (timer2_isr)
INT_TIMER1 Rotina interrupção
TIMER1 (timer1_isr) start
conv
Fig. 9 – Versão final do software do voltimetro
CONT EN INH . . . MC RTG P EXT ALT
0 1 2 3 4 5 15
INT RIU
14 12 13
Fig. 8 – Registo de controlo do Timer1 (FF5Eh)
Note-se que agora o programa principal apenas executa as inicializações necessárias, ficando depois em ciclo infinito sem fazer nada. Como o comando de início de conversão da ADC é agora enviado sob o controlo do timer1, já não precisamos da variável global adcON.
Em linguagem C teremos:
/* Programa do voltimetro completamente interrupt- driven*/
/* Constantes*/
#define ADC ADD 0x2081
#define IO_ADD 0x2080
/* Vectores de interrupção */
#define INT1_VECT 13
#define TIMER2_VECT 19
#define TIMER1_VECT 18
/* Constantes referentes ao PIC */
#define EOI_ADD 0xFF22
#define INT1_CR_ADD 0xFF3A
#define INT1_CR_VAL 0x0007
#define TIMERs_CR_ADD 0xFF32
#define TIMERs_CR_VAL 0x0007
#define TIMERs_EOI_VAL 8
/* Constantes referentes ao Timer2 */
#define TIMER2_MAXCA_ADD 0xFF62
#define TIMER2_MAXCA_VAL 25000
#define TIMER2_CTRL_ADD 0xFF66
#define TIMER2_CTRL_VAL 0xE001
/* Constantes referentes ao Timer1 */
#define TIMER1_MAXCA_ADD 0xFF5A
#define TIMER1_MAXCA_VAL 6
#define TIMER1_CTRL_ADD 0xFF5E
#define TIMER1_CTRL_VAL 0xE009
/* Variáveis globais */
char VadcS;
/* Rotina de serviço da ADC */
void interrupt int1_isr(void) {
int ValADC;
...
ValADC = inportb(ADC_ADD);
...
VadcS = .... ;
/* Envio de comando EOI para o PIC */
outport(EOI_ADD , INT1_VECT);
}
/* Rotina de serviço ao Timer 2 */
void interrupt timer2_isr(void) {
v=VadcS; /* lê valor VadcS */
...
/* Envio de comando EOI para o PIC */
outport(EOI_ADD , TIMERs_EOI_VAL);
}
/* Rotina de serviço ao Timer 1 */
void interrupt timer1_isr(void) {
outportb(ADC ADD, 0); /* start conv.*/
/* Envio de comando EOI para o PIC */
outport(EOI_ADD , TIMERs_EOI_VAL);
}
/* Programa main */
void main (void) {
disable(); /* disable interrupts*/
/* Inicialização do vector do INT1 */
poke(0, INT1_VECT*4, FP_OFF(int1_isr));
poke(0, INT1_VECT*4+2, FP_SEG(int1_isr));
/* Inicialização do vector do TIMER 2 */
poke(0, TIMER2_VECT*4, FP_OFF(timer2_isr));
poke(0, TIMER2_VECT*4+2, FP_SEG(timer2_isr));
/* Inicialização do vector do TIMER 1 */
poke(0, TIMER1_VECT*4, FP_OFF(timer1_isr));
poke(0, TIMER1_VECT*4+2, FP_SEG(timer1_isr));
/* Programação do PIC (INT1 e TIMERs 2 e 1) */
outport(INT1_CR_ADD, INT1_CR_VAL);
outport(TIMER2_CR_ADD, TIMERs_CR_VAL);
/* Programação do Timer 2 */
outport(TIMER2_CTRL_ADD, TIMER2_CTRL_VAL);
outport(TIMER2_MAXCA_ADD, TIMER2_MAXCA_VAL);
/* Programação do Timer 1 */
outport(TIMER1_CTRL_ADD, TIMER1_CTRL_VAL);
outport(TIMER1_MAXCA_ADD, TIMER1_MAXCA_VAL);
enable(); /* enable interrupts*/
while(1);
}