Etapes 3/4/6 : GPIO de commande, PWM et ADC valides sur cible

Campagne de bring-up materiel complete, carte de conversion DECONNECTEE.

pwm.c/h (etape 6) -- valide au scope
  ePWM1 -> etage 1, EPWM1A/GPIO0/broche 29, 200 kHz (TBPRD=299)
  ePWM2 -> etage 2, EPWM2A/GPIO2/broche 37, 100 kHz (TBPRD=599)
  Mode up-count, TBCLK = SYSCLKOUT = 60 MHz, duty 0 au demarrage,
  inhibition par forcage continu AQCSFRC. Tranche le point ouvert §9.1.
  HRPWM non implemente, notes de portage consignees dans pwm.c.

adc.c/h (etape 4) -- 9 voies validees, T1/T2 comprises
  Reference interne (ADCREFSEL=0), SOC declenches par ePWM1 SOCA avec
  SOCAPRD=3 (~66 kHz), jamais en free-run. ISR ADCINT1 courte : rangement
  des valeurs brutes uniquement, aucun flottant.
  Note materielle : les 9 voies lisaient 4095 tant que VDDA (broche 11)
  n'etait pas alimentee -- y compris VREFLO converti en interne, ce qui a
  permis d'ecarter le logiciel.

bsp_gpio (etape 3)
  HV_EN (GPIO32), Stage1/2-EN (GPIO16/17) en sortie, etat sur a 0, et
  pull-ups internes de GPIO16/17 desactivees : sinon elles presentent un 1
  sur une entree de chaque porte ET avant que le firmware ne tourne.
  bsp_gpio_stage_default_override_init() : pilotage manuel de GPIO1/GPIO3
  pour valider les portes ET, exclusif avec safety_init().

safety_arm_emustop_only() -- BRING-UP
  Arme TZ6 seul, sans les comparateurs, pour valider EMUSTOP isolement
  quand les entrees shunt ne sont pas cablees.

Resultats materiels :
  - portes ET IC8/IC9 : sortie = ET logique des deux entrees, verifie au
    scope avec des cadences distinctes (default 400 ms, EN 1 s)
  - comparateur : entree shunt 0 -> 3 V declenche bien le trip
  - EMUSTOP : arret du debugger -> broches 29 et 37 a 0, TZFLG.OST latche,
    et le flag ne se rearme pas apres continue
  - un trip EMUSTOP ne peut PAS etre efface tant que le CPU est halte (la
    source reste assertee) : l'effacement devra passer par une commande
    UART en fonctionnement (etape 7)

main.c contient encore le bloc de bring-up (PWM force a 50 %, telemetrie
transportant les tensions ADC brutes en volts) : A RETIRER, et remplacer
safety_arm_emustop_only() par safety_init(), avant toute mise sous tension
de la puissance (PROMPT §8).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 20:42:14 +02:00
parent d185d73422
commit d26c07d3a1
10 changed files with 634 additions and 22 deletions

141
src/adc.c Normal file
View File

