Interface Serial, Polling e Interrupções

CI064, 2019-1 © Roberto André Hexsel, 2014-2019 Este material foi escrito sob a premissa de que você leu as notas de aula sobre Sistemas Operacionais e Interrupções, e sobre a Interface Serial. Caveat emptor.
Antes de mais nada, crie um prompt vazio, copiando a linha abaixo no seu terminal alias prompt:="" Três clicks no botão esquerdo para copiar, um click no meio para colar.
Atualize o código fonte do simulador. Se você já possui um clone do repositório, atualize-o com prompt: cd cmips prompt: git pull do contrário, execute os comandos abaixo, para fazer uma cópia do repositório para o diretório $HOME/cmips. prompt: cd prompt: git clone https://github.com/rhexsel/cmips.git Acrescente ao seu caminho o diretório dos executáveis: prompt: export PATH=$PATH:~/cmips/cMIPS/bin Talvez seja uma boa ideia acrescentar a linha acima ao seu ~/.bashrc. Reconstrua o simulador com a versão atualizada: prompt: cd ~/cmips/cMIPS ; build.sh Teste sua instalação: prompt: cd tests ; doTests.sh

1- Interface Serial acessada por E/S programada

O cMIPS se comunica, através da interface serial, com uma unidade remota, que simula um "computador remoto". O computador remoto lê um arquivo e o transmite pela interface serial, ou escreve um arquivo com o que ele receber através da interface serial. Os arquivos com dados de entrada e dados de saída são chamados serial.inp e serial.out. Para este laboratório, o arquivo de saída está associado à saída padrão do simulador. No arquivo vhdl/uart.vhd, no modelo da remota, pode-se escolher dentre arquivos ou as entrada/saída padrão do simulador.

Transmissão

Nosso primeiro contato com a UART emprega polling, como discutido em sala. Leia tests/uarttx.c e compile-o (em tests), e execute a simulação: prompt: compile.sh -v uarttx.c && mv prog.bin data.bin .. && (cd .. ; run.sh) A saída é mostrada abaixo. A frase exibida é frequentemente usada para testar enlaces de comunicação porque contém todos os caracteres minúsculos do Inglês. Os caracteres de controle são mostrados em cinza. Esta frase é conhecida como o padrão fox.
\n\tthe quick brown fox jumps over the lazy dog\n
O programa uarttx.c transmite a cadeia de testes "the...dog" e esta é mostrada no terminal do simulador pela unidade remota. Execute novamente a simulação e examine o diagrama de tempos com gtkwave. Para acompanhar a execução, compile o programa e salve o código assembly resultante num arquivo. Use seu editor predileto para percorrer o "executável". prompt: EDIT=meu_editor_predileto prompt: compile.sh -v uarttx.c > xxxx && $EDIT xxxx Recorde: para acompanhar a execução do programa você deve seguir o diagrama de tempos no gtkwave e ler a saída da compilação. Os endereços no código assembly são os endereços das instruções e são exibidos no topo diagrama de tempo, no conteúdo do PC. prompt: (cd .. ; run.sh -w -v v_tx.sav)& A escala de tempo deve ser comprimida até que os eventos da comunicação serial fiquem evidentes na tela. As linhas mostradas na tela são, de cima para baixo: Fetch: PC e a instrução buscada; Decode: instrução por executar e as saídas do banco de registradores; Exec: operação, entradas da ULA e resultado; Memory: seletor de byte, endereço, dados válidos, WR, dados. Estas linhas mostram as referências aos periféricos, que estão nos endereços altos: UART em 0x3c00.00e0 e contador em 0x3c00.00a0; Write-back: sinais de controle/dados do último estágio do processador. UART: s_stat o processador está a acessar o registrador de status status; status registrador de status da UART (laranja); s_ctrl o processador está a acessar o registrador de controle ctrl; ctrl registrador de controle da UART (amarelo); s_tx o processador está a acessar o registrador de transmissão txreg; txreg registrador de transmissão (azul); txcpu_dbg_st estado da máquina de estados na interface UART-CPU; tx_dbg_st estado da ME que controla a transmissão; tx_shr_full indica que o registrador de deslocamento está não-vazio; txclk e txdat são o relógio de transmissão e a saída serial; e q conteúdo do contador externo; REMOTE: rx_dbg_st é o estado da ME da remota; e recv registrador de deslocamento de recepção da remota. Seus 8 bits são inicialmente indefinidos (u), e na medida em que os dados são recebidos tornam-se definidos. Acompanhe a leitura do diagrama de tempos com o código C de uarttx.c. As indicações de tempo no que se segue são aproximadas e servem para facilitar a busca pelos eventos. Procure pelas transições dos sinais na proximidade do instante mencionado no texto. Quando s_tx=1, o processador escreve no registrador de transmissão txreg e inicia-se a transmissão do caractere 0x0a='\n' -- veja o start bit em txdat. O sinal tx_bfr_empty fica ativo em 1 assim que o 0x0a é transferido para o registrador de deslocamento e o processador escreve o segundo caractere 0x09='\t', durante a transmissão do 0x0a com s_tx=1 pela segunda vez. Assim que se inicia a transmissão do 0x09 o processador escreve 0x74='t' em txreg; isso se repete para todos os demais caracteres: assim que o conteúdo do buffer de transmissão é copiado para o registrador de deslocamento, o próximo caractere da string é copiado para aquele buffer. Isso é possível por causa do double buffering no circuito de transmissão da UART. O contador é inicializado ao final da transmissão para garantir que a transmissão do EOT (End Of Transmission) complete antes que a simulação se encerre. Aumente a escala de tempo no gtkwave (lupa com '+') e descubra os instantes em que o registrador de controle da UART é escrito, e os instantes em que os caracteres são lidos da memória com as instruções LB, load-byte.

