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.

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

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:
- Reset
- Estado de espera
- Modo SPI
- 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.

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

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.

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.

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™.

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;

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.

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.

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

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