@ -0,0 +1,141 @@
#include "DSP28x_Project.h"
#include "adc.h"
#include "calib.h"
// Valeurs du champ CHSEL (TRM SPRUI09A) : groupe A = 0x0..0x7,
// groupe B = 0x8..0xF. L'ordre suit adc_channel_t / SOC0..SOC8.
static const uint16_t k_chsel[ADC_CH_COUNT] = {
0x0, // ADCINA0 - VIN
0x1, // ADCINA1 - IIN
0x2, // ADCINA2 - I1 (shunt etage 1)
0x3, // ADCINA3 - IOUT
0x4, // ADCINA4 - I2 (shunt etage 2)
0xA, // ADCINB2 - VOUT
0xC, // ADCINB4 - V1
0xE, // ADCINB6 - T1
0xF // ADCINB7 - T2
};
static volatile uint16_t s_raw[ADC_CH_COUNT];
static volatile uint32_t s_seq_count = 0;
interrupt void adc_int1_isr(void);
void adc_init(void)
{
uint16_t i;
EALLOW;
SysCtrlRegs.PCLKCR0.bit.ADCENCLK = 1;
// Sequence de mise sous tension du bloc analogique (f2802x_adc.c).
AdcRegs.ADCCTL1.bit.ADCBGPWD = 1;
AdcRegs.ADCCTL1.bit.ADCREFPWD = 1;
AdcRegs.ADCCTL1.bit.ADCPWDN = 1;
AdcRegs.ADCCTL1.bit.ADCENABLE = 1;
AdcRegs.ADCCTL1.bit.ADCREFSEL = 0; // reference interne, pleine echelle 3,3 V
EDIS;
DELAY_US(1000L); // stabilisation de la reference avant toute conversion
EALLOW;
// Impulsion d'interruption en fin de conversion, quand le resultat est
// effectivement dans ADCRESULTx (et non en debut de conversion).
AdcRegs.ADCCTL1.bit.INTPULSEPOS = 1;
// ADCCLK = SYSCLKOUT/2 = 30 MHz, et non-overlap : le mode overlap
// raccourcit la fenetre d'echantillonnage, inutile ici.
AdcRegs.ADCCTL2.bit.CLKDIV2EN = 1;
AdcRegs.ADCCTL2.bit.ADCNONOVERLAP = 1;
// Les ADCSOCxCTL sont des registres 16 bits contigus : on les parcourt
// par indexation directe de l'union.
for (i = 0U; i < (uint16_t)ADC_CH_COUNT; i++)
{
volatile union ADCSOCxCTL_REG *soc = (&AdcRegs.ADCSOC0CTL) + i;
soc->bit.CHSEL = k_chsel[i];
soc->bit.TRIGSEL = 5U; // ePWM1 SOCA
soc->bit.ACQPS = ADC_ACQPS_CYCLES; // fenetre S/H
}
// ADCINT1 declenchee par la fin du dernier SOC de la sequence.
AdcRegs.INTSEL1N2.bit.INT1SEL = (uint16_t)ADC_CH_COUNT - 1U;
AdcRegs.INTSEL1N2.bit.INT1CONT = 0;
AdcRegs.INTSEL1N2.bit.INT1E = 1;
PieVectTable.ADCINT1 = &adc_int1_isr;
EDIS;
IER |= M_INT1;
PieCtrlRegs.PIEIER1.bit.INTx1 = 1; // ADCINT1
// Declenchement par ePWM1 : SOCA sur passage a zero du compteur.
// SOCAPRD = 3 -> une sequence tous les 3 cycles PWM. A 200 kHz cela fait
// ~66 kHz, largement suffisant pour la telemetrie et la surveillance,
// sans saturer le CPU d'interruptions.
EALLOW;
EPwm1Regs.ETSEL.bit.SOCASEL = 1; // ET_CTR_ZERO
EPwm1Regs.ETPS.bit.SOCAPRD = 3;
EPwm1Regs.ETSEL.bit.SOCAEN = 1;
EDIS;
}
uint16_t adc_get_raw(adc_channel_t ch)
{
if (ch >= ADC_CH_COUNT)
{
return 0U;
}
return s_raw[ch];
}
float adc_get_volts(adc_channel_t ch)
{
if (ch >= ADC_CH_COUNT)
{
return 0.0f;
}
return (float)s_raw[ch] * ADC_VREF_V / 4096.0f;
}
bool adc_any_below(float volts)
{
uint16_t threshold = (uint16_t)(volts / ADC_VREF_V * 4096.0f + 0.5f);
uint16_t i;
for (i = 0U; i < (uint16_t)ADC_CH_COUNT; i++)
{
if (s_raw[i] < threshold)
{
return true;
}
}
return false;
}
uint32_t adc_get_sequence_count(void)
{
return s_seq_count;
}
// ISR courte : rangement des valeurs brutes, rien d'autre. Aucune conversion
// physique, aucun flottant (PROMPT §7). La protection rapide ne passe PAS
// par l'ADC, c'est le comparateur qui s'en charge.
interrupt void adc_int1_isr(void)
{
// Les resultats sont dans un bloc distinct (AdcResult), a une autre
// adresse que les registres de controle : ADCRESULT0..15 contigus.
volatile uint16_t *res = &AdcResult.ADCRESULT0;
uint16_t i;
for (i = 0U; i < (uint16_t)ADC_CH_COUNT; i++)
{
s_raw[i] = res[i];
}
s_seq_count++;
AdcRegs.ADCINTFLGCLR.bit.ADCINT1 = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}

