En esta lección trataremos las relaciones entre las instrucciones de los lenguajes de programación en los circuitos digitales. Estas relaciones justifican el uso de lenguajes de descripción hardware muy similares a los lenguajes de programación. Y la existencia de herramientas de síntesis automática de sistemas digitales. Algunas de las estructuras de programación clásica como el if-then-else, el case, el for, loop, o las llamadas a procedimiento admiten una implementación hardware inmediata en términos de componentes digitales. Estas son las estructuras que vamos a ver a lo largo de esta lección. Comenzamos por la primera de ellas, la estructura if-then-else. En general una sentencia del tipo if x igual a 0 then coloca en z "a" o, en caso contrario, coloca en z "b", se puede implementar inmediatamente directamente con un multiplexor donde pondremos como señal de control la variable de comparación, como salida el resultado z y entonces, dependiendo de si se cumple la condición x es igual a 0 por la entrada pondremos la "a", y si no se cumple la condición, por la entrada 1 pondremos la "b". Fijaros que esto es una relación inmediata. Una sentencia if-then-else es exactamente igual a un multiplexor. Aquí tenéis una estructura un poco más compleja porque hay unos ifs anidados. Este sería el nivel más interno de anidamiento, tenemos dos, y a continuación tenemos un nivel más externo de anidamiento que es este. Bien, esta es una estructura exactamente igual a la que acabamos de ver ahora. Si llamamos a este if, vamos a llamarlo (A), este if lo vamos a implementarlo con un multiplexor donde la señal de control es la variable de comparación, si se cumple la condición x0 igual a 0 entonces por la salida aparecerá y0 y, en caso contrario aparecerá y1. Haciendo exactamente lo mismo, aquí tenemos un segundo if que hemos implementado con este segundo multiplexor, la señal de control es la misma, x0, porque la variable de comparación es la misma. Y finalmente tendríamos el nivel más externo de anidamiento, vamos a llamarlo (C), que lo hemos implementado con un tercer multiplexor controlado por la variable x1 y que, en caso de que se cumpla la condición, es decir, que x1 sea 0, el resultado de toda esta estructura ha de ser el resultado del if que hemos llamado A, es decir, la salida del multiplexor A, y en caso de que no se cumpla la condición, es decir, x1 es igual a 1, en ese caso la salida ha coincidir con la salida del if que hemos llamado B, que se corresponde con el multiplexor B. Si tuviésemos tres niveles de anidamiento en vez de dos, necesitaríamos tres niveles de multiplexores. En general, si tuviésemos n niveles de anidamiento necesitaríamos n niveles de multiplexores para implementar la estructura. Un segundo ejemplo de estructura de programación es la estructura "case". El mismo algoritmo que antes también puede describirse como una sentencia "case" como esta, en la que dependiendo del valor de x se asigna a f los valores y0, y1, y2 o y3. Fijémonos que en este caso x tiene dos bits, puede tomar los valores 00, 01, 10 y 11. A esta instrucción le corresponde un multiplexor 4 a 1, con las entradas y0, y1, y2 y y3, y la señal de control que vuelve a ser la señal de comparación, con la diferencia que en este caso esta señal tiene ahora dos bits como corresponde a un multiplexor 4 a 1. Bien, pues esta es también la manera, ya véis, inmediata de implementar una estructura "case" con multiplexores. La tercera estructura que veremos es la estructura "for-loop". Los "loops" se asocian a estructuras iterativas. Vamos a verlo en un ejemplo, supongamos que queremos sumar dos números x e y. Vamos a representar los números x e y por vectores, en este caso x será un vector, si tiene 4 dígitos decimales, de 4 posiciones x3, x2, x1, x0. El número "y" también tendrá 4 componentes, y3, ... hasta y0, y el resultado también lo vamos a representar por un vector, en este caso de 5 componentes, z4 ... cuatro hasta z0. Este algoritmo suma los números siguiendo el método manual. Recordemos, 942 más 328, lo que hacíamos es ir sumando columna a columna, 9 más 2 da 10, ponemos aquí el 0 y generamos un acarreo igual a 1. Para cada pareja generamos dos números, lo que es la suma parcial y el acarreo a la etapa siguiente. Bien, pues este algoritmo manual que hemos repetido más de una vez, es el que se implementa en este algoritmo. Fijaros, para todos los valores de "i" desde 0 hasta 3, es decir, va recorriendo los vectores x e y, va generándo s(i), que sería este resultado como la suma de la x más la y más el posible acarreo de la etapa anterior. Si el resultado es mayor que 9 como sería en este caso, le resta 10 y lo coloca en z. Aquí el resultado era 10, le restamos 10, da 0, colocamos aquí el 0 y genera un acarreo igual a 1 que pasa a la etapa siguiente. Y en caso contrario, si el resultado es menor que 9 pues simplemente pone este resultado en z y el acarreo siguiente es 0. Vamos a implementar este algoritmo, el "loop", en dos pasos. En primer lugar, identificamos lo que llamamos el "cuerpo del loop", las operaciones básicas, y construimos un circuito básico que va a ejecutar estas operaciones. En concreto necesitamos un módulo que sea capaz de admitir en cada momento x(i), y(i), y el acarreo(i), y que a partir de ahí calcule z(i), y el acarreo a la etapa siguiente. Bien, este módulo realmente lo hemos utilizado bastante a lo largo del curso, o sea que no hace falta que expliquemos demasiadas cosas más. pero lo importante es esto: el primer paso es construir un módulo que ejecute el cuerpo del loop. Y lo que son las 4 iteraciones del loop, las vamos a implementar conectando en serie 4 módulos de estos que acabamos de definir, de manera que cada módulo ejecuta una de las iteraciones del loop. Este primer módulo ejecuta la iteración i=0, En la iteración i=0, se hace la suma de x0 con y0 con el acarreo0; la suma de x(0) con y(0) con y cero con el acarreo inicial, que en este caso es 0, y se calcula z(i) y el acarreo siguiente. Se calcula z(i) que, como la i es 0 es z0, y el acarreo siguiente. A continuación la i pasa a valer 1. El segundo módulo ejecuta la iteración correspondiente a i=1 sumando x1 con y1 con c1, y calculando z1 y el acarreo(2). Y así tantas veces como fuese necesario. En este caso son 4 iteraciones, 4 módulos. Si ahora la hacemos de n iteraciones necesitaríamos n módulos. Finalmente lo que decimos aquí es que el último acarreo se corresponde con el valor más significativo, con el dígito más significativo de la suma. Resumiendo, ¿cómo implementamos una estructura loop?: Construyendo un módulo básico para el cuerpo del loop y concatenando tantas veces este módulo básico o tantas copias de este módulo básico como número de iteraciones realice el loop. Un par de comentarios acerca de la estructura loop: El primero es que más adelante veremos otras implementaciones cuando hayamos visto los circuitos secuenciales. El segundo comentario, más importante, es que desgraciadamente no todos los loops pueden implementarse de esta manera. Un ejemplo es la estructura "While condition loop operation"; mientras se cumpla esta condición, ejecuta una y otra vez una o varias o un conjunto de operaciones. La estructura loop, hemos visto que se convierte en tantos módulos concatenados como el número de veces que se ejecuta el loop. Si el número de veces en que se cumple la condición es, o bien desconocido, como es este caso, o bien el número de veces en que se cumple la condición es muy grande, el loop no se puede implementar en la práctica tal como hemos visto, y en esos casos se hace necesario utilizar módulos secuenciales, que estudiaremos más adelante. La última estructura que vamos a ver son las llamadas a procedimientos. Las llamadas a procedimientos se asocian a descripciones jerárquicas. Aquí tenemos un ejemplo de una pequeña porción de código que realiza este cálculo. Lo de menos es como realiza el cálculo, es decir, no vamos a entrar en el algoritmo en sí, pero lo importante es que aquí dentro hay una llamada a un procedimiento MAC que realiza esta operación. Bueno, pues la implementación de las llamadas a procedimiento es muy sencilla, de hecho consiste en que se debe definir primero un módulo capaz de realizar las operaciones incluídas dentro del procedimiento, en este caso es la suma de w, con x por y, dando como resultado la variable z módulo básico MAC. Y a continuación, en el algoritmo, cada vez que hay una llamada en procedimiento, deberemos colocar un módulo de estos que acabamos de crear. En este caso concreto, en esta porción de código, la llamada al procedimiento está dentro de un FOR, que se repite un total de 8 veces, por lo tanto el procedimiento MAC se llamará 8 veces, y por eso hemos colocado aquí un total de 8 módulos básicos de tipo MAC. Y lo que es el código es el que nos dice como irán conectadas las entradas y salidas de estos módulos básicos. Bien, un par de comentarios como conclusión. Hemos visto como algunas estructuras clásicas en los lenguajes de programación se pueden implementar de una manera directa con componentes digitales. Esto explica el por qué se han definido lenguajes para especificar los circuitos digitales como son por ejemplo el VHDL o el Verilog, muy similares a los lenguajes de programación software. Por otro lado, las herramientas de síntesis automáticas de circuito se basan precisamente en esta relación entre estructuras de programación y circuitos digitales; sin tal relación las herramientas de síntesis dificilmente existirían tal y como las conocemos ahora. Y con esto acabamos la lección. Como resumen podemos decir que hemos visto como los multiplexores permiten implementar estructuras if-then-else y estructuras Case, como los loops se asocian a estructuras hardware interactivas, como las llamadas a procedimientos se asocian a descripciones jerárquicas donde el procedimiento constituye el nivel más alto, el más sencillo, de jerarquía, y como estas asociaciones entre estructuras de programación y circuitos digitales nos llevan a la definición de lenguajes de descripción hardware similares a los lenguajes de programación más o menos convencionales.