Compare commits

..

3 Commits

Author SHA1 Message Date
46ddf30a64 Etape 2 validee sur cible : trip comparateur, arret global, Stagex-EN
safety_init() complet est maintenant appele a la place de
safety_arm_emustop_only() : comparateurs + Digital Compare + Trip Zone.
Seuils DAC verifies sur cible a 601 / 603, conformes au calcul de
docs/mesure-cartepuissance.md pour 3,0 A.

CORRECTIF -- les trips ne remontaient pas au logiciel (safety.c)

  TZFLG.INT est le drapeau GLOBAL d'interruption Trip Zone : tant qu'il
  n'est pas efface via TZCLR.INT, plus AUCUNE interruption TZ n'est generee.
  Les ISR ne faisaient que PIEACK. Consequence : le premier trip de la
  session (un EMUSTOP provoque par une pause du debogueur) consommait
  l'unique notification, et tous les suivants coupaient bien le PWM en
  materiel mais en SILENCE, sans lever de drapeau logiciel.

  Constate au banc : surintensite reelle sur I2, EPwm2Regs.TZFLG = 0x0D
  (INT + OST + DCAEVT1, donc trip comparateur bien latche), PWM2 coupe,
  et pourtant LED au nominal.

  Ce defaut etait invisible en testant un seul trip a la fois. Il ne serait
  apparu qu'en exploitation, sous la forme "la protection coupe mais l'IHM
  n'affiche aucun defaut".

  Second piege evite : effacer INT alors que la surintensite persiste
  regenere l'evenement immediatement -> tempete d'interruptions qui
  affamerait la boucle principale, donc plus de telemetrie ni de LED, au
  pire moment. L'ISR desarme donc TZEINT.OST : une seule notification par
  episode. Le rearmement est fait par safety_clear_faults() uniquement,
  jamais automatiquement (PROMPT §8).

  safety_clear_faults() efface desormais DCAEVT1 (sinon le prochain trip
  serait attribue a une surintensite meme s'il vient d'EMUSTOP), INT, et
  rearme TZEINT.OST.

Arret global sur defaut (main.c)

  enter_safe_state() : les deux etages inhibes (PWM + Stagex-EN) ET HV_EN
  coupe. Sur un boost en cascade, laisser tourner l'etage 2 pendant que
  l'etage 1 est coupe n'a pas de sens. Couper HV_EN n'est pas redondant :
  un boost a 0 % de duty ne donne PAS 0 V en sortie, le chemin
  Vin -> L -> diode -> Cout reste passant ; HV_EN (VOM1271) est le seul
  organe qui isole reellement la charge (PROMPT §6 etape 6).

  Les Trip Zones restent independantes en materiel : cet arret global est
  la couche systeme au-dessus, executee au rythme de la boucle principale.
  La coupure rapide (~30 ns) reste la porte ET + le Trip Zone.

  Stagex-EN est maintenant pilote en continu : 1 au nominal, 0 sur defaut.
  Inhibition SUPPLEMENTAIRE, le logiciel ne peut jamais outrepasser la
  coupure materielle deja faite par COMPxOUT sur l'autre entree de la porte.

Valide au banc : injection de 2 A sur le shunt 2 avec seuil abaisse a
1,5 A -> les deux etages coupes, LED rouge fixe, defaut latche.

ATTENTION, toujours en place dans main.c : le harnais de bring-up force le
PWM a 50 % en dur, et Stagex-EN passe desormais a 1 automatiquement au
demarrage. Des que les drivers UCC27517 seront montes, la carte se mettra
donc a decouper A LA MISE SOUS TENSION, sans commande -- exactement ce que
le PROMPT §6 etape 6 interdit (demarrage a 0 % + rampe de soft-start).
Retirer ce harnais AVANT de monter les drivers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:30:53 +02:00
a05c1e8124 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 <noreply@anthropic.com>
2026-08-03 19:53:32 +02:00
888723abc7 Etape 5 : measure.c et constantes de calibration reelles
Report des mesures de docs/mesure-cartepuissance.md dans calib.h, avec une
structure par voie (gain, offset) plutot que des constantes isolees.

