From d26c07d3a117e0bfb3496914b27a1afa51481cd3 Mon Sep 17 00:00:00 2001 From: nicoboy Date: Sun, 2 Aug 2026 20:42:14 +0200 Subject: [PATCH] Etapes 3/4/6 : GPIO de commande, PWM et ADC valides sur cible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/adc.c | 141 ++++++++++++++++++++++++++++++++++++++++++++++ src/adc.h | 46 +++++++++++++++ src/bsp_gpio.c | 105 ++++++++++++++++++++++++++++++++++ src/bsp_gpio.h | 34 +++++++++++ src/calib.h | 20 +++++++ src/main.c | 99 ++++++++++++++++++++++++-------- src/pwm.c | 149 +++++++++++++++++++++++++++++++++++++++++++++++++ src/pwm.h | 28 ++++++++++ src/safety.c | 24 ++++++++ src/safety.h | 10 ++++ 10 files changed, 634 insertions(+), 22 deletions(-) create mode 100644 src/adc.c create mode 100644 src/adc.h create mode 100644 src/pwm.c create mode 100644 src/pwm.h diff --git a/src/adc.c b/src/adc.c new file mode 100644 index 0000000..23b977b --- /dev/null +++ b/src/adc.c @@ -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; +} diff --git a/src/adc.h b/src/adc.h new file mode 100644 index 0000000..c5f6a31 --- /dev/null +++ b/src/adc.h @@ -0,0 +1,46 @@ +#ifndef ADC_H +#define ADC_H + +#include +#include + +// 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 diff --git a/src/bsp_gpio.c b/src/bsp_gpio.c index c0bb2c3..9ba9afd 100644 --- a/src/bsp_gpio.c +++ b/src/bsp_gpio.c @@ -26,6 +26,111 @@ void bsp_gpio_analog_init(void) 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) { #if LED_ACTIVE_LOW diff --git a/src/bsp_gpio.h b/src/bsp_gpio.h index b33b9dd..158682e 100644 --- a/src/bsp_gpio.h +++ b/src/bsp_gpio.h @@ -9,6 +9,12 @@ typedef enum LED_RED } led_id_t; +typedef enum +{ + STAGE_1 = 0, + STAGE_2 +} stage_id_t; + void bsp_gpio_leds_init(void); 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(). 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 diff --git a/src/calib.h b/src/calib.h index 098a547..2d7aeb9 100644 --- a/src/calib.h +++ b/src/calib.h @@ -24,6 +24,26 @@ #define SAFETY_DAC_CODE \ ((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. // LSPCLK = SYSCLKOUT/4 (LOSPCP laisse a sa valeur par defaut par InitSysCtrl). #define UART_BAUD_RATE 57600UL diff --git a/src/main.c b/src/main.c index 3ab1e3c..ba18d9f 100644 --- a/src/main.c +++ b/src/main.c @@ -4,6 +4,9 @@ #include "safety.h" #include "uart_link.h" #include "protocol.h" +#include "pwm.h" +#include "adc.h" +#include "calib.h" extern uint16_t RamfuncsLoadStart; extern uint16_t RamfuncsLoadSize; @@ -32,17 +35,46 @@ void main(void) InitPieVectTable(); bsp_gpio_analog_init(); - // safety_init() desactive temporairement : bring-up isole sur LED+UART - // 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). + bsp_gpio_control_init(); + + // 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(); + // ---- 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; PieVectTable.TINT0 = &cpu_timer0_isr; EDIS; 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 IER |= M_INT1; @@ -54,30 +86,44 @@ void main(void) for (;;) { command_state_t cmd; + safety_faults_t faults; if (uart_link_poll(&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) { telemetry_t t; - // Bring-up : valeurs figees, seul le lien TX/RX est valide ici. - t.freq1_hz = 100000.0f; - t.freq2_hz = 100000.0f; - t.duty1_pct = 0.0f; - t.duty2_pct = 0.0f; - t.vin_v = 12.3f; - t.iin_a = 0.5f; - t.v1_v = 20.0f; - t.i1_a = 0.1f; - t.t1_c = 25.0f; - t.vout_v = 0.0f; - t.i2_a = 0.0f; - t.t2_c = 25.0f; - t.iout_a = 0.0f; + // 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. + 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); uart_link_send_telemetry(&t); 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) { - static bool blue_on = false; + static uint16_t tick = 0; CpuTimer0.InterruptCount++; - s_send_telemetry = true; + tick++; - blue_on = !blue_on; - led_set(LED_BLUE, blue_on); + if ((tick % 30U) == 0U) + { + s_send_telemetry = true; // telemetrie toutes les 300 ms + } PieCtrlRegs.PIEACK.all = PIEACK_GROUP1; } diff --git a/src/pwm.c b/src/pwm.c new file mode 100644 index 0000000..8f89e7f --- /dev/null +++ b/src/pwm.c @@ -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; +} diff --git a/src/pwm.h b/src/pwm.h new file mode 100644 index 0000000..6cd583b --- /dev/null +++ b/src/pwm.h @@ -0,0 +1,28 @@ +#ifndef PWM_H +#define PWM_H + +#include +#include +#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 diff --git a/src/safety.c b/src/safety.c index a20f5e9..0e1124a 100644 --- a/src/safety.c +++ b/src/safety.c @@ -73,6 +73,30 @@ void safety_init(void) 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; diff --git a/src/safety.h b/src/safety.h index e1b44c8..cd66052 100644 --- a/src/safety.h +++ b/src/safety.h @@ -10,6 +10,16 @@ typedef struct } safety_faults_t; 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); // Ecrit TZCLR[OST] sur les deux etages. Ne doit JAMAIS etre appele