Projecto de Arquitectura de Computadores Sistema de Gestão de Comboios IST - Taguspark 2005/2006 1 – Objectivos Este projecto pretende exercitar as componentes fundamentais de uma cadeira de Arquitectura de Computadores, nomeadamente a programação em linguagem assembly, os periféricos e as interrupções. O objectivo deste projecto é implementar um sistema de gestão com dois comboios sobre um circuito predefinido, com semáforos e sensores, de modo que os comboios possam circular nesse circuito sem chocarem. 2 – Especificação do projecto 2.1 – O circuito de comboios A figura seguinte representa o circuito das maquetas a usar neste trabalho, podendo verse: • os elementos do circuito, que podem ser: - simples (troço de linha recta): - agulhas, em forma de Y (uma entrada, duas saídas), que podem ser colocados em duas posições (esquerda e direita, vista por quem entra no cruzamento pelo ponto comum); • os semáforos, numerados (incluindo os da passagem de nível). Cada semáforo tem quatro cores possíveis (verde, amarelo, vermelho e cinzento – esta última correspondente ao semáforo apagado); • os sensores, numerados. Quando uma locomotiva passa por um sensor, gera dois eventos, correspondentes a cada um dos topos (frente e trás); • dois comboios, com uma locomotiva (vermelha) e 2 ou 3 carruagens, em que se assume que: − os comboios só andam para a frente (no sentido contrário ao dos ponteiros do relógio), o que significa que só as agulhas nos elementos 1 e 22H podem afectar o percurso dos comboios; − só há três velocidades: nula, média (3) e alta (7) • uma estação, onde os comboios devem parar para receber/largar passageiros. 1 2.2 – Troços, semáforos e sensores O circuito está dividido logicamente em 4 troços, delimitados por semáforos, que correspondem à unidade mínima de controlo do circuito. Os semáforos 4 e 5 não são para os comboios, mas para carros numa passagem de nível. A zona das agulhas (com duas entradas e duas saídas) não é um troço, mas de qualquer modo constitui uma zona que não pode ser percorrida simultaneamente pelos comboios. O conjunto das agulhas só tem dois estados possíveis, correspondentes a trocar de oval (interna externa e vice-versa). Os semáforos dão indicação visual se um comboio pode prosseguir ou não. Sempre que um comboio entrar num troço, o semáforo final do troço anterior deve ficar vermelho. Cada troço (ou zona das agulhas) só pode ser percorrido por um comboio de cada vez. Um comboio só pode avançar para um troço se este estiver livre. Um semáforo a verde indica que um comboio pode prosseguir à velocidade máxima, enquanto um semáforo amarelo indica que a velocidade máxima permitida é a média. Neste circuito, todos os semáforos são verde/vermelhos, com excepção do semáforo que antecede a estação (o 3), que é amarelo/vermelho, e dos da passagem de nível (4 e 5), que são cinzento/vermelho, um ao contrário do outro (para a luz vermelha alternar entre um e outro). Cada troço tem tipicamente 3 sensores de passagem (inicial, médio e final). As excepções são: • o troço da estação, que não tem sensor inicial, • o troço que termina no semáforo 0, que tem dois sensores intermédios. 2 2.3 – Funcionamento global do sistema É a passagem das locomotivas pelos sensores que deve determinar o estado dos semáforos. Assim: • quando um comboio avança para um troço, o semáforo que o precede deve mudar para vermelho mal a locomotiva passa pelo sensor inicial do novo troço; • no entanto, o troço do qual o comboio acabou de sair (troço anterior) só deve ser libertado quando o comboio estiver já totalmente dentro do novo troço. Isto significa que, num dado instante, um comboio pode ter dois troços reservados para si. O sensor em que se considera que um comboio já está totalmente dentro do troço depende das suas características. No troço da estação, por exemplo, que é pequeno, tal só deve ser feito no sensor final. Nos outros, todavia, pode logo ser feito no sensor intermédio. No caso particular da zonas das agulhas, um comboio só pode avançar se: • a zona das agulhas estiver reservada para esse comboio • o troço seguinte à zona das agulhas também estiver reservado para esse comboio. É muito importante que só se reserve o conjunto (agulhas + troço seguinte) e não apenas de um deles, senão os comboios podem bloquear-se mutuamente (como exercício, veja em que caso isto pode suceder). A zona das agulhas deve ser libertada quando o troço que a precede (relativo ao comboio que acabou de passar por essa zona) também for libertado. Os comboios devem andar à velocidade máxima, excepto quando: 1. passarem num sensor intermédio de um troço cujo semáforo no seu final está a vermelho (caso em que devem reduzir para velocidade média), ou 2. passarem num sensor final imediatamente antes de um semáforo amarelo (caso em que devem reduzir para velocidade média), ou 3. chegarem a um sensor final imediatamente antes de um semáforo vermelho (caso em que devem parar até o semáforo deixar de estar a vermelho), ou 4. no troço da estação (em que deve parar durante algum tempo, após o qual o comboio só deve arrancar quando o semáforo final desse troço não estiver a vermelho). Um comboio em velocidade média deve passar à máxima mal passe num sensor em que não se verifique uma das condições anteriores. Quando um comboio passa pelo sensor de entrada na estação, deve escrever “Estação: comboio #” na 1ª linha do LCD, em que “#” é o número do comboio que entrou na estação. Quando esse comboio sai da estação (passa no sensor após a estação ou o topo de trás da locomotiva passa no sensor final do troço da estação – à escolha), a linha do LCD deve ficar em branco. Quando um comboio passa pelo sensor antes da passagem de nível (sensor 6), os semáforos desta (normalmente ambos a cinzento) começam a piscar, com vermelho alternadamente num e noutro, com uma cadência de 0,5 segundos para cada lado. Quando passar pelo sensor a seguir (sensor 9), os dois semáforos voltam a ficar 3 cinzentos (indicando o fim da passagem do comboio). A passagem do comboio na passagem de nível deve ser indicada na 2ª linha do LCD (com “Passagem de nível: comboio #”, em que “#” é o número do comboio a passar). 3 – Implementação 3.1 – Circuito a usar no projecto O circuito a montar no simulador é fornecido (ficheiro proj.cmod) e é semelhante ao já utilizado nos trabalhos anteriores, mas ao qual foi acrescentado um relógio de tempo real, que gera um sinal de relógio de tempo real1, programável (já está pré-programado para um período de 0,5 seg). A15..A0 BGT BRT WAIT 0 INTA INT3 INT2 INT1 INT0 16 16 PEPE A15..A0 8 DATA_I DATA_P DATA_I DATA_P 8 RD WR RD WR CS A15 BA RESET MemBank BA CLOCK 5 ADDRESS RD Real-time Clock Reset WR Clock 8 CS DATA Pista de comboios RESET Interrupt 3.2 – Estrutura de processos O programa tem de estar continuamente a monitorizar o estado de cada comboio e a sua passagem pelos sensores, gerir temporizações (tempo de paragem na estação, semáforos da passagem de nível que acendem e apagam), ao mesmo tempo que tem de estar atento aos comandos de interface do utilizador (botões de pressão e barras de velocidade). É um conjunto de actividades que se pretendem simultâneas, e algumas delas com razoável independência. Em software, isto conduz à noção de processo. Um processo é uma espécie de um programa independente dos restantes processos, quase como se tivesse um processador só para si. Isto permite ao programador pensar apenas num processo de cada vez, o que é muito mais simples do que misturar as actividades todas. Assim, poderemos ter pelo menos os seguintes processos: • um processo para cada um dos comboios (porque têm de ter comportamento independente do outro); 1 Este módulo difere do relógio normal no facto de este último não ser de tempo real (o que por um lado permite que o PEPE funcione à velocidade máxima conseguida pelo computador que corre o simulador, mas por outro não permite implementar temporizações de valor fixo). 4 • • • um processo gestor do sistema, que trate dos semáforos e da reserva dos troços; um que tome conta dos semáforos da passagem de nível; um que consiga fazer temporizações (para medir o tempo de paragem na estação). Estas são as actividades que precisam de ser executadas ao “mesmo tempo” que as restantes. Na realidade, o processador só consegue executar uma instrução de cada vez, pelo que o que acontece é o processador executar algumas instruções de cada processo antes de passar ao próximo, voltando depois ao primeiro processo depois de ter executado parte de cada um dos vários processos. Algo do género: ciclo: CALL processo_comboio_0 CALL processo_comboio_1 CALL gestor_sistema CALL passagem_nível CALL temporizador JMP ciclo Cada processo é uma rotina, que ao ser chamada faz algum processamento desse processo e depois retorna, NÃO SE BLOQUEANDO INTERNAMENTE (p.ex. ficando em ciclo à espera da ocorrência de determinado acontecimento) para permitir que as rotinas dos outros processos sejam também executadas. Optimização: quando há processos “iguais”, como é o caso dos comboios, apenas deve existir uma rotina que controla o movimento de um comboio. em vez de estar a repetir o código para os dois comboios. Deve, para isso, chamar-se a rotina passando-lhe um ou mais valores como parâmetros; neste caso em particular, deve-se passar para esta rotina o número que identifica o comboio. Um aspecto fundamental é que um dado processo não pode bloquear o programa, com por exemplo um ciclo de espera incluído na própria rotina (senão as outras rotinas não eram executadas). Se um processo quiser esperar que uma dada posição de memória tenha um dado valor, por exemplo, não deve fazer: ; rotina que implementa o processo 1 processo1: Lê posição de memória CMP com valor pretendido JNZ processo1 ... RET ; vê se valor é o esperado ; se ainda não é, vai tentar de novo ; processamento caso o valor for o esperado ; acabou, regressa mas sim ; rotina que implementa o processo 1 processo1: Lê posição de memória CMP com valor pretendido JNZ fim ... fim: RET ; vê se valor é o esperado ; se ainda não é, vai tentar de novo ; processamento caso o valor for o esperado ; acabou, regressa. Há-de voltar no próximo ; ciclo A espera deve ser externa à rotina e não interna. Dito de outra forma, a espera deve incluir uma volta aos restantes processos, o que permite esperar sem bloquear todo o sistema. 5 O mais natural é cada processo implementar uma máquina de estados. Por exemplo, o processo que trata de um comboio tem de saber se a locomotiva está parada junto a um semáforo vermelho, parada na estação, a avançar, etc., pois o comportamento a adoptar depende do estado em que está. Para permitir que um dado processo tenha um comportamento diferente consoante a situação utiliza-se uma variável de estado que identifica o estado (identificado por um número, único para cada estado) em que o processo se encontra, ficando com uma estrutura deste género: ; rotina que implementa o processo 1 processo1: Lê variável com o estado do processo estado0: CMP com 0 ; vê se é o estado 0 JNZ estado1 … ; faz o processamento do estado 0 estado = novo valor ; indica qual o próximo estado JMP fim ; acabou execução do estado 0 estado1: estado2: estado3: fim: CMP com 1 JNZ estado2 … estado = novo valor JMP fim ; vê se está no estado 1 CMP com 2 JNZ estado3 … estado = novo valor JMP fim ; vê se está no estado 2 ... ... RET ; etc. Outros estados ; faz o processamento do estado 1 ; indica qual o próximo estado ; acabou execução do estado 1 ; faz o processamento do estado 2 ; indica qual o próximo estado ; acabou execução do estado 2 ; sai, para permitir que os outros processos corram Quando esta rotina é executada, os testes ao estado agulham para a parte do código que processa esse estado. Em seguida sai (retorna), e quando for executada novamente (após uma volta completa ao ciclo principal), já a variável “estado” terá eventualmente um novo valor (nada impede que seja o mesmo), o que permite ao processo, na prática, evoluir no seu comportamento. Nota: convém que as variáveis de estado sejam concretizadas com posições de memoria e não com registos; se utilizar registos é preciso que fique ciente que esses registos ficam dedicados a essa função e não poderão ser utilizados para outros propósitos (a não ser que recorra a armazenamentos temporários na pilha que complicam desnecessariamente o código). A comunicação entre processos, se houver, faz-se por meio de variáveis em memória, em que um processo vai lendo uma dada variável até que outro lá coloque o valor que ele à espera (por exemplo está a 0 até que o outro processo lá coloque 1). Isto pode servir para passar informação ao mesmo tempo que pode sincronizar os dois processos. 6 3.3 – Estrutura dos dados A descrição de que troços há, a que troço pertence a cada sensor, se um dado troço tem estação ou não, o que deve ser feito em cada sensor quando lá passa um comboio, etc, deve ser feita em tabelas, em que para cada troço, sensor, semáforo, etc, se coloque informação relevante. Deve também notar-se que o estado dos semáforos não pode ser lido do módulo dos comboios. Se for preciso ler este estado, deve sempre ter-se em memória o valor que se pretende ter para cada um dos semáforos, estado esse que deve ser colocado no valor pretendido e depois escrito no porto conveniente. Idem para as agulhas. Para construir estas tabelas sugere-se o uso das directivas STRING, WORD e/ou TABLE. 3.4 – Interrupções O relógio de tempo real gera um sinal que se repete de 0,5 em 0,5 segundos, gerando assim duas interrupções INT1 por segundo. Esta interrupção deve ser usada para gerar as temporizações de paragem dos comboios nas estações e do ritmo de alternância dos semáforos da passagem de nível. Os eventos de passagem das locomotivas pelos sensores devem ser detectados pela interrupção INT0 e não estar sempre a ler o porto 0EH do módulo dos comboios. Note-se que (em relação a INT0): • cada passagem completa da locomotiva por um sensor gera dois eventos (ou duas interrupções); • continua a ser necessário ir ler o evento em si no porto 0DH depois de saber que há um evento; • para ler um evento é preciso ler dois bytes no porto 0DH (ver manual do módulo dos comboios) e não apenas um (isto é necessário para suportar mais sensores do que um só byte permitia). As rotinas de atendimento das interrupções devem essencialmente actualizar variáveis (nomeadamente, os bytes de informação para os comboios) e não fazer processamento propriamente dito. Os processos é que devem depois ler essas variáveis e actuar em conformidade. ATENÇÃO: os níveis de sensibilidade das interrupções têm de ser programados usando o registo RCN (ver manual do PEPE). A interrupção INT0 deve estar programada para reagir ao nível 1 (gera interrupções enquanto o pino estiver a 1), enquanto a interrupção INT1 deve apenas reagir ao flanco de 01 (uma só vez em cada flanco). 3.5 – Desenvolvimento do software A primeira coisa a fazer é estruturar o programa, identificando as rotinas a usar e fazendo um fluxograma (ou um algoritmo) para cada uma delas. Devem fazer-se pequenos passos de cada vez, testando o que já estiver feito. Para testar um programa já com alguma complexidade devem testar-se pequenas partes de cada vez, em vez de testar tudo junto. É que quando não funcionar fica-se sem saber qual parte é a responsável pelo erro… Ainda por cima, este é um sistema de tempo real, em que os comboios andam e geram eventos de passagem pelos sensores 7 independentemente do PEPE. Por isso, é impossível testar o sistema completo em single-step, por exemplo. Recomenda-se o teste das partes individuais (cada processo, por exemplo) de forma simulada, com pequenos programas que testam partes do programa ou actuando directamente em variáveis na memória, se necessário. Desta forma, é muito mais fácil o todo funcionar bem. Dado que a manipulação de interrupções é uma tarefa não trivial, cujo debug é difícil de realizar, sugere-se que, num primeiro passo, desenvolva o código sem recorrer a interrupções; isto implica que a leitura dos sensores terá de ser realizada por polling no ciclo de processamento principal; implica também que não existem temporizações e, portanto: • a passagem de nível não funciona nesta 1ª fase; • a paragem na estação deve ser simulada (p. ex. à custa de uma rotina que, após cada chamada, decrementa um contador – quando este chegar a zero passou a temporização pretendida) Numa 2ª fase, coloque apenas uma das interrupções a funcionar, e verifique o funcionamento pretendido. Apenas quando esta interrupção estiver operacional avance para a realização da segunda. Uma hipótese é colocar em funcionamento a rotina de serviço do INT0 e só depois a do relógio de tempo real. 8