← Volver al inicio

Laboratorio 6: Comunicación Serie SPI

Serial Peripheral Interface: Velocidad y Sincronización

1. ¿Qué es SPI?

SPI es un protocolo de comunicación síncropo, Full-Duplex y de arquitectura Maestro-Esclavo. A diferencia del UART, utiliza una señal de reloj dedicada, lo que permite velocidades mucho mayores (hasta decenas de MHz).

Señales del Bus:

SCK

Serial Clock

Generado por el Maestro. Sincroniza los bits.

MOSI

Master Out Slave In

Datos del Maestro al Esclavo.

MISO

Master In Slave Out

Datos del Esclavo al Maestro.

NSS / CS

Chip Select (Active Low)

Selecciona al Esclavo activo.

Funcionamiento Clave: SPI funciona como un registro de desplazamiento circular. SIEMPRE que el Maestro envía un bit, recibe otro simultáneamente del Esclavo. No existe "solo enviar" o "solo recibir"; siempre es un intercambio (Transfer).

2. Modos de Operación (CPOL y CPHA)

Para comunicarse correctamente, Maestro y Esclavo deben acordar cómo funciona el reloj (SCK). Esto se define con dos parámetros en el registro CR1:

CPOL (Polaridad del Reloj)

  • 0: SCK en bajo (Low) cuando está inactivo.
  • 1: SCK en alto (High) cuando está inactivo.

CPHA (Fase del Reloj)

  • 0: Captura de datos en el 1er flanco.
  • 1: Captura de datos en el 2do flanco.

Modo 0 (CPOL=0, CPHA=0) es el más común en sensores y memorias flash.

🛠️ Simulador Interactivo SPI Abrir en pantalla completa ↗

3. Pines SPI1 en STM32F103 (APB2)

Para usar el periférico SPI1, debemos configurar los pines GPIOA correspondientes.

Señal Pin Modo GPIO Requerido Configuración (CNF, MODE)
SCK PA5 Alternate Function Push-Pull CNF=10, MODE=11 (50MHz)
MISO PA6 Input Floating / Pull-Up CNF=01, MODE=00 (Floating)
MOSI PA7 Alternate Function Push-Pull CNF=10, MODE=11 (50MHz)
NSS (CS) PA4 (u otro) General Purpose Output CNF=00, MODE=11 (Software Control)

Nota: El Chip Select (CS/NSS) suele controlarse por software (GPIO normal) para mayor flexibilidad. Configura el bit SSM=1 y SSI=1 en CR1 para controlarlo manualmente.

4. Configuración Paso a Paso: SPI Maestro

Configuraremos SPI1 como Maestro, velocidad f_PCLK/32, Modo 0, MSB First.

Paso 1: Habilitar Relojes

Habilitar reloj de GPIOA y SPI1 (ambos en APB2).

RCC->APB2ENR |= (1 << 2);  // GPIOA Enable
RCC->APB2ENR |= (1 << 12); // SPI1 Enable
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;

Paso 2: Configuración GPIO

PA5 (SCK) y PA7 (MOSI) como AF Push-Pull. PA6 (MISO) como Input.

// PA5 (SCK) y PA7 (MOSI): AF Push-Pull 50MHz
GPIOA->CRL &= 0x0F0FFFFF; // Limpiar PA5/PA7
GPIOA->CRL |= 0xB0B00000; // 0xB = 1011 (AF PP, 50MHz)

// PA6 (MISO): Input Floating (0x4 por defecto)
GPIOA->CRL &= 0xF0FFFFFF;
GPIOA->CRL |= 0x04000000; 

// PA4 (CS Software): GP Output Push-Pull
GPIOA->CRL &= 0xFFF0FFFF;
GPIOA->CRL |= 0x00030000; // 50MHz GP Output
GPIOA->BSRR = (1 << 4);   // CS Alto (Inactivo)
// PA5 (SCK) | PA7 (MOSI)
GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5 | GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
GPIOA->CRL |= (GPIO_CRL_CNF5_1 | GPIO_CRL_MODE5 | GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7);

// PA6 (MISO)
GPIOA->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6);
GPIOA->CRL |= GPIO_CRL_CNF6_0;

// PA4 (CS)
GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
GPIOA->CRL |= GPIO_CRL_MODE4;
GPIOA->BSRR = GPIO_BSRR_BS4;

Paso 3: Configurar SPI (CR1)

Baudrate, Modo Maestro, CPOL/CPHA y Gestión de Software Slave (SSM).

SPI1->CR1 = 0; // Reset
// Baudrate: f_PCLK/32 -> Bits BR[2:0] = 100 (4)
SPI1->CR1 |= (4 << 3); 
// Master Mode (MSTR)
SPI1->CR1 |= (1 << 2);
// Software Slave Management (SSM=1, SSI=1)
SPI1->CR1 |= (1 << 9) | (1 << 8); 
// Habilitar SPI (SPE)
SPI1->CR1 |= (1 << 6);
SPI1->CR1 = 0;
// Baudrate Prescaler = 32
SPI1->CR1 |= SPI_CR1_BR_2; 
// Master Mode
SPI1->CR1 |= SPI_CR1_MSTR;
// SSM y SSI (Evita error de modo maestro)
SPI1->CR1 |= SPI_CR1_SSM | SPI_CR1_SSI;
// Enable SPI
SPI1->CR1 |= SPI_CR1_SPE;