calib.h
  Tensions   : VIN 11,11 / V1 31,25 / VOUT 181,82 V/V (mesures)
  IIN        : 1,2019 A/V -- un seul point mesure, offset suppose nul
  IOUT       : 0,016667 A/V -- NON MESURE, valeur theorique de conception
  Shunts     : I1 offset 47,3 mV gain 0,631 V/A
               I2 offset 34,0 mV gain 0,637 V/A
               Deux jeux separes : les voies different d'environ 1 % en gain
               et n'ont pas le meme offset, ne jamais moyenner.

Seuil de protection : le code DAC est maintenant calcule par macro a partir
de SAFETY_ISHUNT_THRESHOLD_A et des gain/offset mesures, et il est PROPRE A
CHAQUE ETAGE (601 et 603 au lieu d'un 558 commun).

  Correction d'analyse : le 558 precedent supposait un gain de 30 pile. Le
  gain reel etant plus eleve (~31,5), le seuil de 1,8 V etait atteint a
  2,78 A et non a 3 A -- la protection declenchait donc TROP TOT, et non
  trop tard comme indique au §5 du document de mesures. Conservatif, sans
  danger, mais corrige.

measure.c/h (etape 5)
  measure_vin/iin/v1/vout/iout/i1/i2/temp, signature float f(uint16_t raw).
  Deux fonctions distinctes pour les shunts, puisque gain ET offset
  different. measure_i1/i2 peuvent renvoyer une valeur negative a courant
  nul : volontaire, une derive d'offset apres changement d'ampli devient
  ainsi visible en telemetrie au lieu d'etre masquee par une saturation.

  NTC : la relation du PROMPT §5 est INVERSEE. Avec la NTC cote 3,3 V c'est
  R_ntc = R_fixe * (VREF - Vadc) / Vadc. Verifie sur les points de repere du
  PROMPT lui-meme : 0,75 V a 0 degC donne 34,2 kOhm, ce que beta=4000
  predit, alors que la forme inversee donnerait 2,94 kOhm (donc du chaud).
  Telle qu'ecrite, la protection thermique aurait ete inexploitable.

La telemetrie transporte desormais de vraies grandeurs physiques.

Reste en suspens (voir docs/mesure-cartepuissance.md §6) : IOUT jamais
mesure, second point pour IIN, valeur reelle de la resistance fixe des
ponts NTC, et surtout gain/offset des shunts A REMESURER apres passage du
MCP6001 au TLV9151 -- l'offset actuel est domine par le Vos de l'ampli.

Note : l'"auto-zero logiciel au boot" evoque dans le document de mesures
n'existe pas dans le firmware et n'a pas ete ajoute ; mesurer un offset au
boot figerait une valeur fausse si du courant circule deja.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 18:50:21 +02:00
9 changed files with 577 additions and 39 deletions

View File

@ -17,12 +17,92 @@
// Valeur de champ AIOMUX1 pour basculer une broche AIOx en mode analogique
#define GPIO_ANALOG_MODE 2
// Seuil de protection courant shunt (identique etage 1 et 2)
// Shunt 0.02 ohm, gain x30 -> Vadc = I * 0.02 * 30 = I * 0.6
// =====================================================================
// Constantes de calibration des chaines de mesure.
// Source : docs/mesure-cartepuissance.md (mesures reelles sur la carte
// elevateur). Aucune de ces valeurs ne doit apparaitre ailleurs.
// =====================================================================
// ---- Chaines de tension (doc §1) ------------------------------------
// Gain = Vreel / Vadc, soit l'inverse du rapport du pont diviseur.
// VIN et V1 tombent sur les valeurs theoriques ; VOUT s'en ecarte de
// 1,8 % (tolerance des resistances), sans consequence : la pleine
// echelle reelle (600 V) reste au-dessus des 400 V vises.
#define MEAS_VIN_GAIN_V_PER_V 11.11f // broche 10, coef mesure 0,09
#define MEAS_V1_GAIN_V_PER_V 31.25f // broche 16, coef mesure 0,032
#define MEAS_VOUT_GAIN_V_PER_V 181.82f // broche 14, coef mesure 0,0055
// ---- Courant d'entree (doc §2) --------------------------------------
// Un seul point mesure (250 mA -> 0,208 V) : la caracteristique est
// SUPPOSEE passer par zero, offset non verifie. Un second point reste
// a faire.
#define MEAS_IIN_A_PER_V 1.2019f
// ---- Courant de sortie (doc §4) -------------------------------------
// NON MESURE : valeur theorique de conception (3,0 V @ 50 mA).
// A remplacer des qu'une mesure reelle sera disponible.
#define MEAS_IOUT_A_PER_V 0.016667f
// ---- Shunts MOSFET 0,02 ohm (doc §3) --------------------------------
// PROVISOIRE : mesures faites avec les MCP6001 de banc. Le gain, fixe
// par le reseau RF/RG, doit rester valable apres passage au TLV9151 ;
// l'OFFSET en revanche est domine par le Vos de l'ampli et devra etre
// ENTIEREMENT REMESURE. Les deux voies different d'environ 1 % :
// garder deux jeux de constantes, ne jamais moyenner.
#define MEAS_I1_OFFSET_V 0.0473f
#define MEAS_I1_GAIN_V_PER_A 0.631f
#define MEAS_I2_OFFSET_V 0.0340f
#define MEAS_I2_GAIN_V_PER_A 0.637f
// ---- NTC B57451V5103J062 (PROMPT §5) --------------------------------
// Montage : 3,3 V -- NTC -- R_fixe -- 0 V, mesure au point milieu.
// La NTC etant du cote 3,3 V, la tension MONTE avec la temperature :
// R_ntc = R_fixe * (VREF - Vadc) / Vadc
// (le PROMPT §5 donne cette relation inversee ; verifie sur ses propres
// points de repere : 0,75 V a 0 C -> 34,2 kOhm, ce que beta=4000 predit,
// alors que la forme inversee donnerait 2,94 kOhm, donc du chaud.)
// R_fixe suppose a 10 kOhm : reste a confirmer sur la carte (doc §6).
#define MEAS_NTC_R_FIXED_OHM 10000.0f
#define MEAS_NTC_R25_OHM 10000.0f
#define MEAS_NTC_BETA_K 4000.0f
#define MEAS_NTC_T25_K 298.15f
#define MEAS_KELVIN_OFFSET 273.15f
// ---- Seuil de protection rapide (doc §5) ----------------------------
// Le seuil est defini en AMPERES ; le code DAC est calcule par macro a
// partir du gain et de l'offset mesures, jamais ecrit en dur. Ainsi le
// futur passage au TLV9151 ne touchera que les constantes ci-dessus.
//
// Formule TI : V = DACVAL * (VDDA - VSSA) / 1023 -> 1023, pas 4096.
// Seuil nominal (PROMPT §5). Le declenchement a ete valide au banc a 1,5 A
// (courant injectable), puis le seuil remis a sa valeur de service.
#define SAFETY_ISHUNT_THRESHOLD_A 3.0f
#define SAFETY_DAC_VREF_V 3.3f
#define SAFETY_DAC_CODE \
((uint16_t)((SAFETY_ISHUNT_THRESHOLD_A * 0.6f) / SAFETY_DAC_VREF_V * 1023.0f + 0.5f))
#define SAFETY_DAC_CODE_FROM_V(v_) \
((uint16_t)((v_) / SAFETY_DAC_VREF_V * 1023.0f + 0.5f))
// Seuil ramene a la sortie de l'ampli : Vadc = offset + I * gain.
#define SAFETY_DAC_CODE_STAGE1 \
SAFETY_DAC_CODE_FROM_V(MEAS_I1_OFFSET_V \
+ SAFETY_ISHUNT_THRESHOLD_A * MEAS_I1_GAIN_V_PER_A)
#define SAFETY_DAC_CODE_STAGE2 \
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.

View File

@ -6,6 +6,8 @@
#include "protocol.h"
#include "pwm.h"
#include "adc.h"
#include "measure.h"
#include "status_led.h"
#include "calib.h"
extern uint16_t RamfuncsLoadStart;
@ -14,11 +16,76 @@ 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));
}
}
// Etat sur : les deux etages inhibes ET la sortie HT coupee.
//
// Couper HV_EN est indispensable et n'a rien de redondant : un boost a 0 %
// de duty ne donne PAS 0 V en sortie, le chemin Vin -> L -> diode -> Cout
// reste passant en permanence. HV_EN (VOM1271) est le seul organe qui isole
// reellement la charge (PROMPT §6 etape 6).
static void enter_safe_state(void)
{
pwm_enable(STAGE_1, false);
pwm_enable(STAGE_2, false);
stage_enable_set(STAGE_1, false);
stage_enable_set(STAGE_2, false);
hv_enable_set(false);
}
// Priorite : surintensite > surtemperature > EMUSTOP > liaison perdue.
static led_state_t compute_led_state(const safety_faults_t *f)
{
safety_faults_t faults = *f;
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
@ -27,6 +94,7 @@ void main(void)
bsp_clock_init();
bsp_gpio_leds_init();
status_led_init(); // bleu fixe des le depart : "je suis parti"
DINT;
InitPieCtrl();
@ -48,13 +116,10 @@ void main(void)
// que le Trip Zone n'est pas arme.
pwm_init();
// safety_init() complet volontairement NON appele : le comparateur a
// deja ete valide (trip observe en passant une entree de 0 a 3 V), et
// avec les entrees shunt flottantes il declencherait aussitot, ce qui
// masquerait le test d'EMUSTOP. On arme donc uniquement TZ6.
// A REMPLACER par safety_init() avant toute mise sous tension de la
// puissance (PROMPT §8).
safety_arm_emustop_only();
// Protection complete : comparateurs + Digital Compare + Trip Zone, et
// TZ6/EMUSTOP. Les entrees shunt sont maintenant reellement cablees, le
// comparateur ne declenchera donc pas spontanement.
safety_init();
// ADC : declenche par ePWM1, donc apres pwm_init().
adc_init();
@ -91,39 +156,60 @@ void main(void)
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).
update_overtemp();
faults = safety_get_fault_flags();
// ARRET GLOBAL : n'importe quel defaut, sur n'importe quel etage,
// arrete l'ensemble. Sur un boost en cascade, laisser tourner
// l'etage 2 alors que l'etage 1 est coupe n'a pas de sens : il
// travaillerait sans tension d'entree.
//
// Les Trip Zones restent independantes en materiel (une surintensite
// etage 2 ne coupe que EPWM2A) : cet arret global est la couche
// systeme qui vient PAR-DESSUS. Le logiciel peut inhiber en plus,
// jamais outrepasser la protection materielle (PROMPT §2).
//
// Aucun redemarrage automatique : rien ne reactive le PWM ensuite,
// meme si la temperature redescend sous l'hysteresis ou si le defaut
// disparait. Il faut un reset, ou une commande explicite (etape 8).
if (faults.stage1_fault || faults.stage2_fault || s_overtemp)
{
bool tripped = faults.stage1_fault || faults.stage2_fault;
led_set(LED_RED, tripped);
led_set(LED_BLUE, !tripped);
enter_safe_state();
}
else
{
stage_enable_set(STAGE_1, true);
stage_enable_set(STAGE_2, true);
}
status_led_set_state(compute_led_state(&faults));
if (s_send_telemetry)
{
telemetry_t t;
// BRING-UP : chaque champ transporte la TENSION BRUTE de la
// broche ADC correspondante, en volts (0..3,3) -- et non la
// grandeur physique. Permet de verifier les 9 voies d'un coup
// sur l'IHM ESP32 apres ressoudure. measure.c fera la vraie
// conversion (ponts diviseurs, gain de shunt, NTC) a l'etape 5.
// Grandeurs physiques reelles (etape 5) : conversion par
// measure.c a partir des coefficients mesures sur la carte
// (docs/mesure-cartepuissance.md).
// FREQ/DUTY restent les valeurs de consigne du bloc de bring-up
// ci-dessus, pas encore une mesure.
t.freq1_hz = (float)PWM_STAGE1_FREQ_HZ;
t.freq2_hz = (float)PWM_STAGE2_FREQ_HZ;
t.duty1_pct = 50.0f;
t.duty2_pct = 50.0f;
t.vin_v = adc_get_volts(ADC_CH_VIN);
t.iin_a = adc_get_volts(ADC_CH_IIN);
t.v1_v = adc_get_volts(ADC_CH_V1);
t.i1_a = adc_get_volts(ADC_CH_I1);
t.t1_c = adc_get_volts(ADC_CH_T1);
t.vout_v = adc_get_volts(ADC_CH_VOUT);
t.i2_a = adc_get_volts(ADC_CH_I2);
t.t2_c = adc_get_volts(ADC_CH_T2);
t.iout_a = adc_get_volts(ADC_CH_IOUT);
t.vin_v = measure_vin(adc_get_raw(ADC_CH_VIN));
t.iin_a = measure_iin(adc_get_raw(ADC_CH_IIN));
t.v1_v = measure_v1(adc_get_raw(ADC_CH_V1));
t.i1_a = measure_i1(adc_get_raw(ADC_CH_I1));
t.t1_c = measure_temp(adc_get_raw(ADC_CH_T1));
t.vout_v = measure_vout(adc_get_raw(ADC_CH_VOUT));
t.i2_a = measure_i2(adc_get_raw(ADC_CH_I2));
t.t2_c = measure_temp(adc_get_raw(ADC_CH_T2));
t.iout_a = measure_iout(adc_get_raw(ADC_CH_IOUT));
uart_link_send_telemetry(&t);
s_send_telemetry = false;
@ -149,6 +235,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

76
src/measure.c Normal file
View File

@ -0,0 +1,76 @@
#include <math.h>
#include "measure.h"
#include "calib.h"
// Brut -> tension a la broche. Reference interne, pleine echelle 3,3 V.
static float raw_to_volts(uint16_t raw)
{
return (float)raw * ADC_VREF_V / 4096.0f;
}
float measure_vin(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_VIN_GAIN_V_PER_V;
}
float measure_iin(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_IIN_A_PER_V;
}
float measure_v1(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_V1_GAIN_V_PER_V;
}
float measure_vout(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_VOUT_GAIN_V_PER_V;
}
float measure_iout(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_IOUT_A_PER_V;
}
// Vadc = offset + I * gain -> I = (Vadc - offset) / gain.
// Le resultat peut etre legerement negatif a courant nul (bruit autour de
// l'offset) : on ne le sature pas, une valeur negative en telemetrie est un
// indice utile de derive de l'offset apres changement d'ampli.
float measure_i1(uint16_t raw)
{
return (raw_to_volts(raw) - MEAS_I1_OFFSET_V) / MEAS_I1_GAIN_V_PER_A;
}
float measure_i2(uint16_t raw)
{
return (raw_to_volts(raw) - MEAS_I2_OFFSET_V) / MEAS_I2_GAIN_V_PER_A;
}
// Pont 3,3 V -- NTC -- R_fixe -- 0 V, mesure au point milieu.
// R_ntc = R_fixe * (VREF - Vadc) / Vadc
// T(K) = 1 / ( 1/T25 + ln(R_ntc / R25) / beta )
//
// Precision attendue : la tolerance de +/-3 % sur beta donne environ
// +/-2 degC vers 100 degC sans calibration individuelle. Suffisant pour de
// la protection thermique, pas pour de la mesure fine (PROMPT §5).
float measure_temp(uint16_t raw)
{
float vadc = raw_to_volts(raw);
float r_ntc;
float inv_t;
// Bornes : hors de cette plage le pont est en butee (broche en l'air,
// NTC en court-circuit ou coupee) et le calcul n'a plus de sens.
if (vadc <= 0.01f || vadc >= (ADC_VREF_V - 0.01f))
{
return 0.0f;
}
r_ntc = MEAS_NTC_R_FIXED_OHM * (ADC_VREF_V - vadc) / vadc;
inv_t = 1.0f / MEAS_NTC_T25_K
+ logf(r_ntc / MEAS_NTC_R25_OHM) / MEAS_NTC_BETA_K;
return (1.0f / inv_t) - MEAS_KELVIN_OFFSET;
}

30
src/measure.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef MEASURE_H
#define MEASURE_H
#include <stdint.h>
// Conversion des valeurs brutes de l'ADC (0..4095) vers les grandeurs
// physiques, a partir des seules constantes de calib.h (elles-memes issues
// de docs/mesure-cartepuissance.md).
//
// Ces fonctions utilisent du flottant et, pour les NTC, un logarithme :
// a n'appeler QUE depuis la boucle principale, jamais depuis une ISR
// (PROMPT §7). La protection rapide ne passe pas par ici.
float measure_vin(uint16_t raw); // V - tension d'entree batterie
float measure_iin(uint16_t raw); // A - courant d'entree
float measure_v1(uint16_t raw); // V - tension intermediaire (etage 1)
float measure_vout(uint16_t raw); // V - tension de sortie HT
float measure_iout(uint16_t raw); // A - courant de sortie HT
// Courant shunt MOSFET. Deux fonctions distinctes et non une seule
// parametree : les deux voies ont leur propre gain ET leur propre offset,
// mesures separement.
float measure_i1(uint16_t raw); // A - shunt etage 1
float measure_i2(uint16_t raw); // A - shunt etage 2
// Temperature NTC, en degres Celsius. Meme chaine de mesure sur les deux
// etages, donc une seule fonction.
float measure_temp(uint16_t raw); // degC
#endif

View File

@ -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);
@ -31,7 +33,9 @@ void safety_init(void)
Comp1Regs.COMPCTL.bit.SYNCSEL = 0; // asynchrone -> ~30 ns vers le Trip Zone
Comp1Regs.COMPCTL.bit.CMPINV = 1; // driver actif haut : 1=OK, 0=defaut
Comp1Regs.COMPCTL.bit.QUALSEL = 0; // pas de qualification pour l'instant
Comp1Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE;
// Seuil propre a l'etage 1 : les deux chaines de mesure different d'environ
// 1 % en gain et n'ont pas le meme offset (docs/mesure-cartepuissance.md).
Comp1Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE_STAGE1;
GpioCtrlRegs.GPAMUX1.bit.GPIO1 = 3; // COMP1OUT
// Comparateur 2 : idem, shunt etage 2 sur COMP2A.
@ -40,7 +44,7 @@ void safety_init(void)
Comp2Regs.COMPCTL.bit.SYNCSEL = 0;
Comp2Regs.COMPCTL.bit.CMPINV = 1;
Comp2Regs.COMPCTL.bit.QUALSEL = 0;
Comp2Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE;
Comp2Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE_STAGE2;
GpioCtrlRegs.GPAMUX1.bit.GPIO3 = 3; // COMP2OUT
// Digital Compare : COMPxOUT -> DCAEVT1 -> Trip Zone one-shot (latche)
@ -102,18 +106,35 @@ 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;
}
void safety_clear_faults(void)
{
EALLOW;
// OST libere la sortie PWM, DCAEVT1 remet a zero la cause (sinon le
// prochain trip serait attribue a une surintensite meme s'il vient
// d'EMUSTOP), INT autorise a nouveau la generation d'interruptions, et
// TZEINT.OST rearme la notification desarmee par l'ISR.
EPwm1Regs.TZCLR.bit.DCAEVT1 = 1;
EPwm1Regs.TZCLR.bit.OST = 1;
EPwm1Regs.TZCLR.bit.INT = 1;
EPwm1Regs.TZEINT.bit.OST = 1;
EPwm2Regs.TZCLR.bit.DCAEVT1 = 1;
EPwm2Regs.TZCLR.bit.OST = 1;
EPwm2Regs.TZCLR.bit.INT = 1;
EPwm2Regs.TZEINT.bit.OST = 1;
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)
@ -124,16 +145,64 @@ 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.
//
// Deux subtilites, decouvertes au banc :
//
// 1. TZFLG.INT est le drapeau GLOBAL d'interruption Trip Zone. Tant qu'il
// n'est pas efface via TZCLR.INT, plus AUCUNE interruption TZ n'est
// generee. Sans ce clear, seul le tout premier trip de la session etait
// signale au logiciel : les suivants coupaient bien le PWM en materiel,
// mais en silence, sans lever de drapeau.
//
// 2. Effacer INT alors que la condition de trip persiste (surintensite
// toujours presente) regenere l'evenement immediatement -> tempete
// d'interruptions qui affamerait la boucle principale. On desarme donc
// TZEINT.OST ici : une seule notification par episode de defaut. Le
// rearmement est fait par safety_clear_faults(), c'est-a-dire jamais
// automatiquement (PROMPT §8).
//
// Le drapeau DCAEVT1 n'est volontairement PAS efface ici : il porte la
// cause du defaut et doit rester lisible jusqu'a l'effacement explicite.
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;
}
EALLOW;
EPwm1Regs.TZEINT.bit.OST = 0;
EPwm1Regs.TZCLR.bit.INT = 1;
EDIS;
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;
}
EALLOW;
EPwm2Regs.TZEINT.bit.OST = 0;
EPwm2Regs.TZCLR.bit.INT = 1;
EDIS;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP2;
}

View File

@ -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);

96
src/status_led.c Normal file
View File

@ -0,0 +1,96 @@
#include <stdbool.h>
#include <stdint.h>
#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);
}

41
src/status_led.h Normal file
View File

@ -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

View File

@ -1,6 +1,5 @@
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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