Script Ejemplos Creacionales (Microcontroller)

Enunciado

Microcontrolador (ejemplos creacionales)

Respecto al enunciado original

  • La interfaz del microcontroller es más áspera, más parecida a lo que pasa abajo, hay menos abstracción. Y aparecen mezclados bytes que son códigos de operación y bytes que son datos propiamente dichos.
    • Que lo que sigue sea un dato o un código de instrucción, depende del código de instrucción actual
    • Tenemos que parsear, o sea, interpretar el input
  • No obstante, ya tenemos instrucciones reificadas: NOP, ADD, SUB, LODV, etc. Y queremos aprovechar lo que ya tenemos

Un test

Vamos a armar el programa que hace 3 NOPs y el que suma 8 + 5:

    Microcontroller micro;
    private byte[] programNOP;
    private byte[] programSuma8y5;

    @Before
    public void setUp() {
        micro = new MicrocontrollerImpl();

        programNOP = new byte[1024];
        programNOP[0] = (byte) 1;  // NOP
        programNOP[1] = (byte) 1;  // NOP
        programNOP[2] = (byte) 1;  // NOP

        programSuma8y5 = new byte[1024];
        programSuma8y5[0] = (byte) 9;  // LODV 
        programSuma8y5[1] = (byte) 8;     // dato: 8  
        programSuma8y5[2] = (byte) 5;  // SWAP
        programSuma8y5[3] = (byte) 9;  // LODV 
        programSuma8y5[4] = (byte) 5;     // dato: 5
        programSuma8y5[5] = (byte) 2;  // ADD
    }

Ahora es cuestión de definir los tests en base a este fixture:
@Test
    public void nop() {
        micro.loadProgram(programNOP);
        micro.start();
        micro.step();
        micro.step();
        micro.step();
        micro.stop();
        Assert.assertEquals(3, micro.getPC());
    }

    @Test
    public void suma() {
        micro.loadProgram(programSuma8y5);
        micro.start();
        micro.step();
        micro.step();
        micro.step();
        micro.step();
        micro.stop();
        Assert.assertEquals(13, micro.getBAcumulator());
        Assert.assertEquals(0, micro.getAAcumulator());
    }

El test no cambia mucho, si implementaron el start(), step() y stop() y el load anterior la diferencia es que en lugar de pasar un List<Instruccion> tenemos que armar un byte[]. Claro, lo distinto va a ser cómo vamos a pensar la implementación del step()

Implementación del Microcontroller

Tenemos que agregar una variable de instancia, que sea el programa. La primera tentación es que esa variable sea un byte[], pero en lugar de eso vamos a definir un ProgramIterator:

    private ProgramIterator programIterator;
    private boolean programStarted;

    @Override
    public void loadProgram(byte[] program) {
        this.reset();
        this.programIterator = new ProgramIterator(program);
    }

    @Override
    public void step() {
        if (!this.programIterator.hasNext()) {
            throw new BusinessException("El programa ya terminó");
        }
        programIterator.next().execute(this);
    }

En la implementación final aparecen más validaciones, aquí nos concentramos más en mostrar cómo funciona el ProgramIterator:
Microcontroller%20-%20ProgramIterator.PNG

Responsabilidades del ProgramIterator:

  • parsear un programa de manera de decir:
    • si hay más instrucciones
    • cuál es la próxima instrucción

Es muy parecido al iterador externo de Java 1.4.
Ah, y el Iterator es un patrón, cuyo fin es separar la forma en que se almacena una estructura y la forma en que se accede/recorre. Sirve por ejemplo para trabajar con colecciones, de hecho el iterador puede ser interno (en el caso del foreach) o bien externo, cuando hacemos:

Iterator it = this.alumnos.iterator();
while (it.hasNext()) {
    Alumno alumno = it.next();
    ...
}

Pero también podemos trabajar con iteradores sin necesidad de que haya una colección:
  • en un partido de ajedrez, un iterador nos permite manejar el turno de las blancas y las negras
  • si consideramos dos números (un mínimo y un máximo), el iterador puede ayudarnos a recorrer ese rango de números (next(), next(), hasNext()?)

