Laboratórios de Computadores 2010/2011 - 1º Semestre

Lab 7: A Porta Série do PC Baseado em trabalho semelhante de João Cardoso (jcard@fe.up.pt).


Objectivos

Este trabalho tem como objectivo principal que aprenda a usar a UART do PC para realizar comunicação série. Em particular, pretende-se que aprenda a realizar comunicação série em modo polled e com interrupções, bem como use as FIFO da UART para maior eficiência e fiabilidade da comunicação.

Introdução

A porta série do PC é um dispositivo de E/S conforme à norma RS-232C que permite a interligação a baixo custo entre PCs ou entre um PC e outros dispositivos exteriores tais como modems. Para gerar os sinais necessários os primeiros PCs usavam um circuito integrado conhecido por Universal Asynchronous Receiver/Transmitter (UART). Os PCs actuais que ainda têm uma porta série incluem a funcionalidade da UART, possivelmente integrada com a funcionalidade de outros controladores de E/S.

Este trabalho compreende 3 partes. Na primeira parte, deverá apresentar a configuraração inicial da UART, programá-la de acordo com os argumentos da linha de comando e finalmente apresentar a configuração resultante. Na segunda parte, deverá realizar comunicação em modo polled. Finalmente, na 3ª parte, deverá realizar comunicação com interrupções.

Durante a fase de desenvolvimento do programa, deverá configurar a UART para funcionar em modo loopback, i.e. de modo a que qualquer caráter enviado seja recebido pelo mesmo porto série. Posteriormente, deverá testar a sua solução usando o cabo ligado ao porto série com uma ficha que faz a ligação entre os seus pinos 2 e 3, de modo que qualquer carácter enviado continuará a ser recebido pelo mesmo porto série do computador. Finalmente, quando o programa estiver a funcionar correctamente nesta modalidade, pode ligar-se o extremo livre do cabo ao porto série de um computador próximo, sendo os caracteres enviados por um computador escritos no ecrã do outro.

Trabalho a realizar

Primeira parte

Esta parte do trabalho consiste em desenvolver funções em C que mostrem e interpretem a configuração inicial da porta série/UART do PC. O programa deve também programar a UART de acordo com os parâmetros que recebe através da linha de comandos e apresentar a configuração resultante por leitura dos registos de estado da UART.

Mais especificamente, os objectivos desta parte são, escrever as seguintes funções:

test_conf()
A qual invoca as funções apropriadas para apresentar/alterar a configuração do UART. Se o argumento for NULL deverá apenas ler, de outro modo deverá alterar.
get_parity()/set_parity()
Para ler/alterar a paridade a usar (apenas par ou ímpar)
get_stop()/set_stop()
Para ler/alterar o número de stop bits
get_nbits()/set_nbits()
Para ler alterar o número de bits de dados por caráter
get_bitrate()/set_bitrate()
Para ler alterar a taxa de transmissão

Segunda parte

Nesta parte deverá desenvolver o código necessário para realizar a comunicação em modo polled. Este código tem essencialmente 2 partes: uma para o transmissor e outra para o receptor. Do lado do transmissor, deverá ler os carácteres premidos pelo utilizador sem bloquear e enviar esses caráteres para o receptor. Do lado do receptor, deverá receber os caráteres e imprimi-los no terminal. O programa deverá terminar quando a tecla @ for premida/recebida.

Mais especificamente, os objectivos desta parte são, escrever as seguintes funções:

test_poll_xmtr()
A qual é invocada pela função main() e é a função principal do lado do transmissor
test_poll_rcvr()
A qual é invocada pela função main() e é a função principal do lado do receptor
send_poll_char()
Para enviar caráteres em modo polled
recv_poll_char()
Para receber caráteres em modo polled

Terceira parte

Nesta parte deverá desenvolver o código necessário para fazer uso das interrupções geradas pela UART na comunicação série. Além disso, a comunicação entre a ISR e o resto do programa deverá ser feita através de duas queues, uma para transmitir e outra para receber. Tal como na segunda parte, é conveniente separar o código do transmissor do código do receptor. Do lado do transmissor, deverá ler os carácteres premidos pelo utilizador e passá-los para a ISR através da queue de transmissão. Do lado do receptor, a ISR deverá receber os caráteres da UART e inseri-los na queue de recepção para o programa principal que deverá apresentá-los no ecrã.

Mais especificamente, os objectivos desta parte são, escrever as seguintes funções:

test_int_xmtr()
A qual é invocada pela função main() e é a função principal do lado do transmissor
test_int_rcvr()
A qual é invocada pela função main() e é a função principal do lado do receptor
ser_isr()
A ISR que processa os pedidos de interrupção gerados pela UART

Para avaliar o seu trabalho usaremos o ficheiro grade7.c. Poderá usar este ficheiro para compilar e testar a sua solução durante o desenvolvimento. No entanto, não deve alterá-lo para evitar que quando fizermos a avaliação do seu programa este se comporte de forma inesperada.

O código em C para comunicação com a UART que julgue poder vir a ser útil no projecto deverá ser criado no ficheiro uart.c. O código em C a desenvolver específico deste trabalho, deverá ser acrescentado ao ficheiro lab7.c, o qual inclui já stubs para as funções de teste invocadas em grade7.c. O ficheiro de inclusão lab7.h contém os protótipos das funções em lab7.c.

Valorização Altere a solução da 3ª parte de forma a permitir comunicação bidireccional, i.e. qualquer dos lados deverá poder transmitir ou receber. Numa fase inicial poderá usar comunicação half-duplex, i.e. se um dos lados funciona como transmissor o outro funciona como receptor e vice-versa. Posteriormente deverá usar comunicação full-duplex, i.e. deverá ser possível que ambos os lados transmitam simultaneamente.

Note que deverá usar o código de trabalhos anteriores, nomeadamente o código relacionado com a gestão de interrupções.

IMPORTANTE O cumprimento ou não destes objectivos dependerá da sua capacidade em explicar todo o código que usar.

Programação da UART

Cada PC tem pelo menos uma porta série, designada por COM1, controlada por uma UART. Portas série adicionais são designadas por COM2, COM3, etc.

A Tabela 1 lista os 10 registos da UART que deverá usar neste trabalho. Estes registos são acessíveis a partir do endereço base da porta série, normalmente 0x3F8 para a COM1 e 0x2F8 para a COM2. O endereço da primeira coluna especifica o endereço do registo correspondente, relativamente a esse endereço base.

Addr. DLAB Nome Leitura Escrita
0 0 DATA Receiver Buffer Transmitter Holding
1 0 IER Interrupt Enable
2 X IIR/FCR Interrupt Identification FIFO Control
3 X LCR Line Control
4 X MCR Modem Control
5 X LSR Line Status -
6 X MSR Modem Status -
0 1 DLL Divisor Latch Least Significant Byte
1 1 DLM Divisor Latch Most Significant Byte
Tabela 1: Endereço, nome e principal funcionalidade dos registos da UART.

Note-se que os registos DLL e DLM têm os mesmos endereços que os registos DATA (de facto este último, são 2 registos, o Receiver Buffer Register e o Transmitter Holding Register) e IER, sendo distinguidos pelo bit DLAB, o qual é o bit 7 do registo LCR; assim, para aceder ao registo DLL é necessário activar (colocar a 1) primeiro o bit DLAB, aceder ao registo DLL, e depois desactivar DLAB.

Programação dos Parâmetros de Comunicação

Na comunicação série, ambos os lados do canal de comunicação devem concordar previamente num certo número de características da comunicação, como a taxa de transmissão, o número de bits, o número de stop bits e a paridade de cada carácter. Para programar a UART de acordo com esses parâmetros deve-se usar os registos LCR (ver Tabela 2) , DLL e DLM.

Line Control Register (LCR)
Bit Função
0,1 Word Length Select
0 0 5
0 1 6
1 0 7
1 1 8
2 No. of stop bits
0 1
1 2
5,4,3 Parity
X X 0 None
0 0 1 Odd
0 1 1 Even
1 0 1 1
1 1 1 0
6 Set Break Enable
7 DLAB
1 Selects DL
0 Selects Data
Tabela 2: Bits do registo LCR

A taxa de transmissão e recepção de bits designa-se por bit-rate e obtém-se dividindo uma frequência fixa de 115200Hz por um divisor dado por 115200/bit-rate, devendo-se arredondar ou truncar o resultado da divisão para o inteiro mais próximo. Os 8 bits mais significativos (MSB) do divisor devem ser escritos nos registos DLM enquanto que os oito bits menos significativos (LSB) devem ser escritos no registo DLL, programando assim a UART para funcionar a um determinado bit-rate. Para aceder a estes dois registos é necessário activar primeiro o bit DLAB, que é o bit 7 do registo LCR. Após ler ou escrever os registos DLL ou DLM é necessário repor DLAB a 0. Por exemplo, para programar a UART para funcionar com 8 bits, 1 stop bit, com paridade par e com bit-rate brate, pode usar-se o seguinte segmento de programa:

#include "serial.h"
/* base é o endereço base do porto, 0x2F8 ou 0x3F8 */
outport(base + SER_LCR, SER_CHAR_8 | SER_STOP_1 | SER_PAR_EVEN);
/* coloca DLAB a 1 mantendo inalterados os outros bits */
outportb(base + SER_LCR, inportb(base + SER_LCR) | SER_DLAB);
outportb(base + SER_DLM, MSB(115200/brate)); /* escreve MSB do divisor */
outportb(base + SER_DLL, LSB(115200/brate)); /* escreve LSB do divisor */
/* coloca DLAB a 0 mantendo inalterados os outros bits */
outportb(base + SER_LCR, inportb(base + SER_LCR) & ~SER_DLAB);

Note-se que no segmento de programa acima se utilizou extensivamente nomes simbólicos, sendo essa a prática recomendada devido ao elevado número de registos e de bits existentes na UART. O bit 6 do registo LCR deve ser mantido a 0, pois a sua função é a de detectar falhas no canal de comunicação, mas a sua utilização não será contemplada neste trabalho.

Envio e recepção em modo polled

No modo polled, a UART deve ser constantemente interrogada sobre a possibilidade de nela ser escrito um novo caráter para ser enviado, ou sobre a existência de um novo caráter entretanto recebido. O registo relevante para esta operação é o Line Status Register (LSR) cujos bits estão apresentados na Tabela 3.

Line Status Register (LSR)
Bit Função
0 Receiver Ready
1 Overrun Error
2 Parity Error
3 Framing Error
4 Break Interrupt
5 Transmitter Ready
6 Transmitter Empty
7 Error in Receiver FIFO
Tabela 3: Line Status Register (LSR)

Quando um caráter é recebido pela UART, o bit Receiver Ready (RX_RDY) do registo LSR, é colocado a 1, podendo então ler-se o caráter recebido no registo DATA. Quando um caráter que tenha sido escrito no registo DATA tiver sido enviado, o bit TX_RDY do registo LSR, (Transmitter Ready), será activado, indicando que a UART se encontra pronta para enviar um novo carácter.

Assim, antes de ler ou escrever no registo DATA é necessário verificar que tal pode ser feito, lendo primeiro os bits RX_RDY e TX_RDY do registo LSR. Para escrever na UART um caráter, e assim enviá-lo via porta série, pode-se usar o seguinte segmento de programa:

/* busy wait for transmiter ready */
while((inportb(ser_port + SER_LSR) & SER_TX_RDY) == 0)
    ; /* repeat while not ready */
outportb(ser_port + SER_DATA, c); /* OK, write char to send  */

É razoável fazer busy-waiting para enviar um caráter, pois mais cedo ou mais tarde o caráter anterior será enviado e um novo caráter poderá ser escrito na UART. Contudo, quando, ao contrário do que acontece neste programa, se usa controlo de fluxo de hardware através das linhas RTS/CTS e DTR/DSR, esta solução poderá conduzir ao "bloqueio" do programa por um tempo excessivo.

Para efectuar a leitura de caracteres deve proceder-se de modo semelhante, mas só se deve fazer busy-waiting se houver a certeza que haverá um caráter disponível para ser lido num curto intervalo de tempo, pois de outro modo o programa "bloqueará" por um tempo excessivo.

Como os computadores do laboratório têm apenas uma porta série, durante a fase de desenvolvimento deverá usar um único porto série, devendo qualquer caráter enviado por esse porto ser recebido pelo mesmo porto. O loopback pode ser realizado de 2 formas: em hardware ou em software. Em hardware deverá ligar a ficha disponibilizada ao cabo ligado ao porto série: esta ficha liga o pinos 3 (de envio) ao pino 2 (de recepção) do cabo. Em software deverá activar o bit Loopback do Modem Control Register (MCR), Tabela 5

Modem Control Register (MCR)
Bit Função
0 Data Terminal Ready (DTR)
1 Request to Send (RTS)
2 Output _OUT1
3 Output _OUT2
4 Loopback
5 Reserved
7,6 Reserved
Tabela 5: Modem Control Register (MCR)

Envio e recepção de caracteres com interrupções

Se a geração de interrupções pela UART estiver habilitada através da programação do registo IER, ver Tabela 4, sempre que for recebido um carácter ou sempre que possa ser enviado um novo carácter, os bits TX_RDY e RX_RDY do registo LSR são activados e será gerada uma interrupção.

