AVR Programación en C – 13 SPI modo maestro por software (Bit Bang)

En este post explicaré como escribir un programa que implemente el protocolo SPI en modo maestro por software, es decir, generando a mano los niveles de 1’s y 0’s necesario para comunicarnos con el dispositivo que lo requiera. La ventaja de implementar el protocolo SPI por software es que puede implementarse en cualquier pin y puerto del micro, en caso de que este no cuente con un módulo dedicado al SPI o en caso de que se necesite un bus SPI adicional. Aemás ayuda a comprender mas a fondo cómo funciona este protocolo.

Contenido:


Descripción

En la entrada dedicada al Protocolo SPI se describen a detalle su funcionamiento y sus características, y el porqué es tan popular. En esta entrada se muestra como programar el protocolo SPI por software utilizando cualquier pin de cualquier puerto de un microcontrolador AVR como interfaz del bus SPI.

Cabe destacar que las funciones del protocolo expuestas aquí pueden ser aplicadas a cualquier otro microcontrolador que soporte programación en C, ya que lo único que varía entre microcontroladores son las instrucciones para leer y escribir en sus puertos.

En este modo del SPI, el micro que funge como maestro es el que controla tanto el intercambio de datos como la señal de reloj hacia y desde todos los dispositivos conectados al bus. Para probar el programa que implementa el protocolo SPI, se utiliza un registro de corrimiento de 8 bits 74HC595, el cual implementa una interfaz serial y es perfecto para ilustrar el protocolo, además de el microcontrolador ATmega32 (es el que tengo disponible en estos momentos) actuando como dispositivo maestro. El programa funciona en cualquier micro AVR que incorpore el puerto B del AVR (disponible en la mayoría de AVR’s). El circuito propuesto es el siguiente:

74hc595-sch
Conexión del circuito 74xx595 al bus SPI

Los pines MOSI, MISO, SCK y SS se conectan a los pines PB0, PB1, PB2 y PB3  del AVR (respectivamente). Para enviar un dato, lo primero que se hace es seleccionar el dispositivo escribiendo a 0 el pin SS, en seguida se establece el valor del pin MOSI dependiendo del bit a enviar, ya sea 0 o 1 y seguida se genera un pulso de reloj para enviar el dato. Al terminar el pulso de envío, el dispositivo esclavo responde con un bit por el pin MISO, que estará disponible hasta el siguiente pulso de reloj y debe ser procesado en caso de necesitarlo. Al terminar el envío y recepción de todos los bits se libera dispositivo del bus estableciendo a 1 el pin SS.

Para generar los pulsos de reloj se escribe un 1 o 0 en el pin SCK (dependiendo de la polaridad), se espera un tiempo TF, se invierta el estado del pin SCK y se vuelve a esperar otro tiempo TF. El cálculo del tiempo TF en microsegundos se puede realizar de la siguiente manera:

TF = ( 1 / SPI_FREQ * 1000000 ) / 2

Se divide entre 2 ya que se desea generar un ciclo de trabajo del 50% de la señal del reloj. Por ejemplo, para calcular el tiempo TF para una frecuencia de 10kHz (10000 hz) se tiene:

TF = ( 1 /  10000 * 1000000 ) / 2    =   50us

Por lo que el tiempo de espera entre cada cambio del pin SCK debe ser de al menos 50us para una frecuencia de 10kHz.

Para el caso del registro 74xx595, los datos a enviar contienen una longitud de 8 bits, representando el bit QA como el menos significativo y el bit QH como el más significativo con un pulso de subida de reloj para el muestreo de datos. Con esto, el maestro debe enviar los bits de datos del bit más significativo al bit menos significativo por el pin MOSI, recibiéndolos de igual manera por el pin MISO.

spi-595

Ejemplo de programación

La implementación de este protocolo es relativamente sencilla, aquí se muestra una manera de hacerlo utilizando macros para especificar los puertos, los pines y el cambio de estado de los pines.

/******************************************************/
#include <avr/io.h>
#include <util/delay.h>

// Define los puertos y los pines del micro para SPI
#define SPI_DDR		DDRB
#define SPI_PORT	PORTB
#define SPI_PIN		PINB
#define SPI_MOSI	0
#define SPI_MISO	1
#define SPI_SCK		2
#define SPI_CS		3
#define SPI_FREQ	8000 // 8 KHz

// Macros para escribir en pines de salida (1 y 0)
#define SPI_MOSI_HIGH (SPI_PORT |= (1<<SPI_MOSI))
#define SPI_MOSI_LOW (SPI_PORT &= ~(1<<SPI_MOSI))

#define SPI_SCK_HIGH (SPI_PORT |= (1<<SPI_SCK))
#define SPI_SCK_LOW (SPI_PORT &= ~(1<<SPI_SCK))

#define SPI_CS_HIGH (SPI_PORT |= (1<<SPI_CS))
#define SPI_CS_LOW (SPI_PORT &= ~(1<<SPI_CS))

// Macro para leer pin de entrada MISO
#define SPI_MISO_STATE (SPI_PIN & (1<<SPI_MISO))

// Macros para retardos.
#define delay_ms(MS) for( unsigned long i = 0; i< MS; i++) _delay_ms(1)
#define delay_us(US) for( unsigned long i = 0; i< US; i++) _delay_us(1)

// Guarda el tiempo de retardo para el periodo de SCK
unsigned long spi_delay;

