Skip to content

L15: Practica 6‐3

Juan Gonzalez-Gomez edited this page Jan 31, 2026 · 264 revisions

L15: Entrada/Salida a bajo nivel

  • Objetivos de la sesión:
    • Estudiar los mecanismos de E/S a bajo nivel
    • Aprender el funcionamiento de las UARTs
    • Implementar Funciones de alto nivel independientes de la plataforma
    • Entender la Arquitectura para hacer sistemas de E/S portables

Contenido

Introducción

La comunicación de información entre el procesador y el exterior es lo que conocemos como Entrada/Salida. La más importante es la consola, donde se muestra información al usuario en la pantalla, y este nos introduce información a través del teclado

La consola la estamos utilizando a través del sistema operativo. Pero... ¿Cómo se programa a bajo nivel? ¿Cómo funciona esta comunicación?. Esto es lo que aprenderemos en esta sesión

La programación de la consola es un tema importantísimo, y que sobre todo se utiliza mucho cuando programamos sistemas empotrados, a bajo nivel. También nos sirve para entender cómo se implementa esta parte en un sistema operativo, y así entender cómo funciona todo desde cero

Si quieres entender la historia de los terminales, y cómo ha ido evolucionando, puedes encontrar más información en este enlace: Sesión L1: Terminales: De la pluma al terminal

Comunicación con humanos: El terminal

La comunicación más básica entre un humano y un computador es mediante un terminal. En los sistemas empotrados, que normalmente no incluyen pantalla ni teclado, utilizamos un terminal externo, que típicamente es otro ordenador, para escribir comandos a través del teclado y ver los resultados en la pantalla. Al teclado y la pantalla nos referimos también como la consola

El RARs incluye su propio terminal, la consola simulada que vemos en la parte inferior, y que hemos estado utilizando desde el principio. La manera de interactuar con esta consola es a través del sistema operativo

Lo que nos interesa ahora es estudiar los mecanismos de bajo nivel para comunicarnos con el terminal y poder tanto enviar información para mostrarla en la pantalla como recibir caracteres de las teclas pulsadas en el teclado, sin utilizar el sistema operativo

Puertos de salida

En la sesión L4 aprendimos el mecanismo básico, a bajo nivel, para sacar información hacia el exterior del computador: mediante un puerto de salida. Físicamente hay unos pines asociados a este puerto, a los que podemos conectar LEDS o Displays de 7 segmentos.

Desde nuestros programas estos puertos de salida son direcciones de memoria. Utilizando inistrucciones store escribimos en esas direcciones y la información sale hacia el exterior

En los puertos de salida los bits salen en paralelo. Se utiliza un cable por cada bit para la conexión del dispositivo. Por ejemplo, para el caso del display de 7 segmento hay que tirar 8 cables

Sin embargo, en la comunicación con el terminal, los bits se envían en serie

Salida en pantalla por el terminal

Para mostrar información en la pantalla del terminal, el computador envía los caracteres (de 1 byte) mediante una comunicación serie. Los bits del carácter salen uno a uno, en serie, por un pin especial de transmisión serie que recibe el nombre de TX. A este pin se conecta un cable hacia el pin RX del terminal, que es por donde se reciben los bits. El terminal cada vez que recibe un carácter completo lo muestra en la pantalla

Este tipo de transmisión es muy sencilla, y sólo requiere de un cable entre el transmisor y el receptor. Por ello prácticamente todos los sistemas empotrados y microcontroladores incluyen los pines de comunicaciones serie

Por contra, la transmisión serie es más lenta. Se mide en bits por segundos, que en este contexto es igual a los baudios. Hasta que no se ha terminado de enviar el último bit, la pantalla no recibe el dato completo y por tanto no lo puede mostrar

Entrada por el teclado del terminal

La entrada se implementa mediante otro cable serie, que llega al pin RX del computador. Es decir, que para el manejo de la entrada/salida por terminal necesitamos 2 cables: uno para transmitir del computador a la pantalla y otro para recibir los datos del teclado. En total se necesita que haya en el computador los 2 pines: TX y RX

Cada vez que el usuario aprieta una tecla, como por ejemplo la A, el terminal envía los bits correspondientes a su código ASCII hacia el computador

La UART

La comunicación serie la realiza un chip denominado UART (Universal Asynchronous Receiver and Transmitter), que se encuentra mapeado en la memoria. La visión que tenemos como programadores, es la de una serie de registros mapeados en la memoria, que desempeñan funciones como configurar la comunicación, transmitir el dato, almacenar el estado de la uart, etc...

Estos registros dependen de la UART en concreto que tenga cada computador, así como las direcciones de memoria. En los siguientes apartamos describimos los registros típicos, de manera genérica

Registros de configuración

La comunicación serie se tiene que configurar. Hay que especificar la velocidad de transmisión de los bits (baudios) y el formato del envío de bits: bits de datos, bits de stop, bits de paridad... Estas configuraciones se realizan escribiendo en los registros de configuración. No vamos a entrar en los detalles técnicos de la comunicación serie, ya que no son necesarios a nivel de programación. Y también porque la UART del RARs no los incluye

Los parámetros típicos de comunicación, usados por ejemplo en placas como Arduino, son:

  • Bits de stop: 1
  • Bits de datos: 8
  • Bits de start: 1
  • Bit de paridad: Ninguno
  • Velocidades típicas (en baudios): 1200, 2400, 9600 y 115200

Registros de transmisión y recepción (datos)

Para realizar la transmisión de un byte hay que escribirlo en el registro de transmisión. Es decir, que para nosotros, como programadores, enviar un carácter a la pantalla es simplemente escribir en la dirección de memoria donde esté mapeado el registro de transmisión. Al hacerse esa escritura, la UART comienza su transmisión. Esto llevará su tiempo, porque la transmisión serie es lenta en comparación con la ejecución de instrucciones de la cpu

La recepción de datos se realiza leyendo el registro de recepción. Cada vez que se recibe un dato se almacena en este registro, y lo leemos con una instrucción de load

Vemos cómo realizar la transmisión y recepción es tan simple como ejecutar instrucciones de load o store. Sin embargo, no se pueden realizar en cualquier momento. Hay que esperar a que tanto el transmisor como el receptor estén listos, y esto lo sabemos leyendo la información disponible en el registro de estado y control

Registro de control y estado

El registro de control y estado contiene información sobre cómo se encuentran tanto el transmisor como el receptore. Típicamente este registro tiene esta información codificada en 2 bits, que reciben nombres como TXREADY y RXREADY. Si el bit TXREADY se pone a 1, significa que el transmisor está listo para enviar. Es decir, que ahora es cuando se puede escribir en el registro de transmisión. Al realizar esta escritura comienza una nueva transmisión y TXREADY se pone a 0 para indicar que No está disponible

De la misma manera, cuando el bit RXREADY está a 0, significa que todavía no se ha recibido un carácter, y por tanto que no se puede leer. Cuando RXREADY está a 1, el carácter se ha recibido y ya podemos leer

Dependiendo del fabricante, algunas UARTS tiene registros de control y estado separados, uno para el transmisor y otro para el receptor

Entrada/salida a bajo nivel en el RARs

El RARs dispone de un simulador de entrada/salida a bajo nivel, que incluye una UART. Se llama Keyboard and Display MMIO Simulator (Simulador de teclado y pantalla) y está disponible desde el menú Tools

Esta es la pinta que tiene. En la parte superior tenemos la pantalla: todos los caracteres enviados por la UART se muestran en esa parte. La parte inferior es por donde tecleamos los caracteres que queremos enviar a la CPU

La UART del RARs

La UART del simulador RARs disponde de un transmisor y un receptor independientes. Cada uno tiene 2 registros: Un registro de datos y un registro de control. El bit de menor peso (bit 0) de los registros de control es el bit de ready

El transmisor

El transmisor es el que nos permite enviar información a la consola, y verla en la pantalla. Para enviar un byte lo primero que hay que hacer es comprobar si el transmisor está listo. Esto se hace comprobando el bit TXREADY (transmisor listo) del registro de control del transmisor, que se encuentra en la dirección 0xFFFF0008. Una vez que este bit se pone a 1, ya podemos escribir el byte a enviar en el registro de datos del transmisor, en la dirección 0xFFFF000C

Vamos a aprender a usar el transmisor mediante ejemplos graduales, en los que se van introduciendo los conceptos y las ideas poco a poco

Ejemplo 1: Mostrar el bit TXRDY

En este primer ejemplo ponemos en marcha la UART. Lazamos el Keyboard and Display MMIO Simulator y pinchamos en el botón Connect to Program. En ese momento todos los registros del periférico ya están disponibles en el mapa de memoria

Al conectarse, el botón cambia y muestra el mensaje Disconnect from program. Así es como dejaremos la ventana durante la ejecución de todos los ejemplos

El primer programa lo vamos a usar para monitorizar el registro de control del transmisor. Lo primero es definir las direcciones de interés. Definimos la dirección base de la UART en la constante UART_BASE, y el desplazamiento relativo del registro de control del transmisor TX_CTRL

  #--- Direccion base de la UART
  .eqv UART_BASE 0xFFFF0000
	
  #------ Offset de los registros
  #-- Registro de control del transmisor
  .eqv TX_CTRL 0x08

