Champ FAULT, decharge HT active, et surtemperature par voie
Circuit de decharge rapide (GPIO5, broche 40)
Commande DISCHARGE du circuit de la carte de puissance : MOSFET en serie
avec 47 kOhm aux bornes du condensateur de sortie, soit tau = 0,94 s et
environ 2 s pour passer de 500 V a 50 V. Sans lui, le seul bleeder de
1 MOhm demandait ~42 s.
POLARITE INVERSEE, et c'est le coeur du montage. Le GPIO pilote un NPN
qui court-circuite la grille du MOSFET :
niveau haut -> NPN conduit -> grille a la masse -> decharge INHIBEE
niveau bas ou broche non pilotee -> grille tiree a 12 V -> decharge ACTIVE
GPIO5 est une broche a fonction PWM : pas de pull-up au reset (PROMPT §6
etape 3), elle flotte. La decharge est donc active au reset, pendant un
plantage, et surtout ALIMENTATION COUPEE -- exactement le cas ou le
firmware ne garantit plus rien et ou l'operateur croit la carte
inoffensive. C'est precisement le cas impose par le choix de n'effacer un
defaut que par cycle d'alimentation.
Sequencement explicite :
- enter_safe_state() active la decharge APRES avoir coupe HV_EN : la
charge est isolee d'abord, le condensateur vide ensuite ;
- le retour au nominal inhibe la decharge AVANT d'autoriser les etages,
jamais l'inverse ;
- bsp_gpio_control_init() la laisse ACTIVE au demarrage : au boot le
condensateur peut encore etre charge par une session precedente.
Champ FAULT dans la trame $T
Codes 0-5, priorite 2 > 3 > 4 > 5 > 1. EMUSTOP (code 1) a donc la
priorite la PLUS BASSE malgre son numero : en developpement il se
declenche a chaque halte du debogueur et ne doit jamais masquer une
surintensite reelle.
Pas de code court-circuit : il franchit le meme comparateur et le meme
seuil qu'une surintensite, le firmware ne peut pas les distinguer.
Aucun effacement automatique ni par commande UART : le verrouillage
n'est leve que par un cycle d'alimentation (PROMPT §8).
Surtemperature suivie PAR VOIE
s_overtemp_t1 / s_overtemp_t2 separes, avec hysteresis chacun. L'ancien
maximum des deux voies ne permettait pas de distinguer les codes 4 et 5.
Seuil ramene a 80 degC pour coller au protocole.
Divers
UART_LINE_MAX porte a 200 (buffer ESP32 g_lineBuf[200]).
append_field_int() pour les champs entiers, meme convention de bornage
que append_field : un champ qui ne tient pas est abandonne entierement.
Documentation
docs/mesure-cartepuissance.md ajoute au depot (source de tracabilite des
coefficients de calib.h).
docs/ESP32-UART.md mis a jour, avec deux corrections de fond :
- la mention "remettre FAULT=0 automatiquement des que la cause a
disparu" est remplacee par une section sur le verrouillage ; elle
contredisait le comportement reel, le TMS320 reemettant indefiniment
le code latche ;
- l'icone danger est specifiee comme dependant UNIQUEMENT de VOUT et
jamais de FAULT. Un defaut ne decharge pas le condensateur : la faire
disparaitre reviendrait a rassurer l'operateur quand le risque est
maximal.
Le piege strtol / uint8_t 16 bits y est documente pour eviter qu'il soit
reintroduit ailleurs.
A cabler cote materiel : liaison broche 40 vers la carte de puissance, et
pull-down 10 kOhm sur la base du NPN.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -43,12 +43,20 @@ void bsp_gpio_control_init(void)
|
||||
// HV_EN = GPIO32 (broche 31)
|
||||
GpioCtrlRegs.GPBMUX1.bit.GPIO32 = 0;
|
||||
GpioCtrlRegs.GPBDIR.bit.GPIO32 = 1;
|
||||
|
||||
// DISCHARGE = GPIO5 (broche 40)
|
||||
GpioCtrlRegs.GPAMUX1.bit.GPIO5 = 0;
|
||||
GpioCtrlRegs.GPADIR.bit.GPIO5 = 1;
|
||||
EDIS;
|
||||
|
||||
// Etat sur avant toute autre configuration.
|
||||
// Etat sur avant toute autre configuration. La decharge est laissee
|
||||
// ACTIVE : au demarrage, le condensateur de sortie peut encore etre
|
||||
// charge par une session precedente, rien ne justifie de l'inhiber
|
||||
// avant que la conversion ne soit reellement demandee.
|
||||
stage_enable_set(STAGE_1, false);
|
||||
stage_enable_set(STAGE_2, false);
|
||||
hv_enable_set(false);
|
||||
hv_discharge_set(true);
|
||||
}
|
||||
|
||||
void stage_enable_set(stage_id_t stage, bool enabled)
|
||||
@ -89,6 +97,20 @@ void hv_enable_set(bool enabled)
|
||||
}
|
||||
}
|
||||
|
||||
void hv_discharge_set(bool active)
|
||||
{
|
||||
// Niveau BAS = decharge active (NPN bloque, grille du MOSFET libre).
|
||||
// Voir bsp_gpio.h pour le detail de la polarite inversee.
|
||||
if (active)
|
||||
{
|
||||
GpioDataRegs.GPACLEAR.bit.GPIO5 = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
GpioDataRegs.GPASET.bit.GPIO5 = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void bsp_gpio_stage_default_override_init(void)
|
||||
{
|
||||
EALLOW;
|
||||
|
||||
@ -36,6 +36,26 @@ void stage_enable_set(stage_id_t stage, bool enabled);
|
||||
// donne PAS 0 V en sortie (PROMPT §6 etape 6).
|
||||
void hv_enable_set(bool enabled);
|
||||
|
||||
// DISCHARGE (GPIO5, broche 40) : circuit de decharge rapide du condensateur
|
||||
// de sortie, sur la carte de puissance. 47 kOhm en serie avec un MOSFET,
|
||||
// soit ~2 s pour passer de 500 V a 50 V (tau = 0,94 s).
|
||||
//
|
||||
// POLARITE INVERSEE, et c'est le coeur du montage. Le GPIO pilote un NPN
|
||||
// qui court-circuite la grille du MOSFET :
|
||||
// niveau HAUT -> NPN conduit -> grille a la masse -> MOSFET bloque
|
||||
// -> decharge INHIBEE (fonctionnement normal)
|
||||
// niveau BAS, ou broche non pilotee
|
||||
// -> NPN bloque -> grille tiree a 12 V (chaine 3x1 MOhm
|
||||
// depuis le +500 V, ecretee par Zener) -> MOSFET conduit
|
||||
// -> decharge ACTIVE
|
||||
//
|
||||
// GPIO5 est une broche a fonction PWM : ses pull-ups ne sont PAS actives au
|
||||
// reset (PROMPT §6 etape 3), elle flotte donc. La decharge est par
|
||||
// consequent active au reset, pendant un plantage, et surtout ALIMENTATION
|
||||
// COUPEE -- c'est-a-dire exactement le cas ou le firmware ne peut plus rien
|
||||
// garantir et ou l'operateur croit la carte inoffensive.
|
||||
void hv_discharge_set(bool active);
|
||||
|
||||
// BRING-UP UNIQUEMENT -- bascule Stage1/2-default (GPIO1/GPIO3) en sortie
|
||||
// GPIO pilotee par logiciel, pour verifier le cablage des portes ET sans
|
||||
// etage de puissance actif. Laisse les deux broches a 0.
|
||||
|
||||
@ -96,7 +96,7 @@
|
||||
// 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_C 80.0f
|
||||
#define SAFETY_OVERTEMP_HYST_C 10.0f
|
||||
|
||||
// ---- Timeout de la liaison ESP32 (PROMPT §6 etape 7) ----------------
|
||||
@ -133,6 +133,6 @@
|
||||
((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])
|
||||
#define UART_LINE_MAX 200U // limite cote ESP32 (g_lineBuf[200])
|
||||
|
||||
#endif
|
||||
|
||||
66
src/main.c
66
src/main.c
@ -27,24 +27,54 @@ static volatile bool s_send_telemetry = false;
|
||||
#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;
|
||||
// Surtemperature, suivie PAR VOIE : le protocole distingue les codes 4 (T1)
|
||||
// et 5 (T2), un simple maximum ne permettrait pas de les separer.
|
||||
// Hysteresis pour eviter le battement autour du seuil.
|
||||
static bool s_overtemp_t1 = false;
|
||||
static bool s_overtemp_t2 = false;
|
||||
|
||||
static bool update_one_overtemp(bool current, float temp_c)
|
||||
{
|
||||
if (!current)
|
||||
{
|
||||
return (temp_c >= SAFETY_OVERTEMP_C);
|
||||
}
|
||||
return (temp_c > (SAFETY_OVERTEMP_C - SAFETY_OVERTEMP_HYST_C));
|
||||
}
|
||||
|
||||
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;
|
||||
s_overtemp_t1 =
|
||||
update_one_overtemp(s_overtemp_t1, measure_temp(adc_get_raw(ADC_CH_T1)));
|
||||
s_overtemp_t2 =
|
||||
update_one_overtemp(s_overtemp_t2, measure_temp(adc_get_raw(ADC_CH_T2)));
|
||||
}
|
||||
|
||||
if (!s_overtemp)
|
||||
// Priorite 2 > 3 > 4 > 5 > 1 : EMUSTOP est le code le moins prioritaire
|
||||
// malgre son numero, pour ne jamais masquer une surintensite reelle.
|
||||
static fault_code_t compute_fault_code(const safety_faults_t *f)
|
||||
{
|
||||
if (f->overcurrent && f->stage1_fault)
|
||||
{
|
||||
s_overtemp = (tmax >= SAFETY_OVERTEMP_C);
|
||||
return FAULT_OVERCURRENT_I1;
|
||||
}
|
||||
else
|
||||
if (f->overcurrent && f->stage2_fault)
|
||||
{
|
||||
s_overtemp = (tmax > (SAFETY_OVERTEMP_C - SAFETY_OVERTEMP_HYST_C));
|
||||
return FAULT_OVERCURRENT_I2;
|
||||
}
|
||||
if (s_overtemp_t1)
|
||||
{
|
||||
return FAULT_OVERTEMP_T1;
|
||||
}
|
||||
if (s_overtemp_t2)
|
||||
{
|
||||
return FAULT_OVERTEMP_T2;
|
||||
}
|
||||
if (f->emustop)
|
||||
{
|
||||
return FAULT_EMUSTOP;
|
||||
}
|
||||
return FAULT_NONE;
|
||||
}
|
||||
|
||||
// Etat sur : les deux etages inhibes ET la sortie HT coupee.
|
||||
@ -60,6 +90,12 @@ static void enter_safe_state(void)
|
||||
stage_enable_set(STAGE_1, false);
|
||||
stage_enable_set(STAGE_2, false);
|
||||
hv_enable_set(false);
|
||||
|
||||
// Decharge active. Elle vient APRES la coupure de HV_EN : la charge est
|
||||
// d'abord isolee, puis le condensateur vide en ~2 s. Sans elle, les
|
||||
// 400 V resteraient presents pres d'une minute sur le seul bleeder de
|
||||
// 1 MOhm, sans aucune indication une fois l'alimentation coupee.
|
||||
hv_discharge_set(true);
|
||||
}
|
||||
|
||||
// Priorite : surintensite > surtemperature > EMUSTOP > liaison perdue.
|
||||
@ -71,7 +107,7 @@ static led_state_t compute_led_state(const safety_faults_t *f)
|
||||
{
|
||||
return LED_STATE_OVERCURRENT;
|
||||
}
|
||||
if (s_overtemp)
|
||||
if (s_overtemp_t1 || s_overtemp_t2)
|
||||
{
|
||||
return LED_STATE_OVERTEMP;
|
||||
}
|
||||
@ -176,12 +212,17 @@ void main(void)
|
||||
// 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)
|
||||
if (faults.stage1_fault || faults.stage2_fault
|
||||
|| s_overtemp_t1 || s_overtemp_t2)
|
||||
{
|
||||
enter_safe_state();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Inhiber la decharge AVANT d'autoriser les etages : jamais
|
||||
// l'inverse, sinon on ferait travailler la conversion contre le
|
||||
// circuit de decharge.
|
||||
hv_discharge_set(false);
|
||||
stage_enable_set(STAGE_1, true);
|
||||
stage_enable_set(STAGE_2, true);
|
||||
}
|
||||
@ -210,6 +251,7 @@ void main(void)
|
||||
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));
|
||||
t.fault = compute_fault_code(&faults);
|
||||
|
||||
uart_link_send_telemetry(&t);
|
||||
s_send_telemetry = false;
|
||||
|
||||
@ -6,6 +6,26 @@
|
||||
// Squelette du protocole UART TMS320<->ESP32 (voir docs/ESP32-UART.md).
|
||||
// Rempli/utilise a partir de l'etape 7 (uart_link.c).
|
||||
|
||||
// Codes du champ FAULT de la trame $T (docs/ESP32-UART.md).
|
||||
//
|
||||
// Attention a la priorite d'affichage quand plusieurs defauts coexistent :
|
||||
// elle est 2 > 3 > 4 > 5 > 1, donc EMUSTOP a la priorite la PLUS BASSE
|
||||
// malgre son numero. C'est voulu : en developpement EMUSTOP se declenche a
|
||||
// chaque halte du debogueur et ne doit jamais masquer une surintensite.
|
||||
//
|
||||
// Il n'existe volontairement AUCUN code de court-circuit : un court-circuit
|
||||
// franchit le meme comparateur et le meme seuil qu'une surintensite, le
|
||||
// firmware ne dispose d'aucune information permettant de les distinguer.
|
||||
typedef enum
|
||||
{
|
||||
FAULT_NONE = 0,
|
||||
FAULT_EMUSTOP = 1,
|
||||
FAULT_OVERCURRENT_I1 = 2,
|
||||
FAULT_OVERCURRENT_I2 = 3,
|
||||
FAULT_OVERTEMP_T1 = 4,
|
||||
FAULT_OVERTEMP_T2 = 5
|
||||
} fault_code_t;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
float freq1_hz;
|
||||
@ -21,6 +41,7 @@ typedef struct
|
||||
float i2_a;
|
||||
float t2_c;
|
||||
float iout_a;
|
||||
fault_code_t fault;
|
||||
} telemetry_t;
|
||||
|
||||
typedef struct
|
||||
|
||||
@ -151,6 +151,26 @@ static int append_field(char *out, int room, const char *tag, float value,
|
||||
return n;
|
||||
}
|
||||
|
||||
// Champ entier (FAULT). Meme convention de bornage que append_field :
|
||||
// renvoie 0 et n'ecrit rien si le champ ne tient pas.
|
||||
static int append_field_int(char *out, int room, const char *tag, int value)
|
||||
{
|
||||
int n;
|
||||
|
||||
if (room <= 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
n = snprintf(out, (size_t)room, ",%s=%d", tag, value);
|
||||
if (n < 0 || n >= room)
|
||||
{
|
||||
out[0] = '\0';
|
||||
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
|
||||
@ -195,6 +215,12 @@ bool uart_link_send_telemetry(const telemetry_t *t)
|
||||
TX_ADD("T2", t->t2_c, 1);
|
||||
TX_ADD("IOUT", t->iout_a, 2);
|
||||
|
||||
{
|
||||
int added_ = append_field_int(&s_tx_frame[n], TX_ROOM(), "FAULT",
|
||||
(int)t->fault);
|
||||
if (added_ == 0) { fields_dropped++; } else { n += added_; }
|
||||
}
|
||||
|
||||
#undef TX_ADD
|
||||
#undef TX_ROOM
|
||||
|
||||
|
||||
Reference in New Issue
Block a user