Interrupt Enable Register (IER)
Bit Função
0 Enable Received Data Interrupt
1 Enable Transmitter Empty Interrupt
2 Enable Receiver Line Status Interrupt
3 Enable Modem Status Interrupt
5,4 Not relevant for Lab 7
7,6 Reserved
Tabela 4: Interrupt Enable Register (IER)

Os bits do registo IER que interessam para o presente trabalho são os bits Enable Received DATA Interrupt e Enable Transmiter Empty Interrupt. O primeiro, quando activado pelo utilizador, indica à UART que deve ser gerada uma interrupção sempre que um carácter for recebido e estiver disponível para leitura no registo DATA; o segundo, quando activado pelo utilizador, indica à UART que ela deve gerar uma interrupção sempre que estiver disponível para enviar um novo carácter.

Além de habilitar a geração de interrupções na UART, é necessário habilitá-las também no PIC-1. O porto COM1 está geralmente associado à interrupção de nível 4, enquanto que o porto COM2 está associado a interrupção de nível 3. Em algumas motherboards é ainda necessário habilitar a geração de interrupções da UART, activando a saída OUT2 através do registo MCR, ver Tabela 5, pelo que é conveniente fazê-lo sempre.

Como habitualmente, as interrupções só devem ser habilitadas após a UART ter sido correctamente inicializada e a rotina de atendimento da interrupção ter sido instalada. Ao terminar o programa é necessário proceder de modo inverso, isto é, proibir as interrupções no PIC, depois na UART, e só depois remover a rotina de atendimento da interrupção e reinstalar a original.

Interrupções múltiplas

A geração de uma interrupção pela UART pode ter múltiplas origens, pelo que a rotina de atendimento da interrupção tem que descobrir qual foi a causa real, o que se consegue lendo-se os bits 0, 1 e 2 do registo Interrupt Identification Register (IIR), ver Tabela 6.

Interrupt Identification Register (IIR)
Bit Função
0 Interrupt Status
0 Pending
1 Not Pending
3,2,1 Interrupt Origin
0 0 0 Modem Status
0 0 1 Transmitter Empty
1 0 0 Character Timeout Indication
0 1 0 Received Data Available
0 1 1 Line Status
4 Reserved
5 64-byte FIFO (only some UARTs)
7,6 FIFO Status
0 0 No FIFO
1 0 Unusable
1 1 Enabled
Tabela 6: Interrupt Indication Register (IIR)

O bit 0, Interrupt Status, indica se a UART gerou ou não uma interrupção; se o bit estiver a 1 não foi gerada uma interrupção pela UART, pelo que a rotina de atendimento não deve tentar ler ou escrever dados; se o bit estiver a 0 devem ser examinados os bits 1, 2 e 3 que identificam a fonte da interrupção, devendo então a rotina tomar a acção adequada ao tratamento da interrupção. O seguinte segmento de programa ilustra o método a utilizar:

void ser_isr(void) {
   st = inportb(ser_port + SER_IIR) & SER_INT_ID; /* origem da interrupção */
   switch (st) {
   case SER_INT_ST:
       ... /* falso alarme. Interrupção de outro dispositivo? */
   case SER_RX_INT:
       ... /* processa carácter recebido */
   case SER_TX_INT:
       ... /* retira carácter da queue e envia-o */
   }
   outportb(...); /* sinaliza EOI para o PIC */
}

Uso de queues

Para o envio ou recepção de grandes quantidades de dados, como um ficheiro, nada impede que este seja enviado carácter a carácter, quer usando o modo polled quer o modo de interrupção. Mas como a velocidade de processamento do CPU é muito superior à velocidade de envio de caracteres, no modo polled o programa iria passar a maior parte do tempo a perguntar à UART se já podia enviar um novo carácter, e no modo de interrupção o programa teria de testar as variáveis partilhadas frequentemente para garantir que a transferência de dados era feita com uma taxa próxima do bit-rate. Se o programa for o único em execução no computador não haverá grande problema, mas como a maior parte dos sistemas operativos modernos são multi-tarefa e multi-utilizador, transferir arquivos deste modo é desperdiçar recursos de CPU.

