• Autor de la entrada:
  • Tiempo de lectura:12 minutos de lectura

Para medir temperatura con MAX31865 y MicroBlaze se creará una IP AXI-Lite, realizando el diseño y verificación desde cero de un driver en VHDL conectado a través de un bus AXI.

Introducción

El MAX31865 es un circuito integrado conocido por la facilidad con la que se puede medir resistencia a dos, tres o cuatro hilos. Aunque no es el método más económico para grandes producciones, descarga gran cantidad de trabajo de las fases de diseño y verificación.

Medir temperatura con MAX31865 y MicroBlaze en una Basys 3
Medir temperatura con MAX31865 y MicroBlaze en una Basys 3

Integrarlo con un circuito digital es sencillo gracias a su interfaz SPI. En este caso, se requerirá que MicroBlaze pueda acceder al resultado de la conversión analógico-digital mediante la lectura de una dirección, al estilo de un mapa de periféricos.

Dando por hecho que la temperatura varía muy lentamente respecto a las capacidades de una FPGA, bastará una interfaz tipo AXI-Lite para conectar MicroBlaze y el driver del MAX31865.

La idea general para solucionar este problema se plantea de la siguiente manera

Concepto de diseño para medir temperatura con MAX31865 y MicroBlaze mediante bus AXI
Concepto de diseño para medir temperatura con MAX31865 y MicroBlaze mediante bus AXI

Se tendrán 3 capas:

  • Acceso físico al MAX31865 mediante una interface que gestione el bus SPI.
  • Implementación de la máquina de estados que obtenga las lecturas.
  • Encapsulado de lo anterior en un bus AXI-Lite que permitirá acceder a los resultados leyendo una dirección de memoria.

Driver en VHDL para MAX31865

El primer paso será obtener lecturas correctas del MAX31865. Siguiendo la idea de la introducción por un lado estará la implementación de la interfaz SPI, por otro el control del MAX31865 a través de ella.

Interfaz SPI en VHDL para el MAX31865

La interfaz SPI será específica para esta aplicación con la intención de utilizar el menor número posible de recursos de la FPGA. Existe infinidad de información en la red sobre el funcionamiento de este bus, por lo que no se entrará en los detalles del mismo.

Cumplir el requisito de de pocos recursos requiere reducir la versatilidad de la interfaz a la mínima expresión. Únicamente se permitirá la lectura y escritura de registros en un modo SPI concreto y enfocado en la forma que trabaja el MAX31865.

Como señales de entrada/salida al interface MAX31865_serial_interface.vhd (entity) se tiene

entity MAX31865_serial_interface is
    Port ( cs_n     : out std_logic; --MAX31865 Control Signals
           sclk     : out std_logic;
           mosi     : out std_logic;
           miso     : in  std_logic;
           drdy_n   : in  std_logic;
   
           reset_n  : in  std_logic; --Driver Control Signals
           clk_spi      : in  std_logic;
           wr       : in  std_logic;
           addr     : in  std_logic_vector(7 downto 0);
           dat_i    : in  std_logic_vector(7 downto 0);
           dat_o    : out std_logic_vector(7 downto 0));
end MAX31865_serial_interface;
  • Señales propias del MAX31865
  • reset_n: Reset a nivel bajo de la interfaz.
  • clk_spi: Señal de reloj para el bus SPI y para la lógica de la interfaz, se aprovecharán los flancos de subida y de bajada.
  • wr: Esta entrada dará la orden de iniciar la transmisión/recepción de datos por le bus SPI.
  • addr (8 bit): Tras la señal wr se transmite por mosi la dirección a la que se quiere acceder en el MAX31865.
  • dat_i (8 bit): Una vez transmitida la dirección addr se transmite el dato a escribir en dicha dirección por la señal mosi de nuevo. Si la dirección es de lectura el MAX31865 lo ignorará.
  • dat_o (8 bit): De forma paralela a la transmisión se guardará el dato recibido por la señal miso en este registro.

Utilizando el modo SPI 3 el flujo de información entre el MAX31865 y la interfaz dependerá del flanco de la señal de reloj clk_spi:

  • Flanco de subida: De interfaz a MAX31865.
  • Flanco de bajada: De MAX31865 a interfaz.

Esto se consigue mediante dos bloques process, uno para cada flanco.

