A diferencia de la recepción (RX) donde no sabemos cuándo llegarán los datos, en la transmisión (TX) nosotros tenemos el control absoluto. El registro de estado de la UART tiene un bit fundamental: TXE (Transmit Data Register Empty). Este bit nos indica cuándo el hardware ha terminado de empujar el último byte por el cable y está listo para recibir el siguiente.
Un código bloqueante se queda atrapado en un bucle while(!(USART->SR & TXE));. Un código asíncrono invierte la lógica: el Súper Bucle solo verifica el bit TXE con un simple if. Si está en 1, saca una letra de la Cola de Impresión (Spooler), la deposita en el UART y sigue su camino al instante. Si está en 0, no hace nada y sigue procesando la metrología.
Para no saturar la memoria RAM almacenando todo el ticket en un arreglo gigante antes de imprimir (lo cual consumiría demasiados Kilobytes), crearemos una Máquina de Estados que ensamble el ticket al vuelo. Pasará por las fases: INIT -> ENCABEZADO -> DATOS_VENTA -> PIE_PAGINA -> CORTE, inyectando cadenas cortas a la Cola de Impresión según se vaya vaciando.
Ejercicio 1: Construiremos una Cola de Transmisión tx_ring_buffer. Luego, crearemos la función despachadora impresora_tick() que se ejecutará en cada ciclo del main(), evaluando el bit TXE. Finalmente, diseñaremos la impresora_maquina_estados() que inyectará las frases del ticket a la cola solo cuando detecte que hay espacio suficiente, evitando saturar la memoria RAM.
/**
* @file printer_async.c
* @brief Máquina de Estados y Cola de Impresión (Spooler TX) Bare-Metal
*/
#include "stm32f401xe.h"
#include <stdbool.h>
#define TX_BUFFER_SIZE 256
// --- COLA DE IMPRESIÓN (RING BUFFER) ---
volatile uint8_t tx_buffer[TX_BUFFER_SIZE];
volatile uint16_t tx_head = 0; // Por donde ingresamos letras
volatile uint16_t tx_tail = 0; // Por donde las enviamos al UART
// --- MÁQUINA DE ESTADOS DEL TICKET ---
typedef enum {
TICKET_IDLE,
TICKET_ENCABEZADO,
TICKET_DATOS_VENTA,
TICKET_PIE_PAGINA,
TICKET_CORTE
} EstadoTicket_t;
EstadoTicket_t estado_ticket = TICKET_IDLE;
// Dependencias de nuestro sistema
extern uint32_t litros_despachados_x1000;
extern void u32_a_string(uint32_t valor, char* buffer);
/**
* @brief Añade un arreglo de bytes a la Cola de Impresión (Non-blocking)
* @return bool True si hubo espacio, False si el búfer está lleno.
*/
bool impresora_encolar_datos(const uint8_t* datos, uint16_t longitud) {
// Calculamos el espacio libre en el Ring Buffer
uint16_t espacio_libre = (TX_BUFFER_SIZE + tx_tail - tx_head - 1) % TX_BUFFER_SIZE;
if (longitud > espacio_libre) {
return false; // No hay espacio. Intentaremos en el siguiente ciclo.
}
// Si hay espacio, copiamos los datos al búfer circular
for (uint16_t i = 0; i < longitud; i++) {
tx_buffer[tx_head] = datos[i];
tx_head = (tx_head + 1) % TX_BUFFER_SIZE;
}
return true;
}
/**
* @brief Extrae datos de la Cola y los envía a la Impresora Gota a Gota
* ¡Debe llamarse incesantemente en el while(1)!
*/
void impresora_tick(void) {
// 1. ¿Hay caracteres esperando en la cola?
if (tx_head != tx_tail) {
// 2. ¿El cable UART ya terminó de enviar el caracter anterior? (TXE = 1)
if (USART1->SR & USART_SR_TXE) {
// Empujamos el byte al hardware
USART1->DR = tx_buffer[tx_tail];
// Avanzamos el puntero de cola
tx_tail = (tx_tail + 1) % TX_BUFFER_SIZE;
}
}
}
/**
* @brief Construye el Ticket por fases para no desbordar la memoria RAM.
*/
void impresora_maquina_estados(void) {
switch(estado_ticket) {
case TICKET_IDLE:
// No hacemos nada hasta que alguien cambie el estado a ENCABEZADO
break;
case TICKET_ENCABEZADO:
// Intentamos encolar el bloque. Si regresa True, pasamos al siguiente estado.
{
uint8_t header[] = {0x1B, '@', 0x1B, 'a', 1, 'G','A','S',' ','E','L',' ','S','O','L','\n'};
if (impresora_encolar_datos(header, sizeof(header))) {
estado_ticket = TICKET_DATOS_VENTA;
}
}
break;
case TICKET_DATOS_VENTA:
{
char buf_str[30] = "Litros: ";
char buf_num[10];
u32_a_string(litros_despachados_x1000 / 1000, buf_num);
// Concatenación rudimentaria (En producción usarías tu motor de logs del Día 131)
uint8_t i = 8, j = 0;
while(buf_num[j] != '\0') { buf_str[i++] = buf_num[j++]; }
buf_str[i++] = '\n';
if (impresora_encolar_datos((uint8_t*)buf_str, i)) {
estado_ticket = TICKET_PIE_PAGINA;
}
}
break;
case TICKET_PIE_PAGINA:
{
uint8_t footer[] = "Gracias por su compra!\n\n\n";
if (impresora_encolar_datos(footer, 25)) {
estado_ticket = TICKET_CORTE;
}
}
break;
case TICKET_CORTE:
{
uint8_t cut[] = {0x1D, 'V', 66, 0}; // Comando Guillotina
if (impresora_encolar_datos(cut, sizeof(cut))) {
// ¡Ticket Terminado! Regresamos a reposo.
estado_ticket = TICKET_IDLE;
}
}
break;
}
}
int main(void) {
// inicializar_usart1_impresora();
while(1) {
// El corazón de la máquina: Flujo de Metrología al 100% de CPU
// calcular_presion_y_volumen();
// Si el operador aprueba la venta, disparamos la FSM
if (bandera_solicitar_ticket) {
estado_ticket = TICKET_ENCABEZADO;
bandera_solicitar_ticket = false;
}
// 1. Ensambla el texto poco a poco si hay espacio
impresora_maquina_estados();
// 2. Extrae las letras del búfer gota a gota sin bloquear jamás
impresora_tick();
}
return 0;
}
Objetivo del día: Multitarea Real y Erradicación de Retardos Mecánicos.
Las impresoras térmicas son lentas por naturaleza física: deben quemar el papel y mover un motor a pasos. Si obligaras a tu microcontrolador a enviar el ticket de venta usando una función bloqueante, tu CPU estaría inactivo para cualquier otra tarea durante una eternidad relativa (decenas de milisegundos). Si en ese preciso instante el medidor de gas emite un pulso, lo perderías. Y si pierdes pulsos, regalas hidrocarburo.
La **Cola de Impresión (Spooler)** que acabas de diseñar aísla la lentitud de la mecánica del mundo real, de la hiper-velocidad de tu procesador ARM Cortex-M. Con la verificación atómica del bit `TXE`, el CPU solo deposita la letra si el cable está libre, tomando solo 1 ciclo de reloj. Esto significa que la impresora puede tardarse lo que desee en sacar el ticket, y tu Despachador seguirá midiendo fluidos, validando PINs y atendiendo la red RS485 simultáneamente. Acabas de dotar a tu firmware de una verdadera "Multitarea Cooperativa".
while(!TXE)) son fatales cuando se interactúa con dispositivos lentos (Impresoras, Módems GPRS), ya que condenan al procesador a trabajar a la velocidad del dispositivo esclavo.