Usamos el registro S0 para almacenar esta dirección base. Para leer el registro de control usamos esta instrucción: lw t0, TX_CTRL(s0). Mediante un bucle infinito leemos este registro cada segundo y lo imprimimos en la consola. Usamos las macros definidas en la sesión anterior. Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del transmisor de la UART del RARs
#--  Se muestra el registro de control del transmisor  
#--
#--  La información se muestra en la consola original
#--  del RARs, cada segundo. El programa no termina nunca
#--  Es necesario detenerlo manualmente
#-------------------------------------------------------


	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#------ Offset de los registros
	#-- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	
	.text
	
	#-- Direccion de acceso a la uart
	li s0, UART_BASE
	
	#-- Bucle infinito
bucle:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(s0)
	
	#-- Imprimir su valor en la consola, 
	#-- a través del sistema operativo
	PRINT_INTR(t0)
	PRINT_CHARI('\n')
	
	#-- Esperar un segundo
	SLEEPI(1000)
	
	#-- Repetir
	j bucle

Lo ensamblamos y lo ejecutamos. Observamos que inicialmente el registro de control está a 0

Esto es debido a que al compilar e iniciar un programa, el RARs pone a 0 todas las posiciones de memoria, incluidas las de los periféricos mapeados. Para que la UART comience con su funcionamiento normal hay que apretar el botón de RESET del periférico.

En ese momento observamos cómo el registro de control se pone a 1, indicando que el transmisor está listo para enviar datos

Ejemplo 2: Esperar a que el transmisor esté disponible

Para poder transmitir un carácter a la pantalla hay que esperar a que el transmisor esté listo. Sabemos que esto ocurre cuando se pone a 1 el bit TX_READY. Por la documentación del RARs sabemos que este bit se encuentra en el bit 0 del registro de control. Para leer este bit definimos la máscara TX_READY:

#-- Máscaras de acceso a los bits
#-- Bit Ready del transmisor
  .eqv TX_READY 0x01  

En este ejemplo se implementa un bucle que está constantemente leyendo el bit TX_READY. Mientras su valor sea 0, se sigue esperando. Pero en cuanto se pone a 1 se termina el bucle

Esta técnica se denomina espera activa. La CPU se queda bloqueada en este bucle, y no puede hacer nada más, hasta que el transmisor esté disponbile

Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del transmisor de la UART del RARs
#--  Esperar a que el transmisor esté disponible  
#--
#--  Se realiza un bucle de espera donde se esta
#--  constantemente leyendo el bit TX_RDY de
#--  transmisor. Cuando este bit se pone a 1 se imprime
#--  un mensaje
#-------------------------------------------------------


	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#------ Offset de los registros
	#-- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  
	
	.text
	
	#-- Direccion de acceso a la uart
	li s0, UART_BASE
	
	
	PRINT_STRINGI("Esperando al transmisor...")
	
	#-- Esperar a que el transmisor este listo
wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(s0)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx
	
	#-- El transmisor está listo!!! 
	PRINT_STRINGI("\n")
	PRINT_STRINGI("Transmisor LISTO!\n")
	
	#-- Terminar
	EXIT

Ejecutamos el programa. Observamos que el programa no termina, sino que se queda esperando. Y es que como hemos dicho antes, al reiniciarse un programa del RARs todos los registros mapeados se ponen a cero. Y por ello el bit TX_REDY está a 0 indefinidamente

Para arrancar la UART es necesario apretar el botón de RESET del panel Keyboard and Display MMIO Simulator, como hemos visto antes. Al hacerlo el bit TX_READY se pone a 1, el programa lo detecta, imprime el mensaje en la consola y termina

En esta animación se muestra el proceso completo:

Ejemplo 3: Envío de un carácter a la pantalla

Para mostrar un carácter en la pantalla primero esperamos a que se active el bit TX_READY, y luego escribimos el carácter en el registro de datos. Para acceder a este registro definimos la constante TX_DATA. Así es como quedan las definiciones del transmisor:

	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Registro de DATOS del transmisor
	.eqv TX_DATA 0x0C
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  

En este ejemplo se imprime el carácter A en la pantalla, y se termina. La transmisión en concreto se realiza con la instrucción sw t0, TX_DATA(s0)

Important

El RARS simula un retraso real en el transmisor, que por defecto es de 5 ciclos. Es decir, que hasta que no pasan 5 ciclos no se muestra el carácter en el pantalla. Debido a esto, si llamamos al servicio EXIT antes de 5 ciclos, NO SE MOSTRARÁ NADA EN LA PANTALLA

Antes de terminar realizamos una espera de 5 ciclos con instrucciones nop, para dar tiempo al carácter a mostrarse en la pantalla

Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del transmisor de la UART del RARs
#--  Envío de un carácter 
#--
#--  Escritura de un carácter en la pantalla a bajo nivel
#--  a través de la UART, mediante espera activa
#-------------------------------------------------------


	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Registro de DATOS del transmisor
	.eqv TX_DATA 0x0C
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  
	
	.text
	
	#-- Direccion de acceso a la uart
	li s0, UART_BASE
	
	PRINT_STRINGI("Esperando al transmisor...")
	
	#-- Esperar a que el transmisor este listo
wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(s0)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx
	
	#-- El transmisor está listo!!! 
	#-- Escribir en el registro de datos del transmisor
	#-- para enviar un carácter
	li t0, 'A'
	sw t0, TX_DATA(s0)
	
	#-- Terminar
	#-- Antes de terminar hay que esperar a que el carácter llegue a la 
	#-- pantalla. Está configurado por defecto un tiempo de 5 ciclos
	#-- Tras esos ciclos se muesta el carácter y terminamos
	#-- Si se termina antes de que transcurran esos ciclos,
	#-- el carácter no aparecerá en la pantalla
	nop
	nop
	nop
	nop
	nop
	EXIT

Al ejecutar el ejemplo, el programa se queda esperando a que se active la UART, igual que en el ejemplo anterior:

Pulsamos en RESET para inicializar la UART. En ese momento se activa el bit TX_READY y se transmite el carácter A:

El carácter aparece en la pantalla y por último se termina

En esta animación se muestra el proceso:

Ejemplo 4: Envío de un carácter a la pantalla y comprobación del bit de READY

En este ejemplo vamos a terminar de entender el funcionamiento del bit READY del transmisor. Tras una escribura en el registro de datos, el bit READY se pone a 0 porque el transmisor está ocupado con la transmisión del carácter. Transcurridos unos ciclos (5 por defecto), el carácter ya se ha enviado y por tanto el transmisor está de nuevo listo para transmitir el siguiente. Por elllo del bit READY se pone a 1

En este ejemplo se envía el carácter A a la pantalla (una vez que la UART se ha inicializado) y justo tras la escritura del carácter se lee el bit de READY. Su valor es de 0. Luego se espera unos ciclos y se vuelve a leer el bit, comprobándose que efectivamente su valor vuelve a 1

#-------------------------------------------------------
#--  Pruebas del transmisor de la UART del RARs
#--  Envío de un carácter y lectura del bit de READY
#--  para comprobar su evolucion
#-------------------------------------------------------


	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Registro de DATOS del transmisor
	.eqv TX_DATA 0x0C
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  
	
	.text
	
	#-- Direccion de acceso a la uart
	li s0, UART_BASE
	
	PRINT_STRINGI("Esperando al transmisor...")
	
	#-- Esperar a que el transmisor este listo
wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(s0)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx
	
	#-- El transmisor está listo!!! 
	#-- Escribir en el registro de datos del transmisor
	#-- para enviar un carácter
	li t0, 'A'
	sw t0, TX_DATA(s0)
	
	#--- Ahora vamos a comprobar el funcionamiento del bit READY
	#--- Nada mas escribir un caracter el transmisor esta OCUPADO
	#-- Leer el bit READY
	lw t0, TX_CTRL(s0)
	
	#-- Imprimir el bit READy
	PRINT_CHARI('\n')
	PRINT_INTR(t0)
	PRINT_CHARI('\n')
	
	#-- Esperar un tiempo
	nop
	nop
	nop
	nop
	nop
	
	#-- Transcurrido un tiempo, el carácter se termina de transmitir,
	#-- se imprime en la pantall. El bit de READY se pone a 1 para
	#-- indicar que ya está listo de nuevo
	#-- Volver a imprimir el bit READY
	lw t0, TX_CTRL(s0)
	PRINT_INTR(t0)
	PRINT_CHARI('\n')
	
	#-- Terminar
	EXIT

Este es el resultado tras su ejecución:

Comprobamos que efectivametne el bit de READY se pone a 0 y luego a 1. Es decir, que queda listo para enviar otro carácter

Ejemplo 5: Envío de dos caracteres

En este ejemplo se envían dos caracteres a la pantalla a través de la UART: A y B. Como hay que esperar dos veces a que el bit de READY del transmisor se ponga a 1, definimos las función uart_wait_tx_ready() que hace justamente eso:

#--------------------------------------------------------------
#-- uart_wait_tx_ready()
#--
#-- Esperar hasta que el bit READY del transmisor
#-- se ponga a 1
#--
#-- El registro gp debe contener la direccion base de la uart
#--------------------------------------------------------------
uart_wait_tx_ready:

wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(gp)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx

	#-- Trasmisor listo!
	ret

La dirección base de la uart es algo que se debería pasar como parámetro, pero para simplificar, supondremos que se encuentra en el registro gp

Para enviar el primer carácter, llamamos primero a uart_wait_tx_ready(), luego se escribe A en el registro de datos, despúes se vuelve a llamar a uart_wait_tx_ready() y se escribe nuevamente en el registro de datos

Para garantizar que el segundo carácter llega correctamente a la pantalla, realizamos una tercera llamada a uart_wait_tx_ready(). Esto nos garantiza que se ha terminado de enviar y que por tanto podemos salir del programa con seguridad (y ya no hace falta utilizar las instrucciones nop)

Este es el programa completo

#-------------------------------------------------------
#--  Pruebas del transmisor de la UART del RARs
#--  Envío de dos caracteres seguidos
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Registro de DATOS del transmisor
	.eqv TX_DATA 0x0C
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	PRINT_STRINGI("Esperando al transmisor...")
	
	#----- Envio del primer caracter
	#-- Esperar a que el transmisor este listo
	jal uart_wait_tx_ready
	
	#-- para enviar un carácter
	li t0, 'A'
	sw t0, TX_DATA(gp)
	
	#---- Envío del segundo caracter
	jal uart_wait_tx_ready
	li t0, 'B'
	sw t0, TX_DATA(gp)
	
	#--- Esperar hasta que se envie el segundo caracter
	jal uart_wait_tx_ready
	
	#-- Terminar
	EXIT
	
	
#--------------------------------------------------------------
#-- uart_wait_tx_ready()
#--
#-- Esperar hasta que el bit READY del transmisor
#-- se ponga a 1
#--
#-- El registro gp debe contener la direccion base de la uart
#--------------------------------------------------------------
uart_wait_tx_ready:

wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(gp)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx

	#-- Trasmisor listo!
	ret

Este es el resultado tras la ejecución:

Ejemplo 6: putchar(): envío de caracteres individuales

Ahora que ya sabemos cómo enviar caracteres, es el momento de encapsular este código en su propia función: putchar(). Recibe como parámetro el carácter a enviar, y finaliza cuando se ha escrito en el registro de datos del transmisor. Esta función no sólo nos permite reutilizar código y hacerlo más fácil de usar, sino que define un nuevo nivel de abstracción

La función putchar() contiene el conocimiento, a bajo nivel, para imprimir un carácter en la pantalla. Las funciones que se escriban por encima, y que llamen a putchar(), NO CONOCEN los detalles de cómo se imprime el carácter en la pantalla. Da igual si hay una UART, o si se usa un sistema operativo

Por ello, podemos tener diferentes funciones putchar() asociados a cada sistema de entrada. Habrá una putchar() para realizar E/S con la UART. Pero podemos crear otra putchar() que usa el servicio PRINT_CHAR del sistema operativo de RARS. En cualquier caso, esto es algo que es INDIFERENTE para las funciones de nivel superior. Decimos que hemos hecho ABSTRACCION de los detalles

Esta es la implementación de putchar() a bajo nivel, usando la UART. Seguimos suponiendo que la dirección base de la UART está situada en el registro gp

#---------------------------------------------------------------
#-- putchar(c): Imprimir un caracter
#--
#-- ENTRADA:
#--   - a0 (c): Carácter a imprimir
#--
#-- El registro gp debe contener la direccion base de la uart
#---------------------------------------------------------------
putchar:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar a que el transmisor esté listo
	jal uart_wait_tx_ready
	
	#-- Escribir el caracter en el registro de datos
	sw a0, TX_DATA(gp)
	
	#-- Recuperar la dirección de retorno y liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

En el siguiente programa de ejemplo se imprime la palabra HOLA llamando 4 veces a putchar(). Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del transmisor de la UART del RARs
#--  Envío de caracteres individuales mediante la
#--  funcion putchar()
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Registro de DATOS del transmisor
	.eqv TX_DATA 0x0C
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	PRINT_STRINGI("Esperando al transmisor...")
	
	#----- Enviar caracteres individuales
	li a0, 'H'
	jal putchar
	
	li a0, 'O'
	jal putchar
	
	li a0, 'L'
	jal putchar
	
	li a0, 'A'
	jal putchar
	
	#--- Esperar hasta que se envie el ultimo caracter
	jal uart_wait_tx_ready
	
	#-- Terminar
	EXIT
	
	
#--------------------------------------------------------------
#-- uart_wait_tx_ready()
#--
#-- Esperar hasta que el bit READY del transmisor
#-- se ponga a 1
#--
#-- El registro gp debe contener la direccion base de la uart
#--------------------------------------------------------------
uart_wait_tx_ready:

wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(gp)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx

	#-- Trasmisor listo!
	ret
	
#---------------------------------------------------------------
#-- putchar(c): Imprimir un caracter
#--
#-- ENTRADA:
#--   - a0 (c): Carácter a imprimir
#--
#-- El registro gp debe contener la direccion base de la uart
#---------------------------------------------------------------
putchar:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar a que el transmisor esté listo
	jal uart_wait_tx_ready
	
	#-- Escribir el caracter en el registro de datos
	sw a0, TX_DATA(gp)
	
	#-- Recuperar la dirección de retorno y liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

Este es el resultado al ejecutarlo:

En esta animación se muestra la mecánica que hay que realizar para volver a ejecutarlo. Hay que seguir estos pasos:

  1. Apretar RESET en el simulador del teclado y pantalla, para que se borre la pantalla
  2. Hacer reset del RARS
  3. Pulsar el botón de Play para ejecutar de nuevo el programa
  4. Apretar RESET de nuevo en el simulador de teclado y pantalla para inicializar la UART

El receptor

El receptor es el componente que nos permite recibir información de la consola. Es decir, leer los caracteres que llegan desde el teclado. Para leer una tecla lo primero es comprobar cuándo se ha pulsado. Esto se hace comprobando el bit RXRDY (receptor listo) del registro de control del receptor, que se encuentra en la dirección 0xFFFF0000. Una vez que este bit se pone a 1, ya podemos leer el carácter pulsado en el teclado en el registro de datos del receptor, situado en la dirección 0xFFFF0004

Igual que con el transmisor, vamos a aprender a usar el receptor mediante ejemplos graduales, en los que se van incorporando conceptos poco a poco

Ejemplo 7: Mostrar el bit RXRDY

Comenzamos monitorizando el registro de control del receptor. Lo primero es definir las direcciones de interés mediante constantes. El offset para el registro de control del receptor es 0. Uamos la constante RX_CTRL

  #--- Direccion base de la UART
  .eqv UART_BASE 0xFFFF0000
	
  #---------- Offset de los registros
  #----- Registro de control del receptor
  .eqv RX_CTRL 0x00

Usamos el registro gp para almacenar la dirección base de la UART. Para leer el registro de control usamos esta instrucción: lw t0, RX_CTRL(g0). Mediante un bucle infinito leemos este registro cada segundo y lo imprimimos en la consola. Usamos las macros definidas en la sesión anterior. Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del receptor de la UART del RARs
#--  Se muestra el registro de control del receptor 
#--
#--  La información se muestra en la consola original
#--  del RARs, cada segundo. El programa no termina nunca
#--  Es necesario detenerlo manualmente
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	#-- Bucle infinito
bucle:
	#-- Leer el registro de control del receptor
	lw t0, RX_CTRL(gp)
	
	#-- Imprimir su valor en la consola, 
	#-- a través del sistema operativo
	PRINT_INTR(t0)
	PRINT_CHARI('\n')
	
	#-- Esperar un segundo
	SLEEPI(1000)
	
	#-- Repetir
	j bucle

Ensamblamos el ejemplo y lo ejecutamos. Observamos que en la consola el registro de control vale 0. Esto nos indica que NO SE HA RECIBIDO ningún carácter

Ahora apretamos una tecla, por ejeplo la a. Al hacelo la vemos en el panel inferior de la ventana del simulador de teclado y pantalla. También comprobamos que el registro de control tiene ahora el valor 1 para indicarnos que ya hay un carácter recibido, listo para leerse

Ejemplo 8: Esperar a que llegue un carácter

Para recibir un carácter primero tenemos que esperar a que se active el bit de READY del receptor. Se encuentra en el bit 0 del registro de control del receptor. Para leer este carácter definimos la máscara RX_READY:

  #-- Máscaras de acceso a los bits
  #-- Bit Ready del receptor
 .eqv RX_READY 0x01   

En este ejemplo se implementa un bucle que está constantemente leyendo el bit RX_READY. Mientras su valor sea 0, se sigue esperando. Pero en cuanto se pone a 1 se termina el bucle. Es decir, usamos la tecnica de la espera activa. La CPU no hace nada hasta que no se reciba el carácter. Esto también recibe el nombre de espera bloqueante

Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del receptor de la UART del RARs
#--  Esperar a que se reciba un caracter por la UART
#--  En cuanto se recibe se termina el programa
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  
	
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	PRINT_STRINGI("Esperando caracter...")
	
	#-- Esperar a que el receptor este listo
	#-- Es decir, que reciba una tecla
