Para esta implementación se está utilizando un DAC LTC1661. Ya había escrito
una entrada sobre comunicación SPI con este mismo DAC con Arduino. La ventaja didáctica de una implementación hardware, como en este caso, es que permite un mejor entendimiento de este protocolo de comunicación. Si más preámbulo, entremos al tema. Como quizá ya lo haya mencionado antes, las hojas de especificaciones lo son todo. Antes de siquiera pensar en escribir nuestro programa debemos entender que queremos hacer. Asumiré que ya se tiene cierto conocimiento del protocolo y me centraré en lo necesario para mantener breve la descripción del programa.
De la
hoja de especificaciones, tenemos el siguiente diagrama de tiempos:
Los periodos que nos interesan son t5, que es el periodo mínimo entre datos ( el tiempo mínimo que el FPGA debe esperar para enviar el siguiente dato de manera que el DAC pueda reaccionar) y t3 y t4 que nos indican que SCK debe tener un duty cycle de 50% con un periodo T = t3+t4. En la página 4 de la hoja podemos encontrar que los tiempos mínimos requeridos son:
t5 = 100 ns y una frecuencia para
SCK de 10 Mhz. Los periodos t1 y t2 nos indican una especificación igual de importante. Indican que el flanco de subida de SCK debe ocurrir justo a la mitad del bit enviado. Entendiendo estas restricciones, podemos ahora comenzar a escribir nuestro programa. Nuestra entidad deberá tener 2 entradas: clk y un reset, y 3 salidas: SCK, CD y datos:
La implementación estará formada por los siguientes bloques elementales:
- Divisor de frecuencia (50 a 10 MHz)
- Divisor de frecuencia para reloj de CS
- Memoria ROM
- Registro PISO (Parallel Input Serial Output)
- Generador de reloj SCK
Sólo describiré los bloques más relevantes:
Memoria ROM
Los datos almacenados por esta memoria deben tener la estructura especificada por el fabricante (pag. 8), que en este caso es la siguiente:
A3A2A1A0D9D8D7D6D5D4D3D2D1D0X1X0
Control Datos Don't Care
Para este ejemplo debemos enviar la mitad del voltaje de referencia al canal A y actualizar su salida, por lo que los valores para el código de control deben ser "1001" (ver tabla de la pag. 9). Se crean entonces las siguientes señales que serán usadas en la memoria ROM:
type rom is array(0 to 2) of std_logic_vector(15 downto 0);
constant control: std_logic_vector(3 downto 0):="1001";
constant dc: std_logic_vector(1 downto 0):= "00"; -- Bits don't care constant myrom: rom
:=( control&"0111111111"&dc,
control&"0111111111"&dc,
control&"0111111111"&dc);
La razón de implementar una memoria ROM como un arreglo de varios elementos para un ejemplo tan simple es para que pueda ser modificada fácilmente para hacer cosas más interesantes como enviar una función triangular o cosenoidal (en cuyo caso habrá que incrementar el tamaño del array a entre 50 o 100 elementos). Otro detalle que debo mencionar es que, como se verá en el programa completo, el incremento de la dirección se hace con el reloj CS. Esto se hace a modo de tener el bit-rate máximo. Se puede agregar un nuevo divisor a una frecuencia más baja para cambiar el dato si así lo desean.
Registro PISO
Este registro serializará los datos recibidos de la salida de la memoria ROM y los enviará de forma síncrona en cada flanco de bajada* del reloj secundario de 10 MHz. La serialización únicamente se efectuará después de recibir un flanco de subida del reloj CS.
*Esto de modo que se cumpla la especificación de los tiempos t1 y t2, así SCK deberá estar en fase con el reloj secundario.
Generador de reloj SCK
Este reloj deberá tener una frecuencia de 10 MHz pero requiere cumplir una condición: debe activarse durante el siguiente flanco de subida del reloj secundario (10 MHz) después del flanco de bajada del pulso CS. Esto se logra mediante una implementación
if síncrona normal. Sin embargo, dado que no es posible implementar físicamente una instrucción en flanco de bajada inmediatamente después de un flanco de subida (
"bad synchronous description") se recuré a un truco combinacional para generar el reloj SCK que puede verse en el programa completo.
Teniendo el programa listo, así luce en simulación con ISim:
(Click para agrandar)
La implantación física se realizó en una tarjeta de desarollo Basys2 y funcionó perfectamente. Desafortunadamente, como había comentado en
alguna entrada, sufrí un robo durante un congreso en Monterrey, por lo que no habrá fotos esta vez.
Programa completo
LIBRARY IEEE;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.STD_LOGIC_ARITH.ALL;
USE IEEE.STD_LOGIC_UNSIGNED.ALL;
entity DAC_SPI is
port(
clk : in std_logic; -- 50 MHz
reset : in std_logic;
CS : out std_logic;
SCK : out std_logic; -- 10 MHz
datos : out std_logic); -- Salida serial
end entity;
architecture rtl of DAC_SPI is
-- Señales
-- Divisor
signal count: integer :=1;
signal clk_10: std_logic:='0';
-- ROM
signal address: integer range 0 to 2;
type rom is array(0 to 2) of std_logic_vector(15 downto 0);
constant control: std_logic_vector(3 downto 0):="1001"; -- Cargar y actualizar canal A
constant dc: std_logic_vector(1 downto 0):= "00"; -- Bits don't care
constant myrom: rom
:=( control&"0111111111"&dc,
control&"0111111111"&dc,
control&"0111111111"&dc);
signal rom_out: std_logic_vector(15 downto 0):=(others=>'0');
signal clk_cs: std_logic:='0';
-- PISO
signal temp: std_logic_vector(15 downto 0):= (others =>'0');
signal t: std_logic;
-- Otros
signal a: integer range 0 to 18 :=1;
signal sck_t: std_logic;
--
begin
-- Divisor de frecuencia (10 MHz)
process(clk) begin
if(clk'event and clk='1') then
count <=count+1;
if(count = 5) then -- dónde count = frec de reloj / frec deseada
clk_10 <= not clk_10;
count <=1;
end if;
end if;
end process;
--
-- PISO
process (clk_10,rom_out,clk_cs)begin
if (CLK_10'event and CLK_10='0') then
if (clk_cs='1') then
temp(15 downto 0) <= rom_out(15 downto 0);
else
t <= temp(15);
temp(15 downto 1) <= temp(14 downto 0);
temp(0) <= '0';
end if;
end if;
end process;
datos <= t;
--
-- SCK
process(clk_10,clk_cs) begin
if (clk_10'event and clk_10 = '1') then
if(clk_cs='0')then
SCK_t <= '1';
else
SCK_t <= '0';
end if;
end if;
end process;
SCK <= (sck_t) and (clk_10) and not(clk_cs);
--
-- Divisor de frecuencia para CLK_CS
process(clk_10) begin
if(clk_10'event and clk_10='1') then
a <=a+1;
if(a = 17) then
clk_cs <= '1';
a <=0;
else
clk_cs <= '0';
end if;
end if;
end process;
--
-- ROM
process(clk_cs,reset) begin
if reset = '1' then
address <= 0;
elsif( clk_cs'event and clk_cs = '1' ) then
if address < 2 then
address <= address + 1;
else
address <= 0;
end if;
rom_out <= myrom(address);
end if;
end process;
-- CS
CS <= clk_cs;
--
end rtl;