Recepção

Leia tests/uartrx.c e compile-o (em tests), e execute a simulação: prompt: compile.sh -v uartrx.c && mv prog.bin data.bin .. && (cd .. ; run.sh) A saída é mostrada abaixo. A remota envia a cadeia "\nabcdef\n01234...765\n" que é exibida na tela do simulador durante a execução de uartrx.c. A cadeia transmitida está no arquivo serial.inp.
\n abcdef\n 012345\n \n core.vhd:836:7:@62055ns:(assertion failure): cMIPS BREAKPOINT at PC=0000004c opc=010000 fun=100000 brk=10000000000000000000 SIMULATION ENDED (correctly?) AT exit(); /home/prof/roberto/cmips/cMIPS/tb_cmips:error: assertion failed /home/prof/roberto/cmips/cMIPS/tb_cmips:error: simulation failed
Execute novamente a simulação e examine o diagrama de tempos do gtkwave: prompt: (cd .. ; run.sh -w -v v_rx.sav)& O arquivo de configuração do gtkwave é distinto daquele da transmissão. As linhas mostradas na tela são as mesmas que no diagrama anterior, exceto pelas linhas da UART, que são: UART: mesmos sinais para registradores de status e controle; s_rx o processador está a acessar o registrador de recepção rxreg; rxreg registrador de recepção da UART (violeta); received registrador de deslocamento da recepção; rxcpu_dbg_st é o estado da ME da interface UART-CPU; rx_bfr_full indica que rxreg está não-vazio; rx_dbg_st é o estado ME de recepção; sta_recv_sto é o registrador de deslocamento de recepção; e rxclk e rxdat são o relógio de recepção e o sinal de entrada. O relógio de recepção da UART (rxclk) amostra o bit recebido na borda de descida, aproximadamente no centro do bit; REMOTE: o sinal start dispara a transmissão assim que RTS for ativado pelo processador; tx_dbg_st é o estado da ME de transmissão; e outdat o sinal serial transmitido. Ajuste a escala de tempo no gtkwave (lupa com '-') para que os eventos na interface serial fiquem evidentes. Com uma escala de tempo em que se observam as instruções é difícil perceber o que ocorre na interface serial. rxdat=0 marca o envio do primeiro caractere, 0x0a='\n' e este é copiado para o registrador de recepção quando o stop bit é amostrado por rxclk. Em seguida, o registrador de status da UART passa a indicar que o registrador de recepção está cheio (0xe0); quando o processador lê o caractere, o valor volta para 0xc0, indicando que o registrador de recepção está agora vazio. Neste exemplo o processador não processa nada de útile é sempre mais rápido do que a UART, e portanto o double buffering na recepção não chega a ser necessário e nem útil. A transmissão se encerra quando a UART amostra o stop bit do caractere EOT='0x04', que marca o fim da transmissão.