wait_rx:
	#-- Leer el registro de control del transmisor
	lw t0, RX_CTRL(gp)
	
	#-- Aislar el bit RX_READY
	andi t0, t0, RX_READY
	
	#-- Si el bit es 0, no se ha recibido ningun caracter
	#-- (el receptor NO está listo)
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_rx
	
	#-- Caracter recibido!!! 
	PRINT_STRINGI("\n")
	PRINT_STRINGI("Caracter RECIBIDO!\n")

	#-- Terminar	
	EXIT

Ejecutamos el programa. Se imprime el mensaje "Esperando caracter..." y se queda esperando

Pinchamos en el panel inferior del simulador de teclado y pantalla para seleccionarlo y apretamos, por ejemplo, la tecla a. En ese momento se activa el bit RXREAY y la espera acaba. Se imprime el mensaje "Carácter RECIBIDO!"

En esta animación se muestra el proceso completo:

Ejemplo 9: Recepción de un carácter

Para leer un carácter primero esperamos a que se active el bit RX_READY, y luego leemos el carácter del registro de datos. Para acceder a este registro definimos la constante RX_DATA. Así es como quedan las definiciones del receptor:

#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#----- Registro de datos del receptor
	.eqv RX_DATA 0x04
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  

En este ejemplo se imprime en la consola normal el carácter recibido, y se termina. La lectura del carácter se realiza con la instrucción lw t0, RX_DATA(gp)

Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del receptor de la UART del RARs
#--  Esperar a que se reciba un caracter por la UART
#--  El carácter se lee y se muestra en la consola normal
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#----- Registro de datos del receptor
	.eqv RX_DATA 0x04
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  
	
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	PRINT_STRINGI("Esperando caracter...")
	
	#-- Esperar a que el receptor este listo
	#-- Es decir, que reciba una tecla
wait_rx:
	#-- Leer el registro de control del transmisor
	lw t0, RX_CTRL(gp)
	
	#-- Aislar el bit RX_READY
	andi t0, t0, RX_READY
	
	#-- Si el bit es 0, no se ha recibido ningun caracter
	#-- (el receptor NO está listo)
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_rx
	
	#-- Caracter recibido!!! 
	PRINT_STRINGI("\n")
	PRINT_STRINGI("Caracter RECIBIDO: ")

	#-- Leer el caracter del registro de datos
	lw t0, RX_DATA(gp)
	PRINT_CHARR(t0)
	PRINT_CHARI('\n')

	#-- Terminar	
	EXIT

Al ejecutar el ejemplo, el programa se queda esperando a recibir el carácter, igual que en el ejemplo anterior. Apretamos, por ejemplo, el carácter a. Se imprime el mensaje Caracter RECIBIDO: a y se termina

En esta animación se muestra el proceso:

Ejemplo 10: Función keypressed(): Tecla pulsada

Para comprobar si se ha pulsado una tecla hay que leer el bit RX_READY. Este comportamiento lo encapsulamos en la función Keypressed(), que devuelve 1 si se ha pulsado una tecla y 0 en caso contrario

#-------------------------------------------------------
#-- keypressed()
#--   Detectar si hay una tecla pulsada
#--
#-- SALIDA:
#--   0: Tecla no pulsada
#--   1: Tecla pulsada
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#------------------------------------------------------
keypressed:

	#-- Leer el registro de control del transmisor
	lw a0, RX_CTRL(gp)
	
	#-- Quedarse solo con el bit 0
	andi a0, a0, RX_READY
	
	#-- y devolverlo
	ret

En este programa de ejemplo se imprimen los números enteros, uno cada segundo, hasta que se aprieta una tecla. En cada pasada del bucle se llama a keypressed() para comprobar si hay tecla pulsada o no

#-------------------------------------------------------
#--  Pruebas del receptor de la UART del RARs
#--  Probando la funcion keypressed()
#--  Se imprimen los numeros enteros, cada segundo
#--  Al apretar una tecla se termina
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#----- Registro de datos del receptor
	.eqv RX_DATA 0x04
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  
	
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	#-- Inicialiar contador
	li s0, 0
	
bucle:
	#-- Imprimir valor actual del contador
	PRINT_INTR(s0)
	PRINT_CHARI('\n')
	
	#-- Esperar un segundo
	SLEEPI(1000)
	
	#-- INcrementar contador
	addi s0, s0, 1
	
	#-- Si no se ha pulsado una teclar, repetir
	jal keypressed
	beq a0, zero, bucle

	#-- Terminar	
	EXIT
	

#-------------------------------------------------------
#-- keypressed()
#--   Detectar si hay una tecla pulsada
#--
#-- SALIDA:
#--   0: Tecla no pulsada
#--   1: Tecla pulsada
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#------------------------------------------------------
keypressed:

	#-- Leer el registro de control del transmisor
	lw a0, RX_CTRL(gp)
	
	#-- Quedarse solo con el bit 0
	andi a0, a0, RX_READY
	
	#-- y devolverlo
	ret

Este es el resultado. Tras unos segundos se aprieta la tecla a y termina

En esta animación se muestra el funcionamiento

Ejemplo 11: Función uart_wait_rx(): Esperando una tecla

En este ejemplo se encapsula la función uart_wait_rx(), que se queda esperando a que se reciba una tecla. Esta es la función:

#-------------------------------------------------------
#-- uart_wait_rx()
#--
#--  Esperar hasta que se reciba un caracter
#--
#----------------------------------------------------------
uart_wait_rx:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar hasta que se reciba una tecla
wait_rx:
	jal keypressed
	beq a0, zero, wait_rx
	
	#-- Tecla recibida!
	#-- retornar
	
	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

El ejemplo es el mismo que el 9: se espera a que se reciba un carácter y se imprime en la consola normal, pero ahora se usa la función uart_wait_tx()

Este es el ejemplo completo:

#-------------------------------------------------------
#--  Pruebas del receptor de la UART del RARs
#--  Probando la funcion uart_wait_rx()
#--  Se espera a que se pulse una tecla, se imprime su
#--  valor y se termina
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#----- Registro de datos del receptor
	.eqv RX_DATA 0x04
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  
	
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	#-- Inicialiar contador
	li s0, 0
	
	#-- Esperar a que se pulse una tecla
	PRINT_STRINGI("Esperando tecla...")
	jal uart_wait_rx
	
	#-- Caracter recibido!!! 
	PRINT_STRINGI("\n")
	PRINT_STRINGI("Caracter RECIBIDO: ")

	#-- Leer el caracter del registro de datos
	lw t0, RX_DATA(gp)
	PRINT_CHARR(t0)
	PRINT_CHARI('\n')

	#-- Terminar	
	EXIT
	

#-------------------------------------------------------
#-- uart_wait_rx()
#--
#--  Esperar hasta que se reciba un caracter
#--
#----------------------------------------------------------
uart_wait_rx:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar hasta que se reciba una tecla
wait_rx:
	jal keypressed
	beq a0, zero, wait_rx
	
	#-- Tecla recibida!
	#-- retornar
	
	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

#-------------------------------------------------------
#-- keypressed()
#--   Detectar si hay una tecla pulsada
#--
#-- SALIDA:
#--   0: Tecla no pulsada
#--   1: Tecla pulsada
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#------------------------------------------------------
keypressed:

	#-- Leer el registro de control del transmisor
	lw a0, RX_CTRL(gp)
	
	#-- Quedarse solo con el bit 0
	andi a0, a0, RX_READY
	
	#-- y devolverlo
	ret

El resultado es el mismo que el ejemplo 9

Ejemplo 12: La función getchar(): Leyendo una tecla

Vamos a encapsular la lectura bloqueante de una tecla en la función getchar(). Al llamar a esta función la CPU se queda esperando a recibir un carácter. Cuando llega se devuelve y el programa continua. Esta es la función:

#--------------------------------------------------------
#-- getchar()
#--
#-- Esperar a que se pulse una tecla
#-- Esta funcion es BLOQUEANTE
#--
#-- SALIDA:
#--   a0 (car): Caracter introducido por el usuario
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#-------------------------------------------------------
getchar:
	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)

	#-- Esperar a que se pulse una tecla
	jal uart_wait_rx
	
	#-- Leer el caracter recibido. Se lee del registro
	#-- de datos del receptor
	lw a0, RX_DATA(gp)

	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

El programa de ejemplo espera a que se aprieten 2 teclas, que se muestran en la consola normal. Este es el programa completo:

#-------------------------------------------------------
#--  Pruebas del receptor de la UART del RARs
#--  Probando la funcion getchar()
#--  Se Leen 2 caracteres introducidos por el usuario
#--   y se termina
#-------------------------------------------------------

	#-- Servicios del sistema operativo
	.include "so.h"
	
	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#----- Registro de datos del receptor
	.eqv RX_DATA 0x04
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  
	
	
	.text
	
	#-- Direccion de acceso a la uart
	li gp, UART_BASE
	
	#-- Esperar a que se pulse una tecla
	PRINT_STRINGI("Pulsa una tecla: ")
	jal getchar
	
	#-- Imprimir la tecla
	PRINT_CHARR(a0)
	PRINT_CHARI('\n')
	
	#-- Esperar a que se pulse una segunda tecla
	PRINT_STRINGI("Pulsa otra tecla: ")
	jal getchar
	
	#-- Imprimir la tecla
	PRINT_CHARR(a0)
	PRINT_CHARI('\n')

	#-- Terminar	
	EXIT
	