É preferível portanto usar queues; para enviar dados o programa vai colocando caracteres na queue, enquanto a rotina de interrução os vai retirando; para receber dados a rotina de interrupção coloca os caracteres recebidos noutra queue e o programa retira-os. O exemplo mais conhecido deste mecanismo é quando se imprime um arquivo, o que é efectuado por um spooler que vai enviando mais caracteres de um ficheiro para a impressora à medida que esta vai consumindo os anteriores, e entretanto o utilizador pode continuar a trabalhar com o computador sem ter que esperar pelo fim da impressão.

No nosso caso, o ficheiro deve ser transferido para uma queue que é depois esvaziada byte a byte pela rotina de interrupção de envio de caracteres. Após ter enviado um carácter, a UART gera uma interrupção a avisar que está pronta a enviar novo carácter, e a rotina que atende essa interrupção vai à queue buscar outro carácter e envia-o. Entretanto o programa pode efectuar outras operações, ou ficar simplemente à espera que a queue seja esvaziada. Para receber um ficheiro o processo é semelhante.

Em sistemas operativos multitarefa, o programa cede tempo de CPU a outros programas executando a função usleep() ou outras similares; no caso do MS-DOS pode ser também utilizada a função delay(). Assim, o esqueleto relevante da rotina de interrupção e da função que envia um ficheiro será:

void serial_isr(void){
    ...
    se fonte da interrupção for Transmitter Empty
        se a queue não estiver vazia
            tira carácter da queue
            escreve carácter na UART
    ...
}

envia_arquivo(){
    abre ficheiro
    para todos os caracteres do ficheiro
        enquanto a queue estiver cheia
            delay(sometime) // aguarda que a interrupção esvazie um pouco a queue
        coloca carácter na queue
    fecha ficheiro
}

FIFOs da UART

A utilização de uma queue do modo acima descrito torna mais eficiente a transmissão do arquivo, mas ainda há possibilidade de melhoramentos. Se se transmitisse mais do que um carácter por interrupção haveria menos interrupções, e tendo em conta que a ocorrência de uma interrupção tem gastos de processamento bem superiores aos de uma chamada de função, o programa seria mais eficiente.

Este facto foi reconhecido pelos fabricantes de UARTs, pelo que estas incorporam internamente dois buffers First In First Out (FIFO) com capacidade para vários caracteres. É assim possível escrever na UART vários caracteres consecutivamente enquanto o primeiro carácter ainda está a ser enviado; um mecanismo idêntico aplica-se a caracteres recebidos pela UART. Para activar estas FIFO é necessário programar o registo FIFO Control Register (FCR), ver Tabela 7, activando os bits 7, 6, 2, 1 e 0, mas como há vários tipos de UART, e nem todas suportam as mesmas possibilidades, é necessário após programar o registo FCR ler os bits 7, 6 e 5 do registo IIR para verificar se a FIFO ficou efectivamente activada.

FIFO Control Register (FCR)
Bit Função
0 Enable FIFO
1 Clear Receive FIFO
2 Clear Transmit FIFO
3 DMA Mode Select
4 Reserved
5 Enable 64-byte FIFO (on some UARTs only)
7,6 Interrupt Trigger Level
0 0 1
0 1 4
1 0 8
1 1 14
Tabela 7: FIFO Control Register (FCR)

Os bits 7 e 6 do FCR permitem configurar a UART para interromper apenas quando o nº de caracteres na FIFO de recepção atingir o valor correspondente. Esta técnica, conhecida por interrupt moderation, permite reduzir o nº de interrupções geradas, desde que a ISR leia mais do que um carácter por interrupção. Note-se que o registo IIR indicará a existência de dados recebidos apenas se o nº de caracteres no FIFO for igual ou superior àquele valor. No entanto, o bit 0, Receiver Ready, do LSR é activado assim que a FIFO tiver pelo menos um carácter, sendo inactivado apenas quando a FIFO ficar vazia.

No caso da transmissão, o registo IIR indicará uma interrupção por transmissor vazio apenas quando a FIFO de transmissão estiver vazia. Neste caso, a ISR deverá transferir o maior nº de caracteres possível, limitado, obviamente, pelo tamanho da FIFO.

Notas Relativas às Interrupções

Registos e bits adicionais Omitiu-se neste guião a descrição de alguns dos registos e de outras características mais avançadas da UART, por não serem relevantes para o trabalho. Nas tabelas apresentadas o nome dos bits que poderão interessar neste trabalho encontram-se em negrito.


Material Adicional


Agradecimentos

Este guião é uma versão do guião homónimo criado por João Cardoso, tendo sido usada na sua criação a versão HTML do mesmo guião por Rui Maranhão.