void spi_init();
void spi_write(uint8_t data);
uint8_t spi_readwrite(uint8_t data);

void AVRInit()
{
	spi_init();
	spi_write(0);
}

int main()
{
	uint8_t i = 0;
	AVRInit();

	while (1)
	{
		i=0;
		while(i<8)
		{
			spi_write(1 << i++);
			delay_ms(80);
		}
	}

	return 0;
}

void spi_init()
{
	// Pines MOSI, SCK y CS como salida, pin MISO como entrada
	DDRB |= (1 << SPI_MOSI) | (1 << SPI_SCK) | (1 << SPI_CS) | (0 << SPI_MISO);
	spi_delay = ((1.0 / SPI_FREQ) * 1000000UL) / 2;
}

void spi_write(uint8_t data) {
	// Selecciona el dispositivo SPI con CS=0
	SPI_CS_LOW;

	for (int i = 0; i < 8; i++)
	{
		// Checa el bit mas significativo y lo pone en
		// el bus (PIN MOSI)
		if ((data << i) & 0x80)
			SPI_MOSI_HIGH;
		else
			SPI_MOSI_LOW;

		// Envía pulso de reloj (CHPOL = )
		SPI_SCK_LOW;
		delay_us(spi_delay);
		SPI_SCK_HIGH;
		delay_us(spi_delay);
	}

	// Libera el dispositivo SPI con CS=1
	SPI_CS_HIGH;
}

uint8_t spi_readwrite(uint8_t data)
{
	uint8_t rd = 0;

	// Selecciona el dispositivo SPI con CS=0
	SPI_CS_LOW;

	for (int i = 0; i < 8; i++)
	{
		rd =  rd <<1;
		// Checa el bit mas significativo y lo pone en
		// el bus (PIN MOSI)
		if ((data << i) & 0x80)
			SPI_MOSI_HIGH;
		else
			SPI_MOSI_LOW;

		// Envía pulso de reloj (CHPOL = )
		SPI_SCK_LOW;
		delay_us(spi_delay);

		// Lee el bit recibido en MISO
		if (SPI_MISO_STATE)
			rd |= 1;

		SPI_SCK_HIGH;
		delay_us(spi_delay);

	}

	// Libera el dispositivo SPI con CS=1
	SPI_CS_HIGH;

	return rd;
}

El programa genera una secuencia en los leds del circuito propuesto, utiliza 3 funciones para realizar la comunicación: spi_init, spi_write y spi_readwrite. 

Las macros definidas al principio del código definen los puertos, los pines y la escritura en los pines dependiendo del microcontrolador a usar. Para este ejemplo se utiliza el puerto B como puerto a usar y las macros para poner a 1 o 0 los pines de ese puerto. Tambien define macros para retardos en milisegundos y microsegundos.

La función spi_init, configura los pines PB0, PB2 y PB3 como salida para las señales MOSI, SCK y SS, el pin MISO como entrada y calcula el tiempo de retardo según la frecuencia, que para este caso es de 8 kHz (8000 Hz). Aquí se debe cambiar la inicialización de puertos para cada microcontrolador.

la función spi_write, escribe un dato de 8 bits por el pin MOSI generando las señales de activación por SCK y SS. Esta función no toma en cuenta el dato recibido por el pin MISO.

La función spi_readwrite, es similar que spi_write, la diferencia es que devuelve el dato recibido por el pin MISO.

Con este sencillo ejemplo se muestra como funciona la comunicación SPI y su implementación por software. En otro post mostraré como utilizar este protocolo de comunicación utilizando el módulo de hardware de los AVR.

Conclusiones

Como se puede observar, es muy útil implementar el protocolo de comunicación SPI en cualquier puerto y pin de un microcontrolador, aunque no se recomienda para transferencias de datos a muy alta velocidad,  funciona con la mayoría de dispositivos de baja-media velocidad, tomando en cuenta la velocidad de funcionamiento del micro.

c1-blue
Cödigo fuente
github
Repositorio en GitHub

5 comentarios sobre “AVR Programación en C – 13 SPI modo maestro por software (Bit Bang)

Add yours

  1. Claro me quede pensando que data al correrse una vez luego dos veces luego tres veces hasta la octava vez, imagine que data se modificaba pero ahora caigo en cuenta que data nunca se modifica, en assembler siempre el registro al hacer el corrimiento se modifica pero en lenguaje C al no haber una asignacion nueva de data no se ve modificado por lo tanto tienes razon, data conserva siempre el mismo valor, error mio al no pensarlo bien, de igual forma pienso que si hiciera (data<<1) y luego actualizara data con data<<=1 serviria igual el corrimiento, en fin debe existir mil formas de hacerlo, la idea detras es crear el desplazamiento y obtener el bit a enviar al pin MOSI del protocolo SPI. Gracias por tu respuesta y muy utiles tus explicaciones. Saludos

  2. Estuve analizando el codigo de transmision y utilizas la instruccion (data << i) y me parece que estaria incorrecto, deberia ser (data << 1) para ir desplazando al MSB cada bit del dato para transmitirlo en cada flanco de subida del .Saludos

    1. De hecho si es (data << i) ya que envía del dato más significativo al menos significativo en cada iteracion, y la i te da el desplazamiento hacia la derecha y la máscara AND con 0x80 te da el bit

Dejar un comentario

Crea una web o blog en WordPress.com

Subir ↑