#--------------------------------------------------------
#-- getchar()
#--
#-- Esperar a que se pulse una tecla
#-- Esta funcion es BLOQUEANTE
#--
#-- SALIDA:
#--   a0 (car): Caracter introducido por el usuario
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#-------------------------------------------------------
getchar:
	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)

	#-- Esperar a que se pulse una tecla
	jal uart_wait_rx
	
	#-- Leer el caracter recibido. Se lee del registro
	#-- de datos del receptor
	lw a0, RX_DATA(gp)

	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

#-------------------------------------------------------
#-- uart_wait_rx()
#--
#--  Esperar hasta que se reciba un caracter
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#----------------------------------------------------------
uart_wait_rx:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar hasta que se reciba una tecla
wait_rx:
	jal keypressed
	beq a0, zero, wait_rx
	
	#-- Tecla recibida!
	#-- retornar
	
	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

#-------------------------------------------------------
#-- keypressed()
#--   Detectar si hay una tecla pulsada
#--
#-- SALIDA:
#--   0: Tecla no pulsada
#--   1: Tecla pulsada
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#------------------------------------------------------
keypressed:

	#-- Leer el registro de control del transmisor
	lw a0, RX_CTRL(gp)
	
	#-- Quedarse solo con el bit 0
	andi a0, a0, RX_READY
	
	#-- y devolverlo
	ret

Este es el resultado cuando el usuario aprieta las teclas a y luego b:

En esta animación se muestra el funcionamiento:

Biblioteca de entrada/salida de bajo nivel: io_rars_uart.s

Vamos a encapsular las funciones putchar() y getchar(), así como todas sus dependencias, en un nuevo módulo llamado io_rars_uart.s. Este módulo depende del computador donde se ejecute. En este caso es válido para la uart del simulador RARS, pero NO SERIA VÁLIDO, por ejemplo, para una placa como la Raspberry pi Pico 2. Decimos que es una biblioteca de bajo nivel, y que por tanto es dependiente de los detalles del computador

Funciones de interfaz

Estas son las funciones públicas accesibles desde fuera del módulo:

  • io_init(): Inicialización del módulo. Hay que llamarla siempre antes de utilizar cualquiera de las otras funciones de interfaz. Su misión es configurar el módulo. En nuetro caso lo único que se hace en esta función es inicializar el registro gp con la dirección base de la UART
#-------------------------------------------------------------
#-- io_init()
#--
#--  Configurar el modulo de Entrada/salida de bajo nivel
#--  Se debe llamar antes de ejecutar cualquier otra funcion
#--  de este modulo
#-------------------------------------------------------------
.global io_init
io_init:

	#-- Direccion de acceso a la uart
	li gp, UART_BASE

	ret
  • io_close(): Terminar. Antes de acabar el programa hay que llamar a esta función. Lo que hace es liberar los recursos que pudiese estar usando el módulo. En nuestro caso lo que se hace es esperar a que se termine de enviar el caracter que haya en vuelo, si lo hubiese, para evitar que se pierda y no se muestre en la pantalla
#-------------------------------------------------------------
#-- io_close()
#--
#-- Terminar de usar el modulo de entrada/salida. Finalizar
#-- las operaciones pendientes
#-------------------------------------------------------------
.global io_close
io_close:
	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)

	#-- Esperar a que se termine de enviar
	#-- el caracter en vuelo (si lo hay)
	jal uart_wait_tx_ready

	#-- Recuperar la dirección de retorno y liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret
  • putchar(car): Enviar un carácter y mostrarlo en la pantalla
  • getchar(cad). Esperar q que se reciba un carácter desde el teclado
  • keypressed(): Comprobar si hay un carácter pendiente de ser leido

Implementación del módulo

Este es el contenido completo del fichero io_rars_uart.s:

#----------------------------------------------------------------
#-- Modulo io_rars_uart 
#--   Entrada/salida a traves de la UART del RARs
#--
#--  Antes de utilizar el modulo hay que llamar a la funcion 
#--    io_ini()
#--
#--  Antes de acabar el program ahay que llamar a io_close()
#----------------------------------------------------------------

	#--- Direccion base de la UART
	.eqv UART_BASE 0xFFFF0000
	
	#---------- Offset de los registros
	#----- Registro de control del receptor
	.eqv RX_CTRL 0x00
	
	#----- Registro de datos del receptor
	.eqv RX_DATA 0x04
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del receptor
	.eqv RX_READY 0x01  
	
	#----- Registro de control del transmisor
	.eqv TX_CTRL 0x08
	
	#-- Registro de DATOS del transmisor
	.eqv TX_DATA 0x0C
	
	#-- Máscaras de acceso a los bits
	#-- Bit Ready del transmisor
	.eqv TX_READY 0x01  


	.text
	
#-------------------------------------------------------------
#-- io_init()
#--
#--  Configurar el modulo de Entrada/salida de bajo nivel
#--  Se debe llamar antes de ejecutar cualquier otra funcion
#--  de este modulo
#-------------------------------------------------------------
.global io_init
io_init:

	#-- Direccion de acceso a la uart
	li gp, UART_BASE

	ret	
	
#-------------------------------------------------------------
#-- io_close()
#--
#-- Terminar de usar el modulo de entrada/salida. Finalizar
#-- las operaciones pendientes
#-------------------------------------------------------------
.global io_close
io_close:
	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)

	#-- Esperar a que se termine de enviar
	#-- el caracter en vuelo (si lo hay)
	jal uart_wait_tx_ready

	#-- Recuperar la dirección de retorno y liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret
	
	
#--------------------------------------------------------------
#-- uart_wait_tx_ready()
#--
#-- Esperar hasta que el bit READY del transmisor
#-- se ponga a 1
#--
#-- El registro gp debe contener la direccion base de la uart
#--------------------------------------------------------------
uart_wait_tx_ready:

wait_tx:
	#-- Leer el registro de control del transmisor
	lw t0, TX_CTRL(gp)
	
	#-- Aislar el bit TX_READY
	andi t0, t0, TX_READY
	
	#-- Si el bit es 0, el transmisor no esta listo
	#-- repetimos la accion hasta que se ponga a 1
	beq t0, zero, wait_tx

	#-- Trasmisor listo!
	ret
	
#---------------------------------------------------------------
#-- putchar(c): Imprimir un caracter
#--
#-- ENTRADA:
#--   - a0 (c): Carácter a imprimir
#--
#-- El registro gp debe contener la direccion base de la uart
#---------------------------------------------------------------
.global putchar
putchar:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar a que el transmisor esté listo
	jal uart_wait_tx_ready
	
	#-- Escribir el caracter en el registro de datos
	sw a0, TX_DATA(gp)
	
	#-- Recuperar la dirección de retorno y liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

#--------------------------------------------------------
#-- getchar()
#--
#-- Esperar a que se pulse una tecla
#-- Esta funcion es BLOQUEANTE
#--
#-- SALIDA:
#--   a0 (car): Caracter introducido por el usuario
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#-------------------------------------------------------
.global getchar
getchar:
	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)

	#-- Esperar a que se pulse una tecla
	jal uart_wait_rx
	
	#-- Leer el caracter recibido. Se lee del registro
	#-- de datos del receptor
	lw a0, RX_DATA(gp)

	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

#-------------------------------------------------------
#-- uart_wait_rx()
#--
#--  Esperar hasta que se reciba un caracter
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#----------------------------------------------------------
uart_wait_rx:

	#-- Crear la pila y guardar la direccion de retorno
	addi sp, sp, -16
	sw ra, 12(sp)
	
	#-- Esperar hasta que se reciba una tecla
wait_rx:
	jal keypressed
	beq a0, zero, wait_rx
	
	#-- Tecla recibida!
	#-- retornar
	
	#-- Recuperar la direccion de retorno y
	#-- liberar la pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

#-------------------------------------------------------
#-- keypressed()
#--   Detectar si hay una tecla pulsada
#--
#-- SALIDA:
#--   0: Tecla no pulsada
#--   1: Tecla pulsada
#--
#-- Se supone que el registro gp contiene la direccion
#--   base de la UART
#------------------------------------------------------
.global keypressed
keypressed:

	#-- Leer el registro de control del transmisor
	lw a0, RX_CTRL(gp)
	
	#-- Quedarse solo con el bit 0
	andi a0, a0, RX_READY
	
	#-- y devolverlo
	ret

Ejemplo 13: Programa de Eco

Cuando se trabaja con una UART, en cualquier computador, una de las primeras pruebas es lo que se denomina un programa de Eco. Todos los caracteres recibidos se transmiten de vuelta. El resultado es que todo lo que se escribe en el teclado, se muetra en la pantalla

Este ejemplo está haciendo eco todo el rato de los caracteres, hasta que se pulsa 0, momento en el que se termina programa