46
src/adc.h Normal file
View File

@ -0,0 +1,46 @@
#ifndef ADC_H
#define ADC_H
#include <stdbool.h>
#include <stdint.h>
// Ordre des voies = ordre des SOC0..SOC8. Brochage LQFP48 PT, cf. PROMPT §2.
typedef enum
{
ADC_CH_VIN = 0, // broche 10, ADCINA0 (partagee avec VREFHI)
ADC_CH_IIN, // broche 8, ADCINA1
ADC_CH_I1, // broche 9, ADCINA2 / COMP1A / AIO2
ADC_CH_IOUT, // broche 7, ADCINA3
ADC_CH_I2, // broche 5, ADCINA4 / COMP2A / AIO4
ADC_CH_VOUT, // broche 14, ADCINB2 / AIO10
ADC_CH_V1, // broche 16, ADCINB4 / AIO12
ADC_CH_T1, // broche 17, ADCINB6 / AIO14
ADC_CH_T2, // broche 18, ADCINB7
ADC_CH_COUNT
} adc_channel_t;
// Reference interne (ADCREFSEL = 0, pleine echelle 3,3 V) : imposee car
// VREFHI est partagee avec ADCINA0, utilisee pour VIN (PROMPT §2).
// Les SOC sont declenches par ePWM1, jamais en free-run, pour echantillonner
// a un instant stable loin des fronts de commutation (PROMPT §6 etape 4).
//
// A appeler APRES bsp_gpio_analog_init() (bascule des AIO en mode analogique)
// et APRES pwm_init() (c'est ePWM1 qui fournit le declenchement).
void adc_init(void);
// Derniere valeur brute (0..4095) rangee par l'ISR ADCINT1.
uint16_t adc_get_raw(adc_channel_t ch);
// Meme valeur convertie en volts a l'entree de la broche (0..3,3 V).
// Ce n'est PAS la grandeur physique : la conversion vers V/A/°C (ponts
// diviseurs, gain de shunt, NTC) sera le role de measure.c.
float adc_get_volts(adc_channel_t ch);
// true si au moins une voie est sous le seuil donne, exprime en volts.
bool adc_any_below(float volts);
// Compteur de conversions completes, pour verifier au debogueur que les SOC
// tournent bien.
uint32_t adc_get_sequence_count(void);
#endif

View File

