diff --git a/.cproject b/.cproject index d575ce2..989af13 100644 --- a/.cproject +++ b/.cproject @@ -22,8 +22,8 @@ - - diff --git a/.gitignore b/.gitignore index 3931b03..2d75b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ Release/ # IDE-generated, machine-specific (hardcoded local tool paths) .mcp.json CLAUDE.md + +# Outillage agent (local, non partage) +.claude/ +.codex/ diff --git a/.theia/launch.json b/.theia/launch.json new file mode 100644 index 0000000..3efb6ce --- /dev/null +++ b/.theia/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + "version": "0.2.0", + "configurations": [ + { + "name": "TMS320F28027-dualboost", + "type": "ccs-debug", + "request": "launch", + "projectInfo": { + "name": "TMS320F28027-dualboost", + "resourceId": "/TMS320F28027-dualboost" + } + } + ] +} diff --git a/F2802x_generic_flash.cmd b/F2802x_generic_flash.cmd index 8721a45..1f57e90 100644 --- a/F2802x_generic_flash.cmd +++ b/F2802x_generic_flash.cmd @@ -99,11 +99,16 @@ PAGE 0: /* Program Memory */ RAMM0 : origin = 0x000050, length = 0x0003B0 /* on-chip RAM block M0 */ OTP : origin = 0x3D7800, length = 0x000400 /* on-chip OTP */ - FLASHA : origin = 0x3F7000, length = 0x000F80 /* on-chip FLASH */ + /* Les 4 secteurs flash du F28027 (32K mots au total, cf. common/cmd/F28027.cmd + de C2000Ware). Le fichier "generic" d'origine ne declarait que le secteur A + (~8K), ce qui faisait deborder le link des que le code grossissait. */ + FLASHD : origin = 0x3F0000, length = 0x002000 /* on-chip FLASH */ + FLASHC : origin = 0x3F2000, length = 0x002000 /* on-chip FLASH */ + FLASHB : origin = 0x3F4000, length = 0x002000 /* on-chip FLASH */ + FLASHA : origin = 0x3F6000, length = 0x001F80 /* on-chip FLASH */ CSM_RSVD : origin = 0x3F7F80, length = 0x000076 /* Part of FLASHA. Program with all 0x0000 when CSM is in use. */ BEGIN : origin = 0x3F7FF6, length = 0x000002 /* Part of FLASHA. Used for "boot to Flash" bootloader mode. */ CSM_PWL_P0 : origin = 0x3F7FF8, length = 0x000008 /* Part of FLASHA. CSM password locations in FLASHA */ - FLASHB : origin = 0x3F6000, length = 0x001000 /* on-chip FLASH */ @@ -121,7 +126,7 @@ PAGE 1 : /* Data Memory */ BOOT_RSVD : origin = 0x000000, length = 0x000050 /* Part of M0, BOOT rom will use this for stack */ RAMM1 : origin = 0x000400, length = 0x000400 /* on-chip RAM block M1 */ - RAML0 : origin = 0x008000, length = 0x000400 /* on-chip RAM block L0 */ + RAML0 : origin = 0x008000, length = 0x001000 /* on-chip RAM block L0 (4K mots sur F28027) */ } @@ -145,25 +150,29 @@ SECTIONS RUN_START(_RamfuncsRunStart), PAGE = 0 - .cinit : > FLASHA | FLASHB, PAGE = 0 - .pinit : > FLASHA | FLASHB, PAGE = 0 - .text : >> FLASHA | FLASHB, PAGE = 0 + .cinit : > FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 + .pinit : > FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 + .text : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 csmpasswds : > CSM_PWL_P0, PAGE = 0 csm_rsvd : > CSM_RSVD, PAGE = 0 /* Allocate uninitalized data sections: */ + /* .ebss place en RAML0 EN PREMIER (et non RAMM1) : sinon les globales se + retrouvent collees juste apres .stack, et un debordement de pile les + ecrase silencieusement au lieu de tomber dans du vide. RAMM1 est ainsi + reserve a la pile seule. */ .stack : > RAMM1, PAGE = 1 - .ebss : >> RAMM1 | RAML0, PAGE = 1 - .esysmem : >> RAMM1 | RAML0, PAGE = 1 + .ebss : >> RAML0 | RAMM1, PAGE = 1 + .esysmem : >> RAML0 | RAMM1, PAGE = 1 /* Initalized sections go in Flash */ /* For SDFlash to program these, they must be allocated to page 0 */ - .econst : >> FLASHA | FLASHB, PAGE = 0 - .switch : >> FLASHA | FLASHB, PAGE = 0 + .econst : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 + .switch : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 /* Allocate IQ math areas: */ - IQmath : >> FLASHA | FLASHB, PAGE = 0 /* Math Code */ + IQmath : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 /* Math Code */ IQmathTables : > IQTABLES, PAGE = 0, TYPE = NOLOAD /* Uncomment the section below if calling the IQNexp() or IQexp() diff --git a/src/calib.h b/src/calib.h index 3697ca2..098a547 100644 --- a/src/calib.h +++ b/src/calib.h @@ -9,7 +9,10 @@ #define CLK_SYSCLKOUT_HZ 60000000UL // LEDs (LQFP48 PT) : bleue = GPIO12 (broche 47), rouge = GPIO33 (broche 36) -#define LED_ACTIVE_LOW 1 +// Point ouvert §9 du prompt : polarite a confirmer au premier test. +// Observation bring-up (carte 2) : GPIO33 haut = LED rouge visuellement allumee +// -> cablage actif-haut, pas actif-bas comme suppose initialement. +#define LED_ACTIVE_LOW 0 // Valeur de champ AIOMUX1 pour basculer une broche AIOx en mode analogique #define GPIO_ANALOG_MODE 2 @@ -21,4 +24,15 @@ #define SAFETY_DAC_CODE \ ((uint16_t)((SAFETY_ISHUNT_THRESHOLD_A * 0.6f) / SAFETY_DAC_VREF_V * 1023.0f + 0.5f)) +// Liaison UART SCI-A (voir docs/ESP32-UART.md) : 57600 8N1. +// LSPCLK = SYSCLKOUT/4 (LOSPCP laisse a sa valeur par defaut par InitSysCtrl). +#define UART_BAUD_RATE 57600UL +#define UART_LSPCLK_HZ (CLK_SYSCLKOUT_HZ / 4UL) +// BRR = round(LSPCLK / (8 * baud)) - 1 (TRM SPRUI09A, registre SCIHBAUD:SCILBAUD) +#define UART_SCIBRR \ + ((uint16_t)(((UART_LSPCLK_HZ + 4UL * UART_BAUD_RATE) / (8UL * UART_BAUD_RATE)) - 1U)) + +#define UART_RX_RING_SIZE 64U +#define UART_LINE_MAX 160U // limite cote ESP32 (g_lineBuf[160]) + #endif diff --git a/src/main.c b/src/main.c index b17ad42..3ab1e3c 100644 --- a/src/main.c +++ b/src/main.c @@ -2,6 +2,8 @@ #include "bsp_clock.h" #include "bsp_gpio.h" #include "safety.h" +#include "uart_link.h" +#include "protocol.h" extern uint16_t RamfuncsLoadStart; extern uint16_t RamfuncsLoadSize; @@ -9,6 +11,11 @@ extern uint16_t RamfuncsRunStart; interrupt void cpu_timer0_isr(void); +// Bring-up etape 7 (UART) : dernier CommandState recu, pour inspection au +// debogueur. Valeurs de telemetrie figees tant que measure.c n'existe pas. +static volatile command_state_t g_last_cmd = {false, false, false}; +static volatile bool s_send_telemetry = false; + void main(void) { #ifdef _FLASH @@ -25,14 +32,18 @@ void main(void) InitPieVectTable(); bsp_gpio_analog_init(); - safety_init(); + // safety_init() desactive temporairement : bring-up isole sur LED+UART + // uniquement (voir §8 du prompt -- sans risque ici, pwm.c n'existe pas + // encore, donc aucun PWM ne peut sortir avec ou sans TZ arme). + uart_link_init(); EALLOW; PieVectTable.TINT0 = &cpu_timer0_isr; EDIS; InitCpuTimers(); - ConfigCpuTimer(&CpuTimer0, 60, 500000); // 500 ms -> 1 Hz sur la LED + ConfigCpuTimer(&CpuTimer0, 60, 300000); // 300 ms -> rythme d'envoi bring-up UART + StartCpuTimer0(); // ConfigCpuTimer laisse TSS=1 (timer a l'arret) par conception IER |= M_INT1; PieCtrlRegs.PIEIER1.bit.INTx7 = 1; // TINT0 @@ -42,8 +53,39 @@ void main(void) for (;;) { - // Etape 1-2 : validation clock/LED/securite. Rien d'autre pour - // l'instant, tout se passe dans les ISR. + command_state_t cmd; + + if (uart_link_poll(&cmd)) + { + g_last_cmd = cmd; + } + + if (s_send_telemetry) + { + telemetry_t t; + + // Bring-up : valeurs figees, seul le lien TX/RX est valide ici. + t.freq1_hz = 100000.0f; + t.freq2_hz = 100000.0f; + t.duty1_pct = 0.0f; + t.duty2_pct = 0.0f; + t.vin_v = 12.3f; + t.iin_a = 0.5f; + t.v1_v = 20.0f; + t.i1_a = 0.1f; + t.t1_c = 25.0f; + t.vout_v = 0.0f; + t.i2_a = 0.0f; + t.t2_c = 25.0f; + t.iout_a = 0.0f; + + uart_link_send_telemetry(&t); + s_send_telemetry = false; + } + + // Emission en tache de fond : pousse au plus 4 octets (profondeur de + // la FIFO) puis rend la main. Ne doit jamais retarder la regulation. + uart_link_service_tx(); } } @@ -52,6 +94,7 @@ interrupt void cpu_timer0_isr(void) static bool blue_on = false; CpuTimer0.InterruptCount++; + s_send_telemetry = true; blue_on = !blue_on; led_set(LED_BLUE, blue_on); diff --git a/src/uart_link.c b/src/uart_link.c new file mode 100644 index 0000000..c0ccd24 --- /dev/null +++ b/src/uart_link.c @@ -0,0 +1,391 @@ +#include +#include +#include +#include +#include "DSP28x_Project.h" +#include "uart_link.h" +#include "calib.h" + +static volatile uint8_t s_rx_ring[UART_RX_RING_SIZE]; +static volatile uint16_t s_rx_head = 0; // ecrit par l'ISR uniquement +static uint16_t s_rx_tail = 0; // ecrit par uart_link_poll() uniquement + +static char s_line[UART_LINE_MAX]; +static uint16_t s_line_len = 0; + +interrupt void scia_rx_isr(void); + +// Profondeur de la FIFO d'emission du SCI (TRM SPRUI09A) : on ne pousse +// jamais au-dela, ce qui garantit que uart_link_service_tx() rend la main +// immediatement au lieu d'attendre le debit de la ligne. +#define SCI_TX_FIFO_DEPTH 4U + +static uint8_t checksum_of(const char *s, int len) +{ + uint8_t cs = 0; + int i; + for (i = 0; i < len; i++) + { + cs ^= (uint8_t)s[i]; + } + return cs; +} + +void uart_link_init(void) +{ + EALLOW; + GpioCtrlRegs.GPAPUD.bit.GPIO28 = 0; // pull-up sur RX + GpioCtrlRegs.GPAPUD.bit.GPIO29 = 1; // pas de pull-up sur TX (sortie) + GpioCtrlRegs.GPAQSEL2.bit.GPIO28 = 3; // RX asynchrone, pas de qualification + GpioCtrlRegs.GPAMUX2.bit.GPIO28 = 1; // GPIO28 -> SCIRXDA + GpioCtrlRegs.GPAMUX2.bit.GPIO29 = 1; // GPIO29 -> SCITXDA + EDIS; + + // 1 stop bit, pas de parite, 8 bits, mode async, protocole idle-line + SciaRegs.SCICCR.all = 0x0007; + // TX/RX actives, horloge SCI interne + SciaRegs.SCICTL1.all = 0x0003; + SciaRegs.SCICTL2.bit.TXINTENA = 0; // TX en polling, pas d'interruption + SciaRegs.SCICTL2.bit.RXBKINTENA = 1; + + SciaRegs.SCIHBAUD = (UART_SCIBRR >> 8) & 0xFF; + SciaRegs.SCILBAUD = UART_SCIBRR & 0xFF; + + SciaRegs.SCIFFTX.all = 0xE040; // FIFO active, TX en polling (pas d'IT FIFO) + SciaRegs.SCIFFRX.all = 0x2021; // FIFO active, RXFFIENA=1, seuil=1 octet + SciaRegs.SCIFFCT.all = 0x0; + + SciaRegs.SCICTL1.all = 0x0023; // sort le SCI de son reset local + + EALLOW; + PieVectTable.SCIRXINTA = &scia_rx_isr; + EDIS; + + IER |= M_INT9; + PieCtrlRegs.PIEIER9.bit.INTx1 = 1; // SCIRXINTA +} + +// Buffer d'emission statique et non local : evite 160 mots de pile a chaque +// envoi (la pile ne fait que 1024 mots et sprintf en consomme deja beaucoup). +static char s_tx_frame[UART_LINE_MAX]; +static uint16_t s_tx_len = 0; // longueur de la trame en attente d'emission +static uint16_t s_tx_pos = 0; // prochain octet a pousser dans la FIFO +static uint16_t s_tx_dropped = 0; // trames abandonnees (ligne trop lente) +static uint16_t s_tx_truncated = 0; // trames ayant perdu des champs (borne) + +// Place a reserver en fin de buffer : "*XX\n" + le NUL ecrit par snprintf. +#define UART_TX_TAIL_LEN 5 + +// Garde-fou sur les valeurs formatees : un flottant aberrant (capteur non +// encore cable, ADC non initialise, NaN) rendrait le cast en int32 +// indefini, donc un champ de longueur arbitraire. +#define UART_FIELD_ABS_MAX 999999.0f + +// Formatage decimal a la main, SANS "%f". Sur C28x le support flottant de +// printf passe par du long double 64 bits emule (frexpl/scalbnl/L$$DIV) : +// tres couteux en pile et en temps, et constate au bring-up comme faisant +// partir le CPU dans le decor (PC=0) des le premier envoi de telemetrie. +// Voir aussi PROMPT §7 etape 7, qui prevoyait deja ce repli. +// +// `room` = octets disponibles pour ce champ, NUL compris. Renvoie 0 (champ +// entierement abandonne, rien d'ecrit) s'il ne tient pas : le protocole +// autorise les champs manquants, l'ESP32 garde alors sa derniere valeur +// (docs/ESP32-UART.md). Mieux vaut une trame courte mais valide qu'un +// debordement de s_tx_frame. +static int append_field(char *out, int room, const char *tag, float value, + uint16_t decimals) +{ + static const int32_t k_pow10[3] = {1L, 10L, 100L}; + const char *sign = ""; + int32_t scaled; + int32_t ip; + int32_t fp; + int n; + + if (room <= 1) + { + return 0; + } + + // value != value n'est vrai que pour un NaN. + if (value != value) + { + value = 0.0f; + } + else if (value < -UART_FIELD_ABS_MAX) + { + value = -UART_FIELD_ABS_MAX; + } + else if (value > UART_FIELD_ABS_MAX) + { + value = UART_FIELD_ABS_MAX; + } + + scaled = (int32_t)(value * (float)k_pow10[decimals] + + ((value >= 0.0f) ? 0.5f : -0.5f)); + if (scaled < 0) + { + scaled = -scaled; + sign = "-"; + } + ip = scaled / k_pow10[decimals]; + fp = scaled % k_pow10[decimals]; + + if (decimals == 0) + { + n = snprintf(out, (size_t)room, ",%s=%s%ld", tag, sign, ip); + } + else if (decimals == 1) + { + n = snprintf(out, (size_t)room, ",%s=%s%ld.%01ld", tag, sign, ip, fp); + } + else + { + n = snprintf(out, (size_t)room, ",%s=%s%ld.%02ld", tag, sign, ip, fp); + } + + if (n < 0 || n >= room) + { + out[0] = '\0'; // tronque : on annule le champ au lieu de le laisser coupe + return 0; + } + return n; +} + +// Construit la trame et la met en attente. NE BLOQUE PAS : l'emission reelle +// se fait ensuite par uart_link_service_tx(), appelee quand la boucle +// principale a du temps disponible. Renvoie false si la trame precedente +// n'est pas encore partie (nouvelle trame abandonnee, la telemetrie est par +// nature perissable : mieux vaut la suivante que du retard accumule). +bool uart_link_send_telemetry(const telemetry_t *t) +{ + int n; + int fields_dropped; + uint8_t cs; + + if (uart_link_tx_busy()) + { + s_tx_dropped++; + return false; + } + + s_tx_frame[0] = '$'; + s_tx_frame[1] = 'T'; + n = 2; + fields_dropped = 0; + +#define TX_ROOM() ((int)UART_LINE_MAX - UART_TX_TAIL_LEN - n) +#define TX_ADD(tag_, val_, dec_) \ + do { \ + int added_ = append_field(&s_tx_frame[n], TX_ROOM(), (tag_), (val_), \ + (dec_)); \ + if (added_ == 0) { fields_dropped++; } else { n += added_; } \ + } while (0) + + TX_ADD("FREQ1", t->freq1_hz, 0); + TX_ADD("FREQ2", t->freq2_hz, 0); + TX_ADD("DUTY1", t->duty1_pct, 1); + TX_ADD("DUTY2", t->duty2_pct, 1); + TX_ADD("VIN", t->vin_v, 1); + TX_ADD("IIN", t->iin_a, 2); + TX_ADD("V1", t->v1_v, 1); + TX_ADD("I1", t->i1_a, 2); + TX_ADD("T1", t->t1_c, 1); + TX_ADD("VOUT", t->vout_v, 1); + TX_ADD("I2", t->i2_a, 2); + TX_ADD("T2", t->t2_c, 1); + TX_ADD("IOUT", t->iout_a, 2); + +#undef TX_ADD +#undef TX_ROOM + + if (fields_dropped != 0) + { + s_tx_truncated++; + } + + // Checksum XOR sur le corps seul, entre '$' et '*' (docs/ESP32-UART.md). + // La place a ete reservee par UART_TX_TAIL_LEN, l'ecriture tient toujours. + cs = checksum_of(&s_tx_frame[1], n - 1); + n += snprintf(&s_tx_frame[n], (size_t)((int)UART_LINE_MAX - n), + "*%02X\n", cs); + + s_tx_len = (uint16_t)n; + s_tx_pos = 0; + return true; +} + +bool uart_link_tx_busy(void) +{ + return (s_tx_pos < s_tx_len); +} + +void uart_link_service_tx(void) +{ + while ((s_tx_pos < s_tx_len) + && (SciaRegs.SCIFFTX.bit.TXFFST < SCI_TX_FIFO_DEPTH)) + { + SciaRegs.SCITXBUF = (uint16_t)(uint8_t)s_tx_frame[s_tx_pos]; + s_tx_pos++; + } +} + +// body pointe juste apres "C," ; len = nb de caracteres avant le '*'. +static bool parse_command(const char *body, command_state_t *cmd) +{ + const char *p; + bool ht_found = false, pwm1_found = false, pwm2_found = false; + + p = strstr(body, "HT="); + if (p != NULL) + { + cmd->ht_enabled = (p[3] == '1'); + ht_found = true; + } + + p = strstr(body, "PWM1="); + if (p != NULL) + { + cmd->pwm1_enabled = (p[5] == '1'); + pwm1_found = true; + } + + p = strstr(body, "PWM2="); + if (p != NULL) + { + cmd->pwm2_enabled = (p[5] == '1'); + pwm2_found = true; + } + + return ht_found && pwm1_found && pwm2_found; +} + +// s_line contient une ligne complete (sans le '\n' terminal). +static bool process_line(command_state_t *cmd) +{ + int len = (int)s_line_len; + int i; + int star = -1; + uint8_t cs_calc, cs_recv; + + if (len > 0 && s_line[len - 1] == '\r') + { + len--; + } + + if (len < 4 || s_line[0] != '$') + { + return false; + } + + for (i = 1; i < len; i++) + { + if (s_line[i] == '*') + { + star = i; + break; + } + } + if (star < 0 || (len - star) < 3) + { + return false; // pas de checksum complet + } + + cs_calc = checksum_of(&s_line[1], star - 1); + cs_recv = (uint8_t)strtol(&s_line[star + 1], NULL, 16); + if (cs_calc != cs_recv) + { + return false; // trame corrompue -> ignoree silencieusement + } + + if (s_line[1] != 'C' || s_line[2] != ',') + { + return false; // pas une trame de commande + } + + s_line[star] = '\0'; + return parse_command(&s_line[3], cmd); +} + +// Compteur de recuperations, expose au debogueur : une valeur qui grimpe +// signale un probleme physique sur la ligne (baud, masse, niveaux), pas un +// simple alea. +static uint16_t s_rx_error_recoveries = 0; + +// Sur C28x, un FE / OE / PE / BRKDT positionne SCIRXST.RXERROR, qui est +// LATCHE : le recepteur reste bloque tant qu'on ne fait pas un SW RESET du +// SCI (TRM SPRUI09A, SCICTL1.SWRESET) ou un reset systeme. Sans ce +// traitement, une seule perturbation sur la ligne arrete definitivement la +// reception -- constate au bring-up (RXERROR+FE+BRKDT latches, plus aucune +// trame $C recue ensuite). +static void scia_recover_if_rx_error(void) +{ + if (SciaRegs.SCIRXST.bit.RXERROR == 0) + { + return; + } + + SciaRegs.SCICTL1.bit.SWRESET = 0; + SciaRegs.SCICTL1.bit.SWRESET = 1; + + SciaRegs.SCIFFRX.bit.RXFIFORESET = 0; + SciaRegs.SCIFFRX.bit.RXFIFORESET = 1; + SciaRegs.SCIFFRX.bit.RXFFOVRCLR = 1; + SciaRegs.SCIFFRX.bit.RXFFINTCLR = 1; + + // La ligne en cours d'assemblage est forcement tronquee : on repart propre. + s_line_len = 0; + s_rx_error_recoveries++; +} + +bool uart_link_poll(command_state_t *cmd) +{ + bool got_command = false; + + scia_recover_if_rx_error(); + + while (s_rx_tail != s_rx_head) + { + uint8_t c = s_rx_ring[s_rx_tail]; + s_rx_tail = (uint16_t)((s_rx_tail + 1U) % UART_RX_RING_SIZE); + + if (c == '\n') + { + if (process_line(cmd)) + { + got_command = true; + } + s_line_len = 0; + } + else if (s_line_len < (UART_LINE_MAX - 1U)) + { + s_line[s_line_len++] = (char)c; + } + else + { + // Ligne trop longue (> limite cote ESP32) : abandonnee. + s_line_len = 0; + } + } + + return got_command; +} + +interrupt void scia_rx_isr(void) +{ + uint16_t next_head = (uint16_t)((s_rx_head + 1U) % UART_RX_RING_SIZE); + + if (next_head != s_rx_tail) + { + s_rx_ring[s_rx_head] = (uint8_t)SciaRegs.SCIRXBUF.bit.RXDT; + s_rx_head = next_head; + } + else + { + (void)SciaRegs.SCIRXBUF.bit.RXDT; // anneau plein : octet jete + } + + SciaRegs.SCIFFRX.bit.RXFFOVRCLR = 1; + SciaRegs.SCIFFRX.bit.RXFFINTCLR = 1; + PieCtrlRegs.PIEACK.all = PIEACK_GROUP9; +} diff --git a/src/uart_link.h b/src/uart_link.h new file mode 100644 index 0000000..1dc3296 --- /dev/null +++ b/src/uart_link.h @@ -0,0 +1,29 @@ +#ifndef UART_LINK_H +#define UART_LINK_H + +#include +#include "protocol.h" + +void uart_link_init(void); + +// Construit une trame $T,...*XX et la met en attente (docs/ESP32-UART.md). +// NE BLOQUE PAS : rien n'est emis ici, c'est uart_link_service_tx() qui +// alimente la ligne ensuite. Renvoie false si la trame precedente n'est pas +// encore partie -- la nouvelle est alors abandonnee. +bool uart_link_send_telemetry(const telemetry_t *t); + +// Pousse dans la FIFO du SCI ce qui y rentre, puis rend la main +// immediatement. A appeler depuis la boucle principale, en tache de fond : +// l'emission ne doit jamais passer avant la regulation. +void uart_link_service_tx(void); + +// true tant qu'une trame reste a emettre. +bool uart_link_tx_busy(void); + +// A appeler dans la boucle principale (jamais en ISR). Si une trame +// $C,...*XX complete et de checksum valide a ete recue depuis le dernier +// appel, decode son contenu dans *cmd et renvoie true. Les trames +// corrompues sont ignorees silencieusement (voir docs/ESP32-UART.md). +bool uart_link_poll(command_state_t *cmd); + +#endif