#-------------------------------------------------------
#--  Prueba de ECO para la UART del Rars
#--  Todo lo que se recibe (teclado) se envía hacia
#--  la pantalla
#-------------------------------------------------------
	
	#-- Servicios del sistema operativo
	.include "so.h"
	
	.text
	 
main:
	
	#-- Configurar el modulo de entrada/salida
	jal io_init

loop:	
			
	#-- Esperar a que se reciba un caracter (tecla pulsada)
	jal getchar
	
	#-- Si el carácter pulsado es '0', se termina
	li t0, '0'
	beq a0, t0, fin
	
	#-- Enviar el caracter recibido (Hacer eco en la pantalla)
	jal putchar
	
	#-- Repetir 
	j loop
	
fin:
    #-- Terminar con la UART
	jal io_close
	EXIT

En este pantallazo se muestra el resultado. Esto es lo que ocurre cuando se introduce una frase por el teclado:

El panel inferior siempre vemos lo que se está tecleando. Es lo que se denomina eco local. En el panel superior sólo vemos lo que envía el programa a la pantalla. Por ejemplo, sabemos que se ha apretado la tecla '0' porque la vemos en la parte inferior, pero NO está en la pantalla superior

En esta animación se muestra el funcionamiento

Ejemplo 14: Opciones

En este ejemplo se espera a que el usuario pulse las teclas 1, 2 o q. Según la que se apriete se realiza una acción un otra. Con la tecla 1 se imprimen los dígitos del 0 al 9. Con la tecla 2 las letras de la A a la F y con la q se termina

#-------------------------------------------------------
#--  Prueba de la biblioteca io_rars_uart.s
#--  Realizar dos acciones diferentes según las teclas
#--  pulsadas:
#--    '1': Mostrar los digitos del 0 al 9
#--    '2': Mostrar las letras de la A a la F
#--    'q': Terminar el programa
#-------------------------------------------------------
	
	#-- Servicios del sistema operativo
	.include "so.h"
	
	.text
	 
main:
	
	#-- Configurar el modulo de entrada/salida
	jal io_init

menu:	
	#-- Imprimir el prompt
    li a0, '?'
    jal putchar

	#-- Esperar a que se pulse una tecla
	jal getchar
	
	#-- Imprimir la tecla pulsada
	mv s0, a0
	jal putchar
	
	#-- Imprimir un salto de linea
	li a0, '\n'
	jal putchar
	
	
	#-- Según el valor de la tecla se hace una cosa u otra
	li t0, '1'
	beq s0, t0, opcion_1
	li t0, '2'
	beq s0, t0, opcion_2
	li t0, 'q'
	beq s0, t0, fin
	
	#-- Opcion incorrecta. volver al menu
	j menu
	 

	#-- Mostrar los digitos del '0' al '9' 	  
opcion_1: 
    li s0, '0'
   
next_1:
    #-- Imprimir digito actual
    mv a0, s0
    jal putchar 
    
    #-- Obtener el siguiente digito
	addi s0, s0, 1
	
	#-- Repetir si es <= '9'
	li t0, '9'
	ble s0, t0, next_1
	
	#-- Imprimir salto de linea
	li a0, '\n'
	jal putchar
	
	#-- Volver al menu
	j menu

	#-- Mostrar las letras de la 'A' a la 'F'
opcion_2:
	li s0, 'A'
	
next_2:
    #-- Imprimir la letra
    mv a0, s0
    jal putchar
    
    #-- Obtener la siguiente letra
    addi s0, s0, 1
    
    #-- Repetir si es <= 'F'
    li t0, 'F'
	ble s0, t0, next_2
	
	#-- Imprimir salto de linea
	li a0, '\n'
	jal putchar
	
	#-- Volver al menu
	j menu

fin:
    #-- Terminar con la UART
	jal io_close
	EXIT

Este es el resultado. Primero se aprieta 1, luego 2 y por último q para terminar. Este es el resultado:

En esta animación se muestra el funcionamiento

Biblioteca de alto nivel: stdio.s

Una vez resuelta la comunicación a bajo nivel, que contiene básicamente la implementación de las funciones para enviar un carácter a la pantalla, y para leer un carácter del teclado (putchar() y getchar()), ahora podemos definir funciones de más alto nivel. Cuando decimos que son de más alto nivel nos referimos a que se hace abstracción de los mecanismos necesarios para realizar la entrada/salida. Es decir, que estas funciones no tienen que saber nada sobre cómo está implementada esta comunicación

Estas funciones de alto nivel utilizan en última instancia las funciones putchar() y getchar(). La gran ventaja de estas funciones de alto nivel es que son portables. Como no dependen de los mecanismos de entrada/salida, las podemos emplear en cualquier computador. Basta con tener disponibles en este computador las funciones putchar() y getchar()

Así, por ejemplo, las podemos utilizar en el RARs pero también en una Raspberry pi pico2, siempre y cuando dispongamos de las funciones getchar() y putchar() en esta plataforma

En esta sección vamos a crear dos funciones de alto nivel: puts(), para imprimir una cadena y gets() para leer una cadena (cruda) del teclado. Estas funciones las vamos a englobar en la biblioteca stdio.s, que ampliaremos en las lecciones siguientes

Función puts(): Impresión de una cadena

La función puts() toma como argumento la dirección de una cadena situada en memoria y la imprime en la consola, llamando a putchar(). La función recorre la cadena a imprimir hasta que llega al final (detectando el caraćter '\0'). En cada pasada del bucle se imprime el carácter correspondiente con putchar()

#--------------------------------------------------
#-- puts(cad): Imprimir una cadena en la consola
#--
#-- ENTRADA:
#--   - a0 (cad): Puntero a la cadena a imprimir
#-------------------------------------------------
puts:
    
    #-- Crear la pila y guardar la direccion de retorno
    addi sp, sp, -16
    sw ra, 12(sp)
    
    #-- Guardar s0
    sw s0, 0(sp)
    
    #-- s0: Puntero a la cadena
    mv s0, a0
    
  puts_next:
    #-- Leer caracter de la memoria
    lb a0, 0(s0)
    
    #-- Si el caracter es '\0', terminar
    beq a0, zero, puts_end
    
    #-- Imprimir caracter actual
    jal putchar
    
    #-- Apuntar al siguiente caracter
    addi s0, s0, 1
    
    #-- Repetir
    j puts_next


  puts_end:
	#-- Recuperar s0
	lw s0, 0(sp)
	
	#-- Recuperar direccion de retorno y liberar pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret

Función gets(): Leer una cadena cruda del teclado

La función gets() lee una cadena desde el teclado (introducida por el usuario) y la guarda en el buffer de memoria pasado como parámetro. También se le pasa el tamaño máximo de este buffer, para no desbordarlo

La función se implementa mediante un bucle en el que se lee un carácter y se almacena en su posición correspondiente. La tecla ENTER se guarda como '\n' (Igual que el servicio READ_STRING del sistema operativo del RARs)

#-----------------------------------------------------------------
#-- gets(cad): Leer una cadena (cruda) desde el teclado	
#--
#-- ENTRADA:
#--   - a0 (cad): Buffer donde guardar la cadena
#--   - a1 (max): Numero maximo de caracteres para la cadena
#--               (incluyendo el '\n' y '\0' finales)
#--
#-- SALIDA:
#--   - Ninguna
#----------------------------------------------------------------
.global gets
gets:
    #-- Crear la pila y guardar la direccion de retorno
    addi sp, sp, -16
    sw ra, 12(sp)
    
    #-- Salvar registros estaticos usados
    sw s0, 0(sp)
    sw s1, 4(sp)
    
    #-- Guardar el puntero en so
    mv s0, a0
    
    #-- Caso particular
    #-- El tamaño del buffer tiene que ser al menos 1, para
    #-- guardar el \0 final
    li t0, 2
    blt a1, t0, gets_end
    
    #-- Guardar el numero de caracteres (menos uno) en s1
    #-- (Hay que dejar 1 byte para el \0 final)
    addi s1, a1, -1

gets_next:
	#-- Leer un caracter
	jal getchar
	
	#-- Almacenar el carácter en la posicion actual
	sb a0, 0(s0)
	
	#-- Apuntar a la siguiente posicion
	addi s0, s0, 1
	
	#-- Si el caracter es '\n', hemos terminado
	li t0, '\n'
	beq a0, t0, gets_end
	
	#-- Decrementar contador de caracters
	addi s1, s1, -1
	
	#-- Ya no caben más caracteres: terminar
	beq s1, zero, gets_end
	
	#-- Repetir
	j gets_next
	
	
gets_end:

	#-- Almacenar el '\0' final
	sb zero, 0(s0)
	
	#-- Recuperar registros estaticos
	lw s0, 0(sp)
	lw s1, 4(sp)

	#-- Recuperar direccion de retorno y liberar pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret		

Implementación de stdio.s

Este es el contenido del fichero stdio.s, con las dos funciones de alto nivel gets() y puts(). Se pone aquí para usarlo directamente haciendo copy & paste