@ -26,6 +26,111 @@ void bsp_gpio_analog_init(void)
EDIS; EDIS;
} }
void bsp_gpio_control_init(void)
{
EALLOW;
// Stage1-EN = GPIO16 (broche 27), Stage2-EN = GPIO17 (broche 26).
// GPIO16/17 ne sont pas des broches PWM : leurs pull-ups sont actives au
// reset, ce qui presente un 1 sur une entree de chaque porte ET avant que
// le firmware ne tourne. On les coupe des l'init (PROMPT §6 etape 3).
GpioCtrlRegs.GPAPUD.bit.GPIO16 = 1;
GpioCtrlRegs.GPAPUD.bit.GPIO17 = 1;
GpioCtrlRegs.GPAMUX2.bit.GPIO16 = 0;
GpioCtrlRegs.GPAMUX2.bit.GPIO17 = 0;
GpioCtrlRegs.GPADIR.bit.GPIO16 = 1;
GpioCtrlRegs.GPADIR.bit.GPIO17 = 1;
// HV_EN = GPIO32 (broche 31)
GpioCtrlRegs.GPBMUX1.bit.GPIO32 = 0;
GpioCtrlRegs.GPBDIR.bit.GPIO32 = 1;
EDIS;
// Etat sur avant toute autre configuration.
stage_enable_set(STAGE_1, false);
stage_enable_set(STAGE_2, false);
hv_enable_set(false);
}
void stage_enable_set(stage_id_t stage, bool enabled)
{
if (stage == STAGE_1)
{
if (enabled)
{
GpioDataRegs.GPASET.bit.GPIO16 = 1;
}
else
{
GpioDataRegs.GPACLEAR.bit.GPIO16 = 1;
}
}
else
{
if (enabled)
{
GpioDataRegs.GPASET.bit.GPIO17 = 1;
}
else
{
GpioDataRegs.GPACLEAR.bit.GPIO17 = 1;
}
}
}
void hv_enable_set(bool enabled)
{
if (enabled)
{
GpioDataRegs.GPBSET.bit.GPIO32 = 1;
}
else
{
GpioDataRegs.GPBCLEAR.bit.GPIO32 = 1;
}
}
void bsp_gpio_stage_default_override_init(void)
{
EALLOW;
// GPIO1 = COMP1OUT (broche 28), GPIO3 = COMP2OUT (broche 38).
// Mux 0 = GPIO : on reprend la main sur des broches normalement pilotees
// par les comparateurs. Bring-up uniquement, voir bsp_gpio.h.
GpioCtrlRegs.GPAMUX1.bit.GPIO1 = 0;
GpioCtrlRegs.GPAMUX1.bit.GPIO3 = 0;
GpioCtrlRegs.GPADIR.bit.GPIO1 = 1;
GpioCtrlRegs.GPADIR.bit.GPIO3 = 1;
EDIS;
stage_default_set(STAGE_1, false);
stage_default_set(STAGE_2, false);
}
void stage_default_set(stage_id_t stage, bool level)
{
if (stage == STAGE_1)
{
if (level)
{
GpioDataRegs.GPASET.bit.GPIO1 = 1;
}
else
{
GpioDataRegs.GPACLEAR.bit.GPIO1 = 1;
}
}
else
{
if (level)
{
GpioDataRegs.GPASET.bit.GPIO3 = 1;
}
else
{
GpioDataRegs.GPACLEAR.bit.GPIO3 = 1;
}
}
}
void led_set(led_id_t led, bool on) void led_set(led_id_t led, bool on)
{ {
#if LED_ACTIVE_LOW #if LED_ACTIVE_LOW

View File

@ -9,6 +9,12 @@ typedef enum
LED_RED LED_RED
} led_id_t; } led_id_t;
typedef enum
{
STAGE_1 = 0,
STAGE_2
} stage_id_t;
void bsp_gpio_leds_init(void); void bsp_gpio_leds_init(void);
void led_set(led_id_t led, bool on); void led_set(led_id_t led, bool on);
@ -16,4 +22,32 @@ void led_set(led_id_t led, bool on);
// COMP1A/COMP2A, VOUT, V1, NTC1). A appeler avant safety_init()/adc_init(). // COMP1A/COMP2A, VOUT, V1, NTC1). A appeler avant safety_init()/adc_init().
void bsp_gpio_analog_init(void); void bsp_gpio_analog_init(void);
// Sorties de commande dans leur etat sur : HV_EN, Stage1-EN et Stage2-EN a 0.
// Desactive aussi les pull-ups internes de GPIO16/17, qui sinon presentent un
// niveau haut sur une entree de chaque porte ET des la mise sous tension
// (PROMPT §6 etape 3).
void bsp_gpio_control_init(void);
// Stagex-EN : entree "autorisation logicielle" de la porte ET de l'etage.
void stage_enable_set(stage_id_t stage, bool enabled);
// HV_EN (GPIO32) : commande de la sortie HT via l'optocoupleur VOM1271.
// Seul organe qui isole reellement la charge -- un boost a 0 % de duty ne
// donne PAS 0 V en sortie (PROMPT §6 etape 6).
void hv_enable_set(bool enabled);
// 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.
//
// INCOMPATIBLE avec safety_init(), qui mux ces memes broches vers COMP1OUT /
// COMP2OUT : en fonctionnement normal ce sont les comparateurs qui les
// pilotent, et le logiciel ne doit JAMAIS pouvoir outrepasser la protection
// materielle (PROMPT §2 et §8). Ne jamais appeler les deux.
void bsp_gpio_stage_default_override_init(void);
// Pilote une broche Stagex-default apres l'override ci-dessus. N'ecrit que
// GPASET/GPACLEAR (non proteges par EALLOW) : utilisable depuis une ISR.
void stage_default_set(stage_id_t stage, bool level);
#endif #endif

View File

