Steps 1+2: clock init (INTOSC1 60MHz), LED heartbeat, comparator/DAC/TripZone safety + EMUSTOP (build OK, HW validation pending)

This commit is contained in:
2026-08-01 20:34:27 +02:00
parent 8d71489f48
commit 616ae539cc
13 changed files with 913 additions and 0 deletions

138
docs/ESP32-UART.md Normal file
View File

@ -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).

View File

@ -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.

BIN
docs/sprui09a.pdf Normal file

Binary file not shown.

BIN
docs/tms320f28027.pdf Normal file

Binary file not shown.

12
src/bsp_clock.c Normal file
View File

@ -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();
}

6
src/bsp_clock.h Normal file
View File

@ -0,0 +1,6 @@
#ifndef BSP_CLOCK_H
#define BSP_CLOCK_H
void bsp_clock_init(void);
#endif

59
src/bsp_gpio.c Normal file
View File

@ -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;
}
}
}

19
src/bsp_gpio.h Normal file
View File

@ -0,0 +1,19 @@
#ifndef BSP_GPIO_H
#define BSP_GPIO_H
#include <stdbool.h>
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

24
src/calib.h Normal file
View File

@ -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

60
src/main.c Normal file
View File

@ -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;
}

33
src/protocol.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef PROTOCOL_H
#define PROTOCOL_H
#include <stdbool.h>
// 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

115
src/safety.c Normal file
View File

@ -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;
}

24
src/safety.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef SAFETY_H
#define SAFETY_H
#include <stdbool.h>
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