#--------------------------------------------------
#-- puts(cad): Imprimir una cadena en la consola
#--
#-- ENTRADA:
#--   - a0 (cad): Puntero a la cadena a imprimir
#-------------------------------------------------
.global puts
puts:
    
    #-- Crear la pila y guardar la direccion de retorno
    addi sp, sp, -16
    sw ra, 12(sp)
    
    #-- Guardar s0
    sw s0, 0(sp)
    
    #-- s0: Puntero a la cadena
    mv s0, a0
    
  puts_next:
    #-- Leer caracter de la memoria
    lb a0, 0(s0)
    
    #-- Si el caracter es '\0', terminar
    beq a0, zero, puts_end
    
    #-- Imprimir caracter actual
    jal putchar
    
    #-- Apuntar al siguiente caracter
    addi s0, s0, 1
    
    #-- Repetir
    j puts_next


  puts_end:
	#-- Recuperar s0
	lw s0, 0(sp)
	
	#-- Recuperar direccion de retorno y liberar pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret
	
	
#-----------------------------------------------------------------
#-- gets(cad): Leer una cadena (cruda) desde el teclado	
#--
#-- ENTRADA:
#--   - a0 (cad): Buffer donde guardar la cadena
#--   - a1 (max): Numero maximo de caracteres para la cadena
#--               (incluyendo el '\n' y '\0' finales)
#--
#-- SALIDA:
#--   - Ninguna
#----------------------------------------------------------------
.global gets
gets:
    #-- Crear la pila y guardar la direccion de retorno
    addi sp, sp, -16
    sw ra, 12(sp)
    
    #-- Salvar registros estaticos usados
    sw s0, 0(sp)
    sw s1, 4(sp)
    
    #-- Guardar el puntero en so
    mv s0, a0
    
    #-- Caso particular
    #-- El tamaño del buffer tiene que ser al menos 1, para
    #-- guardar el \0 final
    li t0, 2
    blt a1, t0, gets_end
    
    #-- Guardar el numero de caracteres (menos uno) en s1
    #-- (Hay que dejar 1 byte para el \0 final)
    addi s1, a1, -1

gets_next:
	#-- Leer un caracter
	jal getchar
	
	#-- Almacenar el carácter en la posicion actual
	sb a0, 0(s0)
	
	#-- Apuntar a la siguiente posicion
	addi s0, s0, 1
	
	#-- Si el caracter es '\n', hemos terminado
	li t0, '\n'
	beq a0, t0, gets_end
	
	#-- Decrementar contador de caracters
	addi s1, s1, -1
	
	#-- Ya no caben más caracteres: terminar
	beq s1, zero, gets_end
	
	#-- Repetir
	j gets_next
	
	
gets_end:

	#-- Almacenar el '\0' final
	sb zero, 0(s0)
	
	#-- Recuperar registros estaticos
	lw s0, 0(sp)
	lw s1, 4(sp)

	#-- Recuperar direccion de retorno y liberar pila
	lw ra, 12(sp)
	addi sp, sp, 16
	ret		

Ejemplo 15: Hola Mundo!

Comenzamos a hacer programa de ejemplo para usar las funciones de stdio.s. Este es el ejemplo hola mundo, donde se imprime la cadena Hola Mundo!\n en la consola

#-------------------------------------------------------
#--  Prueba de la funcion puts()
#--  Ejemplo "hola mundo"
#-------------------------------------------------------
	
	#-- Servicios del sistema operativo
	.include "so.h"
	
	
	.data
msg1:  .string "Hola Mundo!\n"
	
	.text
	 
main:
	
	#-- Configurar el modulo E/S de bajo nivel
	jal io_init

	#-- Imprmir una cadena
	la a0, msg1
	jal puts

    #-- Cerrar la salida de bajo nivel
	jal io_close
	EXIT

Este es el resultado:

Ejemplo 16: Login

Ejemplo para probar la función gets(). Se solicita al usuario su nombre para hacer un Login al sistema. Luego se imprime un mensaje con su nombre, para darle la bienvenida

#-------------------------------------------------------
#--  Prueba de la funcion gets()
#--  Pedir el Login al usuario, y saludarle
#-------------------------------------------------------
	
	#-- Servicios del sistema operativo
	.include "so.h"
	
	#-- Tamaño maximo del buffer donde guardar la cadena del login
	.eqv MAX 20
	
	.data
msg1:  .string "Login: "
msg2:  .string "\nBienvenido "
login: .space MAX
	
	.text
	 
main:
	
	#-- Configurar el modulo E/S de bajo nivel
	jal io_init

	#-- Imprimir una cadena
	la a0, msg1
	jal puts
	
	#-- Esperar hasta que se introduzca una cadena
	la a0, login
	li a1, MAX
	jal gets
	
	#-- Imprimir Mensaje de bienvenida
	la a0, msg2
	jal puts
	
	#-- Imprimir nombre del usuario
	la a0, login
	jal puts
	
    #-- Cerrar la salida de bajo nivel
	jal io_close
	EXIT

Este es el resultado, cuando el usuario introduce el Login obijuan:

En esta animación se muestra su funcionamiento:

Ejemplo 17: Menu de opciones

En este ejemplo se muestra un menú de opciones con 3 posibilidades: volver a sacar el menú, imprimir un mensaje de prueba y terminar. El objetivo es comprobar la interactividad con la aplicación

Este es el programa:

#-------------------------------------------------------
#--  Ejemplo de un programa que presenta un menu al usuario
#-------------------------------------------------------
	
	#-- Servicios del sistema operativo
	.include "so.h"
	
	
	.data
menu:	.ascii "----MENU-----\n"
        .ascii "1.- Mostrar el menu\n"
        .ascii "2.- Test\n"
        .ascii "q.- Quit\n"
	    .ascii "\nOpcion?: "
	    .byte 0
	
msg_end: .string "\nFin\n"
msg_test: .string "\nProbando la opcion 2...\n\n"

	.text
	 
main:
	
	#-- Configurar el modulo E/S de bajo nivel
	jal io_init

loop:
	#-- Imprimir el menu
	la a0, menu
	jal puts
	
wait_user:
	#-- Esperar a que se pulse una tecla
	jal getchar

	#-- Saltar a la opcion correspondiente
	li t0, '1'
	beq a0, t0, opcion1
	
	li t0, '2'
	beq a0, t0, opcion2
	
	li t0, 'q'
	beq a0, t0, fin
	
	#-- Opcion incorrecta, volver a pedir otra
	j wait_user


	#-- Opcion 1: volver a imprimir el menu
opcion1:
    li a0, '\n'
	jal putchar
	
	li a0, '\n'
	jal putchar
	
	j loop
	
	#-- Opcion 2: Imprimir una cadena de prueba
opcion2:
	la a0, msg_test
	jal puts
	j loop

	#-- Terminar
fin: 	
	la a0, msg_end
	jal puts

    #-- Cerrar la salida de bajo nivel
	jal io_close
	EXIT

Esto es lo que se aparece al arrancar el programa y ejecutar todas las opciones. Primero la opción 2, después la 1 y por último se termina

En esta animación se ve en acción:

Biblioteca io_rars_os.s

En el RARs hemos visto que hay dos sistemas para realizar la entrada/salida. Uno es el que ya conocíamos desde el principio: las llamadas al sistema operativo. El otro es el que hemos visto en esta sesición: acceso directo a la UART

La biblioteca stdio.s y los ejemplos que la usan (Ejemplos 15, 16 y 17) son independientes del sistema de entrada/salida. En última instancia llaman a las funciones putchar() y getchar(). Y por tanto, decimos que son funciones portables

Para demostrar este concepto, vamos a hacer una segunda implementación estas funciones de entrada/salida usando llamadas al sistema. En esta arquitectura tenemos partes independientes de la salida (stdio.s y los programas de ejemplo), y dos módulos que dependen del sistema en concreto. Lo esquematizamos en esta figura:

Los módulos de bajo nivel son los que lidian con los detalles de la entrada/salida, y es necesario implementarlos para cada sistema. En esta sesión estamos usando el simulador RARs, que tiene 2 sistemas diferentes de E/S. Es necesario implementar estos detalles en cada módulo. Si quisiéramos utilizar otro computador, como por ejemplo la raspberry pi pico 2, habría que crear un módulo especifico de bajo nivale para ella (Que se podría llamar, por ejemplo io_pico2_uart1.s)

Estas son las funciones de interfaz a implementar para cada sistema:

  • io_init()
  • io_close()
  • putchar()
  • getchar()

Implementación del módulo

Esta es la implementación de las funciones de interfaz de entrada/salida del módulo io_rars_os.s:

#----------------------------------------------------------------
#-- Modulo io_rars_so
#--   Entrada/salida a traves del sistema operativo del rars
#--
#----------------------------------------------------------------
.include "so.h"

#-------------------------------------------------------------
#-- io_init()
#--
#--  Configurar el modulo de Entrada/salida de bajo nivel
#--  Se debe llamar antes de ejecutar cualquier otra funcion
#--  de este modulo
#--
#--  En la implementacion usando el so de RARs, no hace
#--  falta configurar nada, por lo que esta funcion
#--  simplemente retorna
#-------------------------------------------------------------
.global io_init
io_init:

	#-- No es necesario configurar nada

	ret	
	
	
