Protocolos y API — SRAM Ring Buffer
Dependencias externas
| Librería | Proveedor | Propósito |
|---|---|---|
| libopencm3 | libopencm3.org | Capa de abstracción hardware para STM32 (SPI, GPIO, RCC, USART, SysTick, NVIC) |
| newlib-nano | ARM GNU Toolchain | printf, snprintf, syscalls (_write) |
El proyecto no depende de librerías de terceros más allá de libopencm3 y la toolchain estándar de ARM.
API del ring buffer SRAM
La librería sram.h implementa un ring buffer de
registros heterogéneos sobre la SRAM externa 23AA04M. Cada registro puede
tener un tipo y tamaño distinto, identificado por un type_id de 1 byte.
Tipos de datos
typedef uint8_t sram_record_type_t;
enum {
SRAM_TYPE_ANY = 0x00, // Wildcard: solo lectura (iterador)
SRAM_TYPE_SENSOR = 0x01, // sensor_log_t
SRAM_TYPE_CIRCUIT = 0x02, // circuit_cell_t
/* ── USER TYPES (0x03 – 0xFD) ── */
SRAM_TYPE_INVALID = 0xFE,
};
| Rango | Uso |
|---|---|
0x00 |
Wildcard para iteración (SRAM_TYPE_ANY) |
0x01 – 0x02 |
Tipos predefinidos de la demo |
0x03 – 0xFD |
Reservado para tipos de usuario |
0xFE |
Tipo inválido (marcador de fin) |
0xFF |
Marcador interno de WRAP (nunca usar como tipo de registro) |
Códigos de retorno
| Código | Valor | Significado |
|---|---|---|
SRAM_OK |
0 | Operación exitosa |
SRAM_ERR_FULL |
-1 | Buffer lleno sin registros que sobrescribir (num_records == 0) |
SRAM_ERR_NOT_INIT |
-2 | Cabecera mágica no válida (SRAM no formateada o corrompida) |
SRAM_ERR_PARAM |
-3 | Parámetro inválido (tipo no válido, puntero nulo, tamaño 0, registro > 524272 B) |
Nota:
SRAM_ERR_FULLsolo puede ocurrir si se intenta escribir un registro de más de 524272 bytes y el buffer está vacío. En la práctica,sram_write_recordsiempre sobrescribe los registros más antiguos para hacer hueco.
Funciones de la API
Inicialización
void sram_init(void);
Activa el modo secuencial SPI de la SRAM (comando WRMR + 0x40). Verifica la
cabecera mágica; si no es válida (primer uso o corrupción), ejecuta
sram_format() automáticamente. Imprime el estado de ocupación por USART1.
void sram_format(void);
Resetea la cabecera a valores iniciales sin tocar el área de datos:
- magic = 0x6D17
- num_records = 0
- head_pos = SRAM_RECORDS_START (0x000010)
- tail_pos = SRAM_RECORDS_START
- bytes_used = 0
⚠️ Advertencia:
sram_format()borra el índice de registros. Los datos antiguos en el área de registros quedan inaccesibles pero no se sobreescriben físicamente.
Escritura
sram_result_t sram_write_record(sram_record_type_t type,
const void *data,
uint16_t size);
Escribe size bytes del struct apuntado por data como un registro del tipo
type.
Algoritmo de escritura:
- Valida parámetros (tipo, puntero, tamaño)
- Lee la cabecera y verifica el magic number
- Si el nuevo registro no cabe antes del final de la SRAM → escribe WRAP marker
- Mientras no haya espacio suficiente → sobreescribe los registros más antiguos
- Escribe cabecera del registro (
type+payload_size) y payload - Actualiza la cabecera (posición, contador, bytes usados)
Ejemplo:
sensor_log_t log = {
.timestamp_ms = get_clock_ticks(),
.line_sensors = {100, 200, 300, 400},
.speed_left = 120,
.speed_right = 118,
.battery_pct = 79,
};
sram_result_t r = sram_write_record(SRAM_TYPE_SENSOR, &log, sizeof(log));
if (r != SRAM_OK) {
printf("Error: %d\n", r);
}
Lectura mediante iterador
void sram_iterator_init(sram_iterator_t *iterator, sram_record_type_t type);
bool sram_iter_next(sram_iterator_t *iterator, void *buf,
uint16_t buf_size, uint16_t *out_size);
Recorre los registros del tipo indicado (o todos con SRAM_TYPE_ANY) sin
necesitar un buffer completo en RAM.
| Parámetro | Significado |
|---|---|
iterator |
Estructura de estado del iterador (puntero a variable local) |
type |
Tipo de registros a iterar (SRAM_TYPE_ANY = todos) |
buf |
Buffer donde copiar el payload (puede ser NULL para solo contar) |
buf_size |
Tamaño máximo del buffer (truncación segura si < payload) |
out_size |
Recibe el payload_size real del registro leído |
Retorno: true si se leyó un registro, false si no hay más.
Ejemplo:
sram_iterator_t iterator;
sram_iterator_init(&iterator, SRAM_TYPE_SENSOR);
sensor_log_t log;
uint16_t stored_size;
while (sram_iter_next(&iterator, &log, sizeof(log), &stored_size)) {
if (stored_size != sizeof(log)) {
continue; // versión distinta del struct, saltar
}
printf("t=%lums L=%d R=%d bat=%u%%\n",
log.timestamp_ms, log.speed_left,
log.speed_right, log.battery_pct);
}
Estadísticas
uint16_t sram_get_total_records(void); // Total de registros
uint16_t sram_count_records(sram_record_type_t type); // Por tipo
char *sram_free_bytes(void); // String "usado/total (xx.xx%)"
| Función | Retorno | Nota |
|---|---|---|
sram_get_total_records() |
0 – 65535 | Lectura directa de la cabecera (O(1)) |
sram_count_records(type) |
0 – 65535 | Itera la SRAM entera (O(n)) |
sram_free_bytes() |
Cadena estática | Buffer interno de 32 bytes — no reentrante |
💡 Consejo: Usa
sram_get_total_records()en lugar desram_count_records(SRAM_TYPE_ANY)para obtener el total — la primera es O(1) leyendo la cabecera, la segunda itera toda la SRAM.
Layout de memoria en SRAM
┌──────────────────────────────────────────────────────┐
│ SRAM 23AA04M — 512 KB (0x000000 → 0x07FFFF) │
├──────────────┬───────────────────────────────────────┤
│ 0x000000 │ Cabecera — 16 bytes │
│ │ magic : uint16_t (0x6D17) │
│ │ num_records : uint16_t │
│ │ head_pos : uint32_t │
│ │ tail_pos : uint32_t │
│ │ bytes_used : uint32_t │
├──────────────┼───────────────────────────────────────┤
│ 0x000010 │ Área de registros — 524 272 bytes │
│ │ ┌─[type:1][size:2][payload:size]─┐ │
│ │ ├─[type:1][size:2][payload:size]─┤ │
│ │ │ ... │ │
│ │ └─[0xFF ][gap :2] ← WRAP marker │ │
└──────────────┴───────────────────────────────────────┘
Cada registro ocupa 3 bytes de cabecera (1 byte type_id + 2 bytes
payload_size) seguidos del payload.
Mecanismo WRAP
Cuando head_pos está demasiado cerca del final físico para albergar el
siguiente registro, se escribe un WRAP marker:
type = 0xFF(valor reservado, no válido como tipo de registro)payload_size = bytes restantes hasta el final de la SRAM
Tras escribir el WRAP:
1. bytes_used se incrementa en el espacio consumido por el marcador
2. head_pos salta a SRAM_RECORDS_START (0x000010)
Cuando el proceso de sobreescritura (sram_overwrite_oldest) encuentra un
WRAP en tail_pos, salta igualmente al inicio del área de registros.
Esto garantiza que en todo momento hay como máximo un único WRAP activo.
Sobreescritura (ring buffer)
Cuando el área de registros está llena:
1. sram_overwrite_oldest() lee el registro en tail_pos
2. Resta su tamaño de bytes_used
3. Avanza tail_pos al siguiente registro
4. Decrementa num_records
5. Si el registro eliminado libera suficiente espacio, se escribe el nuevo
6. Si no, se repite el proceso hasta tener espacio suficiente
Protocolo SPI de la 23AA04M
Formato de comando
Cada transacción SPI sigue la secuencia:
[CS low] → [CMD (1 byte)] → [ADDR (3 bytes)] → [DATA (N bytes)] → [CS high]
Comandos utilizados
| Comando | Código | Descripción |
|---|---|---|
| READ | 0x03 |
Lectura de N bytes desde dirección A |
| WRITE | 0x02 |
Escritura de N bytes desde dirección A |
| WRMR | 0x01 |
Escritura del registro de modo |
| RDMR | 0x05 |
Lectura del registro de modo |
Registro de modo
| Modo | Valor | Comportamiento |
|---|---|---|
| Byte | 0x00 |
Cada operación lee/escribe 1 byte (reenviar dirección) |
| Secuencial | 0x40 |
Ráfagas continuas sin reenviar dirección (usado en este proyecto) |
| Página | 0x80 |
Ráfagas de hasta 32 bytes por página |
sequenceDiagram
participant MCU as STM32F401
participant SRAM as 23AA04M
Note over MCU,SRAM: Inicialización — activar modo secuencial
MCU->>SRAM: CS low
MCU->>SRAM: WRMR (0x01)
MCU->>SRAM: Mode byte (0x40)
MCU->>SRAM: CS high
Note over MCU,SRAM: Escritura de registro
MCU->>SRAM: CS low
MCU->>SRAM: WRITE (0x02)
MCU->>SRAM: Address [23:0]
MCU->>SRAM: type (1B) + size (2B) + payload (N B)
MCU->>SRAM: CS high
Note over MCU,SRAM: Lectura de registro
MCU->>SRAM: CS low
MCU->>SRAM: READ (0x03)
MCU->>SRAM: Address [23:0]
MCU->>SRAM: dummy (0x00) × N → read N bytes
MCU->>SRAM: CS high
Funciones internas de bajo nivel
| Función | Descripción | Archivo |
|---|---|---|
sram_cs_select() |
PA15 low + delay 1 µs | sram.c:35 |
sram_cs_deselect() |
PA15 high + delay 1 µs | sram.c:40 |
sram_spi_byte(b) |
Transfiere 1 byte por SPI1 | sram.c:45 |
sram_send_addr(addr) |
Envía dirección de 24 bits | sram.c:49 |
sram_write_bytes(addr, buf, len) |
Escribe N bytes en dirección A | sram.c:55 |
sram_read_bytes(addr, buf, len) |
Lee N bytes desde dirección A | sram.c:66 |
Añadir un nuevo tipo de registro
-
Definir el ID en
sram.h:enum { // ... existing types ... SRAM_TYPE_MI_DATO = 0x03, // nuevo tipo }; -
Definir el struct con
__attribute__((packed))para evitar padding:typedef struct __attribute__((packed)) { uint32_t campo_a; uint16_t campo_b; bool campo_c; } mi_dato_t; -
Escribir y leer:
// Escritura mi_dato_t dato = { .campo_a = 42, .campo_b = 7, .campo_c = true }; sram_write_record(SRAM_TYPE_MI_DATO, &dato, sizeof(dato)); // Lectura con iterador sram_iterator_t it; sram_iterator_init(&it, SRAM_TYPE_MI_DATO); mi_dato_t record; uint16_t stored_size; while (sram_iter_next(&it, &record, sizeof(record), &stored_size)) { if (stored_size != sizeof(record)) continue; // skip version mismatch printf("a=%lu b=%u c=%d\n", record.campo_a, record.campo_b, record.campo_c); }
⚠️ Advertencia: Si cambias la definición del struct después de haber escrito registros, usa
out_sizedesram_iter_nextpara detectar tamaños antiguos y evitar leer datos mal alineados o truncados.
Documento generado el 2026-06-27. Ver también Arquitectura Software, Known Issues.