5. Función de Transferencia (Full-Duplex)

En SPI, enviar y recibir son la misma acción. Escribir en DR inicia el reloj, y cuando termina, el dato recibido está capturado en DR.

uint8_t SPI1_Transfer(uint8_t data) {
    // 1. Esperar a que el buffer de transmisión esté vacío (TXE)
    while (!(SPI1->SR & (1 << 1)));
    
    // 2. Escribir dato a enviar
    SPI1->DR = data;
    
    // 3. Esperar a que llegue el dato (RXNE)
    // El hardware envía y recibe simultáneamente
    while (!(SPI1->SR & (1 << 0)));
    
    // 4. Retornar dato recibido
    return SPI1->DR;
}
uint8_t SPI1_Transfer(uint8_t data) {
    // 1. Esperar TXE (Transmit Empty)
    while (!(SPI1->SR & SPI_SR_TXE));
    
    // 2. Escribir dato
    SPI1->DR = data;
    
    // 3. Esperar RXNE (Receive Not Empty)
    while (!(SPI1->SR & SPI_SR_RXNE));
    
    // 4. Leer dato
    return SPI1->DR;
}
Nota sobre el Flag BSY: No uses el flag BSY para comprobar si puedes enviar. Usa TXE. El flag BSY es útil para saber si la transmisión ha finalizado totalmente antes de deshabilitar el periférico o bajar el Chip Select.

6. Ejemplo Completo: Loopback / Envío Continuo

Este programa envía un byte incrementado continuamente. Puedes conectar MISO con MOSI (PA6 con PA7) para hacer un "Loopback" y verificar que lo que envías es lo que recibes.

#include "stm32f10x.h"

void SPI1_Init(void);
uint8_t SPI1_Transfer(uint8_t data);
void Delay(uint32_t val);

int main(void)
{
    SPI1_Init();
    uint8_t counter = 0;
    uint8_t received;
    
    while(1)
    {
        // 1. Seleccionar esclavo (CS Low)
        GPIOA->BSRR = (1 << 20); // Reset PA4 (4+16)
        
        // 2. Transferir dato
        received = SPI1_Transfer(counter);
        
        // 3. Deseleccionar esclavo (CS High)
        GPIOA->BSRR = (1 << 4);  // Set PA4
        
        counter++;
        Delay(100000);
    }
}

void SPI1_Init(void)
{
    // Habilitar Relojes
    RCC->APB2ENR |= (1 << 2) | (1 << 12); // GPIOA, SPI1
    
    // Configurar GPIOs (PA5=SCK, PA6=MISO, PA7=MOSI)
    // Limpiar Nibbles
    GPIOA->CRL &= 0x000FFFFF; 
    // Configurar: PA5(AF_PP), PA6(IN_FLOAT), PA7(AF_PP)
    // 0xB = 1011, 0x4 = 0100
    GPIOA->CRL |= 0xB4B00000;
    
    // Configurar PA4 (CS) como Output Push-Pull
    GPIOA->CRL &= ~(0xF << 16);
    GPIOA->CRL |=  (0x3 << 16);
    GPIOA->BSRR =  (1 << 4); // CS inactivo (High)
    
    // Configurar SPI1
    // BR=32 (100), MSTR=1, SSM=1, SSI=1, SPE=1
    SPI1->CR1 = (4 << 3) | (1 << 2) | (1 << 9) | (1 << 8) | (1 << 6);
}

uint8_t SPI1_Transfer(uint8_t data) {
    while (!(SPI1->SR & (1 << 1))); // TXE
    SPI1->DR = data;
    while (!(SPI1->SR & (1 << 0))); // RXNE
    return SPI1->DR;
}

void Delay(uint32_t val) {
    for(; val > 0; val--) __NOP();
}
#include "stm32f10x.h"

void SPI1_Init(void);
uint8_t SPI1_Transfer(uint8_t data);
void Delay(uint32_t val);

int main(void)
{
    SPI1_Init();
    uint8_t counter = 0;
    uint8_t received;
    
    while(1)
    {
        // CS Low
        GPIOA->BSRR = GPIO_BSRR_BR4;
        
        // Transfer
        received = SPI1_Transfer(counter);
        
        // CS High
        GPIOA->BSRR = GPIO_BSRR_BS4;
        
        counter++;
        Delay(100000);
    }
}