Vemos la implementación que tiene el ProgramIterator:

    public ProgramIterator(byte[] bytecodes) {
        this.bytecodes = bytecodes;
        this.programCounter = 0;
    }

    @Override
    public boolean hasNext() {
        return this.getByteActual() != 0;
    }

    @Override
    public Instruccion next() {
        Instruccion instruction = InstruccionFactory.getInstance().getInstruction(this.readByteActual());
        instruction.prepare(this);
        return instruction;
    }

    private byte readByteActual() {
        byte value = this.getByteActual();
        this.programCounter++;
        return value;
    }

    private byte getByteActual() {
        return this.bytecodes[this.programCounter];
    }

Vemos que el next involucra dos cosas:
  • delegar a un objeto InstruccionFactory la conversión del número a una instrucción
  • pedirle a la instrucción que se "prepare" (esto es, las instrucciones que necesitan parámetros deberían tomarlo del ProgramIterator en este momento)

Factories

Las factories son objetos que encapsulan la creación de objetos, de hecho existen dos patrones con este nombre:

  • Factory method: son métodos que crean objetos
  • Abstract factory: son objetos polimórficos que permiten crear distintas familias de objetos, puede verse un ejemplo de este pattern en el apunte.

¿Cómo pasamos de un código numérico a una Instrucción?
No queremos hacer un switch, entonces podemos trabajar con un mapa, cuya clave sea un código de instrucción y cuyo valor sea la instrucción NOP, ADD, LODV, etc:

    private InstruccionFactory() {
        this.initialize();
    }

    private void initialize() {
        instructions = new HashMap<Byte, Instruccion>();
        instructions.put((byte) 1, new NOP());
        instructions.put((byte) 2, new ADD());
        instructions.put((byte) 5, new SWAP());
        instructions.put((byte) 9, new LODV());
    }

    public Instruccion getInstruction(byte code) {
        Instruccion instruccionAEjecutar = this.instructions.get(code);
        if (instruccionAEjecutar == null) {
            throw new BusinessException("La instrucción de código " + code + " no es reconocida");
        }
        return instruccionAEjecutar;
    }

Agregar nuevas instrucciones sólo requiere incorporar al mapa el nuevo par clave-valor, el método getInstruccion(byte) no cambia.

Por otra parte vemos cómo se implementa el método prepare() default:

    public void prepare(ProgramIterator programIterator) {
        // Por default no hago nada
    }

Claro, no hago nada, pero en LODV necesitamos pedirle al ProgramIterator que avance una posición para recuperar el valor que hay que cargar luego en el acumulador A:
    @Override
    public void prepare(ProgramIterator programIterator) {
        this.value = programIterator.readValue();
    }

Vemos la implementación de readValue en ProgramIterator:
    public byte readValue() {
        return this.readByteActual();
    }

Ah, claro, es simplemente delegar la lectura del byte actual (podríamos publicar como interfaz el método readByteActual(), exponiendo más detalles de implementación del ProgramIterator, pero preferimos no hacerlo para mantener cierta ingenuidad en la implementación de la instrucción).

Objetos Singleton

No queremos tener n instancias de InstruccionFactory, porque simplemente nos alcanza con tener una sola instancia del factory. Por ese motivo:

  • el constructor por default lo dejamos private, eso impide que podamos hacer new InstruccionFactory() desde ProgramIterator
  • la única instancia de InstruccionFactory estará en una variable de clase (static) dentro de InstruccionFactory
  • y a su vez InstruccionFactory provee un método de clase (static) para acceder a la única instancia, llamado getInstance():
    private static InstruccionFactory instance;

    public static InstruccionFactory getInstance() {
        if (instance == null) {
            instance = new InstruccionFactory();
        }
        return instance;
    }

    private InstruccionFactory() {
        this.initialize();
    }

Eso permite que por cada VM haya un solo objeto.
Otra forma de implementarlo es teniendo métodos estáticos (de clase) en InstruccionFactory, pero eso requere que si el objeto deja de ser Singleton todos los mensajes que antes se hacían a la clase ahora se hagan a un objeto de esa clase.

El pizarrón

Si no llegaste a anotar la explicación te dejamos dos fotos (clickeá sobre la imagen para agrandarla):
diagramaClases_MicroCreacional.jpg

diagramaSecuencia_MicroCreacional.jpg
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License