Por último, se define una máquina de estados que se actualizará en cada flanco de subida de la señal clk_spi, la cual contempla las siguientes circunstancias:

  1. Reset
  2. Estado de espera
  3. Modo SPI
  4. Escritura/lectura

Todo esto se realiza con la siguiente arquitectura de la entidad MAX31865_serial_interface.vhd:

architecture Behavioral of MAX31865_serial_interface is
    signal state : std_logic_vector(1 downto 0);
    signal pol   : std_logic;
    signal pulse : unsigned(3 downto 0);
    signal tmp   : std_logic_vector(15 downto 0);
    signal tmp_o : std_logic_vector(7 downto 0);
    signal cs    : std_logic;
    
begin
    dat_o <= tmp_o;
    sclk <= (not clk_spi) or pol;
    cs_n <= not cs;
    
process (reset_n, clk_spi) is
begin
    if(reset_n = '0') then  --Asynchronous Reset
        state <= "00";
        tmp <= X"0000";
        cs <= '0';
        mosi <= '0';
        pol <= '1';
        pulse <= "0000";
    elsif rising_edge(clk_spi) then
        case state is
            when "00" =>   --Wait for writing or reading pulse
                pol <= '1';
                cs <= '0';
                if(wr = '1') then 
                    state <= "01";
                    pulse <= "0000";
                    tmp(15 downto 8) <= addr;
                    tmp(7 downto 0) <= dat_i;
                end if;
            when "01" =>  --SPI MODE
                pol <= '1';
                cs <= '1';
                state <= "10";
            when "10" =>   --Write and Read SPI
                pol <= '0';
                cs <= '1';
                for i in 14 downto 0 loop
                    tmp(i+1) <= tmp(i);
                end loop;
                mosi <= tmp(15);
                pulse <= pulse + 1;
                if(pulse = 15) then
                    state <= "00";
                end if;
            when others =>
                state <= "00";
        end case;
    end if;
end process;

process (reset_n, clk_spi) is
begin
    if(reset_n = '0') then
        tmp_o <= X"00";
    elsif falling_edge(clk_spi) then
        if(cs = '1') then
            for i in 6 downto 0 loop
                tmp_o(i+1) <= tmp_o(i);
            end loop;
            tmp_o(0) <= miso;
        end if;
    end if;
end process;
end Behavioral;

Driver en VHDL para el MAX31865

Una vez se tiene la interfaz SPI se establece el comportamiento de la máquina de estados del driver. En el siguiente flujograma se pueden ver los eventos necesarios para obtener lecturas válidas en el registro RTD del MAX31865.

Flujograma para medir temperatura con MAX31865 y MicroBlaze
Flujograma para medir temperatura con MAX3186

La implementación del anterior flujograma en VHDL se encuentra en el fichero MAX31865_driver.vhd, alojado en un repositorio de GitHub cuyo enlace se encuentra al final.

Verificando el comportamiento del driver mediante simulación

Evidentemente es muy importante que el driver se comporte bien, en este sentido se debe verificar que los flujos de las máquinas de estados, información y estado de los pines en la FPGA son los esperados. En ningún caso será deseable enfrentar dos salidas o, simplemente, lidiar con un problema en las siguientes fases de desarrollo por un mal funcionamiento de esta parte tan básica.

El código VHDL para la simulación se encuentra en el fichero MAX31865_driver_t.vhd, cuyo concepto es el del siguiente fragmento de código:

architecture Behavioral of MAX31865_driver_t is
    constant PERIODO  : time  := 200 ns;
    --(...)
    --Simulation signals
    --(...)
    component MAX31865_driver is Port ( 
    --Port in/outs
    ); end component MAX31865_driver;
    
