La idea
La idea de esta primera versión es implementar un sintetizador monofónico con un único oscilador de diente de sierra, que sólo lea mensajes de tipo NoteOn y NoteOff y que reproduzca el sonido a través de un DAC I2S.
Como se puede apreciar se trata del típico circuito de entrada MIDI con optoacoplador más un PCM5102A como DAC I2S de alta calidad. Los mensajes MIDI de NoteOn se traducen en tonos que genera el oscilador.
El interfaz de salida I2S para el DAC externo
El protocolo I2S es un estándar definido para transportar sonido digital a muy cortas distancias (dentro de una misma placa, por ejemplo). Es estándar de facto en casi la totalidad de los conversores DAC y ADC de alta calidad del mercado de todos los fabricantes y se trata de un protocolo relativamente ligero y fácil de implementar.
(imagen © Texas Instruments Incorporated, extraida con permiso de la hoja de datos del PCM5102A)
Existe una variante del I2S denominada "Left Justified" que simplifica el uso del reloj LR, evitando el desfase de un bit entre el envío de cada palabra para el canal izquierdo y el canal derecho:
(imagen © Texas Instruments Incorporated, extraida con permiso de la hoja de datos del PCM5102A)
Y que es la variante I2S que se ha usado en este proyecto ya que es más fácil de implementar que el estándar original y actualmente todos los DACs del mercado la soportan. A continuación puede verse lo que sería el diagrama de bloques de la interfaz I2S-LJ dentro de la FPGA:
Las diferentes tablas de verdad de cada uno de los bloques combinacionales serían las siguientes:
Entradas | Salidas | |
---|---|---|
ClkOutDivider == 22 | Reset | MUXcod |
0 | 0 | + |
0 | 1 | 0 |
1 | X | 0 |
Entradas | Salidas | ||
---|---|---|---|
ClkOutDivider == 22 | ClkOutDivider == 10 | Reset | MUXco |
1 | 0 | 1 | 0 |
0 | 1 | 0 | 1 |
en otro caso | ClkOut |
Entradas | Salidas | |
---|---|---|
ClkOutDivider == 22 | Reset | MUXbc |
X | 1 | 0 |
1 | 0 | + mod 32 |
en otro caso | BitCounter |
Entradas | Salidas | |
---|---|---|
BitCounter < 16 | Reset | MUXlrco |
0 | 0 | 0 |
0 | 1 | 1 |
1 | X | 1 |
Entradas | Salidas | |
---|---|---|
ClkOutDivider == 22 | BitCounter == 31 | MUXdata |
0 | X | data |
1 | 0 | << |
1 | 1 | muestra izq + der |
Como se puede apreciar el mecanismo se basa en meter en un registro de desplazamiento de 32 bits las dos palabras de 16 bits de cada canal (izquierdo + derecho) e ir emitiendo bit a bit ese registro cambiando la polaridad de la señal LRCLK cada 16 bits para indicar canal izquierdo o canal derecho.
El oscilador
El oscilador se ha implementado como un sencillo acumulador de fase.
Como lo que se busca es un oscilador de diente de sierra, lo más sencillo es aprovechar el comportamiento natural de cualquier acumulador que, cuando se desborda "da la vuelta". Esto simplifica enormemente todo el diseño ya que, de forma natural, la señal resultante tiene forma de diente de sierra.
(imagen de dominio público extraida de Wikipedia)
Por cada nueva muestra que debe ser calculada, el acumulador es incrementado en una cantidad determinada, lo que provoca que su valor crezca de forma lineal (la rampa del diente de sierra). Al cabo de una cantidad suficiente de muestras, el acumulador se desbordará y "dará la vuelta" empezando de nuevo desde abajo (el "pico" del diente de sierra).
La cantidad que se use para ir incrementando el acumulador de fase determinará la frecuencia de la señal del oscilador:
$$DivisorFrecuenciaRelojI2S = {{32000000 Hz \over 44100 Hz} \over 32 bits}$$
$$inc = {{f \times 65536} \over {{32000000Hz \over DivisorFrecuenciaRelojI2S} \over 32 bits}} \times 65536$$
El incremento (inc) debe estar en formato Q16.16 (punto fijo de 16 bits de parte entera y 16 bits de parte fraccionaria), que es el formato usado por el acumulador de fase del oscilador.
Nótese que el oscilador no se incrementa en cada ciclo de reloj de la FPGA, sino cada vez que se requiere una nueva muestra por parte de la interfaz I2S-LJ para emitirla al DAC.
El parser MIDI
El módulo de procesamiento MIDI se encarga de implementar un receptor UART sencillo a 31250 baudios y una máquina de estados que vaya leyendos los datos MIDI de entrada y determinando en cada momento si hay que reproducir una nota en el oscilador (y con qué frecuencia) o no.
La UART se implementa de forma muy sencilla usando un registro de desplazamiento y un contador para medir el tiempo equivalente a 1.5 bits y a 1 bit.
Y usando la siguiente máquina de estados:
En una entrada anterior de este blog se abordó este proyecto de forma separada. Lo que se ha hecho en este caso ha sido simplificar aquel esquema para que cupiese todo dentro de un único fichero VHDL.
Una vez implementado el receptor UART, el parser MIDI se puede implementar mediante una sencilla máquina de estados que sólo detecte eventos NoteOn y NoteOff.
El parser MIDI en este caso no sólo determina qué nota debe ser reproducida, sino que usando una ROM interna, determina el valor de incremento que debe ser usado por el módulo oscilador para generar el tono correspondiente.
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity NotesRom is port ( AddressIn : in std_logic_vector(6 downto 0); DataOut : out std_logic_vector(31 downto 0) ); end entity; architecture RTL of NotesRom is type RomType is array (0 to 127) of std_logic_vector(31 downto 0); constant Data : RomType := ( x"00000000", -- note 0 x"000cdf51", -- note 1 x"000da345", -- note 2 x"000e72df", -- note 3 x"000f4ed1", -- note 4 x"001037d7", -- note 5 x"00112eb9", -- note 6 x"00123449", -- note 7 x"00134966", -- note 8 x"00146efe", -- note 9 x"0015a60b", -- note 10 x"0016ef97", -- note 11 . . . . . . . . . x"368d1251", -- note 122 x"39cb7a59", -- note 123 x"3d3b4348", -- note 124 x"40df5cc9", -- note 125 x"44bae33a", -- note 126 x"48d12253" -- note 127 ); begin DataOut <= Data(to_integer(unsigned(AddressIn))); end architecture;
Para generar este conjunto de valores se hizo un pequeño programa en C++ que convirtió el valor de cada nota MIDI en el valor de incremento correspondiente para que el oscilador emita a esa frecuencia:
#include <iostream> #include <iomanip> #include <stdint.h> #include <math.h> using namespace std; double getFreq(uint8_t midiNote) { const double A4_FREQ = 440; const int32_t A4_MIDI_NOTE = 69; return A4_FREQ * pow(2.0, ((double) (((int32_t) midiNote) - A4_MIDI_NOTE)) / 12.0); } uint32_t getInc(uint8_t midiNote) { const uint32_t CLK_FREQ = 32000000; const uint32_t SAMPLE_RATE = 44100; double freq = getFreq(midiNote); double div = (((double) CLK_FREQ) / SAMPLE_RATE) / 32; double inc = (freq * 65536) / ((CLK_FREQ / div) / 32); uint32_t ret = round(inc * 65536); return ret; } int main() { for (uint8_t n = 0; n < 128; n++) cout << "\t\tx\"" << hex << setw(8) << setfill('0') << getInc(n) << "\", -- note " << dec << setw(0) << setfill(' ') << ((int) n) << endl; return 0; }
Compilando este programa y ejecutándolo, genera en la salida estándar los valores de incremento de todas las 127 notas MIDI posibles:
g++ -c -o notes_rom_generator.o notes_rom_generator.cc
g++ -o notes_rom_generator notes_rom_generator.o
./notes_rom_generator
Todo junto
A la hora de ponerlo todo junto, basta con interconectar los tres bloques:
Implementación sobre cualquier FPGA
La implementación se ha desarrollado sobre una Spartan3E de Xilinx a 32 MHz pero el proyecto se puede meter en cualquier FPGA siempre y cuando se ajusten las ecuaciones y las constantes para tener en cuenta las diferentes frecuencias de reloj. En caso de que queramos meter el sintetizador en una FPGA que vaya a otra frecuencia de reloj habría que realizar los siguientes cambios:
1. Las constantes CLK_OUT_DIV y CLK_OUT_DIV_BITS de LJI2SOutput.vhd deben se recalculadas.
2. Las constantes TIME_COUNTER_BITS, TIME_COUNTER_1BIT y TIME_COUNTER_1_5BIT de UartRx.vhd deben ser recalculadas.
3. La constante CLK_FREQ dentro de notes_rom_generator.cc debe ser cambiada, hay que recompilar el programa y colocar la salida generada como los nuevos valores de NotesRom.vhd.
Todo el código fuente puede descargarse de la sección soft.
Lo sentimos. No se permiten nuevos comentarios después de 90 días.