Le verrouillage total decide precedemment (aucun effacement, cycle
d'alimentation obligatoire) rendait le developpement intenable : chaque
breakpoint tuait le PWM jusqu'a debranchement. EMUSTOP n'est pourtant pas
un defaut de puissance mais un artefact du debogueur.
La distinction est maintenant explicite dans la boucle principale :
overcurrent / overtemp -> enter_safe_state(), VERROUILLE jusqu'au cycle
d'alimentation (PROMPT §8), aucun acquittement
emustop seul -> reprise controlee
La reprise ne redemarre PAS la conversion la ou elle s'etait arretee :
elle repasse par start_conversion(), le meme chemin qu'au boot, duty remis
a zero d'abord. Quand la rampe de soft-start existera, la reprise apres
breakpoint en beneficiera automatiquement.
uart_link_restart()
Reset logiciel du SCI, vidage des FIFO, abandon de la ligne en cours et
de la trame en attente. Pendant la halte l'ESP32 a continue d'emettre :
l'anneau de reception a deborde et la ligne assemblee est tronquee.
Le vidage avance seulement la QUEUE de l'anneau ; s_rx_head appartient a
l'ISR et n'est pas touche, ce qui exclut toute course.
TZCLR au demarrage dans safety_init()
TZFLG.OST est latche dans le materiel : il survit a un reset logiciel
comme a un rechargement de programme, et sans ce clear le PWM restait
coupe sans raison visible.
Sans danger, pour une raison precise : a ce stade pwm_init() a deja
force les deux sorties a l'etat bas (AQCSFRC), donc rien ne demarre ; et
si la condition de defaut est toujours presente le trip se re-verrouille
immediatement. On n'efface qu'un etat perime, jamais un defaut reel.
start_conversion()
Sequence de demarrage factorisee, appelee au boot et a la reprise.
Inhibe la decharge AVANT d'autoriser les etages -- jamais l'inverse,
sinon la conversion travaillerait contre le circuit de decharge.
Valide sur cible apres cycle pause/reprise/pause :
TZEINT.OST rearme a 1 (l'ISR l'avait mis a 0), AQCSFRC.CSFA a 0,
GPIO5 a 1 (decharge inhibee), GPIO16 a 1.
LIMITE CONNUE, non resolue ici : a la reprise l'effacement est immediat
alors que la telemetrie ne part que toutes les 300 ms. FAULT=1 ne sera
donc quasiment jamais emis, et l'ESP32 ignorera qu'un EMUSTOP a eu lieu
(sans meme voir de timeout si la halte a dure moins de 2 s). Transmettre
FAULT=1 AVANT la halte est structurellement impossible : la halte est
asynchrone, imposee par la logique de debug, sans point d'accroche
prealable -- c'est justement pourquoi TZ6 est un chemin materiel. La
solution sera un drapeau collant maintenu quelques secondes apres la
reprise.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
225 lines
7.3 KiB
C
225 lines
7.3 KiB
C
#include "DSP28x_Project.h"
|
|
#include "safety.h"
|
|
#include "bsp_gpio.h"
|
|
#include "calib.h"
|
|
|
|
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);
|
|
|
|
void safety_init(void)
|
|
{
|
|
EALLOW;
|
|
|
|
// Le comparateur partage la reference bandgap de l'ADC : elle doit etre
|
|
// alimentee meme si l'ADC lui-meme n'est pas encore utilise (etape 4).
|
|
SysCtrlRegs.PCLKCR0.bit.ADCENCLK = 1;
|
|
SysCtrlRegs.PCLKCR3.bit.COMP1ENCLK = 1;
|
|
SysCtrlRegs.PCLKCR3.bit.COMP2ENCLK = 1;
|
|
AdcRegs.ADCCTL1.bit.ADCBGPWD = 1;
|
|
EDIS;
|
|
DELAY_US(1000L);
|
|
|
|
EALLOW;
|
|
|
|
// Comparateur 1 : shunt etage 1 sur COMP1A (entree non-inverseuse fixe),
|
|
// seuil DAC sur l'entree inverseuse (COMPSOURCE=0).
|
|
Comp1Regs.COMPCTL.bit.COMPDACEN = 1;
|
|
Comp1Regs.COMPCTL.bit.COMPSOURCE = 0;
|
|
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
|
|
// 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.
|
|
Comp2Regs.COMPCTL.bit.COMPDACEN = 1;
|
|
Comp2Regs.COMPCTL.bit.COMPSOURCE = 0;
|
|
Comp2Regs.COMPCTL.bit.SYNCSEL = 0;
|
|
Comp2Regs.COMPCTL.bit.CMPINV = 1;
|
|
Comp2Regs.COMPCTL.bit.QUALSEL = 0;
|
|
Comp2Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE_STAGE2;
|
|
GpioCtrlRegs.GPAMUX1.bit.GPIO3 = 3; // COMP2OUT
|
|
|
|
// Digital Compare : COMPxOUT -> DCAEVT1 -> Trip Zone one-shot (latche)
|
|
EPwm1Regs.DCTRIPSEL.bit.DCAHCOMPSEL = DC_COMP1OUT;
|
|
EPwm1Regs.TZDCSEL.bit.DCAEVT1 = TZ_DCAH_LOW;
|
|
EPwm1Regs.TZSEL.bit.DCAEVT1 = 1;
|
|
EPwm1Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
|
|
EPwm1Regs.TZEINT.bit.OST = 1;
|
|
// TZ6 = EMUSTOP (signal cable en dur depuis le CPU, TRM SPRUI09A section
|
|
// 3.2.7) : coupe le PWM des que le debugger arrete le coeur. FREE_SOFT
|
|
// seul ne suffit pas, il ne fait que geler le compteur de base de temps,
|
|
// la broche reste figee dans son dernier etat.
|
|
EPwm1Regs.TZSEL.bit.OSHT6 = 1;
|
|
EPwm1Regs.TBCTL.bit.FREE_SOFT = 0;
|
|
|
|
EPwm2Regs.DCTRIPSEL.bit.DCAHCOMPSEL = DC_COMP2OUT;
|
|
EPwm2Regs.TZDCSEL.bit.DCAEVT1 = TZ_DCAH_LOW;
|
|
EPwm2Regs.TZSEL.bit.DCAEVT1 = 1;
|
|
EPwm2Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
|
|
EPwm2Regs.TZEINT.bit.OST = 1;
|
|
EPwm2Regs.TZSEL.bit.OSHT6 = 1;
|
|
EPwm2Regs.TBCTL.bit.FREE_SOFT = 0;
|
|
|
|
// Efface un trip herite d'AVANT ce demarrage. TZFLG.OST est latche dans
|
|
// le materiel : il survit a un reset logiciel comme a un simple
|
|
// rechargement de programme, et sans ce clear le PWM resterait coupe
|
|
// sans raison visible.
|
|
//
|
|
// Sans danger : a ce stade pwm_init() a laisse les deux sorties forcees
|
|
// a l'etat bas (AQCSFRC), donc rien ne demarre. Et si la condition de
|
|
// defaut est toujours presente, le trip se re-verrouille immediatement
|
|
// -- on n'efface qu'un etat perime, jamais un defaut reel.
|
|
EPwm1Regs.TZCLR.bit.DCAEVT1 = 1;
|
|
EPwm1Regs.TZCLR.bit.OST = 1;
|
|
EPwm1Regs.TZCLR.bit.INT = 1;
|
|
EPwm2Regs.TZCLR.bit.DCAEVT1 = 1;
|
|
EPwm2Regs.TZCLR.bit.OST = 1;
|
|
EPwm2Regs.TZCLR.bit.INT = 1;
|
|
|
|
PieVectTable.EPWM1_TZINT = &epwm1_tzint_isr;
|
|
PieVectTable.EPWM2_TZINT = &epwm2_tzint_isr;
|
|
EDIS;
|
|
|
|
IER |= M_INT2;
|
|
PieCtrlRegs.PIEIER2.bit.INTx1 = 1; // EPWM1_TZINT
|
|
PieCtrlRegs.PIEIER2.bit.INTx2 = 1; // EPWM2_TZINT
|
|
}
|
|
|
|
void safety_arm_emustop_only(void)
|
|
{
|
|
EALLOW;
|
|
// TZ6 = EMUSTOP, cable en dur depuis le CPU (TRM SPRUI09A section 3.2.7).
|
|
// One-shot : le flag latche, il ne se rearme pas seul.
|
|
EPwm1Regs.TZSEL.bit.OSHT6 = 1;
|
|
EPwm1Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
|
|
EPwm1Regs.TZEINT.bit.OST = 1;
|
|
EPwm1Regs.TBCTL.bit.FREE_SOFT = 0;
|
|
|
|
EPwm2Regs.TZSEL.bit.OSHT6 = 1;
|
|
EPwm2Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
|
|
EPwm2Regs.TZEINT.bit.OST = 1;
|
|
EPwm2Regs.TBCTL.bit.FREE_SOFT = 0;
|
|
|
|
PieVectTable.EPWM1_TZINT = &epwm1_tzint_isr;
|
|
PieVectTable.EPWM2_TZINT = &epwm2_tzint_isr;
|
|
EDIS;
|
|
|
|
IER |= M_INT2;
|
|
PieCtrlRegs.PIEIER2.bit.INTx1 = 1; // EPWM1_TZINT
|
|
PieCtrlRegs.PIEIER2.bit.INTx2 = 1; // EPWM2_TZINT
|
|
}
|
|
|
|
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;
|
|
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)
|
|
{
|
|
EALLOW;
|
|
EPwm1Regs.TZFRC.bit.OST = 1;
|
|
EPwm2Regs.TZFRC.bit.OST = 1;
|
|
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;
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|