From a05c1e812402adb95dca4b65f423743a2764e2ff Mon Sep 17 00:00:00 2001 From: nicoboy Date: Mon, 3 Aug 2026 19:53:32 +0200 Subject: [PATCH] Machine d'etats LED, protection thermique, et correctif checksum RX CORRECTIF CRITIQUE -- reception UART cassee (uart_link.c) Le decodage du checksum utilisait strtol sur s_line, qui n'est JAMAIS terminee par un '\0'. strtol lisait donc au-dela de la trame, dans les residus de la ligne precedente. Aggravant, specifique au C28x : uint8_t y fait 16 bits (pas d'adressage par octet), donc le cast (uint8_t) ne tronquait rien. Un "7D" suivi d'un "7D" residuel donnait 0x7D7D = 32125, conserve tel quel, et TOUTE trame etait rejetee. Sur une architecture a octets le cast aurait masque le probleme et le bug serait passe inapercu. Constate sur cible : cs_calc = 0x7D (correct), cs_recv = 0x7D7D. Consequence visible : plus aucune commande $C acceptee, donc timeout de liaison permanent alors que l'ESP32 emettait correctement. Remplace par parse_hex2(), borne a exactement deux chiffres, qui ne depend d'aucune terminaison et rejette un checksum malforme. Supprime au passage la dependance a stdlib. status_led.c/h -- affichage d'etat STARTUP bleu fixe (1 s minimum, sinon jamais visible) NOMINAL bleu 0,2 / 1,8 s LINK_LOST bleu 0,2 / 0,2 s (> 2 s sans trame $C valide) EMUSTOP alternance bleu/rouge 0,5 / 0,5 s OVERTEMP rouge 0,5 / 0,5 s OVERCURRENT rouge fixe Priorite : surintensite > surtemperature > EMUSTOP > liaison perdue. La surintensite passe devant EMUSTOP : en developpement EMUSTOP se declenche en permanence et ne doit jamais masquer un defaut de puissance. Limite de principe : pendant une halte CPU, plus aucun code ne tourne, les GPIO restent figes sur la phase courante du motif. L'alternance EMUSTOP est donc un indicateur a posteriori, jamais un etat live. safety.c/h -- origine du trip TZFLG.DCAEVT1 (bit 3) est distinct de TZFLG.OST (bit 2), ce qui permet de separer une surintensite reelle (comparateur) d'un arret du debugger. Les ISR ne pilotent plus les LED, elles ne font que relever la cause. Protection thermique (main.c, calib.h) Seuil 85 degC avec hysteresis de 10 degC, coupure du PWM et de HV_EN. Lente par nature, donc entierement logicielle -- aucun chemin materiel requis, contrairement a la surintensite. Pas de redemarrage automatique : rien ne reactive le PWM une fois coupe. Seuil PROVISOIRE : les NTC sont a 5 mm des MOSFET, donc elles mesurent le cuivre et non la jonction (ecart statique et retard thermique notables). A recaler quand la temperature de boitier en charge sera connue. Valide sur cible : commandes $C de nouveau acceptees, chaine NTC lue a 24,5 / 24,7 degC sur pont 10k/10k (coherent avec des resistances a 1 %). Note : un pont 10k/10k ne discrimine PAS le sens de la formule NTC, les deux formes coincidant au point d'equilibre. Verification a faire avec une resistance asymetrique (4,7 kOhm -> environ 47 degC). Co-Authored-By: Claude Opus 5 --- src/calib.h | 14 +++++++ src/main.c | 85 +++++++++++++++++++++++++++++++++++++----- src/safety.c | 33 +++++++++++++++-- src/safety.h | 7 ++++ src/status_led.c | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ src/status_led.h | 41 +++++++++++++++++++++ src/uart_link.c | 50 ++++++++++++++++++++++++- 7 files changed, 311 insertions(+), 15 deletions(-) create mode 100644 src/status_led.c create mode 100644 src/status_led.h diff --git a/src/calib.h b/src/calib.h index 58da0ae..cf1fd28 100644 --- a/src/calib.h +++ b/src/calib.h @@ -88,6 +88,20 @@ SAFETY_DAC_CODE_FROM_V(MEAS_I2_OFFSET_V \ + SAFETY_ISHUNT_THRESHOLD_A * MEAS_I2_GAIN_V_PER_A) +// ---- Protection thermique (logicielle, PROMPT §6 etape 4) ----------- +// Contrairement a la surintensite, la thermique est lente : le logiciel +// suffit, aucun chemin materiel n'est requis. +// PROVISOIRE : seuil a confirmer selon la tenue reelle du MOSFET et +// l'implantation des NTC sur la carte. +// L'hysteresis evite que l'etat oscille autour du point de bascule. +#define SAFETY_OVERTEMP_C 85.0f +#define SAFETY_OVERTEMP_HYST_C 10.0f + +// ---- Timeout de la liaison ESP32 (PROMPT §6 etape 7) ---------------- +// Sans trame $C valide au-dela de ce delai, la liaison est declaree +// perdue. La securite ne depend jamais de l'ESP32. +#define UART_LINK_TIMEOUT_MS 2000U + // ADC : reference interne obligatoire (VREFHI partage avec ADCINA0/VIN), // pleine echelle 3,3 V. Conversion brut -> volts : V = raw * 3.3 / 4096. #define ADC_VREF_V 3.3f diff --git a/src/main.c b/src/main.c index 10b5ba8..feee1c7 100644 --- a/src/main.c +++ b/src/main.c @@ -7,6 +7,7 @@ #include "pwm.h" #include "adc.h" #include "measure.h" +#include "status_led.h" #include "calib.h" extern uint16_t RamfuncsLoadStart; @@ -15,11 +16,61 @@ 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. +// Dernier CommandState recu, expose au debogueur. static volatile command_state_t g_last_cmd = {false, false, false}; static volatile bool s_send_telemetry = false; +// Age de la derniere trame $C valide, en ticks de 10 ms. Incremente par +// l'ISR, remis a zero par la boucle principale a chaque commande valide. +// Sature pour ne jamais reboucler. +#define TICK_MS 10U +#define LINK_TIMEOUT_TICKS ((uint16_t)(UART_LINK_TIMEOUT_MS / TICK_MS)) +static volatile uint16_t s_ticks_since_cmd = LINK_TIMEOUT_TICKS; + +// Etat de surtemperature, avec hysteresis pour eviter le battement autour +// du seuil. +static bool s_overtemp = false; + +static void update_overtemp(void) +{ + float t1 = measure_temp(adc_get_raw(ADC_CH_T1)); + float t2 = measure_temp(adc_get_raw(ADC_CH_T2)); + float tmax = (t1 > t2) ? t1 : t2; + + if (!s_overtemp) + { + s_overtemp = (tmax >= SAFETY_OVERTEMP_C); + } + else + { + s_overtemp = (tmax > (SAFETY_OVERTEMP_C - SAFETY_OVERTEMP_HYST_C)); + } +} + +// Priorite : surintensite > surtemperature > EMUSTOP > liaison perdue. +static led_state_t compute_led_state(void) +{ + safety_faults_t faults = safety_get_fault_flags(); + + if (faults.overcurrent) + { + return LED_STATE_OVERCURRENT; + } + if (s_overtemp) + { + return LED_STATE_OVERTEMP; + } + if (faults.emustop) + { + return LED_STATE_EMUSTOP; + } + if (s_ticks_since_cmd >= LINK_TIMEOUT_TICKS) + { + return LED_STATE_LINK_LOST; + } + return LED_STATE_NOMINAL; +} + void main(void) { #ifdef _FLASH @@ -28,6 +79,7 @@ void main(void) bsp_clock_init(); bsp_gpio_leds_init(); + status_led_init(); // bleu fixe des le depart : "je suis parti" DINT; InitPieCtrl(); @@ -87,22 +139,28 @@ void main(void) for (;;) { command_state_t cmd; - safety_faults_t faults; - if (uart_link_poll(&cmd)) { g_last_cmd = cmd; + s_ticks_since_cmd = 0U; } - // Test EMUSTOP : LED rouge = defaut latche, bleue = nominal. - // Le flag ne se rearme jamais seul (PROMPT §8). - faults = safety_get_fault_flags(); + update_overtemp(); + + // Protection thermique : lente par nature, donc logicielle -- aucun + // chemin materiel requis, contrairement a la surintensite. + // Pas de redemarrage automatique : rien ne reactive le PWM une fois + // coupe, meme si la temperature redescend sous l'hysteresis. Il faut + // un reset (ou, a l'etape 8, une commande explicite). + if (s_overtemp) { - bool tripped = faults.stage1_fault || faults.stage2_fault; - led_set(LED_RED, tripped); - led_set(LED_BLUE, !tripped); + pwm_enable(STAGE_1, false); + pwm_enable(STAGE_2, false); + hv_enable_set(false); } + status_led_set_state(compute_led_state()); + if (s_send_telemetry) { telemetry_t t; @@ -150,6 +208,13 @@ interrupt void cpu_timer0_isr(void) CpuTimer0.InterruptCount++; tick++; + status_led_tick(); + + if (s_ticks_since_cmd < LINK_TIMEOUT_TICKS) + { + s_ticks_since_cmd++; // sature au seuil, pas de rebouclage + } + if ((tick % 30U) == 0U) { s_send_telemetry = true; // telemetrie toutes les 300 ms diff --git a/src/safety.c b/src/safety.c index fe59cb3..250e1e1 100644 --- a/src/safety.c +++ b/src/safety.c @@ -5,6 +5,8 @@ static volatile bool s_stage1_fault = false; static volatile bool s_stage2_fault = false; +static volatile bool s_overcurrent = false; +static volatile bool s_emustop = false; interrupt void epwm1_tzint_isr(void); interrupt void epwm2_tzint_isr(void); @@ -104,6 +106,8 @@ safety_faults_t safety_get_fault_flags(void) safety_faults_t f; f.stage1_fault = s_stage1_fault; f.stage2_fault = s_stage2_fault; + f.overcurrent = s_overcurrent; + f.emustop = s_emustop; return f; } @@ -115,7 +119,10 @@ void safety_clear_faults(void) EDIS; s_stage1_fault = false; s_stage2_fault = false; - led_set(LED_RED, false); + s_overcurrent = false; + s_emustop = false; + // Les LED ne sont plus pilotees ici : c'est status_led.c qui detient + // l'affichage, a partir de ces drapeaux. } void safety_force_trip_test(void) @@ -126,16 +133,36 @@ void safety_force_trip_test(void) EDIS; } +// Diagnostic uniquement : la coupure est deja faite en materiel (PROMPT §6 +// etape 2 point 7). On se contente de relever l'origine du trip. interrupt void epwm1_tzint_isr(void) { s_stage1_fault = true; - led_set(LED_RED, true); + + if (EPwm1Regs.TZFLG.bit.DCAEVT1) + { + s_overcurrent = true; + } + else + { + s_emustop = true; + } + PieCtrlRegs.PIEACK.all = PIEACK_GROUP2; } interrupt void epwm2_tzint_isr(void) { s_stage2_fault = true; - led_set(LED_RED, true); + + if (EPwm2Regs.TZFLG.bit.DCAEVT1) + { + s_overcurrent = true; + } + else + { + s_emustop = true; + } + PieCtrlRegs.PIEACK.all = PIEACK_GROUP2; } diff --git a/src/safety.h b/src/safety.h index cd66052..fd1d959 100644 --- a/src/safety.h +++ b/src/safety.h @@ -7,6 +7,13 @@ typedef struct { bool stage1_fault; bool stage2_fault; + + // Origine du trip, relevee dans TZFLG au moment ou l'ISR s'execute. + // TZFLG.DCAEVT1 (bit 3) est distinct de TZFLG.OST (bit 2), ce qui permet + // de separer une vraie surintensite d'un arret du debugger -- les deux + // n'appellent pas du tout la meme reaction. + bool overcurrent; // DCAEVT1 : comparateur, surintensite reelle + bool emustop; // OST sans DCAEVT1 : EMUSTOP (ou TZFRC de test) } safety_faults_t; void safety_init(void); diff --git a/src/status_led.c b/src/status_led.c new file mode 100644 index 0000000..15abb19 --- /dev/null +++ b/src/status_led.c @@ -0,0 +1,96 @@ +#include +#include +#include "status_led.h" +#include "bsp_gpio.h" + +// Durees exprimees en ticks de 10 ms. Ce sont des constantes de +// presentation, pas de calibration materielle : elles restent ici plutot +// que dans calib.h, qui decrit la carte. +#define TICKS_PER_100MS 10U + +#define STARTUP_MIN_TICKS 100U // 1 s : rend l'etat de demarrage visible + +#define NOMINAL_PERIOD 200U // 2,0 s +#define NOMINAL_ON 20U // 0,2 s + +#define LINK_LOST_PERIOD 40U // 0,4 s +#define LINK_LOST_ON 20U // 0,2 s + +#define EMUSTOP_PERIOD 100U // 1,0 s +#define EMUSTOP_HALF 50U // 0,5 s bleu puis 0,5 s rouge + +#define OVERTEMP_PERIOD 100U // 1,0 s +#define OVERTEMP_ON 50U // 0,5 s + +static led_state_t s_state = LED_STATE_STARTUP; +static uint16_t s_phase = 0; // position dans le motif courant +static uint16_t s_startup_hold = STARTUP_MIN_TICKS; + +void status_led_init(void) +{ + s_state = LED_STATE_STARTUP; + s_phase = 0; + s_startup_hold = STARTUP_MIN_TICKS; + + led_set(LED_BLUE, true); + led_set(LED_RED, false); +} + +void status_led_set_state(led_state_t state) +{ + if (s_startup_hold != 0U) + { + return; // duree minimale de l'etat de demarrage non ecoulee + } + + if (state != s_state) + { + s_state = state; + s_phase = 0; // le motif repart proprement a son debut + } +} + +void status_led_tick(void) +{ + bool blue = false; + bool red = false; + + if (s_startup_hold != 0U) + { + s_startup_hold--; + } + + s_phase++; + + switch (s_state) + { + case LED_STATE_NOMINAL: + blue = ((s_phase % NOMINAL_PERIOD) < NOMINAL_ON); + break; + + case LED_STATE_LINK_LOST: + blue = ((s_phase % LINK_LOST_PERIOD) < LINK_LOST_ON); + break; + + case LED_STATE_EMUSTOP: + blue = ((s_phase % EMUSTOP_PERIOD) < EMUSTOP_HALF); + red = !blue; + break; + + case LED_STATE_OVERTEMP: + red = ((s_phase % OVERTEMP_PERIOD) < OVERTEMP_ON); + break; + + case LED_STATE_OVERCURRENT: + red = true; + break; + + case LED_STATE_STARTUP: + default: + blue = true; + break; + } + + led_set(LED_BLUE, blue); + led_set(LED_RED, red); +} diff --git a/src/status_led.h b/src/status_led.h new file mode 100644 index 0000000..f6f5137 --- /dev/null +++ b/src/status_led.h @@ -0,0 +1,41 @@ +#ifndef STATUS_LED_H +#define STATUS_LED_H + +// Affichage d'etat sur la LED bicolore (bleue = GPIO12, rouge = GPIO33). +// Les motifs sont volontairement tous distinguables a l'oeil : +// +// STARTUP bleu fixe +// NOMINAL bleu 0,2 s ON / 1,8 s OFF (battement discret) +// LINK_LOST bleu 0,2 s ON / 0,2 s OFF (clignotement rapide) +// EMUSTOP alternance bleu/rouge 0,5 s / 0,5 s +// OVERTEMP rouge 0,5 s ON / 0,5 s OFF +// OVERCURRENT rouge fixe +// +// L'ordre de l'enumeration EST l'ordre de priorite croissante : quand +// plusieurs conditions sont vraies, on affiche la plus grande valeur. +// La surintensite passe donc devant EMUSTOP -- pendant le developpement +// EMUSTOP se declenche en permanence et ne doit jamais masquer un vrai +// defaut de puissance. +typedef enum +{ + LED_STATE_STARTUP = 0, + LED_STATE_NOMINAL, + LED_STATE_LINK_LOST, + LED_STATE_EMUSTOP, + LED_STATE_OVERTEMP, + LED_STATE_OVERCURRENT +} led_state_t; + +void status_led_init(void); + +// Etat souhaite, calcule dans la boucle principale. Tout changement +// redemarre le motif a zero. Les demandes de sortie de STARTUP sont +// ignorees tant que la duree minimale d'affichage n'est pas ecoulee, +// sans quoi cet etat ne serait jamais visible (l'init dure quelques ms). +void status_led_set_state(led_state_t state); + +// A appeler a chaque tick de 10 ms. Ne fait qu'ecrire GPASET/GPACLEAR, +// donc utilisable depuis une ISR. +void status_led_tick(void); + +#endif diff --git a/src/uart_link.c b/src/uart_link.c index c0ccd24..c6bdee0 100644 --- a/src/uart_link.c +++ b/src/uart_link.c @@ -1,6 +1,5 @@ #include #include -#include #include #include "DSP28x_Project.h" #include "uart_link.h" @@ -260,6 +259,50 @@ static bool parse_command(const char *body, command_state_t *cmd) return ht_found && pwm1_found && pwm2_found; } +// Decode EXACTEMENT deux chiffres hexadecimaux. Renvoie false si l'un des +// deux n'en est pas un. +// +// Remplace strtol, qui etait doublement piegeux ici : +// - s_line n'est jamais terminee par un '\0', donc strtol lisait au-dela +// de la trame, dans les residus de la ligne precedente ; +// - sur C28x, uint8_t fait 16 bits (pas d'adressage par octet), donc le +// cast (uint8_t) ne tronquait pas le resultat. Un "7D" suivi d'un "7D" +// residuel donnait 0x7D7D, retenu tel quel, et toute trame etait +// rejetee. Sur une architecture a octets le bug serait passe inapercu. +static bool parse_hex2(const char *s, uint8_t *out) +{ + uint16_t value = 0U; + uint16_t i; + + for (i = 0U; i < 2U; i++) + { + char c = s[i]; + uint16_t digit; + + if ((c >= '0') && (c <= '9')) + { + digit = (uint16_t)(c - '0'); + } + else if ((c >= 'A') && (c <= 'F')) + { + digit = (uint16_t)(c - 'A' + 10); + } + else if ((c >= 'a') && (c <= 'f')) + { + digit = (uint16_t)(c - 'a' + 10); + } + else + { + return false; + } + + value = (uint16_t)((value << 4) | digit); + } + + *out = (uint8_t)(value & 0xFFU); + return true; +} + // s_line contient une ligne complete (sans le '\n' terminal). static bool process_line(command_state_t *cmd) { @@ -292,7 +335,10 @@ static bool process_line(command_state_t *cmd) } cs_calc = checksum_of(&s_line[1], star - 1); - cs_recv = (uint8_t)strtol(&s_line[star + 1], NULL, 16); + if (!parse_hex2(&s_line[star + 1], &cs_recv)) + { + return false; // checksum malforme -> trame ignoree + } if (cs_calc != cs_recv) { return false; // trame corrompue -> ignoree silencieusement