@ -24,6 +24,26 @@
#define SAFETY_DAC_CODE \ #define SAFETY_DAC_CODE \
((uint16_t)((SAFETY_ISHUNT_THRESHOLD_A * 0.6f) / SAFETY_DAC_VREF_V * 1023.0f + 0.5f)) ((uint16_t)((SAFETY_ISHUNT_THRESHOLD_A * 0.6f) / SAFETY_DAC_VREF_V * 1023.0f + 0.5f))
// 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
// Fenetre d'echantillonnage : ACQPS + 1 cycles ADCCLK (30 MHz ici).
// 25 -> 26 cycles ~ 0,87 us, confortable meme avec une source de quelques
// kilo-ohms (ponts diviseurs, pull-down de test).
#define ADC_ACQPS_CYCLES 25
// Seuil du test de bring-up ADC : LED bleue si une voie passe SOUS cette
// valeur, rouge sinon. 300 mV = 372 LSB en pleine echelle 3,3 V.
#define ADC_TEST_THRESHOLD_V 0.3f
// Frequences de decoupage par etage (point ouvert §9.1 du PROMPT, tranche au
// bring-up). TBCLK = SYSCLKOUT = 60 MHz, TBPRD = SYSCLKOUT/Fpwm - 1 :
// etage 1 : 200 kHz -> TBPRD = 299
// etage 2 : 100 kHz -> TBPRD = 599
#define PWM_STAGE1_FREQ_HZ 200000UL
#define PWM_STAGE2_FREQ_HZ 100000UL
// Liaison UART SCI-A (voir docs/ESP32-UART.md) : 57600 8N1. // Liaison UART SCI-A (voir docs/ESP32-UART.md) : 57600 8N1.
// LSPCLK = SYSCLKOUT/4 (LOSPCP laisse a sa valeur par defaut par InitSysCtrl). // LSPCLK = SYSCLKOUT/4 (LOSPCP laisse a sa valeur par defaut par InitSysCtrl).
#define UART_BAUD_RATE 57600UL #define UART_BAUD_RATE 57600UL

View File