2- Comunicação por interrupções - recepção

O programa que recebe caracteres pela interface serial e os exibe na saída padrão do simulador é composto de duas partes, um "programa" extremamente simples, que fica num laço, testando o valor de flag; quando flag=1, o caractere é copiado do armazenador e exibido, e flag ← 0. A segunda parte é o tratador de interrupções (handler). A cada caractere recebido da remota, o tratador de interrupções faz flag ← 1 e salva o caractere num armazenador em memória. O tratador de interrupções da UART é deveras simples. O código para o tratamento mais realista das interrupções será desenvolvido por você como parte do trabalho desta disciplina. A sequência de execução deste programa "atravessa" três arquivos:
  1. include/start.s contém a inicialização do ambiente de execução: inicializa vários registradores de controle do processador, a pilha, e salta para main(). Neste arquivo também está o demultiplexador de interrupções, que é o tratador da excessão 0x200 (interrupções), a partir de _excp_0200.
  2. include/handlers.s contém os tratadores de interrupções, um para cada dispositivo.
  3. tests/uart_irx.c que contém o programa main().
Estes três programas são ligados para gerar o executável que é simulado no cMIPS. Os dois primeiros contém o ambiente de execução (SO primitivo) para main(). O trecho de código mostrado abaixo é parte de include/handlers.s. Os números das linhas podem ser diferentes dos mostrados abaixo na versão mais recente do arquivo include/handlers.s As linhas 74-81 definem nomes para os deslocamentos dos componentes com relação ao "início" da estrutura de dados Ud. Estes serão usados como o campo "deslocamento" nas instruções LOAD e STORE. As linhas 82-90 definem o leiaute da estrutura de dados Ud. A linha 94 reserva espaço para 16 palavras no armazenador _uart_buff, que é privativo ao tratador e invisível fora de include/handlers.s. As linhas 95 e 96 documentam o uso da área de armazenamento, tal como é necessário para este laboratório, e para o trabalho. As linhas 98 e 99 definem as máscaras do registrador de interrupção da UART para remover os pedidos de interrupções de recepção e transmissão, respectivamente. A linha 108 define UARTinterr como um endereço global e portanto visível para outros arquivos com código objeto, no caso, start.s. As linhas 137 e 138 removem o pedido de interrupção com uma escrita no registrador de interrupção da UART. As linhas 134-135 salvam o conteúdo do registrador status em memória.
65 #================================================================ 66 # interrupt handler for UART attached to IP6=HW4 67 # for UART's address see vhdl/packageMemory.vhd 68 # 69 .bss 70 .align 2 71 .global Ud, tx_has_started 72 73 .equ Q_SZ, (1<<4) # 16, MUST be a power of two 74 .equ RXHD, 0 75 .equ RXTL, RXHD+4 76 .equ RX_Q, RXTL+4 77 .equ TXHD, RX_Q+Q_SZ 78 .equ TXTL, TXHD+4 79 .equ TX_Q, TXTL+4 80 .equ NRX, TX_Q+Q_SZ 81 .equ NTX, NRX+4 82 Ud: 83 rx_hd: .space 4 # reception queue head index 84 rx_tl: .space 4 # tail index 85 rx_q: .space Q_SZ # reception queue 86 tx_hd: .space 4 # transmission queue head index 87 tx_tl: .space 4 # tail index 88 tx_q: .space Q_SZ # transmission queue 89 nrx: .space 4 # characters in RX_queue 90 ntx: .space 4 # spaces left in TX_queue 91 92 tx_has_started: .space 4 # synchronizes transmission with Putc() 93 94 _uart_buff: .space 16*4 # up to 16 registers to be saved here 95 # _uart_buff[0]=UARTstatus, [1]=UARTcontrol, [2]=$v0, [3]=$v1, 96 # [4]=$ra, [5]=$a0, [6]=$a1, [7]=$a2, [8]=$a3 97 98 .set U_clr_rx_irq,0x08 # clear RX irq, bit 3 (STATUS & INTERR) 99 .set U_clr_tx_irq,0x10 # clear TX irq, bit 4 (STATUS & INTERR) 100 101 .equ UCTRL, 0 # UART registers' displacement from base 102 .equ USTAT, 4 103 .equ UINTER, 8 104 .equ UDATA, 12 105 106 .text 107 .set noreorder 108 .global UARTinterr 109 .ent UARTinterr 110 111 UARTinterr: 112 113 #------------------------------------------------------------- 114 # While you are developing the complete handler, uncomment the 115 # line below 116 # 117 # .include "../tests/handlerUART.s" 118 # 119 # Your new handler should be self-contained and do the 120 # return-from-exception. To do that, copy the lines below up 121 # to, but excluding, ".end UARTinterr", to yours handlerUART.s. 122 #------------------------------------------------------------- 123 124 _u_rx: lui $k0, %hi(_uart_buff) # get buffer's address 125 ori $k0, $k0, %lo(_uart_buff) 126 127 sw $a0, 5*4($k0) # save registers $a0,$a1, others? 128 sw $a1, 6*4($k0) 129 sw $a2, 7*4($k0) 130 131 lui $a0, %hi(HW_uart_addr)# get device's address 132 ori $a0, $a0, %lo(HW_uart_addr) 133 134 lw $k1, USTAT($a0) # Read status 135 sw $k1, 0*4($k0) # and save UART status to memory 136 137 li $a1, U_clr_rx_irq # remove interrupt request 138 sw $a1, UINTER($a0) 139 140 and $a1, $k1, $a1 # Is this reception? 141 beq $a1, $zero, UARTret # no, ignore it and return 142 nop 143 144 # handle reception 145 lw $a1, UDATA($a0) # Read data from device 146 147 lui $a2, %hi(Ud) # get address for data & flag 148 ori $a2, $a2, %lo(Ud) # in struct Ud[0], Ud[1] 149 150 sw $a1, 0*4($a2) # save new char in Ud[0=U_DATA] 151 addiu $a1, $zero, 1 # set flag to signal new arrival 152 sw $a1, 1*4($a2) # in Ud[1=U_FLAG] 153 154 UARTret: 155 lw $a2, 7*4($k0) # restore regs $a0,$a1, others? 156 lw $a1, 6*4($k0) 157 lw $a0, 5*4($k0) 158 159 eret # and return from interrupt 160 .end UARTinterr
Os registradores $a0 e $a1 são necessários para o tratamento da interrupção e portanto são preservados nas linhas 127-129, e são recuperados nas linhas 155-157. Nas linhas 140-142 há um teste para descobrir a causa da interrupção; se não for de recepção, para os fins deste laboratório, a interrupção é ignorada. As linhas 145-150 copiam o caractere recebido do registrador rxreg e o armazenam em memória, em Ud[0]. As linhas 151-152 fazem flag ← 1, em Ud[1]. Na linha 159 o tratamento da interrupção retorna para o programa interrompido. O "miolo" de tests/uart_irx.c é mostrado e comentado abaixo. Neste programa usamos um "xunxo": na linha 25, mentimos para o compilador, dizendo que o endereço da estrutura de dados Ud é aquele de um vetor de duas posições, uma para armazenar flag e outra para armazenar o caractere recebido. A estrutura Ud será usada no trabalho. Falaremos dela em breve.
22 int main(void) { // receive a string through the UART serial interface 23 volatile Tserial *uart; // volatiles tell GCC not to optimize away code 24 Tcontrol ctrl; 25 extern int Ud[2]; // allocated in include/handlers.s 26 volatile int *bfr; 27 volatile char c; 28 29 bfr = (int *)Ud; 30 uart = (void *)IO_UART_ADDR; // UART's address 31 32 ctrl.ign = 0; 33 ctrl.rts = 0; // make RTS=0 to hold remote unit inactive 34 ctrl.intTX = 0; 35 ctrl.intRX = 0; 36 ctrl.speed = SPEED; 37 uart->ctl = ctrl; // initialize UART, all inactive 38 39 // handler sets flag=bfr[U_FLAG] to 1 after new character is received; 40 // this program resets the flag on fetching a new character from buffer 41 bfr[U_FLAG] = 0; // reset flag 42 uart->interr.i = UART_INT_progRX; // program only RX interrupts 43 ctrl.igna = 0; 44 ctrl.rts = 1; // make RTS=1 to activate remote unit 45 ctrl.intTX = 0; 46 ctrl.ignb = 1; // do generate interrupts on RXbuffer full 47 ctrl.speed = SPEED; // operate at fraction of highest data rate 48 uart->ctl = ctrl; 49 50 do { 51 while ( (c = (char)bfr[U_FLAG]) == 0 ) // check flag in Ud[1] 52 delay_cycle(1); // nothing new 53 c = (char)bfr[U_DATA]; // get new character 54 bfr[U_FLAG] = 0; // and reset flag 55 if (c != EOT) // if NOT end_of_transmission 56 to_stdout( c ); // then print new char 57 else 58 to_stdout( '\n' ); // else print new-line 59 } while (c != EOT); // end of transmission? 60 61 return c; // so GCC will not optimize code away 62 63 }
A linha 25 declara a variável externa Ud[2] que é usada como o meio de comunicação entre o programa e o tratador da interrupção. Esta variável é definida em include/handlers.s, e é naquele arquivo que o espaço é alocado. O espaço alocado é maior, mas neste programa só usamos dois inteiros. As variáveis locais são declaradas como voláteis para evitar que o compilador as elimine na otimização do código. As linhas 32-37 inicializam a UART, em modo "tudo inativo". A linha 41 inicializa a variável de sincronização entre o handler e este processo (main()), Ud[1] = bfr[U_FLAG]. A linha 42 inicializa o registrador de interrupções da UART, definindo somente interrupções de recepção. As linhas 43-48 programam o dispositivo. As interrupções de recepção são habilitadas e a velocidade de tranmissão (bit rate) é reduzida para a metade dos exemplos anteriores. Tal é necessário para dar tempo ao programa de acessar a variável de controle flag entre a recepção de dois caracteres. Se a velocidade de transmissão fosse maior, a recepção de caracteres aconteceria mais rapidamente do que o processador é capaz de tratar dos eventos e executar as instruções do programa. O laço externo (linhas 50-59) espera pelo caractere EOT ("end of transmission") que sinaliza o fim da transmissão. O laço interno (linhas 51-52) espera até que flag=1, para então ler o caractere do armazenador, fazer flag ← 0, e exibir o caractere recebido na saída padrão do simulador. O EOT é substituído por um '\n' na saída padrão. Compile tests/uart_irx.c e execute a simulação. prompt: compile.sh -v uart_irx.c && mv prog.bin data.bin .. && (cd .. ; run.sh) A saída é mostrada abaixo e é idêntica ao exemplo anterior. A remota envia a cadeia "\n01234...765\n\n" que é exibida na tela do simulador por uart_irx.c.
\n abcdef\n 012345\n \n
Execute novamente a simulação e examine o diagrama de tempos do gtkwave: prompt: (cd .. ; run.sh -w -v v_irx.sav)& O arquivo de configuração do gtkwave é distinto daquele da transmissão por polling. Recorde:
  1. o registrador STATUS do processador determina seu modo de execução (se user ou kernel), habilita as interrupções, e define outras funcionalidades;
  2. o registrador CAUSE do processador mostra quais são as interrupções pendentes, e o código da última excessão;
  3. o registrador EPC contém o endereço da instrução que seria executada não fosse a interrupção;
  4. o registrador de controle da UART (ctrl) define seu modo de operação;
  5. o registrador de status da UART (status) indica se há espaço no registrador de transmissão, se há um caractere novo no registrador de recepção, e se há alguma interrupção pendente.
Os sinais estão agrupados da seguinte forma, de cima para baixo: As linhas mostradas na tela são as mesmas que no diagrama anterior, exceto pelas linhas do COP-0, que são: COP-0: interrupt_taken indica que o processador aceitou o pedido de interrupção; EPC (laranja), CAUSE (laranja), STATUS (amarelo) são os valores dos respectivos registradores. UART: o sinal irq é o pedido de interrupção, desativado pela escrita no registrador interr (s_intwr=1). Acompanhe o diagrama de tempos com a leitura de include/handlers.s e tests/uart_irx.c. Lembre-se do truque de usar um editor de texto para "acompanhar" o código compilado. A primeira interrupção é detectada quando irq=1 e seu tratamento se inicia no ciclo seguinte. CAUSE indica a causa do evento (interrupção) (0x1000.0000.0000.0000) e STATUS mostra que o processador está em Exception Level, em nível de excessão (KU=0, ERL=0, EXL=1, intEn=1, 0xbbbb.bbbb.bbbb.bbbb.ii03). O pedido de interrupção é removido pela escrita no registrador de interrupção da UART (s_intwr=1). O tratamento da interrupção termina com o retorno ao programa interrompido, para o endereço armazenado em EPC. Entre o final da primeira interrupção e a aceitação da segunda decorre o tempo equivalente a aproximadamente 60 instruções. A velocidade da transmissão é quase tão alta quanto a capacidade do processador de tratar os caracteres recebidos. Em sistemas realistas, a diferença de velocidade é da ordem de 400-500 instruções para cada caractere recebido, com o relógio do processador a 50MHz. Esta é uma frequência adequada a sistemas embarcados, mas não para computadores de uso mais geral. Siga a execução das interrupções restantes, até o final do programa, tendo em conta que você escreverá um par de programas similares ao deste laboratório para efetuar o trabalho desta disciplina.

Documentação

O arquivo docs/cMIPS.pdf contem toda a documentação do cMIPS. Para além disso, veja o código fonte. Repetição dos três avisos sobre o simulador: (i) não sei como fazer para terminar a simulação sem que os asserts emitam as mensagens de erro. De qualquer forma, o texto em maiúsculas indica o término normal de uma simulação, ou alguma condição de erro detectada pelo simulador; (ii) o código de inicialização do processador é executado a partir do endereço zero, seguido pelo código de tratamento de excessões (excp_0000 e excp_0180) e interrupções (excp_0200). O "código normal" inicia no endereço 0x0300 para que seja fácil identificar quando o processador executa "programas" ou "tratadores". O compilador preenche a ROM com zeros, que são desmontados para NOPs; (iii) a instrução WAIT é usada para terminar a simulação e seu argumento indica o motivo. Na função _exit() o wait 0 indica terminação normal; em excp_handler, os códigos de terminação são aqueles para as respectivas excessões. --fim da aula--