#-------------------------------------------------------------
#-- io_close()
#--
#-- Terminar de usar el modulo de entrada/salida. Finalizar
#-- las operaciones pendientes
#-------------------------------------------------------------
.global io_close
io_close:
	
	#-- En esta implementacion no es necesario
	#-- hacer nada
	ret
	
	
#---------------------------------------------------------------
#-- putchar(c): Imprimir un caracter
#--
#-- ENTRADA:
#--   - a0 (c): Carácter a imprimir
#--
#---------------------------------------------------------------
.global putchar
putchar:

    #-- Llamar al servicio del so
    #-- para imprimir un caracter
    PRINT_CHAR
   
	ret

#--------------------------------------------------------
#-- getchar()
#--
#-- Esperar a que se pulse una tecla
#-- Esta funcion es BLOQUEANTE
#--
#-- SALIDA:
#--   a0 (car): Caracter introducido por el usuario
#--
#-------------------------------------------------------
.global getchar
getchar:

    #-- Llamar al servicio del so
    #-- para leer un caracter
    READ_CHAR
    
	ret

En esta implementación, no es necesario hacer nada en las funcione io_init() e io_close(), pero deben estar para que se ensamblen correctamente con el código de alto nivel

Pruebas multiplataforma

Lo interesante de esta separación en Alto y bajo nivel, es que los ejemplos 15, 16 y 17, la biblioteca stdio.s y cualquier otro programa que use stdio.s compilan perfectamente si cambiamos el sistema de entrada/salida. NO hay que reescribir nada de código. Decimos que se trata de un software portable

Para demostrarlo vamos a ejecutar en el RARs los ejemplos 15, 16 y 17 usando el nuevo módulo io_rars_os.s

Ejemplo 15: Hola mundo

Al ejecutarlo vemos el mensaje Hola mundo! en la consola del RARs. Cuando usamos la otra biblioteca, este mensaje se muestra en el simulador de teclado y pantalla. ¡No ha habido que tocar nada del código del ejemplo 15 ni de la biblioteca stdio!

Ejemplo 16: Login

Ejemplo 17: Menú de opciones

Conclusiones

La comunicación a bajo nivel con la consola se realiza mediante una UART, que tiene una serie de registros mapeados en la memoria. Para imprimir un carácter hay que esperar a que el transmisor esté listo, comprobando su bit de READY del registro de control. Luego se escribe su registro de datos. Para leer un carácter el proceso es similar. Primero se espera a que el bit del READY del receptor se active, en su registro de control, y luego se lee el registro de datos

Tanto para transmitir como para recbir caracteres, hay por tanto que esperar. Hay momento en los que la cpu NO REALIZA NINGUN TRABAJO UTIL. Es decir, sólo espera que el periférico esté disponible. Este mecanismo es el más sencillo, y se conoce con el nombre de ESPERA ACTIVA (polling). En el caso de la lectura, se dice que es bloqueante

En lecciones posteriores, mejoraremos este mecanismo utilizando interrupciones, que nos permiten que la CPU realice otras tareas mientras se está esperando a los bits de READY

Para que nuesro software sea portable, difinimos una interfaz con el bajo nivel, que en nuestro caso son fundamentalmente las funciones putchar() y getchar(). Desde la biblioteca de alto nivel stdio.s implementamos funciones que llaman a estas funciones de interfaz. Así las hacemos independientes de los mecanismos concretos de la E/S y por tanto, ese mismo software nos vale para otras plataformas

Para migrar a otro sistema, sólo tendremos que reimplementar las funciones de la interfaz de bajo nivel. El resto del programa quedará intacto. para demostrar esa idea hemos definido en esta lección dos bibliotecas de bajo nivel: io_rars_uart.s y io_rars_os.s. El software de alto nivel funcionará de igual manera con ellas

Es el momento de ampliar nuestra biblioteca de alto nivel stdio.s para poder imprimir números, y así podemos depurar mejor nuestros programas. Es lo que veremos en la siguiente lección

Ejercicios para practicar

🚧 TODO 🚧

Profesorado

Licencia

Enlaces

Página principal


Sesiones de Prácticas

P1: Simulador RARs

L1: El simulador RARs
L2: Calculando expresiones
L3: Práctica 1-3. Variables

P2: E/S mapeada. Llamadas al sistema

L4: Pract 2-1. E/S mapeada
L5: Práctica 2-2: Inst. ecall
L6: Prác 2-3: Cadenas

P3: Bucles y Saltos condicionales

L7: Práct 3-1: Bucles y saltos
L8: Práct 3-2: Cadenas II

P4: Subrutinas

L9: Pract 4-1: Subrut. Nivel-1
L10: Pract 4-2: La pila
L11: Pract 4-3: Recursividad

P5: Memoria Dinámica

L12: Pract 5-1. Heap. Listas

VÍDEO DE DESPEDIDA

SESIONES ADICIONALES

L13: RV32I: Resto de instrucciones
L14: Macros
L15: Entrada/Salida a bajo nivel
L16: Multiplicaciones/divisiones. Unit Tests 🚧
L17: Biblioteca stdio: salida a alto nivel 🚧
Los registros de estado y control (CSR)
Interrupciones y excepciones
Borrador

SOLUCIONES

Soluciones a los ejercicios planteados en cada sesión para practicar:
Sesión L1
Sesión L2
Sesión L3
Sesión L4
Sesión L5
Sesión L6
Sesión L7
Sesión L8
Sesión L9
Sesión L10
Sesión L11
Sesión L12

Ejercicios de examen

Simulacro examen 1
GISAM. Ordinario. 2019-Dic-11
GISAM. Extra. 2020-Jul-03
GISAM. Ordinario. 2021-Ene-21
GISAM. Ordinario. 2022-Ene-10
GISAM. Extra. 2022-Jun-29
GISAM. Parcial 1. 2022-Oct-26
GISAM. Parcial 2. 2022-Nov-30
GISAM. Parcial 3. 2022-Dic-21
GISAM. Parcial 1. 2023-Oct-09
GISAM. Parcial 2. 2023-Nov-11
GISAM. Parcial 3. 2023-Dic-20
GISAM. Extra. 2024-Jun-17
GISAM. Parcial 1. 2024-Oct-14
GISAM. Parcial 2. 2024-Nov-13
GISAM. Parcial 3. 2024-Dic-16
GISAM. Extra. 2025-Jun-17
GISAM. Parcial 1. 2025-Oct-20
GISAM. Parcial 2. 2025-Nov-19
GISAM. Parcial 3. 2026-Enero-9
GISAM. Parcial 1. 2026-Oct-21
TELECO. Ordinario. 2019-Dic-13
TELECO. Extra. 2020-Jul-07
TELECO. Ordinario. 2021-Ene-21
TELECO. Extra. 2021-Jul-02
TELECO. Ordinario. 2022-Ene-10
TELECO. Extra. 2022-Jun-29
TELECO. Ordinario. 2023-Ene-10
TELECO. Extra. 2023-Jun-29
TELECO. Parcial 1. 2023-Oct-20
TELECO. Parcial 2. 2023-Nov-17
TELECO. Parcial 3. 2023-Dic-22
TELECO. Extra. 2024-Jun-17
TELECO. Parcial 1. 2024-Oct-10
TELECO. Parcial 2. 2024-Nov-21
TELECO. Parcial 3. 2024-Dic-19
TELECO. Extra. 2025-Jun-17
TELECO. Parcial 1. 2025-Oct-10
TELECO. Parcial 2. 2025-Nov-13
TELECO. Parcial 3. 2026-Ene-21
TELECO. Extra. 2026-Jun-25
TELECO. Parcial 1. 2026-Oct-09
Robótica. Ordinario. 2020-Jun-1
Robótica. Extra. 2020-Jul-13
Robótica. Ordinario. 2021-Mayo-20
Robótica. Extra. 2021-Junio-16
Robótica. Parcial 1. 2022-Feb-25
Robótica. Parcial 2. 2022-Abril-1
Robótica. Parcial 3. 2022-Mayo-6
Robótica. Parcial 1. 2023-Feb-27
Robótica. Parcial 2. 2023-Mar-27
Robótica. Parcial 3. 2023-May-08
Robótica. Parcial 1. 2024-Feb-26
Robótica. Parcial 2. 2024-Mar-20
Robótica. Parcial 3. 2024-May-06
Robótica. Extra. 2024-Junio-24
Robótica. Parcial 1. 2025-Feb-25
Robótica. Parcial 2. 2025-Mar-31
Robótica. Parcial 3. 2025-May-19
Robótica. Parcial 1. 2026-Feb-23
Robótica. Parcial 2. 2026-Abr-13
Robótica. Parcial 3. 2026-May-11
Datos. Parcial 1. 2023-Oct-09
Datos. Parcial 2. 2023-Nov-15
Datos. Parcial 3. 2023-Dic-20
Datos. Parcial 1. 2024-Oct-09
Datos. Parcial 2. 2024-Nov-13
Datos. Parcial 3. 2025-Ene-17
Datos. Parcial 1. 2025-Oct-15
Datos. Parcial 2. 2025-Nov-19

Clone this wiki locally