@ -4,6 +4,9 @@
#include "safety.h" #include "safety.h"
#include "uart_link.h" #include "uart_link.h"
#include "protocol.h" #include "protocol.h"
#include "pwm.h"
#include "adc.h"
#include "calib.h"
extern uint16_t RamfuncsLoadStart; extern uint16_t RamfuncsLoadStart;
extern uint16_t RamfuncsLoadSize; extern uint16_t RamfuncsLoadSize;
@ -32,17 +35,46 @@ void main(void)
InitPieVectTable(); InitPieVectTable();
bsp_gpio_analog_init(); bsp_gpio_analog_init();
// safety_init() desactive temporairement : bring-up isole sur LED+UART bsp_gpio_control_init();
// 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). // Ordre d'init (PROMPT §7) : GPIO en etat sur -> AIOMUX1 -> horloge ->
// securite -> ADC -> UART -> PWM en dernier.
//
// pwm_init() est appele AVANT safety_init() pour une raison materielle :
// il active PCLKCR1.EPWMxENCLK, sans quoi les ecritures de safety_init()
// dans les registres ePWM (TZSEL, TZCTL, DCTRIPSEL) seraient perdues.
// L'esprit de la regle est respecte : pwm_init() laisse les deux sorties
// forcees a l'etat bas (AQCSFRC) et le duty a 0, donc rien ne sort tant
// 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();
// ADC : declenche par ePWM1, donc apres pwm_init().
adc_init();
uart_link_init(); uart_link_init();
// ---- BRING-UP : retest PWM + ADC, puissance DECONNECTEE ---------------
// A RETIRER avant toute mise sous tension de la carte de conversion.
// HV_EN et Stagex-EN restent a 0 (exclus de ce test).
pwm_set_duty(STAGE_1, 0.5f);
pwm_set_duty(STAGE_2, 0.5f);
pwm_enable(STAGE_1, true);
pwm_enable(STAGE_2, true);
// -----------------------------------------------------------------------
EALLOW; EALLOW;
PieVectTable.TINT0 = &cpu_timer0_isr; PieVectTable.TINT0 = &cpu_timer0_isr;
EDIS; EDIS;
InitCpuTimers(); InitCpuTimers();
ConfigCpuTimer(&CpuTimer0, 60, 300000); // 300 ms -> rythme d'envoi bring-up UART ConfigCpuTimer(&CpuTimer0, 60, 10000); // base 10 ms (voir cpu_timer0_isr)
StartCpuTimer0(); // ConfigCpuTimer laisse TSS=1 (timer a l'arret) par conception StartCpuTimer0(); // ConfigCpuTimer laisse TSS=1 (timer a l'arret) par conception
IER |= M_INT1; IER |= M_INT1;
@ -54,30 +86,44 @@ void main(void)
for (;;) for (;;)
{ {
command_state_t cmd; command_state_t cmd;
safety_faults_t faults;
if (uart_link_poll(&cmd)) if (uart_link_poll(&cmd))
{ {
g_last_cmd = cmd; g_last_cmd = cmd;
} }
// Test EMUSTOP : LED rouge = defaut latche, bleue = nominal.
// Le flag ne se rearme jamais seul (PROMPT §8).
faults = safety_get_fault_flags();
{
bool tripped = faults.stage1_fault || faults.stage2_fault;
led_set(LED_RED, tripped);
led_set(LED_BLUE, !tripped);
}
if (s_send_telemetry) if (s_send_telemetry)
{ {
telemetry_t t; telemetry_t t;
// Bring-up : valeurs figees, seul le lien TX/RX est valide ici. // BRING-UP : chaque champ transporte la TENSION BRUTE de la
t.freq1_hz = 100000.0f; // broche ADC correspondante, en volts (0..3,3) -- et non la
t.freq2_hz = 100000.0f; // grandeur physique. Permet de verifier les 9 voies d'un coup
t.duty1_pct = 0.0f; // sur l'IHM ESP32 apres ressoudure. measure.c fera la vraie
t.duty2_pct = 0.0f; // conversion (ponts diviseurs, gain de shunt, NTC) a l'etape 5.
t.vin_v = 12.3f; t.freq1_hz = (float)PWM_STAGE1_FREQ_HZ;
t.iin_a = 0.5f; t.freq2_hz = (float)PWM_STAGE2_FREQ_HZ;
t.v1_v = 20.0f; t.duty1_pct = 50.0f;
t.i1_a = 0.1f; t.duty2_pct = 50.0f;
t.t1_c = 25.0f; t.vin_v = adc_get_volts(ADC_CH_VIN);
t.vout_v = 0.0f; t.iin_a = adc_get_volts(ADC_CH_IIN);
t.i2_a = 0.0f; t.v1_v = adc_get_volts(ADC_CH_V1);
t.t2_c = 25.0f; t.i1_a = adc_get_volts(ADC_CH_I1);
t.iout_a = 0.0f; 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);
uart_link_send_telemetry(&t); uart_link_send_telemetry(&t);
s_send_telemetry = false; s_send_telemetry = false;
@ -89,15 +135,24 @@ void main(void)
} }
} }
// Base de temps 10 ms. Le clignotement des signaux d'etage a ete retire :
// GPIO1/GPIO3 (Stagex-default) appartiennent maintenant aux comparateurs, et
// Stagex-EN est fige a 1 pour rendre le declenchement observable sur les
// sorties des portes ET. HV_EN reste a 0.
//
// Les LED n'affichent plus l'etat de l'ADC mais celui des defauts, pilotees
// depuis la boucle principale.
interrupt void cpu_timer0_isr(void) interrupt void cpu_timer0_isr(void)
{ {
static bool blue_on = false; static uint16_t tick = 0;
CpuTimer0.InterruptCount++; CpuTimer0.InterruptCount++;
s_send_telemetry = true; tick++;
blue_on = !blue_on; if ((tick % 30U) == 0U)
led_set(LED_BLUE, blue_on); {
s_send_telemetry = true; // telemetrie toutes les 300 ms
}
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1; PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
} }

149
src/pwm.c Normal file
View File

