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.
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.
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;
}
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
1SHOTinicia 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":
- Escribir
0x80(Dir 0x00 + Write Gap) y luego0x10(1SHOT bit) para iniciar. - Esperar el tiempo de conversión (aprox 50ms).
- 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;
}