void SPI1_Init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
    
    // PA5(SCK), PA7(MOSI) -> AF Push-Pull
    GPIOA->CRL &= ~(GPIO_CRL_CNF5 | GPIO_CRL_MODE5 | GPIO_CRL_CNF7 | GPIO_CRL_MODE7);
    GPIOA->CRL |= (GPIO_CRL_CNF5_1 | GPIO_CRL_MODE5 | GPIO_CRL_CNF7_1 | GPIO_CRL_MODE7);
    
    // PA6(MISO) -> Input Floating
    GPIOA->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_MODE6);
    GPIOA->CRL |= GPIO_CRL_CNF6_0;
    
    // PA4(CS) -> GP Output
    GPIOA->CRL &= ~(GPIO_CRL_CNF4 | GPIO_CRL_MODE4);
    GPIOA->CRL |= GPIO_CRL_MODE4_1 | GPIO_CRL_MODE4_0; // 50MHz
    GPIOA->BSRR = GPIO_BSRR_BS4; // High
    
    // SPI Config
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI | SPI_CR1_BR_2 | SPI_CR1_SPE;
}

uint8_t SPI1_Transfer(uint8_t data) {
    while (!(SPI1->SR & SPI_SR_TXE));
    SPI1->DR = data;
    while (!(SPI1->SR & SPI_SR_RXNE));
    return SPI1->DR;
}

void Delay(uint32_t val) {
    for(; val > 0; val--) __NOP();
}

7. Sensor Real: Termómetro MAX31723

El MAX31723 es un sensor de temperatura digital de alta precisión (-55°C a +125°C) que utiliza una interfaz SPI conocida como 3-Wire. Es ideal para practicar porque requiere enviar comandos y leer respuestas.

Pinout y Conexión

Aunque funciona por SPI, tiene una particularidad: El Chip Enable (CE) es Activo en ALTO (al revés que el CS estándar).

  • 🔌 CE (Pin 1) → GPIO (Activo High)
  • 🔌 SCLK (Pin 2) → SCK
  • 🔌 SDI (Pin 3) → MOSI
  • 🔌 SDO (Pin 4) → MISO

Registros Importantes

  • 📂 0x00 (Config): Bit 1SHOT inicia conversión.
  • 📂 0x01 (LSB Temp): Parte decimal del dato.
  • 📂 0x02 (MSB Temp): Parte entera del dato.

Para escribir en un registro, se suma 0x80 a la dirección (bit de escritura).

Lectura de Temperatura (Ejemplo Conceptual)

Para leer la temperatura en modo "One Shot":

  1. Escribir 0x80 (Dir 0x00 + Write Gap) y luego 0x10 (1SHOT bit) para iniciar.
  2. Esperar el tiempo de conversión (aprox 50ms).
  3. Leer registro 0x01 (LSB) y 0x02 (MSB).
// Asumiendo SPI1_Init() y SPI1_Transfer() ya definidos
// PA4 conectado a CE (Chip Enable)

void MAX31723_ReadTemp(void) {
    uint8_t msb, lsb;
    
    // 1. Iniciar Conversión (Escribir 0x10 en Reg 0x00)
    GPIOA->BSRR = (1 << 4);        // CE High (Start)
    SPI1_Transfer(0x80);           // Write Address 0x00
    SPI1_Transfer(0x10);           // Data: 1SHOT = 1
    GPIOA->BSRR = (1 << 20);       // CE Low (Stop)
    
    // 2. Esperar conversión
    Delay(500000); 
    
    // 3. Leer Temperatura (Burst Read desde 0x01)
    GPIOA->BSRR = (1 << 4);        // CE High
    SPI1_Transfer(0x01);           // Read Address 0x01
    lsb = SPI1_Transfer(0x00);     // Dummy para leer LSB
    msb = SPI1_Transfer(0x00);     // Dummy para leer MSB
    GPIOA->BSRR = (1 << 20);       // CE Low
    
    // Convertir (MSB es parte entera, LSB decimal)
    volatile int16_t raw_temp = (msb << 8) | lsb;
    volatile float real_temp = raw_temp * 0.00390625; // 1/256 resolución
}
// Asumiendo SPI1_Init() y SPI1_Transfer() ya definidos

void MAX31723_ReadTemp(void) {
    uint8_t msb, lsb;
    
    // 1. Start Conversion
    GPIOA->BSRR = GPIO_BSRR_BS4;   // CE High (Active)
    SPI1_Transfer(0x80 | 0x00);    // Write Reg 0x00
    SPI1_Transfer(0x10);
    GPIOA->BSRR = GPIO_BSRR_BR4;   // CE Low
    
    Delay(500000);
    
    // 2. Read Data
    GPIOA->BSRR = GPIO_BSRR_BS4;
    SPI1_Transfer(0x01);           // Read Reg 0x01
    lsb = SPI1_Transfer(0x00);
    msb = SPI1_Transfer(0x00);
    GPIOA->BSRR = GPIO_BSRR_BR4;
    
    // Convert
    int16_t temp16 = (msb << 8) | lsb;
    float tempC = temp16 / 256.0f;
}