@ -0,0 +1,149 @@
#include "DSP28x_Project.h"
#include "pwm.h"
#include "calib.h"
// Mode up-count, TBCLK = SYSCLKOUT = 60 MHz (HSPCLKDIV = CLKDIV = /1).
// TBPRD = SYSCLKOUT / Fpwm - 1 -> 299 a 200 kHz, 599 a 100 kHz.
//
// HRPWM : volontairement NON implemente a ce stade (PROMPT §6 etape 6), mais
// le routage et la structure s'y pretent. Notes pour l'ajout ulterieur :
// - HRPWM n'existe que sur les sorties EPWMxA : les deux etages sont donc
// compatibles tels que routes (GPIO0 et GPIO2).
// - SYSCLKOUT >= 50 MHz requis (60 MHz ici), MEP 150-310 ps, SFO obligatoire.
// - Limitation de 3 cycles SYSCLK sur le rapport cyclique : a 200 kHz cela
// donne un duty min ~0,67 % et max ~99 %, a confronter aux points de
// fonctionnement de l'etage 1 (D ~ 0,8).
// - A 100 kHz sans HRPWM : 600 pas ~ 9,2 bits, risque de limit cycling en
// boucle fermee. C'est l'etage 2 (D ~ 0,875) qui est le plus concerne.
typedef struct
{
uint32_t freq_hz;
float duty;
bool enabled;
} pwm_stage_t;
static pwm_stage_t s_stage[2];
static volatile struct EPWM_REGS *pwm_regs(stage_id_t stage)
{
return (stage == STAGE_1) ? &EPwm1Regs : &EPwm2Regs;
}
static uint16_t stage_index(stage_id_t stage)
{
return (stage == STAGE_1) ? 0U : 1U;
}
// Recalcule CMPA a partir du duty et du TBPRD courants.
static void pwm_apply_duty(stage_id_t stage)
{
volatile struct EPWM_REGS *p = pwm_regs(stage);
float duty = s_stage[stage_index(stage)].duty;
uint16_t prd = p->TBPRD;
// En up-count avec AQ_SET a zero et AQ_CLEAR sur CMPA, la sortie est
// haute pendant CMPA cycles : duty = CMPA / (TBPRD + 1).
p->CMPA.half.CMPA = (uint16_t)(duty * (float)(prd + 1U) + 0.5f);
}
void pwm_init(void)
{
EALLOW;
SysCtrlRegs.PCLKCR1.bit.EPWM1ENCLK = 1;
SysCtrlRegs.PCLKCR1.bit.EPWM2ENCLK = 1;
// Les bases de temps sont figees pendant la configuration puis relachees
// ensemble, pour que les deux etages demarrent leur compteur en phase.
SysCtrlRegs.PCLKCR0.bit.TBCLKSYNC = 0;
GpioCtrlRegs.GPAMUX1.bit.GPIO0 = 1; // EPWM1A
GpioCtrlRegs.GPAMUX1.bit.GPIO2 = 1; // EPWM2A
EDIS;
s_stage[0].freq_hz = PWM_STAGE1_FREQ_HZ;
s_stage[1].freq_hz = PWM_STAGE2_FREQ_HZ;
{
stage_id_t stages[2] = {STAGE_1, STAGE_2};
uint16_t i;
for (i = 0U; i < 2U; i++)
{
stage_id_t s = stages[i];
volatile struct EPWM_REGS *p = pwm_regs(s);
p->TBCTL.bit.CTRMODE = TB_COUNT_UP;
p->TBCTL.bit.PHSEN = TB_DISABLE;
p->TBCTL.bit.PRDLD = TB_SHADOW;
p->TBCTL.bit.HSPCLKDIV = TB_DIV1;
p->TBCTL.bit.CLKDIV = TB_DIV1;
// FREE_SOFT = 0 : le compteur gele quand le CPU s'arrete. Ne
// suffit PAS a mettre la sortie a l'etat bas (l'Action Qualifier
// a deja positionne la broche, qui reste figee) -- c'est TZ6 =
// EMUSTOP dans safety.c qui garantit la mise a zero.
p->TBCTL.bit.FREE_SOFT = 0;
p->TBCTR = 0;
p->TBPHS.half.TBPHS = 0;
p->CMPCTL.bit.SHDWAMODE = CC_SHADOW;
p->CMPCTL.bit.LOADAMODE = CC_CTR_ZERO;
// Haut a zero, bas sur CMPA -> impulsion en debut de periode.
p->AQCTLA.bit.ZRO = AQ_SET;
p->AQCTLA.bit.CAU = AQ_CLEAR;
s_stage[i].duty = 0.0f;
s_stage[i].enabled = false;
pwm_set_freq(s, s_stage[i].freq_hz);
pwm_enable(s, false);
}
}
EALLOW;
SysCtrlRegs.PCLKCR0.bit.TBCLKSYNC = 1;
EDIS;
}
void pwm_set_freq(stage_id_t stage, uint32_t hz)
{
volatile struct EPWM_REGS *p = pwm_regs(stage);
if (hz == 0UL)
{
return;
}
s_stage[stage_index(stage)].freq_hz = hz;
p->TBPRD = (uint16_t)((CLK_SYSCLKOUT_HZ / hz) - 1UL);
// TBPRD change -> CMPA doit suivre pour conserver le meme duty.
pwm_apply_duty(stage);
}
void pwm_set_duty(stage_id_t stage, float duty_0_1)
{
if (duty_0_1 < 0.0f)
{
duty_0_1 = 0.0f;
}
else if (duty_0_1 > 1.0f)
{
duty_0_1 = 1.0f;
}
s_stage[stage_index(stage)].duty = duty_0_1;
pwm_apply_duty(stage);
}
void pwm_enable(stage_id_t stage, bool enabled)
{
volatile struct EPWM_REGS *p = pwm_regs(stage);
s_stage[stage_index(stage)].enabled = enabled;
// CSFA : 1 = forcage continu a l'etat bas, 0 = action normale de l'AQ.
p->AQCSFRC.bit.CSFA = enabled ? 0U : 1U;
}