begin
    UUT : MAX31865_driver port map ( cs_n_t, sclk_t, mosi_t, miso_t, drdy_n_t,
                                      reset_n_t, clk_t, rtd_val_t);
    
    reset_n_t <= '1', '0' after (PERIODO/4), '1' after (PERIODO+PERIODO/4);
    clk_t <= not clk_t after (PERIODO/2);

    vector_test: process
    begin
        drdy_n_t <= '1'; wait for (146*PERIODO);
        drdy_n_t <= '0'; wait for (14*PERIODO + PERIODO/2);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (13*PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        drdy_n_t <= '1'; wait for (52*PERIODO);
        drdy_n_t <= '0'; wait for (15*PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (13*PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
        miso_t <= '1'; wait for (PERIODO);
        miso_t <= '0'; wait for (PERIODO);
    end process vector_test;
end Behavioral;
AVISO: Para visualizar la simulación completamente en un espacio razonable es necesario reducir la espera de 20ms a 3 pulsos de reloj y la de 230ms a 5 pulsos de reloj en el archivo MAX31865_driver.vhd. Es importante colocar los valores de espera correctos para la implementación final, de lo contrario no funcionará.

Los puntos a verificar son

  • Reset
  • Modo SPI correcto
  • Escritura serie en el orden correcto: MSB primero
  • Lectura serie en el orden correcto: MSB primero
  • Funcionamiento correcto de retardos o esperas

Vivado® devuelve la siguiente simulación

Behavioral Simulation del driver SPI para MAX31865
Behavioral Simulation del driver SPI para MAX31865

Verificando el driver en una Basys3

La verificación hasta este punto se realizará conectando una tarjeta de evaluación del MAX31865 a una Basys3 de Digilent®. El objetivo será visualizar el resultado de la conversión en los 16 indicadores LED de la tarjeta, que coinciden con el ancho de palabra del conversor.

Detalle de conexiones MAX31865 y Basys 3
Detalle de conexiones MAX31865 y Basys 3

Se crea un proyecto donde se instancia, además del driver, un generador de señal de reloj de 5MHz, el cual alimentará la señal clk_spi.

Vista del proyecto para verificar el Driver MAX31865 en una Basys 3
Vista del proyecto para verificar el Driver MAX31865 en una Basys 3

En un fichero de constraints se indican los pines de la interfaz serie y el registro RTD.

## Clock signal
set_property PACKAGE_PIN W5 [get_ports clk_in]							
   set_property IOSTANDARD LVCMOS33 [get_ports clk_in]
   create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk_in]
## LEDs
set_property PACKAGE_PIN U16 [get_ports {rtd_val[0]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[0]}]
set_property PACKAGE_PIN E19 [get_ports {rtd_val[1]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[1]}]
set_property PACKAGE_PIN U19 [get_ports {rtd_val[2]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[2]}]
set_property PACKAGE_PIN V19 [get_ports {rtd_val[3]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[3]}]
set_property PACKAGE_PIN W18 [get_ports {rtd_val[4]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[4]}]
set_property PACKAGE_PIN U15 [get_ports {rtd_val[5]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[5]}]
set_property PACKAGE_PIN U14 [get_ports {rtd_val[6]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[6]}]
set_property PACKAGE_PIN V14 [get_ports {rtd_val[7]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[7]}]
set_property PACKAGE_PIN V13 [get_ports {rtd_val[8]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[8]}]
set_property PACKAGE_PIN V3 [get_ports {rtd_val[9]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[9]}]
set_property PACKAGE_PIN W3 [get_ports {rtd_val[10]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[10]}]
set_property PACKAGE_PIN U3 [get_ports {rtd_val[11]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[11]}]
set_property PACKAGE_PIN P3 [get_ports {rtd_val[12]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[12]}]
set_property PACKAGE_PIN N3 [get_ports {rtd_val[13]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[13]}]
set_property PACKAGE_PIN P1 [get_ports {rtd_val[14]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[14]}]
set_property PACKAGE_PIN L1 [get_ports {rtd_val[15]}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {rtd_val[15]}]
##Central Button
set_property PACKAGE_PIN U18 [get_ports reset_in]		  
   set_property IOSTANDARD LVCMOS33 [get_ports reset_in]
##Pmod Header JB
##Sch name = JB1
set_property PACKAGE_PIN A14 [get_ports {cs_n}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {cs_n}]
##Sch name = JB2
set_property PACKAGE_PIN A16 [get_ports {sclk}]						
   set_property IOSTANDARD LVCMOS33 [get_ports {sclk}]
##Sch name = JB3
set_property PACKAGE_PIN B15 [get_ports {mosi}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {mosi}]
##Sch name = JB4					
set_property PACKAGE_PIN B16 [get_ports {miso}]
   set_property IOSTANDARD LVCMOS33 [get_ports {miso}]
##Sch name = JB7
set_property PACKAGE_PIN A15 [get_ports {drdy_n}]					
   set_property IOSTANDARD LVCMOS33 [get_ports {drdy_n}]

El siguiente video muestra el resultado, aunque no es muy intuitivo visualizar el resultado en binario, es más que suficiente para verificar el diseño hasta este punto.


Creando una IP AXI-Lite y MAX31865

Por último, queda crear la IP AXI-Lite que permitirá medir temperatura con MAX31865 y MicroBlaze.

Jerarquía de fuentes para el AXI driver y MAX31865
Jerarquía de fuentes para el AXI driver y MAX31865

La implementación es sumamente sencilla, pues solo se requiere conectar el resultado de la conversión a un registro de lectura del bus AXI, así como generar los puertos para la interfaz serie y la entrada de reloj. Se utilizará el reset del propio bus AXI para el driver e interfaz.

	    -- Address decoding for reading registers
	    loc_addr := axi_araddr(ADDR_LSB + OPT_MEM_ADDR_BITS downto ADDR_LSB);
	    case loc_addr is
	      when b"00" =>
	        reg_data_out(31 downto 16) <= X"0000";
	        reg_data_out(15 downto 0) <= rtd_val_out;
IP-Package del AXI Driver MAX31865
IP-Package del AXI Driver MAX31865

fatal error: xil_printf.h: No such file or directory

Este paso es necesario para evitar problemas con Vitis™ desde al menos la versión 2020 a la 2022 de Vivado®. Antes de utilizar la IP editar el Makefile en el sub-directorio de la misma:

MAX31865_AXI_1.0\drivers\MAX31865_AXI_v1_0\src

Las siguientes líneas deberían quedar como se indica a continuación:

RELEASEDIR=../../../lib
INCLUDEDIR=../../../include
INCLUDES=-I./. -I${INCLUDEDIR}

INCLUDEFILES=$(wildcard *.h)
LIBSOURCES=$(wildcard *.c)
OUTS=$(wildcard *.o
OBJECTS = $(addsuffix .o, $(basename $(wildcard *.c)))
ASSEMBLY_OBJECTS = $(addsuffix .o, $(basename $(wildcard *.S)))

libs:
	echo "Compiling MAX31865_AXI..."
	$(COMPILER) $(COMPILER_FLAGS) $(EXTRA_COMPILER_FLAGS) $(INCLUDES) $(LIBSOURCES)
	$(ARCHIVER) -r ${RELEASEDIR}/${LIB} ${OBJECTS} ${ASSEMBLY_OBJECTS}
	make clean

include:
	${CP} $(INCLUDEFILES) $(INCLUDEDIR)

clean:
	rm -rf ${OBJECTS} ${ASSEMBLY_OBJECTS}

Medir temperatura con MAX31865 y MicroBlaze

Por último, se procede a medir temperatura con MAX31865 y MicroBlaze mediante el siguiente diseño.

Block Design en Vivado para medir temperatura con MAX31865 y MicroBlaze
Block Design en Vivado® para medir temperatura con MAX31865 y MicroBlaze

Indicar en un archivo de constraints las conexiones de la interfaz del MAX31865 y exportar el hardware a un archivo «*.xsa«.

Con el Address Map de Vivado® para los periféricos AXI ver la dirección del driver del MAX31865, esta será necesaria para decirle a MicroBlaze donde leer los resultados de las conversiones.

Address Map Vivado y uso en Vitis
Address Map Vivado® y uso en Vitis

Al conectarse a la UART se verá la medida del RTD sin procesar y su correspondencia en grados celsius como en la siguiente captura.

Medida de temperatura con MAX31865 y MicroBlaze
Medida de temperatura con MAX31865 y MicroBlaze

Trabajo futuro

Mejorar este driver podría implicar:

  • Leer y escribir otros registros del MAX31865 a través de MicroBlaze.
  • Poder configurar en tiempo real el tiempo de muestreo de 230ms.
  • Implementar la conversión de RTD a temperatura mediante Vitis™ HLS.

La última propuesta requeriría un uso posiblemente desmedido de los recursos de la FPGA, pues resulta más económico realizar este tipo de operación desde MicroBlaze. De todas formas cualquier decisión dependerá de la solución requerida.

Repositorio GitHub «Medir temperatura con MAX31865 y MicroBlaze»

Todos los fuentes de esta entrada se pueden obtener en el siguiente repositorio de GitHub

https://github.com/abrahanlp/Temperature-measure-with-MAX31865-and-MicroBlaze