diff --git a/docs/ESP32-UART.md b/docs/ESP32-UART.md new file mode 100644 index 0000000..6b43054 --- /dev/null +++ b/docs/ESP32-UART.md @@ -0,0 +1,138 @@ +# Liaison UART ESP32-C3 <-> TMS320 + +Ce document decrit le protocole que le firmware TMS320 doit implementer +pour dialoguer avec l'ESP32-C3 (cote ESP32 deja fonctionnel, voir +`src/tms_link.cpp` / `include/protocol.h`). Le TMS320 envoie sa telemetrie +periodiquement, et recoit periodiquement les commandes de l'operateur. + +## Cablage + +| Signal | ESP32-C3 (config.h) | TMS320 | +|---|---|---| +| ESP32 TX -> TMS320 RX | GPIO7 | RX | +| ESP32 RX <- TMS320 TX | GPIO6 | TX | +| GND commun | GND | GND | + +A verifier avant de cabler : le TMS320 doit etre en logique 3,3V (comme +l'ESP32-C3). Si sa liaison serie sort en 5V, prevoir un level shifter +avant de relier les deux cartes. + +## Parametres UART + +- Vitesse : **57600 bauds** (adapte cote TMS320 pour la marge de derive de + l'oscillateur interne INTOSC1 ; l'ESP32 doit etre mis a jour en + consequence, `Serial1.begin(57600, ...)`) +- Format : **8N1** (8 bits de donnees, pas de parite, 1 bit de stop) +- Cote ESP32 : `Serial1`, defini dans `TmsLink::begin()` + (`src/tms_link.cpp`) + +## Format des trames + +Protocole ASCII texte, inspire du format NMEA, terminé par `\n` (LF). +Chaque trame est encadree par `$` ... `*XX`, ou `XX` est un checksum XOR +en hexadecimal (2 chiffres, majuscules). + +### Telemetrie : TMS320 -> ESP32 (periodique) + +``` +$T,FREQ1=100000,FREQ2=100000,DUTY1=45.2,DUTY2=50.0,VIN=400.5,IIN=1.20,V1=200.3,I1=2.50,T1=45.2,VOUT=200.1,I2=2.48,T2=44.8,IOUT=1.05*7A +``` + +Champs (`TAG=valeur`, separes par des virgules, ordre libre) : + +| Tag | Unite | Description | +|---|---|---| +| `FREQ1` | Hz | Frequence de decoupage etage 1 | +| `FREQ2` | Hz | Frequence de decoupage etage 2 | +| `DUTY1` | % | Rapport cyclique etage 1 | +| `DUTY2` | % | Rapport cyclique etage 2 | +| `VIN` | V | Tension d'entree | +| `IIN` | A | Courant d'entree | +| `V1` | V | Tension etage 1 | +| `I1` | A | Courant shunt MOSFET etage 1 | +| `T1` | °C | Temperature NTC etage 1 | +| `VOUT` | V | Tension de sortie (sortie boost, etage 2) | +| `I2` | A | Courant shunt MOSFET etage 2 | +| `T2` | °C | Temperature NTC etage 2 | +| `IOUT` | A | Courant de sortie | + +**Frequence d'envoi recommandee : toutes les 200-500 ms.** L'ESP32 +considere la liaison perdue (`TMS:KO` affiche sur l'OLED/web) si aucune +trame `$T,...*XX` valide n'est recue depuis plus de **2 secondes** +(`TmsLink::linkOk()`). + +Tous les champs sont optionnels a l'envoi individuel (un champ absent +garde sa derniere valeur connue cote ESP32), mais en pratique il est plus +simple d'envoyer tous les champs a chaque trame. + +### Commande : ESP32 -> TMS320 (periodique + a chaque changement) + +``` +$C,HT=1,PWM1=1,PWM2=0*3E +``` + +| Tag | Valeurs | Description | +|---|---|---| +| `HT` | 0 ou 1 | Sortie HT activee/desactivee | +| `PWM1` | 0 ou 1 | PWM etage 1 activee/desactivee | +| `PWM2` | 0 ou 1 | PWM etage 2 activee/desactivee | + +L'ESP32 envoie cette trame toutes les **500 ms**, et immediatement a +chaque changement d'etat depuis l'IHM (OLED/web). Le TMS320 doit +simplement appliquer le dernier etat recu et valide (checksum correct). + +## Calcul du checksum + +XOR de tous les octets entre `$` et `*` (exclus), formate en hexadecimal +majuscule sur 2 chiffres (`%02X`). + +Exemple en C pour une trame a construire : + +```c +uint8_t checksum_of(const char *s, int len) { + uint8_t cs = 0; + for (int i = 0; i < len; i++) cs ^= (uint8_t)s[i]; + return cs; +} + +// Construction d'une trame telemetrie : +char body[128]; +int n = sprintf(body, "T,FREQ1=%.0f,FREQ2=%.0f,DUTY1=%.1f,DUTY2=%.1f," + "VIN=%.1f,IIN=%.2f,V1=%.1f,I1=%.2f,T1=%.1f," + "VOUT=%.1f,I2=%.2f,T2=%.1f,IOUT=%.2f", + freq1, freq2, duty1, duty2, vin, iin, v1, i1, t1, vout, i2, t2, iout); +uint8_t cs = checksum_of(body, n); +char frame[160]; +sprintf(frame, "$%s*%02X\n", body, cs); +// envoyer frame sur l'UART +``` + +Pour la reception des trames `$C,...*XX` : appliquer le meme calcul sur +la partie entre `$` et `*`, comparer au checksum recu, et ignorer la +trame si ca ne correspond pas (trame corrompue). + +## Cote ESP32 : ce qui existe deja + +- `include/protocol.h` : structures `Telemetry` et `CommandState`, + description du protocole (source de verite en cas de doute). +- `src/tms_link.cpp` : parsing des trames `$T,...`, envoi des trames + `$C,...`, gestion du timeout de liaison. Contient aussi + `updateSimulation()`, qui simule des valeurs realistes (utile comme + reference de plage de valeurs attendues : `Vin` ~400V, `Iin` ~1.2A, + `V1`/`Vout` ~200V, `I1`/`I2` ~2.5A, `T1`/`T2` ~40°C). +- `include/config.h` : `TMS_DUMMY_MODE` (actuellement `true`) fait + tourner l'ESP32 sur la simulation interne au lieu de lire l'UART reel. + **A passer a `false` une fois le firmware TMS320 pret et teste**, pour + activer la reception reelle des trames `$T,...*XX`. + +## Points de vigilance + +- Le format hexadecimal du checksum doit etre en **majuscules** sur 2 + chiffres (`%02X`), sinon la trame sera rejetee cote ESP32 (silencieux, + pas d'erreur visible — la telemetrie n'avancera simplement pas). +- Terminer chaque trame par `\n` uniquement (le `\r` est tolere/ignore + cote ESP32 mais pas necessaire). +- Ne pas depasser une longueur de ligne de 160 caracteres (buffer fixe + cote ESP32, `g_lineBuf[160]` dans `tms_link.cpp`). +- Verifier au multimetre/oscilloscope le niveau logique du TMS320 avant + de le relier a l'ESP32-C3 (3,3V attendu). diff --git a/docs/PROMPT-claude-code-TMS320.md b/docs/PROMPT-claude-code-TMS320.md new file mode 100644 index 0000000..ff8dbd7 --- /dev/null +++ b/docs/PROMPT-claude-code-TMS320.md @@ -0,0 +1,423 @@ +# Prompt pour Claude Code — Firmware TMS320F28027 « dual boost » + +> Version 2 — brochage corrigé, constantes de calibration réelles. +> À coller dans Claude Code (CCS v21) après avoir ouvert le dépôt +> `TMS320F28027-dualboost`. + +--- + +## 1. Contexte + +Firmware d'une alimentation haute tension à deux étages boost en cascade, +pour préamplis à tubes (charge ~10-15 mA, modulée à fréquence audio). + +- **MCU** : TMS320F28027, LQFP48 (PT), carte sur mesure +- **IDE** : Code Composer Studio v21 (Theia), compilateur C2000 +- **Bibliothèque** : C2000Ware, headers **bitfield** (`DSP28x_Project.h`, + `F2802x_Device.h`) — **pas driverlib** +- **Sonde** : Olimex TMS320-XDS100-V3 +- **Horloge** : **oscillateur interne INTOSC1 (10 MHz)**, PLL → SYSCLKOUT = 60 MHz +- **Dépôt** : `git@git.syoul.fr:alim-High-Volt-boost/TMS320F28027-dualboost.git` + +Un ESP32-C3 (dépôt `ESP32-C3-dualboost`, déjà fonctionnel : OLED SSD1322 +256x64 + serveur web) gère l'IHM. Protocole dans `docs/ESP32-UART.md` — +**source de vérité, ne rien inventer**. + +Puissance par étage : MOSFET IPD60R360 (600 V), diode SiC STPSC406 (600 V/4 A), +inductance 47-100 µH. Sortie HT commutée par MOSFET N piloté par optocoupleur +photovoltaïque VOM1271 (~265-325 µs — coupure de sécurité lente, **pas** une +protection rapide). + +--- + +## 2. Brochage définitif (LQFP48 PT) + +### Entrées analogiques + +| Signal | Broche | Fonction MCU | Rôle | Échelle | +|---|---|---|---|---| +| `V-Batt-adc` | 10 | ADCINA0 / **VREFHI** | VIN | 11,11 V/V (PE 36,7 V) | +| `I-Batt-adc` | 8 | ADCINA1 | IIN | 1,125 A/V (PE 3,71 A) | +| `I-shunt1-adc` | **9** | ADCINA2 / **COMP1A** / AIO2 | I1 + **protection étage 1** | 0,02 Ω × 30 → 1,667 A/V (PE 5,5 A) | +| `I-HV-adc` | 7 | ADCINA3 | IOUT | 16,67 mA/V (PE 55 mA) | +| `I-shunt2-adc` | **5** | ADCINA4 / **COMP2A** / AIO4 | I2 + **protection étage 2** | idem | +| `V-HV-adc` | **14** | ADCINB2 / AIO10 | VOUT | 178,6 V/V (PE 589 V) | +| `V-inter-adc` | **16** | ADCINB4 / AIO12 | V1 | 31,25 V/V (PE 103 V, V1max = 50 V → 1,6 V) | +| `Temp1` | 17 | ADCINB6 / AIO14 | NTC étage 1 | voir §5 | +| `Temp2` | 18 | ADCINB7 | NTC étage 2 | voir §5 | +| *libre* | 13 | ADCINB1 | — | | +| *libre* | 15 | ADCINB3 | — | | + +**Points d'attention obligatoires :** + +1. Les broches 5, 9, 14, 16, 17 sont des **AIO** (AIO4, AIO2, AIO10, AIO12, + AIO14) et sont en **mode numérique par défaut au reset**. Sans écriture + explicite dans `AIOMUX1` (valeur du champ = **2**), ni l'ADC ni les + comparateurs ne voient le signal. **À faire en tout début d'init.** +2. **Les shunts sont obligatoirement sur les entrées `COMPxA` (pins 9 et 5).** + Contrainte de silicium : `COMPxA` est câblée en dur sur l'entrée + non-inverseuse (+) du comparateur ; `COMPxB` n'existe que comme + alternative au DAC sur l'entrée inverseuse (−). Il n'y a aucun mux + permettant de les échanger. Seules les entrées A sont donc utilisées, + avec `COMPSOURCE = 0` (référence = DAC interne). +3. Les broches 9 et 5 servent **simultanément** d'entrée ADC et d'entrée + comparateur — c'est la même broche physique, le signal alimente les deux + chemins en parallèle. Pas de conflit, `I1`/`I2` restent disponibles en + télémétrie. +4. `VREFHI` est partagé avec `ADCINA0`, utilisé pour VIN → **référence ADC + interne obligatoire** (`ADCCTL1.bit.ADCREFSEL = 0`, pleine échelle 3,3 V). + Conséquences documentées : erreur de gain ±60 LSB, tempco −50 ppm/°C. + Le signal sur ADCINA0 ne doit jamais dépasser VDDA. +5. `V-inter` : pleine échelle 103 V pour une tension intermédiaire maximale + de **50 V** → ~48 % de la plage ADC utilisée, ~2000 codes sur la plage + utile. Pas de saturation, diviseur conservé tel quel. + +### Répartition des tensions + +- **Étage 1** : 10 V → **50 V max** (D ≈ 0,8) +- **Étage 2** : 50 V → **400 V max** (D ≈ 0,875), consigne programmable + 200 / 250 / … / 400 V + +### Entrées / sorties numériques + +| Signal | Broche | Fonction MCU | Sens | +|---|---|---|---| +| `UART-RX` | 48 | GPIO28 / SCIRXDA | Entrée (← ESP32 GPIO7) | +| `UART-TX` | 1 | GPIO29 / SCITXDA | Sortie (→ ESP32 GPIO6) | +| `Stage1-PWM` | 29 | GPIO0 / **EPWM1A** | Sortie PWM étage 1 (HRPWM possible) | +| `Stage1-default` | 28 | GPIO1 / **COMP1OUT** | Sortie comparateur 1 | +| `Stage2-PWM` | 37 | GPIO2 / **EPWM2A** | Sortie PWM étage 2 (HRPWM possible) | +| `Stage2-default` | 38 | GPIO3 / **COMP2OUT** | Sortie comparateur 2 | +| `Stage1-EN` | 27 | GPIO16 | Sortie — enable logiciel étage 1 | +| `Stage2-EN` | 26 | GPIO17 | Sortie — enable logiciel étage 2 | +| `HV_EN` | 31 | GPIO32 | Sortie — commande sortie HT (VOM1271) | +| LED bleue | 47 | GPIO12 | Sortie | +| LED rouge | 35 | GPIO33 | Sortie | +| JTAG | 20-23 | TDI/TMS/TDO/TCK | **Ne pas reconfigurer en GPIO** | + +Les LEDs sont câblées avec une résistance de 1 kΩ vers le 3,3 V → **logique +probablement inversée** (LED allumée quand le GPIO est bas) : à vérifier au +premier test et à encapsuler dans `led_set()` pour ne pas polluer le reste +du code. + +**Aucune broche TZ externe n'est disponible** (GPIO12 = TZ1 → LED, GPIO16/17 += TZ2/TZ3 → Stage-EN). Tous les trips passent par le Digital Compare interne +depuis `COMPxOUT`. Conséquence assumée : pas de possibilité d'ajouter un trip +externe (arrêt d'urgence, thermostat matériel) sans reprendre le routage. + +### Logique de sécurité câblée (hors MCU) + +Deux portes ET 74HVC1G08 (IC8, IC9), une par étage : + +``` +COMPxOUT (Stagex-default, CMPINV=1 : 1=OK, 0=défaut) ──┐ + ├─ AND ─→ PWMx-EN (driver) +GPIO Stagex-EN (logiciel : 1=autorisé, 0=inhibé) ──┘ +``` + +Les drivers sont **actifs à l'état haut**, d'où `CMPINV = 1` sur les deux +comparateurs. Le logiciel peut inhiber en plus, **jamais** outrepasser la +protection matérielle. + +--- + +## 3. Horloge — contraintes liées à l'oscillateur interne + +Pas de quartz : INTOSC1 (10 MHz nominal). PLL pour 60 MHz : +`PLLCR = 12`, `DIVSEL = /2` → 10 × 12 / 2 = 60 MHz. + +**Obligatoire** : quand la source est l'oscillateur interne, `PLLLOCKPRD` +doit être écrit avec **10000 minimum** (sinon le verrouillage PLL est mal +temporisé). + +**Dérive à documenter et à gérer** : 3,03 à 4,85 kHz/°C, plage 9,6-10,6 MHz +sur −40/+125 °C, soit ~2 % sur 40 °C d'échauffement. Impacts : + +- **UART fixé à 57600 bauds** (au lieu de 115200) pour élargir la marge de + tolérance sur le timing des trames. Côté ESP32, `Serial1.begin(57600, ...)` + doit être mis à jour en conséquence. +- Fréquence de découpage réelle à ±2 % → `FREQ1`/`FREQ2` en télémétrie sont + indicatifs, à annoter comme tels. +- Prévoir un point d'accroche (fonction vide + commentaire) pour ajouter plus + tard la compensation d'oscillateur via le capteur de température interne + (voir *Oscillator Compensation Guide* + code C2000Ware). + +--- + +## 4. Architecture logicielle demandée + +``` +src/ + main.c boucle principale, ordonnancement + bsp_clock.c/h InitSysCtrl, INTOSC1 + PLL 60 MHz, PLLLOCKPRD, watchdog + bsp_gpio.c/h mux GPIO, AIOMUX1, LEDs, HV_EN, StageX-EN + safety.c/h comparateurs + DAC, Digital Compare, Trip Zone, EMUSTOP + pwm.c/h init ePWM1/ePWM2, duty, fréquence (HRPWM plus tard) + adc.c/h init ADC (réf interne), SOC déclenché par ePWM, lecture brute + measure.c/h conversion brut -> unités physiques (V, A, °C) + control.c/h régulation — SQUELETTE UNIQUEMENT pour l'instant + uart_link.c/h SCI-A 57600 8N1, trames $T,...*XX et $C,...*XX + calib.h TOUTES les constantes matérielles, rien ailleurs + protocol.h tags, structures Telemetry / CommandState +docs/ + ESP32-UART.md protocole (copié depuis le dépôt ESP32) +``` + +--- + +## 5. Constantes de calibration (`calib.h`) + +Aucune de ces valeurs ne doit apparaître ailleurs que dans ce header. + +### Tensions et courants + +| Grandeur | Relation | Coefficient | +|---|---|---| +| VIN | 1,8 V @ 20 V | `VIN = Vadc × 11,111` | +| IIN | 2,4 V @ 2,7 A | `IIN = Vadc × 1,125` | +| V1 (inter) | 3,2 V @ 100 V | `V1 = Vadc × 31,25` | +| VOUT | 2,8 V @ 500 V | `VOUT = Vadc × 178,571` | +| IOUT | 3,0 V @ 50 mA | `IOUT = Vadc × 0,016667` (A) | +| I1, I2 | shunt 0,02 Ω, gain ×30 | `I = Vadc / 0,6 = Vadc × 1,6667` | + +Conversion brut → volts : `Vadc = raw × 3.3f / 4096.0f` (référence interne). + +### Seuil de protection (identique sur les deux étages) + +- Seuil : **3 A** → `Vadc = 3 × 0,02 × 30 = 1,8 V` +- Code DAC 10 bits : `round(1.8 / 3.3 × 1023)` = **558** +- Marges connues : gain DAC −1,5 %, offset DAC 10 mV, offset comparateur + ±5 mV, hystérésis 35 mV (≈ 58 mA ramenés au courant) → précision réelle du + seuil de l'ordre de ±2 %, largement suffisant face à la marge des composants. +- Temps de réponse comparateur → Trip Zone : 30 ns (asynchrone). +- Définir le seuil en **ampères** dans `calib.h`, et calculer le code DAC par + macro — ne jamais écrire 558 en dur. + +### NTC (B57451V5103J062, boîtier 0805) + +Montage : `3,3 V — NTC 10 k — R 10 k — 0 V`, mesure au point milieu. +R25 = 10 kΩ, B25/100 = 4000 K ±3 %. + +``` +R_ntc = R_fixe × Vadc / (3.3 - Vadc) +T(K) = 1 / ( 1/298.15 + ln(R_ntc / 10000) / 4000 ) +``` + +Points de repère attendus : 0,75 V à 0 °C · 1,65 V à 25 °C · 2,94 V à 80 °C · +3,09 V à 100 °C. La tension **monte** avec la température. Sensibilité vers +100 °C : ~7,7 mV/°C, soit ~10 LSB/°C — suffisant pour de la surveillance. +La tolérance ±3 % sur B donne ~±2 °C d'erreur vers 100 °C sans calibration +individuelle : acceptable pour de la protection thermique, pas pour de la +mesure de précision. À documenter. + +Résistance fixe confirmée à **10 kΩ** (pont 10 k NTC / 10 k). + +--- + +## 6. Étapes d'implémentation (un commit par étape) + +### Étape 1 — Squelette + « hello LED » + +- Projet CCS F28027, fichier `.cmd` Flash +- `InitSysCtrl()` adapté : INTOSC1, `PLLLOCKPRD = 10000`, PLLCR 12, DIVSEL /2 +- Watchdog désactivé au début (à réactiver plus tard) +- Clignotement LED pour valider compilation + flash + XDS100v3 + +**Critère** : LED à 1 Hz, compilation sans warning, SYSCLKOUT vérifié à 60 MHz +(sortie XCLKOUT ou mesure d'un timer). + +### Étape 2 — Sécurité (AVANT toute génération PWM) + +Aucun PWM ne doit sortir tant que les protections ne sont pas armées **et +testées**. + +1. `AIOMUX1` : basculer AIO2, AIO4, AIO10, AIO12, AIO14 en mode analogique + (valeur du champ = **2**). +2. **Horloges de module** : `PCLKCR3.bit.COMP1ENCLK = 1` et `COMP2ENCLK = 1`, + sinon `COMPSTS` ne se met jamais à jour. +3. **Comparateurs COMP1 / COMP2** (shunt sur l'entrée A, DAC en référence) : + - `COMPCTL.bit.COMPDACEN = 1` — **sans ça le bloc reste éteint** + - `COMPCTL.bit.COMPSOURCE = 0` (entrée inverseuse = DAC interne) + - `DACVAL.bit.DACVAL` = code du seuil 3 A. Formule TI : + `V = DACVAL × (VDDA − VSSA) / 1023` → **1023, pas 4096**. + Calculé par macro depuis `SAFETY_ISHUNT_THRESHOLD_A`, jamais en dur. + - `COMPCTL.bit.CMPINV = 1` (driver actif haut : sortie 1 = OK, 0 = défaut) + - `COMPCTL.bit.SYNCSEL = 0` (**asynchrone**) — c'est ce qui donne les 30 ns + de temps de réponse vers le Trip Zone. `QUALSEL` n'a d'effet qu'en mode + synchrone, qui ajoute de la latence : n'activer la qualification qu'après + avoir observé le bruit réel au scope, jamais par précaution a priori. + - Hystérésis d'entrée activée par défaut (~100 kΩ de contre-réaction, + 35 mV) : compatible avec la sortie basse impédance de l'ampli. + `COMPHYSTCTL` (module ADC) permet de la désactiver si besoin. + - Sorties routées : GPIO1 (COMP1OUT, mux 3) et GPIO3 (COMP2OUT, mux 3) +4. **Trip Zone via Digital Compare** + - `DCTRIPSEL.bit.DCAHCOMPSEL` = COMP1OUT (code `1000b`) pour ePWM1, + COMP2OUT (`1001b`) pour ePWM2 + - `TZDCSEL.bit.DCAEVT1` : configurer l'événement sur DCAH bas + - `TZSEL.bit.DCAEVT1 = 1` → **one-shot** (latché), pas cycle-by-cycle + - `TZCTL.bit.TZA = TZ_FORCE_LO` +5. **TZ6 = EMUSTOP** sur ePWM1 et ePWM2 : + ```c + EPwm1Regs.TZSEL.bit.OSHT6 = 1; + EPwm1Regs.TZCTL.bit.TZA = TZ_FORCE_LO; + ``` + Le signal `EMUSTOP` du CPU **existe bien** et est câblé en dur sur TZ6 sur + cette famille (TRM SPRUI09A §3.2.7) ; il n'y a simplement pas de macro + nommée `EMUSTOP` dans les headers, on écrit directement `OSHT6`. + Indispensable : sans ça, un breakpoint posé pendant que le MOSFET conduit + laisse l'inductance se charger sans limite. +6. **`TBCTL.bit.FREE_SOFT = 0`** sur les deux ePWM, **en plus et non à la + place** du point 5. Attention : `FREE_SOFT` gèle seulement le compteur de + base de temps — l'Action Qualifier a déjà positionné la sortie, qui reste + figée dans son dernier état. Si le CPU s'arrête pendant le temps de + conduction, **la broche reste haute**. Seul TZ6 + `TZ_FORCE_LO` garantit + la mise à l'état bas. +7. **`EPWMx_TZINT`** : diagnostic logiciel uniquement (log, LED rouge, + notification ESP32). La coupure est déjà faite en matériel. +8. API : `safety_get_fault_flags()`, `safety_clear_faults()` (écriture + `TZCLR[OST]`, **jamais appelée automatiquement**). +9. **Test** : `TZFRC[OSHT]` pour forcer un trip logiciel, sans provoquer de + vrai court-circuit. + +**Critère** : `TZFRC` force les deux sorties à 0 et lève le flag ; un +breakpoint coupe le PWM ; le flag ne se réarme pas seul. + +### Étape 3 — GPIO de commande, état sûr au reset + +- `HV_EN`, `Stage1-EN`, `Stage2-EN` à 0 **avant toute autre configuration** +- ⚠️ **L'état au reset n'est PAS fail-safe** : GPIO1 et GPIO3 (COMPxOUT) sont + des broches à fonction PWM, dont **les pull-ups ne sont pas activées au + reset** → elles flottent. GPIO16/17 (Stage-EN) ne sont pas des broches PWM + → **pull-ups activées** → état haut. Résultat : à la mise sous tension, une + entrée de chaque porte ET flotte et l'autre est à 1 → **sortie + indéterminée, le driver peut être validé avant que le firmware ne tourne**. + - Correctif **matériel obligatoire** : pull-down 10 kΩ sur les deux entrées + de chaque porte ET, et sur les lignes PWM vers les drivers. Le firmware + ne peut rien pour la fenêtre pré-boot. + - Correctif logiciel complémentaire : désactiver les pull-ups internes de + GPIO16/17 via `GPAPUD` dès l'init. +- LED rouge = défaut latché, LED bleue = fonctionnement nominal +- Encapsuler la polarité des LED dans `led_set()` + +### Étape 4 — ADC + +- Référence **interne** (`ADCREFSEL = 0`), `Device_cal()` appelé +- 9 canaux (§2), **SOC déclenché par ePWM**, jamais en free-run : + échantillonner à un instant stable, loin des fronts de commutation +- ISR `ADCINT1` courte : stockage des valeurs brutes, rien de plus +- Moyenne glissante côté `measure.c` pour la télémétrie. La protection rapide + ne passe **pas** par l'ADC. + +### Étape 5 — Conversion physique (`measure.c`) + +Implémenter les relations du §5. Signature du type +`float measure_vin(uint16_t raw)`, unités documentées dans les prototypes. + +### Étape 6 — PWM + +- **ePWM1** → étage 1 (EPWM1A, GPIO0) · **ePWM2** → étage 2 (EPWM2A, GPIO2) +- Mode up-count, TBCLK = SYSCLKOUT = 60 MHz +- `TBPRD = 60e6 / Fpwm - 1` (599 à 100 kHz, 299 à 200 kHz) +- **Fréquences par étage : À CONFIRMER** +- **Duty de départ = 0 %**, puis rampe logicielle montante (soft-start), + indépendante de la régulation. Ne jamais démarrer au duty nominal : le + condensateur de sortie vide provoquerait un appel de courant destructeur. + +> **Important — un boost à 0 % de duty ne donne pas 0 V en sortie.** Le chemin +> `Vin → L → diode → Cout` reste passant en permanence. Avec les deux étages à +> 0 %, la sortie HT est déjà à ~10 V ; si l'étage 1 régule à 50 V pendant que +> l'étage 2 est à 0 %, la sortie est à **50 V**. Le seul organe qui isole +> réellement la charge est `HV_EN` (VOM1271). Conséquence pour le firmware : +> toute séquence d'arrêt, de mise en sécurité ou de timeout UART doit couper +> `HV_EN` **en plus** d'inhiber les PWM, jamais seulement les PWM. +- API : `pwm_set_duty(stage, float duty_0_1)`, `pwm_set_freq(stage, hz)`, + `pwm_enable(stage, bool)` +- **HRPWM : ne pas l'implémenter maintenant**, mais structurer `pwm.c` pour + l'accueillir. Notes à consigner en commentaire : + - disponible uniquement sur les sorties **EPWMxA** → les deux étages sont + compatibles tels que routés + - SYSCLKOUT ≥ 50 MHz requis (OK à 60), MEP 150-310 ps, SFO obligatoire + - **limitation de 3 cycles SYSCLK** sur le rapport cyclique : à 200 kHz, + duty min 0,67 % et max 99 % — à confronter aux points de fonctionnement + - à 100 kHz sans HRPWM : 600 pas ≈ 9,2 bits, risque de limit cycling en + boucle fermée + +### Étape 7 — Liaison UART + +Conforme à `docs/ESP32-UART.md`, avec **57600 bauds 8N1** (et non 115200 — +mettre à jour le document et le firmware ESP32). + +- SCI-A, GPIO28 (RX) / GPIO29 (TX) +- Émission `$T,...*XX` toutes les **200-500 ms** (l'ESP32 déclare la liaison + perdue au-delà de 2 s) +- Réception `$C,HT=x,PWM1=x,PWM2=x*XX` toutes les 500 ms +- Checksum **XOR** entre `$` et `*`, format `%02X` **majuscules** (sinon rejet + silencieux côté ESP32), terminaison `\n`, ligne ≤ 160 caractères +- Réception par ISR + buffer circulaire, **parsing dans la boucle principale** +- Trame au checksum invalide : ignorée silencieusement +- **Timeout côté TMS320** : sans trame `$C` valide depuis > 2 s → retour en + état sûr (PWM inhibés, HT coupée). La sécurité ne dépend jamais de l'ESP32. +- Vérifier le support `%f` du compilateur C28x dans `sprintf` ; si coût trop + élevé, formater les flottants manuellement (entier + décimales) + +Champs : `FREQ1 FREQ2 DUTY1 DUTY2 VIN IIN V1 I1 T1 VOUT I2 T2 IOUT` + +### Étape 8 — Régulation : squelette seulement + +**Aucune régulation active à ce stade.** Le PWM reste à sa valeur de départ. + +```c +typedef struct { + float setpoint; // consigne (V) + float measured; + float duty; // 0..1 + float duty_min, duty_max; + bool enabled; +} control_stage_t; + +void control_init(void); +void control_set_setpoint(int stage, float volts); +float control_update(int stage, float measured, float dt); // renvoie duty inchangé +``` + +Prévoir l'emplacement d'appel cadencé (`dt` constant) dans l'ISR ADC ou PWM. +Documenter en commentaire : le boost à fort gain présente un **zéro dans le +demi-plan droit** qui limite la bande passante atteignable ; la charge +(~10-15 mA, modulée à fréquence audio) impose surtout un bon **PSRR en bande +audio** ; une structure ADRC ou un PI cascadé sera évalué ensuite. + +--- + +## 7. Règles + +- **Headers bitfield uniquement**, pas de driverlib, pas de mélange sur un + même périphérique +- `EALLOW`/`EDIS` autour de tous les registres protégés (TZSEL, TZCTL, TZEINT, + TZCLR, TZFRC, HRCNFG, GPxMUX, AIOMUX1, INTOSCnTRIM, table des vecteurs PIE…) +- Aucune valeur magique : tout dans `calib.h`, commenté avec son origine +- ISR courtes : pas de `sprintf`, pas de flottant lourd, pas d'attente +- Ordre d'init strict : **GPIO en état sûr → AIOMUX1 → horloge → sécurité → + ADC → UART → PWM en dernier** +- Commits atomiques, un par étape + +## 8. Interdits + +- Activer le PWM avant que l'étape 2 soit testée +- Effacer automatiquement un flag de défaut latché +- Faire dépendre une protection du CPU, de l'ADC ou de l'UART +- Implémenter la régulation en boucle fermée maintenant +- Toucher aux broches JTAG (20-23) +- Inventer le protocole UART + +## 9. Points encore ouverts + +1. Fréquence de découpage par étage (100 ou 200 kHz) +2. Polarité réelle des LED (à confirmer au premier test) +3. Valeur d'inductance retenue par étage (47 ou 100 µH) — influe sur le mode + CCM/DCM et sur la vitesse de montée d'un courant de défaut + +## 10. Première action attendue + +Avant d'écrire du code : lire le dépôt, lister l'existant, **poser les +questions sur les points du §9**, puis proposer un plan et attendre validation +avant de commencer l'étape 1. diff --git a/docs/sprui09a.pdf b/docs/sprui09a.pdf new file mode 100644 index 0000000..1635665 Binary files /dev/null and b/docs/sprui09a.pdf differ diff --git a/docs/tms320f28027.pdf b/docs/tms320f28027.pdf new file mode 100644 index 0000000..55d25bf Binary files /dev/null and b/docs/tms320f28027.pdf differ diff --git a/src/bsp_clock.c b/src/bsp_clock.c new file mode 100644 index 0000000..e2b5c71 --- /dev/null +++ b/src/bsp_clock.c @@ -0,0 +1,12 @@ +#include "DSP28x_Project.h" +#include "bsp_clock.h" +#include "calib.h" + +void bsp_clock_init(void) +{ + EALLOW; + SysCtrlRegs.PLLLOCKPRD = CLK_PLLLOCKPRD; + EDIS; + + InitSysCtrl(); +} diff --git a/src/bsp_clock.h b/src/bsp_clock.h new file mode 100644 index 0000000..0f08a60 --- /dev/null +++ b/src/bsp_clock.h @@ -0,0 +1,6 @@ +#ifndef BSP_CLOCK_H +#define BSP_CLOCK_H + +void bsp_clock_init(void); + +#endif diff --git a/src/bsp_gpio.c b/src/bsp_gpio.c new file mode 100644 index 0000000..c0bb2c3 --- /dev/null +++ b/src/bsp_gpio.c @@ -0,0 +1,59 @@ +#include "DSP28x_Project.h" +#include "bsp_gpio.h" +#include "calib.h" + +void bsp_gpio_leds_init(void) +{ + EALLOW; + GpioCtrlRegs.GPAMUX1.bit.GPIO12 = 0; + GpioCtrlRegs.GPADIR.bit.GPIO12 = 1; + GpioCtrlRegs.GPBMUX1.bit.GPIO33 = 0; + GpioCtrlRegs.GPBDIR.bit.GPIO33 = 1; + EDIS; + + led_set(LED_BLUE, false); + led_set(LED_RED, false); +} + +void bsp_gpio_analog_init(void) +{ + EALLOW; + GpioCtrlRegs.AIOMUX1.bit.AIO2 = GPIO_ANALOG_MODE; + GpioCtrlRegs.AIOMUX1.bit.AIO4 = GPIO_ANALOG_MODE; + GpioCtrlRegs.AIOMUX1.bit.AIO10 = GPIO_ANALOG_MODE; + GpioCtrlRegs.AIOMUX1.bit.AIO12 = GPIO_ANALOG_MODE; + GpioCtrlRegs.AIOMUX1.bit.AIO14 = GPIO_ANALOG_MODE; + EDIS; +} + +void led_set(led_id_t led, bool on) +{ +#if LED_ACTIVE_LOW + bool pin_high = !on; +#else + bool pin_high = on; +#endif + + if (led == LED_BLUE) + { + if (pin_high) + { + GpioDataRegs.GPASET.bit.GPIO12 = 1; + } + else + { + GpioDataRegs.GPACLEAR.bit.GPIO12 = 1; + } + } + else + { + if (pin_high) + { + GpioDataRegs.GPBSET.bit.GPIO33 = 1; + } + else + { + GpioDataRegs.GPBCLEAR.bit.GPIO33 = 1; + } + } +} diff --git a/src/bsp_gpio.h b/src/bsp_gpio.h new file mode 100644 index 0000000..b33b9dd --- /dev/null +++ b/src/bsp_gpio.h @@ -0,0 +1,19 @@ +#ifndef BSP_GPIO_H +#define BSP_GPIO_H + +#include + +typedef enum +{ + LED_BLUE = 0, + LED_RED +} led_id_t; + +void bsp_gpio_leds_init(void); +void led_set(led_id_t led, bool on); + +// Bascule AIO2, AIO4, AIO10, AIO12, AIO14 en mode analogique (shunts, +// COMP1A/COMP2A, VOUT, V1, NTC1). A appeler avant safety_init()/adc_init(). +void bsp_gpio_analog_init(void); + +#endif diff --git a/src/calib.h b/src/calib.h new file mode 100644 index 0000000..d2cc868 --- /dev/null +++ b/src/calib.h @@ -0,0 +1,24 @@ +#ifndef CALIB_H +#define CALIB_H + +// Horloge : INTOSC1 (10 MHz nominal) + PLL (DSP28_PLLCR=12, DSP28_DIVSEL=2, +// deja les valeurs par defaut de f2802x_examples.h pour ce device) -> 60 MHz. +// PLLLOCKPRD doit etre ecrit AVANT InitSysCtrl() (qui attend le verrouillage +// PLL en interne) car l'oscillateur interne impose un minimum de 10000. +#define CLK_PLLLOCKPRD 10000 +#define CLK_SYSCLKOUT_HZ 60000000UL + +// LEDs (LQFP48 PT) : bleue = GPIO12 (broche 47), rouge = GPIO33 (broche 35) +#define LED_ACTIVE_LOW 1 + +// 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 +#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)) + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..b17ad42 --- /dev/null +++ b/src/main.c @@ -0,0 +1,60 @@ +#include "DSP28x_Project.h" +#include "bsp_clock.h" +#include "bsp_gpio.h" +#include "safety.h" + +extern uint16_t RamfuncsLoadStart; +extern uint16_t RamfuncsLoadSize; +extern uint16_t RamfuncsRunStart; + +interrupt void cpu_timer0_isr(void); + +void main(void) +{ +#ifdef _FLASH + memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, (size_t)&RamfuncsLoadSize); +#endif + + bsp_clock_init(); + bsp_gpio_leds_init(); + + DINT; + InitPieCtrl(); + IER = 0x0000; + IFR = 0x0000; + InitPieVectTable(); + + bsp_gpio_analog_init(); + safety_init(); + + EALLOW; + PieVectTable.TINT0 = &cpu_timer0_isr; + EDIS; + + InitCpuTimers(); + ConfigCpuTimer(&CpuTimer0, 60, 500000); // 500 ms -> 1 Hz sur la LED + + IER |= M_INT1; + PieCtrlRegs.PIEIER1.bit.INTx7 = 1; // TINT0 + + EINT; + ERTM; + + for (;;) + { + // Etape 1-2 : validation clock/LED/securite. Rien d'autre pour + // l'instant, tout se passe dans les ISR. + } +} + +interrupt void cpu_timer0_isr(void) +{ + static bool blue_on = false; + + CpuTimer0.InterruptCount++; + + blue_on = !blue_on; + led_set(LED_BLUE, blue_on); + + PieCtrlRegs.PIEACK.all = PIEACK_GROUP1; +} diff --git a/src/protocol.h b/src/protocol.h new file mode 100644 index 0000000..ed3350b --- /dev/null +++ b/src/protocol.h @@ -0,0 +1,33 @@ +#ifndef PROTOCOL_H +#define PROTOCOL_H + +#include + +// Squelette du protocole UART TMS320<->ESP32 (voir docs/ESP32-UART.md). +// Rempli/utilise a partir de l'etape 7 (uart_link.c). + +typedef struct +{ + float freq1_hz; + float freq2_hz; + float duty1_pct; + float duty2_pct; + float vin_v; + float iin_a; + float v1_v; + float i1_a; + float t1_c; + float vout_v; + float i2_a; + float t2_c; + float iout_a; +} telemetry_t; + +typedef struct +{ + bool ht_enabled; + bool pwm1_enabled; + bool pwm2_enabled; +} command_state_t; + +#endif diff --git a/src/safety.c b/src/safety.c new file mode 100644 index 0000000..a20f5e9 --- /dev/null +++ b/src/safety.c @@ -0,0 +1,115 @@ +#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; + +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 + Comp1Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE; + 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; + 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; + + 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; + return f; +} + +void safety_clear_faults(void) +{ + EALLOW; + EPwm1Regs.TZCLR.bit.OST = 1; + EPwm2Regs.TZCLR.bit.OST = 1; + EDIS; + s_stage1_fault = false; + s_stage2_fault = false; + led_set(LED_RED, false); +} + +void safety_force_trip_test(void) +{ + EALLOW; + EPwm1Regs.TZFRC.bit.OST = 1; + EPwm2Regs.TZFRC.bit.OST = 1; + EDIS; +} + +interrupt void epwm1_tzint_isr(void) +{ + s_stage1_fault = true; + led_set(LED_RED, true); + PieCtrlRegs.PIEACK.all = PIEACK_GROUP2; +} + +interrupt void epwm2_tzint_isr(void) +{ + s_stage2_fault = true; + led_set(LED_RED, true); + PieCtrlRegs.PIEACK.all = PIEACK_GROUP2; +} diff --git a/src/safety.h b/src/safety.h new file mode 100644 index 0000000..e1b44c8 --- /dev/null +++ b/src/safety.h @@ -0,0 +1,24 @@ +#ifndef SAFETY_H +#define SAFETY_H + +#include + +typedef struct +{ + bool stage1_fault; + bool stage2_fault; +} safety_faults_t; + +void safety_init(void); +safety_faults_t safety_get_fault_flags(void); + +// Ecrit TZCLR[OST] sur les deux etages. Ne doit JAMAIS etre appele +// automatiquement par le firmware (voir document PROMPT, section 8) -- +// uniquement sur commande explicite (UART, etape 7). +void safety_clear_faults(void); + +// Force un trip logiciel (TZFRC[OSHT]) pour valider le mecanisme de +// protection sans provoquer de vrai court-circuit (critere de test etape 2). +void safety_force_trip_test(void); + +#endif