28
src/pwm.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef PWM_H
#define PWM_H
#include <stdbool.h>
#include <stdint.h>
#include "bsp_gpio.h" // stage_id_t
// ePWM1 -> etage 1 (EPWM1A, GPIO0, broche 29)
// ePWM2 -> etage 2 (EPWM2A, GPIO2, broche 37)
//
// Initialise les deux etages a leur frequence nominale (calib.h), duty a 0
// et sorties inhibees. PROMPT §6 etape 6 : ne jamais demarrer au duty
// nominal, le condensateur de sortie vide provoquerait un appel de courant
// destructeur. La rampe de demarrage (soft-start) est du ressort de
// l'appelant, pas de ce module.
void pwm_init(void);
void pwm_set_freq(stage_id_t stage, uint32_t hz);
// duty exprime en 0..1. Sature hors bornes.
void pwm_set_duty(stage_id_t stage, float duty_0_1);
// Inhibition logicielle par forcage continu de la sortie a l'etat bas
// (AQCSFRC). N'a rien a voir avec la protection materielle : le Trip Zone
// reste le seul mecanisme de securite (PROMPT §8).
void pwm_enable(stage_id_t stage, bool enabled);
#endif

View File

@ -73,6 +73,30 @@ void safety_init(void)
PieCtrlRegs.PIEIER2.bit.INTx2 = 1; // EPWM2_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 safety_get_fault_flags(void)
{ {
safety_faults_t f; safety_faults_t f;

View File

@ -10,6 +10,16 @@ typedef struct
} safety_faults_t; } safety_faults_t;
void safety_init(void); void safety_init(void);
// BRING-UP UNIQUEMENT -- arme seulement TZ6 (EMUSTOP) et l'interruption de
// diagnostic, SANS les comparateurs ni le Digital Compare. Permet de valider
// isolement la coupure du PWM a l'arret du debugger, tant que les entrees
// shunt ne sont pas cablees : avec safety_init() complet, elles flottent
// au-dessus du seuil et le trip comparateur masquerait le test.
//
// safety_init() arme deja TZ6 : ces deux fonctions sont exclusives.
void safety_arm_emustop_only(void);
safety_faults_t safety_get_fault_flags(void); safety_faults_t safety_get_fault_flags(void);
// Ecrit TZCLR[OST] sur les deux etages. Ne doit JAMAIS etre appele // Ecrit TZCLR[OST] sur les deux etages. Ne doit JAMAIS etre appele