Compare commits

...

13 Commits

Author SHA1 Message Date
69801df648 InitFlash manquante : tout le firmware tournait 4x trop lentement
DEFAUT PRINCIPAL -- etats d'attente de la flash jamais configures

  Le firmware s'execute depuis la flash (_FLASH), mais InitFlash() n'etait
  jamais appelee. Au reset FBANKWAIT vaut son MAXIMUM (0x0F0F, soit
  RANDWAIT = PAGEWAIT = 15) : chaque acces flash coutait 16 cycles au lieu
  des 3 necessaires a 60 MHz.

  Ironie : le memcpy des ramfuncs en tete de main() existe PRECISEMENT pour
  copier InitFlash() en RAM, puisqu'elle doit s'executer hors flash. La
  recopie se faisait depuis le premier jour sans que la fonction ne soit
  jamais appelee -- la section ramfuncs ne contenait que 4 mots, elle en
  fait 0x21 maintenant.

  Mesure au scope sur l'ISR ADC :
    avant : 22,3 kHz, ISR 30 us, 88 % de charge CPU
    apres : 66,85 kHz, ISR 4,9 us, 33 % de charge CPU
  66,85 kHz est le maximum theorique (200 kHz / SOCAPRD=3), atteint sans
  aucun declenchement manque. Recoupe au compteur : 67,5 kHz.

Optimisation compilateur

  -O2 ajoute : le projet compilait sans aucune option d'optimisation.
  Contribution mesuree : ISR de 41 a 30 us, mais SANS changer la cadence --
  ce n'etait pas le facteur limitant. Conserve, le gain reste reel.

Discipline ISR (ni multiplication ni division en interruption)

  status_led_tick() : quatre modulos (%200, %40, %100, %100) remplaces par
    un compteur qui reboucle sur comparaison.
  cpu_timer0_isr()  : "tick % 30" remplace par un compteur dedie.
  control_tick()    : le tableau stages[2] etait initialise sur la pile a
    chaque appel, des dizaines de milliers de fois par seconde -> static const.
  pwm_set_duty_counts() : une division flottante y mettait a jour la
    consigne, sur un F28027 sans FPU. Supprimee ; pwm_get_duty() relit
    CMPA, la telemetrie reste juste.

control.c/h -- balayage de caracterisation (BOUCLE OUVERTE)

  Ce module ne regule rien : il balaie le duty de 1 % a 50 % puis revient,
  par pas de 1 LSB de CMPA a chaque conversion ADC. Appel cadence depuis
  l'ISR ADC, conformement a PROMPT §6 etape 8.

  API en counts ajoutee a pwm.c : le pas minimal est 1 LSB de CMPA et
  DEPEND DE L'ETAGE (1/300 = 0,333 % a 200 kHz, 1/600 = 0,167 % a 100 kHz).
  Une consigne flottante ne permet pas d'exprimer "le plus petit pas".

  Resultat mesure a 66,7 kHz :
    etage 1 : 1->50 % en 2,2 ms, cycle 4,4 ms, 22 %/ms
    etage 2 : 1->50 % en 4,4 ms, cycle 8,9 ms, 11 %/ms
  L'etage 2 est deux fois plus lent PARCE QUE sa resolution est deux fois
  plus fine, a cadence de mise a jour identique.

  La telemetrie transporte desormais le duty reel, relu depuis CMPA.

L'instrumentation de mesure a ete RETIREE : GPIO32 est rendue a HV_EN, qui
en a de nouveau l'usage exclusif (verifie sur cible, broche stable a 0).

Note : ce defaut de wait states etait present depuis le debut du projet.
Toutes les mesures de timing anterieures etaient faussees d'un facteur 4.
Les frequences PWM et les protections sont materielles et restent valides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:28:34 +02:00
b1bb0fa21a Reprise controlee apres EMUSTOP, sans lever le verrouillage des defauts
Le verrouillage total decide precedemment (aucun effacement, cycle
d'alimentation obligatoire) rendait le developpement intenable : chaque
breakpoint tuait le PWM jusqu'a debranchement. EMUSTOP n'est pourtant pas
un defaut de puissance mais un artefact du debogueur.

La distinction est maintenant explicite dans la boucle principale :

  overcurrent / overtemp -> enter_safe_state(), VERROUILLE jusqu'au cycle
                            d'alimentation (PROMPT §8), aucun acquittement
  emustop seul           -> reprise controlee

La reprise ne redemarre PAS la conversion la ou elle s'etait arretee :
elle repasse par start_conversion(), le meme chemin qu'au boot, duty remis
a zero d'abord. Quand la rampe de soft-start existera, la reprise apres
breakpoint en beneficiera automatiquement.

uart_link_restart()
  Reset logiciel du SCI, vidage des FIFO, abandon de la ligne en cours et
  de la trame en attente. Pendant la halte l'ESP32 a continue d'emettre :
  l'anneau de reception a deborde et la ligne assemblee est tronquee.
  Le vidage avance seulement la QUEUE de l'anneau ; s_rx_head appartient a
  l'ISR et n'est pas touche, ce qui exclut toute course.

TZCLR au demarrage dans safety_init()
  TZFLG.OST est latche dans le materiel : il survit a un reset logiciel
  comme a un rechargement de programme, et sans ce clear le PWM restait
  coupe sans raison visible.
  Sans danger, pour une raison precise : a ce stade pwm_init() a deja
  force les deux sorties a l'etat bas (AQCSFRC), donc rien ne demarre ; et
  si la condition de defaut est toujours presente le trip se re-verrouille
  immediatement. On n'efface qu'un etat perime, jamais un defaut reel.

start_conversion()
  Sequence de demarrage factorisee, appelee au boot et a la reprise.
  Inhibe la decharge AVANT d'autoriser les etages -- jamais l'inverse,
  sinon la conversion travaillerait contre le circuit de decharge.

Valide sur cible apres cycle pause/reprise/pause :
  TZEINT.OST rearme a 1 (l'ISR l'avait mis a 0), AQCSFRC.CSFA a 0,
  GPIO5 a 1 (decharge inhibee), GPIO16 a 1.

LIMITE CONNUE, non resolue ici : a la reprise l'effacement est immediat
alors que la telemetrie ne part que toutes les 300 ms. FAULT=1 ne sera
donc quasiment jamais emis, et l'ESP32 ignorera qu'un EMUSTOP a eu lieu
(sans meme voir de timeout si la halte a dure moins de 2 s). Transmettre
FAULT=1 AVANT la halte est structurellement impossible : la halte est
asynchrone, imposee par la logique de debug, sans point d'accroche
prealable -- c'est justement pourquoi TZ6 est un chemin materiel. La
solution sera un drapeau collant maintenu quelques secondes apres la
reprise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:22:44 +02:00
26c1d499e0 Champ FAULT, decharge HT active, et surtemperature par voie
Circuit de decharge rapide (GPIO5, broche 40)

  Commande DISCHARGE du circuit de la carte de puissance : MOSFET en serie
  avec 47 kOhm aux bornes du condensateur de sortie, soit tau = 0,94 s et
  environ 2 s pour passer de 500 V a 50 V. Sans lui, le seul bleeder de
  1 MOhm demandait ~42 s.

  POLARITE INVERSEE, et c'est le coeur du montage. Le GPIO pilote un NPN
  qui court-circuite la grille du MOSFET :
    niveau haut -> NPN conduit -> grille a la masse -> decharge INHIBEE
    niveau bas ou broche non pilotee -> grille tiree a 12 V -> decharge ACTIVE

  GPIO5 est une broche a fonction PWM : pas de pull-up au reset (PROMPT §6
  etape 3), elle flotte. La decharge est donc active au reset, pendant un
  plantage, et surtout ALIMENTATION COUPEE -- exactement le cas ou le
  firmware ne garantit plus rien et ou l'operateur croit la carte
  inoffensive. C'est precisement le cas impose par le choix de n'effacer un
  defaut que par cycle d'alimentation.

  Sequencement explicite :
   - enter_safe_state() active la decharge APRES avoir coupe HV_EN : la
     charge est isolee d'abord, le condensateur vide ensuite ;
   - le retour au nominal inhibe la decharge AVANT d'autoriser les etages,
     jamais l'inverse ;
   - bsp_gpio_control_init() la laisse ACTIVE au demarrage : au boot le
     condensateur peut encore etre charge par une session precedente.

Champ FAULT dans la trame $T

  Codes 0-5, priorite 2 > 3 > 4 > 5 > 1. EMUSTOP (code 1) a donc la
  priorite la PLUS BASSE malgre son numero : en developpement il se
  declenche a chaque halte du debogueur et ne doit jamais masquer une
  surintensite reelle.

  Pas de code court-circuit : il franchit le meme comparateur et le meme
  seuil qu'une surintensite, le firmware ne peut pas les distinguer.

  Aucun effacement automatique ni par commande UART : le verrouillage
  n'est leve que par un cycle d'alimentation (PROMPT §8).

Surtemperature suivie PAR VOIE

  s_overtemp_t1 / s_overtemp_t2 separes, avec hysteresis chacun. L'ancien
  maximum des deux voies ne permettait pas de distinguer les codes 4 et 5.
  Seuil ramene a 80 degC pour coller au protocole.

Divers
  UART_LINE_MAX porte a 200 (buffer ESP32 g_lineBuf[200]).
  append_field_int() pour les champs entiers, meme convention de bornage
  que append_field : un champ qui ne tient pas est abandonne entierement.

Documentation
  docs/mesure-cartepuissance.md ajoute au depot (source de tracabilite des
  coefficients de calib.h).
  docs/ESP32-UART.md mis a jour, avec deux corrections de fond :
   - la mention "remettre FAULT=0 automatiquement des que la cause a
     disparu" est remplacee par une section sur le verrouillage ; elle
     contredisait le comportement reel, le TMS320 reemettant indefiniment
     le code latche ;
   - l'icone danger est specifiee comme dependant UNIQUEMENT de VOUT et
     jamais de FAULT. Un defaut ne decharge pas le condensateur : la faire
     disparaitre reviendrait a rassurer l'operateur quand le risque est
     maximal.
  Le piege strtol / uint8_t 16 bits y est documente pour eviter qu'il soit
  reintroduit ailleurs.

A cabler cote materiel : liaison broche 40 vers la carte de puissance, et
pull-down 10 kOhm sur la base du NPN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:00:32 +02:00
46ddf30a64 Etape 2 validee sur cible : trip comparateur, arret global, Stagex-EN
safety_init() complet est maintenant appele a la place de
safety_arm_emustop_only() : comparateurs + Digital Compare + Trip Zone.
Seuils DAC verifies sur cible a 601 / 603, conformes au calcul de
docs/mesure-cartepuissance.md pour 3,0 A.

CORRECTIF -- les trips ne remontaient pas au logiciel (safety.c)

  TZFLG.INT est le drapeau GLOBAL d'interruption Trip Zone : tant qu'il
  n'est pas efface via TZCLR.INT, plus AUCUNE interruption TZ n'est generee.
  Les ISR ne faisaient que PIEACK. Consequence : le premier trip de la
  session (un EMUSTOP provoque par une pause du debogueur) consommait
  l'unique notification, et tous les suivants coupaient bien le PWM en
  materiel mais en SILENCE, sans lever de drapeau logiciel.

  Constate au banc : surintensite reelle sur I2, EPwm2Regs.TZFLG = 0x0D
  (INT + OST + DCAEVT1, donc trip comparateur bien latche), PWM2 coupe,
  et pourtant LED au nominal.

  Ce defaut etait invisible en testant un seul trip a la fois. Il ne serait
  apparu qu'en exploitation, sous la forme "la protection coupe mais l'IHM
  n'affiche aucun defaut".

  Second piege evite : effacer INT alors que la surintensite persiste
  regenere l'evenement immediatement -> tempete d'interruptions qui
  affamerait la boucle principale, donc plus de telemetrie ni de LED, au
  pire moment. L'ISR desarme donc TZEINT.OST : une seule notification par
  episode. Le rearmement est fait par safety_clear_faults() uniquement,
  jamais automatiquement (PROMPT §8).

  safety_clear_faults() efface desormais DCAEVT1 (sinon le prochain trip
  serait attribue a une surintensite meme s'il vient d'EMUSTOP), INT, et
  rearme TZEINT.OST.

Arret global sur defaut (main.c)

  enter_safe_state() : les deux etages inhibes (PWM + Stagex-EN) ET HV_EN
  coupe. Sur un boost en cascade, laisser tourner l'etage 2 pendant que
  l'etage 1 est coupe n'a pas de sens. Couper HV_EN n'est pas redondant :
  un boost a 0 % de duty ne donne PAS 0 V en sortie, le chemin
  Vin -> L -> diode -> Cout reste passant ; HV_EN (VOM1271) est le seul
  organe qui isole reellement la charge (PROMPT §6 etape 6).

  Les Trip Zones restent independantes en materiel : cet arret global est
  la couche systeme au-dessus, executee au rythme de la boucle principale.
  La coupure rapide (~30 ns) reste la porte ET + le Trip Zone.

  Stagex-EN est maintenant pilote en continu : 1 au nominal, 0 sur defaut.
  Inhibition SUPPLEMENTAIRE, le logiciel ne peut jamais outrepasser la
  coupure materielle deja faite par COMPxOUT sur l'autre entree de la porte.

Valide au banc : injection de 2 A sur le shunt 2 avec seuil abaisse a
1,5 A -> les deux etages coupes, LED rouge fixe, defaut latche.

ATTENTION, toujours en place dans main.c : le harnais de bring-up force le
PWM a 50 % en dur, et Stagex-EN passe desormais a 1 automatiquement au
demarrage. Des que les drivers UCC27517 seront montes, la carte se mettra
donc a decouper A LA MISE SOUS TENSION, sans commande -- exactement ce que
le PROMPT §6 etape 6 interdit (demarrage a 0 % + rampe de soft-start).
Retirer ce harnais AVANT de monter les drivers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 20:30:53 +02:00
a05c1e8124 Machine d'etats LED, protection thermique, et correctif checksum RX
CORRECTIF CRITIQUE -- reception UART cassee (uart_link.c)

  Le decodage du checksum utilisait strtol sur s_line, qui n'est JAMAIS
  terminee par un '\0'. strtol lisait donc au-dela de la trame, dans les
  residus de la ligne precedente.

  Aggravant, specifique au C28x : uint8_t y fait 16 bits (pas d'adressage
  par octet), donc le cast (uint8_t) ne tronquait rien. Un "7D" suivi d'un
  "7D" residuel donnait 0x7D7D = 32125, conserve tel quel, et TOUTE trame
  etait rejetee. Sur une architecture a octets le cast aurait masque le
  probleme et le bug serait passe inapercu.

  Constate sur cible : cs_calc = 0x7D (correct), cs_recv = 0x7D7D.
  Consequence visible : plus aucune commande $C acceptee, donc timeout de
  liaison permanent alors que l'ESP32 emettait correctement.

  Remplace par parse_hex2(), borne a exactement deux chiffres, qui ne
  depend d'aucune terminaison et rejette un checksum malforme. Supprime au
  passage la dependance a stdlib.

status_led.c/h -- affichage d'etat
  STARTUP      bleu fixe (1 s minimum, sinon jamais visible)
  NOMINAL      bleu 0,2 / 1,8 s
  LINK_LOST    bleu 0,2 / 0,2 s   (> 2 s sans trame $C valide)
  EMUSTOP      alternance bleu/rouge 0,5 / 0,5 s
  OVERTEMP     rouge 0,5 / 0,5 s
  OVERCURRENT  rouge fixe

  Priorite : surintensite > surtemperature > EMUSTOP > liaison perdue.
  La surintensite passe devant EMUSTOP : en developpement EMUSTOP se
  declenche en permanence et ne doit jamais masquer un defaut de puissance.

  Limite de principe : pendant une halte CPU, plus aucun code ne tourne,
  les GPIO restent figes sur la phase courante du motif. L'alternance
  EMUSTOP est donc un indicateur a posteriori, jamais un etat live.

safety.c/h -- origine du trip
  TZFLG.DCAEVT1 (bit 3) est distinct de TZFLG.OST (bit 2), ce qui permet de
  separer une surintensite reelle (comparateur) d'un arret du debugger.
  Les ISR ne pilotent plus les LED, elles ne font que relever la cause.

Protection thermique (main.c, calib.h)
  Seuil 85 degC avec hysteresis de 10 degC, coupure du PWM et de HV_EN.
  Lente par nature, donc entierement logicielle -- aucun chemin materiel
  requis, contrairement a la surintensite.
  Pas de redemarrage automatique : rien ne reactive le PWM une fois coupe.
  Seuil PROVISOIRE : les NTC sont a 5 mm des MOSFET, donc elles mesurent le
  cuivre et non la jonction (ecart statique et retard thermique notables).
  A recaler quand la temperature de boitier en charge sera connue.

Valide sur cible : commandes $C de nouveau acceptees, chaine NTC lue a
24,5 / 24,7 degC sur pont 10k/10k (coherent avec des resistances a 1 %).

Note : un pont 10k/10k ne discrimine PAS le sens de la formule NTC, les
deux formes coincidant au point d'equilibre. Verification a faire avec une
resistance asymetrique (4,7 kOhm -> environ 47 degC).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:53:32 +02:00
888723abc7 Etape 5 : measure.c et constantes de calibration reelles
Report des mesures de docs/mesure-cartepuissance.md dans calib.h, avec une
structure par voie (gain, offset) plutot que des constantes isolees.

calib.h
  Tensions   : VIN 11,11 / V1 31,25 / VOUT 181,82 V/V (mesures)
  IIN        : 1,2019 A/V -- un seul point mesure, offset suppose nul
  IOUT       : 0,016667 A/V -- NON MESURE, valeur theorique de conception
  Shunts     : I1 offset 47,3 mV gain 0,631 V/A
               I2 offset 34,0 mV gain 0,637 V/A
               Deux jeux separes : les voies different d'environ 1 % en gain
               et n'ont pas le meme offset, ne jamais moyenner.

Seuil de protection : le code DAC est maintenant calcule par macro a partir
de SAFETY_ISHUNT_THRESHOLD_A et des gain/offset mesures, et il est PROPRE A
CHAQUE ETAGE (601 et 603 au lieu d'un 558 commun).

  Correction d'analyse : le 558 precedent supposait un gain de 30 pile. Le
  gain reel etant plus eleve (~31,5), le seuil de 1,8 V etait atteint a
  2,78 A et non a 3 A -- la protection declenchait donc TROP TOT, et non
  trop tard comme indique au §5 du document de mesures. Conservatif, sans
  danger, mais corrige.

measure.c/h (etape 5)
  measure_vin/iin/v1/vout/iout/i1/i2/temp, signature float f(uint16_t raw).
  Deux fonctions distinctes pour les shunts, puisque gain ET offset
  different. measure_i1/i2 peuvent renvoyer une valeur negative a courant
  nul : volontaire, une derive d'offset apres changement d'ampli devient
  ainsi visible en telemetrie au lieu d'etre masquee par une saturation.

  NTC : la relation du PROMPT §5 est INVERSEE. Avec la NTC cote 3,3 V c'est
  R_ntc = R_fixe * (VREF - Vadc) / Vadc. Verifie sur les points de repere du
  PROMPT lui-meme : 0,75 V a 0 degC donne 34,2 kOhm, ce que beta=4000
  predit, alors que la forme inversee donnerait 2,94 kOhm (donc du chaud).
  Telle qu'ecrite, la protection thermique aurait ete inexploitable.

La telemetrie transporte desormais de vraies grandeurs physiques.

Reste en suspens (voir docs/mesure-cartepuissance.md §6) : IOUT jamais
mesure, second point pour IIN, valeur reelle de la resistance fixe des
ponts NTC, et surtout gain/offset des shunts A REMESURER apres passage du
MCP6001 au TLV9151 -- l'offset actuel est domine par le Vos de l'ampli.

Note : l'"auto-zero logiciel au boot" evoque dans le document de mesures
n'existe pas dans le firmware et n'a pas ete ajoute ; mesurer un offset au
boot figerait une valeur fausse si du courant circule deja.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 18:50:21 +02:00
d26c07d3a1 Etapes 3/4/6 : GPIO de commande, PWM et ADC valides sur cible
Campagne de bring-up materiel complete, carte de conversion DECONNECTEE.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 20:42:14 +02:00
d185d73422 Etape 7 : liaison UART SCI-A 57600 8N1 validee sur cible
Emission $T,...*XX et reception $C,...*XX conformes a docs/ESP32-UART.md.
Trame mesuree a 129 caracteres, checksum XOR verifie independamment.

Corrections trouvees au bring-up sur materiel :

- StartCpuTimer0() manquant : ConfigCpuTimer() laisse le timer arrete, donc
  le tick d'emission ne se produisait jamais et rien ne partait sur la ligne.

- Suppression de "%f" dans sprintf. Sur C28x le support flottant de printf
  passe par du long double 64 bits emule (frexpl/scalbnl/L$$DIV) : le CPU
  partait a PC=0 des le premier envoi. Formatage decimal manuel a la place,
  comme le prevoyait deja PROMPT §7 etape 7.

- RXFFIENA n'etait pas arme dans SCIFFRX : aucune interruption de reception.

- Recuperation de SCIRXST.RXERROR. Un FE/OE/PE/BRKDT le latche et bloque le
  recepteur jusqu'a un SW RESET du SCI ; sans ca une seule perturbation
  arretait la reception definitivement (constate : RXERROR+FE+BRKDT latches).

- Formatage borne : clamp des valeurs (NaN et saturation) puis snprintf avec
  la capacite restante. Un champ qui ne tient pas est abandonne entierement,
  ce que le protocole autorise, plutot que de deborder s_tx_frame.

- Emission non bloquante : send_telemetry() ne fait que mettre en attente,
  uart_link_service_tx() pousse au plus une FIFO (4 octets) puis rend la
  main. L'ancienne version monopolisait 22 ms par trame, incompatible avec
  la priorite donnee a la boucle de regulation.

Plan memoire (F2802x_generic_flash.cmd) :
- les 4 secteurs flash du F28027 sont declares (32K mots au lieu de 8K)
- .ebss bascule en RAML0 : il etait colle juste apres .stack, donc tout
  debordement de pile ecrasait silencieusement les globales
- pile portee a 1024 mots, seule dans RAMM1

LED : polarite reelle confirmee active-haut (cathode commune a la masse,
anodes pilotees par GPIO12/GPIO33 a travers 1k) -> LED_ACTIVE_LOW = 0.
Repond au point ouvert §9.2 du PROMPT.

safety_init() reste desactive dans main.c : bring-up isole LED+UART, a
reactiver quand l'etage de puissance sera cable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 18:38:18 +02:00
e717d0deec Fix broken self-referential C2000Ware path in .cproject
The previous commit replaced the hardcoded path with a variable
reference to itself (${COM_TI_C2000WARE_INSTALL_DIR}), which broke
include resolution and the build entirely. Restore the absolute path
confirmed via getProjectProductReferences.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 12:24:42 +02:00
e0efeebe94 Ignore IDE-local files; make C2000Ware path portable in .cproject
.mcp.json and CLAUDE.md are auto-generated by CCS with machine-specific
absolute paths and shouldn't be shared via git. The .cproject change
(likely from a CCS reimport) replaces the hardcoded C2000Ware install
path with a variable reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:44:49 +02:00
f9f6909a56 Fix red LED pin number in comment (36, not 35); add corrected pinout doc 2026-08-02 11:12:52 +02:00
616ae539cc Steps 1+2: clock init (INTOSC1 60MHz), LED heartbeat, comparator/DAC/TripZone safety + EMUSTOP (build OK, HW validation pending) 2026-08-01 20:34:27 +02:00
8d71489f48 Restructure project: remove LED demo, switch to Flash linker 2026-08-01 20:33:47 +02:00
33 changed files with 3785 additions and 297 deletions

View File

@ -22,30 +22,30 @@
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<configuration artifactExtension="out" artifactName="Example_2802xLEDBlink" buildProperties="" description="" id="com.ti.ccstudio.buildDefinitions.C2000.Debug.1909800144" name="Debug" parent="com.ti.ccstudio.buildDefinitions.C2000.Debug">
<folderInfo id="com.ti.ccstudio.buildDefinitions.C2000.Debug.1909800144.847706334" name="/" resourcePath="">
<toolChain id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.DebugToolchain.715961659" name="TI Build Tools" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.DebugToolchain" targetTool="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.linkerDebug.1498851383">
<option id="com.ti.ccstudio.buildDefinitions.core.OPT_TAGS.496542915" superClass="com.ti.ccstudio.buildDefinitions.core.OPT_TAGS" valueType="stringList">
<toolChain id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.DebugToolchain.213001512" name="TI Build Tools" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.DebugToolchain" targetTool="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.linkerDebug.813574046">
<option id="com.ti.ccstudio.buildDefinitions.core.OPT_TAGS.1793914701" superClass="com.ti.ccstudio.buildDefinitions.core.OPT_TAGS" valueType="stringList">
<listOptionValue value="DEVICE_CONFIGURATION_ID=TMS320C28XX.TMS320F28027"/>
<listOptionValue value="DEVICE_CORE_ID="/>
<listOptionValue value="DEVICE_ENDIANNESS=UNKNOWN"/>
<listOptionValue value="OUTPUT_FORMAT=COFF"/>
<listOptionValue value="RUNTIME_SUPPORT_LIBRARY="/>
<listOptionValue value="CCS_MBS_VERSION=70.0.0"/>
<listOptionValue value="IS_ASSEMBLY_ONLY=false"/>
<listOptionValue value="PROJECT_KIND=com.ti.ccstudio.managedbuild.core.ProjectKind_Executable"/>
<listOptionValue value="LINK_ORDER="/>
<listOptionValue value="PRODUCTS=C2000WARE:26.1.0.00;"/>
<listOptionValue value="LINKER_COMMAND_FILE="/>
<listOptionValue value="PRODUCT_MACRO_IMPORTS={&quot;C2000WARE&quot;:[&quot;${COM_TI_C2000WARE_INCLUDE_PATH}&quot;,&quot;${COM_TI_C2000WARE_LIBRARY_PATH}&quot;,&quot;${COM_TI_C2000WARE_LIBRARIES}&quot;,&quot;${COM_TI_C2000WARE_SYMBOLS}&quot;,&quot;${COM_TI_C2000WARE_SYSCONFIG_MANIFEST}&quot;]}"/>
<listOptionValue value="OUTPUT_TYPE=executable"/>
<listOptionValue value="CCS_MBS_VERSION=70.0.0"/>
<listOptionValue value="LINK_ORDER="/>
<listOptionValue value="PRODUCTS=C2000WARE:26.1.0.00;"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.core.OPT_CODEGEN_VERSION.1720423258" superClass="com.ti.ccstudio.buildDefinitions.core.OPT_CODEGEN_VERSION" value="25.11.1.LTS" valueType="string"/>
<targetPlatform id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.targetPlatformDebug.636408046" name="Platform" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.targetPlatformDebug"/>
<builder buildPath="${BuildDirectory}" id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.builderDebug.52666393" keepEnvironmentInBuildfile="false" name="GNU Make" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.builderDebug"/>
<tool id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.compilerDebug.49305770" name="C2000 Compiler" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.compilerDebug">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.LARGE_MEMORY_MODEL.753719686" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.LARGE_MEMORY_MODEL" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.UNIFIED_MEMORY.408372431" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.UNIFIED_MEMORY" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.SILICON_VERSION.2006190902" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.SILICON_VERSION" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.SILICON_VERSION.28" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.INCLUDE_PATH.101870543" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.INCLUDE_PATH" valueType="includePath">
<option id="com.ti.ccstudio.buildDefinitions.core.OPT_CODEGEN_VERSION.841378290" superClass="com.ti.ccstudio.buildDefinitions.core.OPT_CODEGEN_VERSION" value="25.11.1.LTS" valueType="string"/>
<targetPlatform id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.targetPlatformDebug.524055294" name="Platform" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.targetPlatformDebug"/>
<builder buildPath="${BuildDirectory}" id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.builderDebug.1769976716" name="GNU Make.Debug" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.builderDebug"/>
<tool id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.compilerDebug.305010793" name="C2000 Compiler" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.compilerDebug">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.LARGE_MEMORY_MODEL.1798142386" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.LARGE_MEMORY_MODEL" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.UNIFIED_MEMORY.1604870518" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.UNIFIED_MEMORY" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.SILICON_VERSION.2122353486" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.SILICON_VERSION" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.SILICON_VERSION.28" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.INCLUDE_PATH.716974798" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.INCLUDE_PATH" valueType="includePath">
<listOptionValue value="${COM_TI_C2000WARE_INCLUDE_PATH}"/>
<listOptionValue value="${CG_TOOL_ROOT}/include"/>
<listOptionValue value="${INSTALLROOT_F2802x}/headers/include"/>
@ -53,34 +53,36 @@
<listOptionValue value="${INSTALLROOT_F2802x}/common/include"/>
<listOptionValue value="${INSTALLROOT_IQMATH}/include"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEFINE.1030597368" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEFINE" valueType="definedSymbols">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEFINE.1404568356" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEFINE" valueType="definedSymbols">
<listOptionValue value="${COM_TI_C2000WARE_SYMBOLS}"/>
<listOptionValue value="_DEBUG"/>
<listOptionValue value="LARGE_MODEL"/>
<listOptionValue value="_FLASH"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEBUGGING_MODEL.851670006" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEBUGGING_MODEL" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEBUGGING_MODEL.SYMDEBUG__DWARF" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_SUPPRESS.1263395190" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_SUPPRESS" valueType="stringList">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEBUGGING_MODEL.1187109720" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEBUGGING_MODEL" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DEBUGGING_MODEL.SYMDEBUG__DWARF" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_SUPPRESS.288154142" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_SUPPRESS" valueType="stringList">
<listOptionValue value="232"/>
<listOptionValue value="10063"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_WARNING.191571893" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_WARNING" valueType="stringList">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_WARNING.923695198" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.DIAG_WARNING" valueType="stringList">
<listOptionValue value="225"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ISSUE_REMARKS.1262034113" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ISSUE_REMARKS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.VERBOSE_DIAGNOSTICS.1947015882" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.VERBOSE_DIAGNOSTICS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.QUIET_LEVEL.920546019" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.QUIET_LEVEL" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.QUIET_LEVEL.QUIET" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ABI.155698373" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ABI" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ABI.coffabi" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ISSUE_REMARKS.1620598451" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ISSUE_REMARKS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.VERBOSE_DIAGNOSTICS.537099600" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.VERBOSE_DIAGNOSTICS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.QUIET_LEVEL.946759491" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.QUIET_LEVEL" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.QUIET_LEVEL.QUIET" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ABI.1717529760" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ABI" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.ABI.coffabi" valueType="enumerated"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.OPT_LEVEL.813239093" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.OPT_LEVEL" value="com.ti.ccstudio.buildDefinitions.C2000_25.11.compilerID.OPT_LEVEL.2" valueType="enumerated"/>
</tool>
<tool id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.linkerDebug.1498851383" name="C2000 Linker" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.linkerDebug">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.STACK_SIZE.671635529" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.STACK_SIZE" value="0x200" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.MAP_FILE.1089801316" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.MAP_FILE" value="&quot;Example_2802xLEDBlink.map&quot;" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.OUTPUT_FILE.376320651" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.OUTPUT_FILE" value="Example_2802xLEDBlink.out" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.LIBRARY.1484799295" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.LIBRARY" valueType="libs">
<tool id="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.linkerDebug.813574046" name="C2000 Linker" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.exe.linkerDebug">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.STACK_SIZE.1069850252" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.STACK_SIZE" value="0x400" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.MAP_FILE.775342445" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.MAP_FILE" value="&quot;Example_2802xLEDBlink.map&quot;" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.OUTPUT_FILE.1826875140" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.OUTPUT_FILE" value="Example_2802xLEDBlink.out" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.LIBRARY.1024492364" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.LIBRARY" valueType="libs">
<listOptionValue value="${COM_TI_C2000WARE_LIBRARIES}"/>
<listOptionValue value="rts2800_ml.lib"/>
<listOptionValue value="IQmath.lib"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.SEARCH_PATH.1742958036" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.SEARCH_PATH" valueType="libPaths">
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.SEARCH_PATH.1469209174" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.SEARCH_PATH" valueType="libPaths">
<listOptionValue value="${COM_TI_C2000WARE_LIBRARY_PATH}"/>
<listOptionValue value="${CG_TOOL_ROOT}/lib"/>
<listOptionValue value="${CG_TOOL_ROOT}/include"/>
@ -88,14 +90,17 @@
<listOptionValue value="${INSTALLROOT_F2802x}/common/lib"/>
<listOptionValue value="${INSTALLROOT_IQMATH}/lib"/>
</option>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ISSUE_REMARKS.948575730" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ISSUE_REMARKS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.VERBOSE_DIAGNOSTICS.534455233" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.VERBOSE_DIAGNOSTICS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.XML_LINK_INFO.2111771756" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.XML_LINK_INFO" value="&quot;Example_2802xLEDBlink_linkInfo.xml&quot;" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ENTRY_POINT.286189493" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ENTRY_POINT" value="code_start" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ISSUE_REMARKS.1110979405" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ISSUE_REMARKS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.VERBOSE_DIAGNOSTICS.113431991" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.VERBOSE_DIAGNOSTICS" value="true" valueType="boolean"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.XML_LINK_INFO.460865356" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.XML_LINK_INFO" value="&quot;Example_2802xLEDBlink_linkInfo.xml&quot;" valueType="string"/>
<option id="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ENTRY_POINT.1851948553" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.linkerID.ENTRY_POINT" value="code_start" valueType="string"/>
</tool>
<tool id="com.ti.ccstudio.buildDefinitions.C2000_25.11.hex.280593134" name="C2000 Hex Utility" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.hex"/>
<tool id="com.ti.ccstudio.buildDefinitions.C2000_25.11.hex.166197469" name="C2000 Hex Utility" superClass="com.ti.ccstudio.buildDefinitions.C2000_25.11.hex"/>
</toolChain>
</folderInfo>
<sourceEntries>
<entry excluding="F2802x_generic_ram.cmd" flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="sourcePath" name=""/>
</sourceEntries>
</configuration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>

8
.gitignore vendored
View File

@ -5,3 +5,11 @@ Release/
*.d_raw
*.out
*.map
# IDE-generated, machine-specific (hardcoded local tool paths)
.mcp.json
CLAUDE.md
# Outillage agent (local, non partage)
.claude/
.codex/

View File

@ -2,5 +2,7 @@ eclipse.preferences.version=1
encoding//Debug/makefile=UTF-8
encoding//Debug/objects.mk=UTF-8
encoding//Debug/sources.mk=UTF-8
encoding//Debug/src/subdir_rules.mk=UTF-8
encoding//Debug/src/subdir_vars.mk=UTF-8
encoding//Debug/subdir_rules.mk=UTF-8
encoding//Debug/subdir_vars.mk=UTF-8

16
.theia/launch.json Normal file
View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"version": "0.2.0",
"configurations": [
{
"name": "TMS320F28027-dualboost",
"type": "ccs-debug",
"request": "launch",
"projectInfo": {
"name": "TMS320F28027-dualboost",
"resourceId": "/TMS320F28027-dualboost"
}
}
]
}

View File

@ -1,265 +0,0 @@
//###########################################################################
//
// FILE: Example_2802xLedBlink.c
//
// TITLE: f2802x LED Blink Getting Started Program.
//
// ASSUMPTIONS:
//
// This program requires the f2802x header files.
// As supplied, this project is configured for "boot to SARAM"
// operation. The 2802x Boot Mode table is shown below.
//
// $Boot_Table
// While an emulator is connected to your device, the TRSTn pin = 1,
// which sets the device into EMU_BOOT boot mode. In this mode, the
// peripheral boot modes are as follows:
//
// Boot Mode: EMU_KEY EMU_BMODE
// (0xD00) (0xD01)
// ---------------------------------------
// Wait !=0x55AA X
// I/O 0x55AA 0x0000
// SCI 0x55AA 0x0001
// Wait 0x55AA 0x0002
// Get_Mode 0x55AA 0x0003
// SPI 0x55AA 0x0004
// I2C 0x55AA 0x0005
// OTP 0x55AA 0x0006
// Wait 0x55AA 0x0007
// Wait 0x55AA 0x0008
// SARAM 0x55AA 0x000A <-- "Boot to SARAM"
// Flash 0x55AA 0x000B
// Wait 0x55AA Other
//
// Write EMU_KEY to 0xD00 and EMU_BMODE to 0xD01 via the debugger
// according to the Boot Mode Table above. Build/Load project,
// Reset the device, and Run example
//
// $End_Boot_Table
//
// DESCRIPTION:
//
// This example configures CPU Timer0 for a 500 msec period, and toggles the
// GPIO34 LED once per interrupt. For testing purposes, this example
// also increments a counter each time the timer asserts an interrupt.
//
// Watch Variables:
// CpuTimer0.InterruptCount
//
// Monitor the GPIO34 LED blink on (for 500 msec) and off (for 500 msec)
// on the 2802x control card.
//
//###########################################################################
// $TI Release: $
// $Release Date: $
// $Copyright:
// Copyright (C) 2009-2026 Texas Instruments Incorporated - http://www.ti.com/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// Neither the name of Texas Instruments Incorporated nor the names of
// its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// $
//###########################################################################
//
// Included Files
//
#include "DSP28x_Project.h" // Device Headerfile and Examples Include File
//
// Function Prototypes
//
__interrupt void cpu_timer0_isr(void);
//
// Main
//
void main(void)
{
//
// WARNING: Always ensure you call memcpy before running any functions from
// RAM InitSysCtrl includes a call to a RAM based function and without a
// call to memcpy first, the processor will go "into the weeds"
//
#ifdef _FLASH
memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, (size_t)&RamfuncsLoadSize);
#endif
//
// Step 1. Initialize System Control:
// PLL, WatchDog, enable Peripheral Clocks
// This example function is found in the f2802x_SysCtrl.c file.
//
InitSysCtrl();
//
// Step 2. Initialize GPIO:
// This example function is found in the f2802x_Gpio.c file and
// illustrates how to set the GPIO to it's default state.
//
//InitGpio(); // Skipped for this example
//
// Step 3. Clear all interrupts and initialize PIE vector table:
// Disable CPU interrupts
//
DINT;
//
// Initialize the PIE control registers to their default state.
// The default state is all PIE interrupts disabled and flags
// are cleared.
// This function is found in the f2802x_PieCtrl.c file.
//
InitPieCtrl();
//
// Disable CPU interrupts and clear all CPU interrupt flags
//
IER = 0x0000;
IFR = 0x0000;
//
// Initialize the PIE vector table with pointers to the shell Interrupt
// Service Routines (ISR).
// This will populate the entire table, even if the interrupt
// is not used in this example. This is useful for debug purposes.
// The shell ISR routines are found in f2802x_DefaultIsr.c.
// This function is found in f2802x_PieVect.c.
//
InitPieVectTable();
//
// Interrupts that are used in this example are re-mapped to
// ISR functions found within this file.
//
EALLOW; // This is needed to write to EALLOW protected registers
PieVectTable.TINT0 = &cpu_timer0_isr;
EDIS; // This is needed to disable write to EALLOW protected registers
//
// Step 4. Initialize the Device Peripheral. This function can be
// found in f2802x_CpuTimers.c
//
//
// For this example, only initialize the Cpu Timers
//
InitCpuTimers();
//
// Configure CPU-Timer 0 to interrupt every 500 milliseconds:
// 60MHz CPU Freq, 50 millisecond Period (in uSeconds)
//
ConfigCpuTimer(&CpuTimer0, 60, 500000);
//
// To ensure precise timing, use write-only instructions to write to the
// entire register. Therefore, if any of the configuration bits are changed
// in ConfigCpuTimer and InitCpuTimers (in f2802x_CpuTimers.h), the
// below settings must also be updated.
//
//
// Use write-only instruction to set TSS bit = 0
//
CpuTimer0Regs.TCR.all = 0x4001;
//
// Step 5. User specific code, enable interrupts
//
//
// Configure GPIO12 (blue LED) and GPIO33 (red LED) as GPIO output pins
//
EALLOW;
GpioCtrlRegs.GPAMUX1.bit.GPIO12 = 0;
GpioCtrlRegs.GPADIR.bit.GPIO12 = 1;
GpioCtrlRegs.GPBMUX1.bit.GPIO33 = 0;
GpioCtrlRegs.GPBDIR.bit.GPIO33 = 1;
GpioDataRegs.GPACLEAR.bit.GPIO12 = 1;
GpioDataRegs.GPBCLEAR.bit.GPIO33 = 1;
EDIS;
//
// Enable CPU INT1 which is connected to CPU-Timer 0
//
IER |= M_INT1;
//
// Enable TINT0 in the PIE: Group 1 interrupt 7
//
PieCtrlRegs.PIEIER1.bit.INTx7 = 1;
//
// Enable global Interrupts and higher priority real-time debug events
//
EINT; // Enable Global interrupt INTM
ERTM; // Enable Global realtime interrupt DBGM
//
// Step 6. IDLE loop. Just sit and loop forever (optional)
//
for(;;);
}
//
// cpu_timer0_isr -
//
__interrupt void
cpu_timer0_isr(void)
{
static Uint16 ledState = 0;
CpuTimer0.InterruptCount++;
//
// Alternate between the blue LED (GPIO12) and the red LED (GPIO33) every 500 milliseconds
//
if (ledState == 0)
{
GpioDataRegs.GPASET.bit.GPIO12 = 1;
GpioDataRegs.GPBCLEAR.bit.GPIO33 = 1;
}
else
{
GpioDataRegs.GPACLEAR.bit.GPIO12 = 1;
GpioDataRegs.GPBSET.bit.GPIO33 = 1;
}
ledState = (ledState + 1) % 2;
//
// Acknowledge this interrupt to receive more interrupts from group 1
//
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}
//
// End of File
//

223
F2802x_generic_flash.cmd Normal file
View File

@ -0,0 +1,223 @@
/*
//###########################################################################
//
// FILE: F2802x_generic_flash.cmd
//
// TITLE: Generic Linker Command File for f2802x devices
//
//###########################################################################
// $TI Release: $
// $Release Date: $
// $Copyright:
// Copyright (C) 2009-2026 Texas Instruments Incorporated - http://www.ti.com/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// Neither the name of Texas Instruments Incorporated nor the names of
// its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// $
//###########################################################################
*/
/* ======================================================
// For Code Composer Studio V2.2 and later
// ---------------------------------------
// In addition to this memory linker command file,
// add the header linker command file directly to the project.
// The header linker command file is required to link the
// peripheral structures to the proper locations within
// the memory map.
//
// The header linker files are found in <base>\headers\cmd
//
// For BIOS applications add: F2802x_Headers_BIOS.cmd
// For nonBIOS applications add: F2802x_Headers_nonBIOS.cmd
========================================================= */
/* ======================================================
// For Code Composer Studio prior to V2.2
// --------------------------------------
// 1) Use one of the following -l statements to include the
// header linker command file in the project. The header linker
// file is required to link the peripheral structures to the proper
// locations within the memory map */
/* Uncomment this line to include file only for non-BIOS applications */
/* -l F2802x_Headers_nonBIOS.cmd */
/* Uncomment this line to include file only for BIOS applications */
/* -l F2802x_Headers_BIOS.cmd */
/* 2) In your project add the path to <base>\headers\cmd to the
library search path under project->build options, linker tab,
library search path (-i).
/*========================================================= */
/* Define the memory block start/length for the F2802x
PAGE 0 will be used to organize program sections
PAGE 1 will be used to organize data sections
Notes:
Memory blocks on F2802x are uniform (ie same
physical memory) in both PAGE 0 and PAGE 1.
That is the same memory region should not be
defined for both PAGE 0 and PAGE 1.
Doing so will result in corruption of program
and/or data.
Contiguous SARAM memory blocks or flash sectors can be
be combined if required to create a larger memory block.
*/
MEMORY
{
PAGE 0: /* Program Memory */
/* Memory (RAM/FLASH/OTP) blocks can be moved to PAGE1 for data allocation */
RAMM0 : origin = 0x000050, length = 0x0003B0 /* on-chip RAM block M0 */
OTP : origin = 0x3D7800, length = 0x000400 /* on-chip OTP */
/* Les 4 secteurs flash du F28027 (32K mots au total, cf. common/cmd/F28027.cmd
de C2000Ware). Le fichier "generic" d'origine ne declarait que le secteur A
(~8K), ce qui faisait deborder le link des que le code grossissait. */
FLASHD : origin = 0x3F0000, length = 0x002000 /* on-chip FLASH */
FLASHC : origin = 0x3F2000, length = 0x002000 /* on-chip FLASH */
FLASHB : origin = 0x3F4000, length = 0x002000 /* on-chip FLASH */
FLASHA : origin = 0x3F6000, length = 0x001F80 /* on-chip FLASH */
CSM_RSVD : origin = 0x3F7F80, length = 0x000076 /* Part of FLASHA. Program with all 0x0000 when CSM is in use. */
BEGIN : origin = 0x3F7FF6, length = 0x000002 /* Part of FLASHA. Used for "boot to Flash" bootloader mode. */
CSM_PWL_P0 : origin = 0x3F7FF8, length = 0x000008 /* Part of FLASHA. CSM password locations in FLASHA */
IQTABLES : origin = 0x3FE000, length = 0x000B50 /* IQ Math Tables in Boot ROM */
IQTABLES2 : origin = 0x3FEB50, length = 0x00008C /* IQ Math Tables in Boot ROM */
IQTABLES3 : origin = 0x3FEBDC, length = 0x0000AA /* IQ Math Tables in Boot ROM */
ROM : origin = 0x3FF27C, length = 0x000D44 /* Boot ROM */
RESET : origin = 0x3FFFC0, length = 0x000002 /* part of boot ROM */
VECTORS : origin = 0x3FFFC2, length = 0x00003E /* part of boot ROM */
PAGE 1 : /* Data Memory */
/* Memory (RAM/FLASH/OTP) blocks can be moved to PAGE0 for program allocation */
/* Registers remain on PAGE1 */
BOOT_RSVD : origin = 0x000000, length = 0x000050 /* Part of M0, BOOT rom will use this for stack */
RAMM1 : origin = 0x000400, length = 0x000400 /* on-chip RAM block M1 */
RAML0 : origin = 0x008000, length = 0x001000 /* on-chip RAM block L0 (4K mots sur F28027) */
}
/* Allocate sections to memory blocks.
Note:
codestart user defined section in DSP28_CodeStartBranch.asm used to redirect code
execution when booting to flash
ramfuncs user defined section to store functions that will be copied from Flash into RAM
*/
SECTIONS
{
/* Allocate program areas: */
codestart : > BEGIN PAGE = 0
ramfuncs : LOAD = FLASHA,
RUN = RAMM0,
LOAD_START(_RamfuncsLoadStart),
LOAD_SIZE(_RamfuncsLoadSize),
RUN_START(_RamfuncsRunStart),
PAGE = 0
.cinit : > FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0
.pinit : > FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0
.text : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0
csmpasswds : > CSM_PWL_P0, PAGE = 0
csm_rsvd : > CSM_RSVD, PAGE = 0
/* Allocate uninitalized data sections: */
/* .ebss place en RAML0 EN PREMIER (et non RAMM1) : sinon les globales se
retrouvent collees juste apres .stack, et un debordement de pile les
ecrase silencieusement au lieu de tomber dans du vide. RAMM1 est ainsi
reserve a la pile seule. */
.stack : > RAMM1, PAGE = 1
.ebss : >> RAML0 | RAMM1, PAGE = 1
.esysmem : >> RAML0 | RAMM1, PAGE = 1
/* Initalized sections go in Flash */
/* For SDFlash to program these, they must be allocated to page 0 */
.econst : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0
.switch : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0
/* Allocate IQ math areas: */
IQmath : >> FLASHA | FLASHB | FLASHC | FLASHD, PAGE = 0 /* Math Code */
IQmathTables : > IQTABLES, PAGE = 0, TYPE = NOLOAD
/* Uncomment the section below if calling the IQNexp() or IQexp()
functions from the IQMath.lib library in order to utilize the
relevant IQ Math table in Boot ROM (This saves space and Boot ROM
is 1 wait-state). If this section is not uncommented, IQmathTables2
will be loaded into other memory (SARAM, Flash, etc.) and will take
up space, but 0 wait-state is possible.
*/
/*
IQmathTables2 : > IQTABLES2, PAGE = 0, TYPE = NOLOAD
{
IQmath.lib<IQNexpTable.obj> (IQmathTablesRam)
}
*/
/* Uncomment the section below if calling the IQNasin() or IQasin()
functions from the IQMath.lib library in order to utilize the
relevant IQ Math table in Boot ROM (This saves space and Boot ROM
is 1 wait-state). If this section is not uncommented, IQmathTables2
will be loaded into other memory (SARAM, Flash, etc.) and will take
up space, but 0 wait-state is possible.
*/
/*
IQmathTables3 : > IQTABLES3, PAGE = 0, TYPE = NOLOAD
{
IQmath.lib<IQNasinTable.obj> (IQmathTablesRam)
}
*/
/* .reset is a standard section used by the compiler. It contains the */
/* the address of the start of _c_int00 for C Code. /*
/* When using the boot ROM this section and the CPU vector */
/* table is not needed. Thus the default type is set here to */
/* DSECT */
.reset : > RESET, PAGE = 0, TYPE = DSECT
vectors : > VECTORS PAGE = 0, TYPE = DSECT
}
/*
//===========================================================================
// End of file.
//===========================================================================
*/

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

@ -0,0 +1,203 @@
# 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.
> **Mise a jour** : coefficients de calibration remplaces par les valeurs
> mesurees sur la carte reelle (voir `mesure-cartepuissance.md`), et ajout
> du champ `FAULT`.
## Cablage
| Signal | ESP32-C3 (config.h) | TMS320 |
|---|---|---|
| ESP32 TX -> TMS320 RX | GPIO7 | GPIO28, broche 48 |
| ESP32 RX <- TMS320 TX | GPIO6 | GPIO29, broche 1 |
| GND commun | GND | GND |
Niveaux logiques 3,3 V des deux cotes -- **verifie, carte CPU validee**.
## Parametres UART
- Vitesse : **57600 bauds**
- Format : **8N1** (8 bits de donnees, pas de parite, 1 bit de stop)
- Cote ESP32 : `Serial1`, defini dans `TmsLink::begin()`
## Format des trames
Protocole ASCII texte, inspire du format NMEA, termine 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=200000,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,FAULT=0*XX
```
Champs (`TAG=valeur`, separes par des virgules, ordre libre) :
| Tag | Unite | Description |
|---|---|---|
| `FREQ1` | Hz | Frequence de decoupage etage 1 (200 kHz) |
| `FREQ2` | Hz | Frequence de decoupage etage 2 (100 kHz) |
| `DUTY1` | % | Rapport cyclique etage 1 |
| `DUTY2` | % | Rapport cyclique etage 2 |
| `VIN` | V | Tension d'entree |
| `IIN` | A | Courant d'entree |
| `V1` | V | Tension intermediaire (sortie etage 1) |
| `I1` | A | Courant shunt MOSFET etage 1 |
| `T1` | degC | Temperature NTC etage 1 |
| `VOUT` | V | Tension de sortie HT |
| `I2` | A | Courant shunt MOSFET etage 2 |
| `T2` | degC | Temperature NTC etage 2 |
| `IOUT` | A | Courant de sortie |
| `FAULT` | code 0-5 | Defaut courant (voir ci-dessous) |
### Codes `FAULT`
| Code | Signification |
|---|---|
| `0` | Aucun defaut |
| `1` | EMUSTOP -- le debogueur a arrete le CPU |
| `2` | Surintensite I1 (etage 1) |
| `3` | Surintensite I2 (etage 2) |
| `4` | Temperature critique T1 (> 80 degC) |
| `5` | Temperature critique T2 (> 80 degC) |
**Priorite quand plusieurs defauts coexistent : 2 > 3 > 4 > 5 > 1.**
EMUSTOP a donc la priorite la plus BASSE malgre son numero : en
developpement il se declenche a chaque halte du debogueur et ne doit
jamais masquer une surintensite reelle.
**Il n'existe volontairement pas de code "court-circuit".** Un
court-circuit franchit le meme comparateur et le meme seuil qu'une
surintensite ; le firmware ne dispose d'aucune information permettant de
les distinguer. Les codes 2 et 3 couvrent les deux cas.
#### Verrouillage : pas d'effacement par le protocole
Contrairement a ce que supposait une version precedente de ce document,
**un defaut ne s'efface JAMAIS automatiquement**, ni a la disparition de
sa cause, ni par commande UART. Il n'existe aucun tag `$C` permettant de
l'acquitter.
Le seul moyen de repartir est de **couper puis retablir l'alimentation**.
C'est un choix deliberé (PROMPT §8) : un defaut de puissance doit exiger
une intervention humaine et un examen de la carte.
Consequence pour l'ESP32 : tant que le TMS320 emet un `FAULT != 0`, il
continuera de l'emettre indefiniment. L'IHM doit donc afficher ce code de
maniere persistante, sans attendre un retour spontane a 0.
> Rappel (PROMPT §2) : la protection reelle -- comparateurs + Trip Zone
> materiel -- ne depend jamais de cette liaison UART. `FAULT` n'est qu'un
> report d'information vers l'IHM, envoye apres coup ; la coupure a deja
> eu lieu en materiel avant meme que la trame ne soit construite.
#### Icone danger : pilotee par VOUT, jamais par FAULT
L'avertissement haute tension doit dependre **uniquement de `VOUT`**, et
rester affiche independamment de l'etat de `FAULT`.
Un defaut ne decharge pas le condensateur de sortie. Apres une coupure,
`HV_EN` isole la charge mais les 400 V restent presents. Faire disparaitre
l'icone danger au moment d'un defaut reviendrait a rassurer l'operateur
precisement quand le risque est maximal.
La carte de puissance comporte deux dispositifs complementaires :
- un **bleeder de 1 MOhm avec LED rouge en serie**, indication passive
qui reste valable alimentation coupee (~42 s pour passer sous 50 V) ;
- un **circuit de decharge actif** (MOSFET + 47 kOhm, ~2 s de 500 V a
50 V), commande par le TMS320 sur GPIO5 / broche 40, **a logique
inversee** : il conduit des que la commande disparait, donc au reset,
en cas de plantage et alimentation coupee.
**Frequence d'envoi : toutes les 300 ms.** L'ESP32 considere la liaison
perdue (`TMS:KO`) si aucune trame `$T` valide n'est recue depuis plus de
**2 secondes**. Le TMS320 applique le meme timeout dans l'autre sens.
Tous les champs sont optionnels individuellement (un champ absent garde
sa derniere valeur cote ESP32) ; en pratique le TMS320 les envoie tous.
Si une trame devait depasser la longueur maximale, le firmware abandonne
des champs entiers plutot que d'en tronquer un.
### Calibration des mesures analogiques (cote TMS320)
Coefficients appliques sur la lecture ADC (`Vadc`, en volts) pour obtenir
la valeur physique. **Valeurs mesurees sur la carte reelle**, sauf mention
contraire -- voir `mesure-cartepuissance.md` pour la tracabilite.
| Grandeur | Pleine echelle (3,3 V) | Relation | Statut |
|---|---|---|---|
| `VIN` | 36,7 V | `VIN = Vadc x 11,11` | mesure |
| `IIN` | 3,97 A | `IIN = Vadc x 1,2019` | mesure (1 point) |
| `V1` | 103,1 V (usage limite a 50 V) | `V1 = Vadc x 31,25` | mesure |
| `VOUT` | 600 V | `VOUT = Vadc x 181,82` | mesure |
| `IOUT` | 50 mA (a confirmer) | `IOUT = Vadc x 0,016667` | **a mesurer** |
| `I1` | 5,15 A | `I1 = (Vadc - 0,0473) / 0,631` | **provisoire (MCP6001)** |
| `I2` | 5,12 A | `I2 = (Vadc - 0,0340) / 0,637` | **provisoire (MCP6001)** |
**Notes :**
- **I1/I2** : le gain differe d'environ 1 % entre les deux voies et chaque
voie a un offset non nul a courant nul (47,3 / 34,0 mV). Ne jamais
utiliser un gain unique pour les deux. Ces valeurs sont mesurees avec
les **MCP6001 de banc** ; le composant definitif est le TLV9151. Le gain
devrait rester valable (fixe par le reseau de resistances) mais
**l'offset devra etre integralement remesure**.
- **MCP6001, limite de bande passante** : 1 MHz de produit gain-bande a un
gain de ~31 plafonne vers 32 kHz. Pour proteger un decoupage a 200 kHz,
les 30 ns du comparateur ne servent a rien si l'ampli en amont met des
dizaines de microsecondes. **La protection rapide n'est pas reellement
rapide tant que le MCP6001 est en place.**
- **NTC** : montage 3,3 V -- NTC -- R_fixe -- 0 V, mesure au point milieu.
La NTC etant cote 3,3 V, la relation est
`R_ntc = R_fixe x (VREF - Vadc) / Vadc`. Les thermistances sont a 5 mm
des MOSFET : elles mesurent le cuivre, pas la jonction, avec un ecart
statique et un retard thermique notables. Le seuil de 80 degC est donc
**provisoire**, a recaler sur la temperature de boitier en charge.
- **IOUT** : aucune mesure reelle a ce jour.
### 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 depuis l'IHM. Le TMS320 applique le dernier etat recu et
valide. Les trois tags doivent etre presents, sinon la trame est ignoree.
## Calcul du checksum
XOR de tous les octets entre `$` et `*` (exclus), en hexadecimal majuscule
sur 2 chiffres (`%02X`).
Pour la reception : meme calcul, comparaison au checksum recu, trame
ignoree silencieusement si ca ne correspond pas.
> **Piege rencontre cote TMS320, a ne pas reproduire.** Decoder le
> checksum avec `strtol` sur un buffer non termine par `\0` fait lire
> au-dela de la trame. Et sur C28x, `uint8_t` fait 16 bits (pas
> d'adressage par octet) : un cast `(uint8_t)` ne tronque donc rien, et un
> `"7D"` suivi d'un residu `"7D"` donne 0x7D7D, conserve tel quel. Toute
> trame etait rejetee. Decoder **exactement deux chiffres**, de maniere
> bornee.
## Points de vigilance
- Checksum en **majuscules** sur 2 chiffres, sinon rejet silencieux cote
ESP32 -- la telemetrie n'avance simplement plus.
- Terminer par `\n` uniquement (`\r` tolere).
- Longueur de ligne maximale : **200 caracteres** (`g_lineBuf[200]` cote
ESP32, `UART_LINE_MAX` cote TMS320).

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.

194
docs/TMS-pin-v2.txt Normal file
View File

@ -0,0 +1,194 @@
TMS320F28027 — LQFP48 (PT) — Brochage
Reference : SPRS523P, tableau 7.2.1 "Signal Descriptions" (faisant foi)
================================================================
V0.01 — ETAT ACTUEL, ANNOTE
================================================================
Pin Signal actuel Fonction reelle du silicium Verdict
--- ----------------- ------------------------------ ----------------
01 UART-TX GPIO29/SCITXDA/SCLA/TZ3 OK
02 JTAG-TRST TRST OK
03 RST XRS OK
04 NC ADCINA6/AIO6 OK
05 I-shunt1-adc ADCINA4/COMP2A/AIO4 DOUBLON (cf. 13)
06 NC ADCINA7 OK
07 I-HV-adc ADCINA3 OK
08 I-Batt-adc ADCINA1 OK
09 V-HV-adc ADCINA2/COMP1A/AIO2 A DEPLACER (E1)
10 V-Batt-adc ADCINA0/VREFHI OK
11 10uF VDDA + 3.3V (E3)
12 GND VSSA/VREFLO OK
13 I-shunt1-adc ADCINB1 DOUBLON (cf. 05)
14 V-inter-adc ADCINB2/COMP1B/AIO10 A DEPLACER (E1)
15 I-shunt2-adc ADCINB3 DOUBLON (cf. 16)
16 I-shunt2-adc ADCINB4/COMP2B/AIO12 DOUBLON (cf. 15)
17 Temp1 ADCINB6/AIO14 OK
18 Temp2 ADCINB7 OK
19 NC GPIO34/COMP2OUT OK
20 JTAG-TDI GPIO35 (TDI) OK
21 JTAG-TMS GPIO36 (TMS) OK
22 JTAG-TDO GPIO37 (TDO) OK
23 JTAG-CK GPIO38/XCLKIN (TCK) OK
24 NC GPIO18 OK
25 NC GPIO19 OK
26 Stage2-En GPIO17/SPISOMIA/TZ3 OK
27 Stage1-En GPIO16/SPISIMOA/TZ2 OK
28 Stage1-default GPIO1/EPWM1B/COMP1OUT OK
29 Stage1-PWM GPIO0/EPWM1A OK
30 NC TEST OK
31 Stage2-PWM GPIO32/SDAA/EPWMSYNCI ERREUR (E2)
32 10uF VDD OK
33 GND VSS OK
34 GND VREGENZ OK
35 3.3V VDDIO + 10uF
36 RED LED GPIO33/SCLA/EPWMSYNCO OK
37 HV-EN GPIO2/EPWM2A ERREUR (E2)
38 Stage2-default GPIO3/EPWM2B/COMP2OUT OK
39 NC GPIO4/EPWM3A OK
40 NC GPIO5/EPWM3B/ECAP1 OK
41 NC GPIO6/EPWM4A OK
42 NC GPIO7/EPWM4B/SCIRXDA OK
43 10uF VDD OK
44 GND VSS OK
45 GND X1 OK
46 NC X2 OK
47 BLUE LED GPIO12/TZ1/SCITXDA OK
48 UART-RX GPIO28/SCIRXDA/SDAA/TZ2 OK
----------------------------------------------------------------
ERREURS RESTANTES
----------------------------------------------------------------
[E1] SHUNTS SUR LES MAUVAISES ENTREES COMPARATEUR --- BLOQUANT ---
COMPxA est cablee en dur sur l'entree NON INVERSEUSE (+) du
comparateur. COMPxB n'existe que comme alternative au DAC sur
l'entree inverseuse (-). Aucun mux ne permet de les echanger.
Pour garder un seuil programmable par le DAC interne, les shunts
doivent etre sur les entrees A :
COMP1A = pin 09 -> sortie COMP1OUT = pin 28 (Stage1-default)
COMP2A = pin 05 -> sortie COMP2OUT = pin 38 (Stage2-default)
V-HV-adc et V-inter-adc migrent vers 14 et 16 (entrees ADC simples).
[E2] STAGE2-PWM ET HV-EN INVERSES --- BLOQUANT ---
31 = GPIO32, ce n'est pas une sortie ePWM -> HV-EN
37 = GPIO2/EPWM2A, sortie PWM etage 2 (et canal HRPWM) -> Stage2-PWM
[E3] ALIMENTATIONS A COMPLETER
11 = VDDA : 3,3 V + 2,2 uF au plus pres de la broche
35 = VDDIO : 3,3 V + condensateur de decouplage (actuellement absent)
32 et 43 = VDD : 1,2 uF minimum par broche (VREG interne actif)
[E4] DOUBLONS DE SIGNAUX
I-shunt1-adc present sur 05 et 13
I-shunt2-adc present sur 15 et 16
Le fichier melange l'affectation d'origine et une correction partielle.
[E5] TYPO
22 : "JTAG-TMO" -> TDO
[E6] PULLDOWN TRST MANQUANT
02 = TRST, actif haut, doit rester bas en fonctionnement normal.
Resistance de pulldown externe obligatoire, 2,2 kohm recommandee.
================================================================
V0.2 — BROCHAGE CORRIGE
================================================================
Pin Signal Fonction MCU Note
--- ------------------ ------------------------------ ------------------
01 UART-TX GPIO29 / SCITXDA vers ESP32 GPIO6
02 JTAG-TRST TRST + pulldown 2,2k
03 RST XRS
04 NC ADCINA6 / AIO6
05 I-shunt2-adc ADCINA4 / COMP2A / AIO4 protection etage 2
06 NC ADCINA7
07 I-HV-adc ADCINA3 IOUT
08 I-Batt-adc ADCINA1 IIN
09 I-shunt1-adc ADCINA2 / COMP1A / AIO2 protection etage 1
10 V-Batt-adc ADCINA0 / VREFHI VIN
11 3.3V + 2.2uF VDDA alim analogique
12 GND VSSA / VREFLO
13 NC ADCINB1 libre
14 V-HV-adc ADCINB2 / AIO10 VOUT
15 NC ADCINB3 libre
16 V-inter-adc ADCINB4 / AIO12 V1
17 Temp1 ADCINB6 / AIO14 NTC etage 1
18 Temp2 ADCINB7 NTC etage 2
19 NC GPIO34 / COMP2OUT
20 JTAG-TDI GPIO35
21 JTAG-TMS GPIO36
22 JTAG-TDO GPIO37 etait "TMO"
23 JTAG-TCK GPIO38 / XCLKIN
24 NC GPIO18
25 NC GPIO19
26 Stage2-EN GPIO17 vers porte ET IC9
27 Stage1-EN GPIO16 vers porte ET IC8
28 Stage1-default GPIO1 / COMP1OUT vers porte ET IC8
29 Stage1-PWM GPIO0 / EPWM1A HRPWM possible
30 NC TEST LAISSER NON CONNECTE
31 HV-EN GPIO32 etait Stage2-PWM
32 VDD + 1.2uF VDD
33 GND VSS
34 GND VREGENZ active le VREG 1,8V
35 3.3V + decouplage VDDIO condo a ajouter
36 RED LED GPIO33
37 Stage2-PWM GPIO2 / EPWM2A etait HV-EN
38 Stage2-default GPIO3 / COMP2OUT vers porte ET IC9
39 NC GPIO4 / EPWM3A
40 NC GPIO5 / EPWM3B
41 NC GPIO6 / EPWM4A
42 NC GPIO7 / EPWM4B
43 VDD + 1.2uF VDD
44 GND VSS
45 GND X1 oscillateur interne
46 NC X2 oscillateur interne
47 BLUE LED GPIO12 TZ1 non disponible
48 UART-RX GPIO28 / SCIRXDA depuis ESP32 GPIO7
----------------------------------------------------------------
MODIFICATIONS V0.01 -> V0.2
----------------------------------------------------------------
05 I-shunt1-adc -> I-shunt2-adc (vers COMP2A)
09 V-HV-adc -> I-shunt1-adc (vers COMP1A)
13 I-shunt1-adc -> NC
14 V-inter-adc -> V-HV-adc
15 I-shunt2-adc -> NC
16 I-shunt2-adc -> V-inter-adc
31 Stage2-PWM -> HV-EN
35 ajout d'un condensateur de decouplage 10uF
37 HV-EN -> Stage2-PWM
Broches 34, 36, 43, 44 : inchangees, la V0.01 etait correcte.
----------------------------------------------------------------
POINTS MATERIELS HORS BROCHAGE
----------------------------------------------------------------
1. PULLDOWN SUR LES PORTES ET --- IMPORTANT ---
GPIO1 et GPIO3 (COMPxOUT) sont des broches a fonction PWM : leurs
pullups internes ne sont PAS activees au reset, elles flottent.
GPIO16/17 ne sont pas des broches PWM : pullups activees -> etat haut.
A la mise sous tension, une entree de chaque porte ET flotte et l'autre
est a 1 -> sortie indeterminee, le driver peut etre valide avant que le
firmware ne demarre.
-> Pulldown 10 kohm sur les DEUX entrees de chaque porte ET (IC8, IC9)
-> Pulldown 10 kohm sur les lignes PWM vers les drivers
Le firmware ne peut rien pour la fenetre pre-boot.
2. Sequencement d'alimentation : alimenter VDD avant ou avec VDDIO
(VDD doit atteindre 0,7 V avant VDDIO) pour eviter un glitch sur les
sorties au demarrage.
3. Oscillateur interne : X1 (45) a la masse, X2 (46) non connecte.
Derive de 3,03 a 4,85 kHz/degC, d'ou le choix de 57600 bauds sur l'UART
au lieu de 115200.
4. TEST (30) : imperativement laisse non connecte (reserve TI).
5. Broches 13 et 15 libres (ADCINB1, ADCINB3) pour une mesure future.

View File

@ -0,0 +1,202 @@
# Mesures carte puissance — TMS320F28027 dual boost
Suivi des mesures de calibration effectuées sur la carte élévateur (à moitié
complète : composants de puissance et drivers MOSFET pas encore montés).
Ce document est la source pour remplir `calib.h` dans le firmware. Les
valeurs numériques finales à utiliser en code sont dans
`PROMPT-claude-code-TMS320.md` (section 5) ; ce fichier-ci garde la trace
des mesures brutes et de leur contexte.
---
## 1. Chaînes de mesure de tension
Méthode : alimentation de labo réglée à une tension connue, lecture de la
tension de sortie du pont diviseur au multimètre (avant l'ADC).
| Voie | Point de mesure | Coefficient mesuré (Vadc/Vréel) | Gain (V/V) | Pleine échelle (3,3 V) | Statut |
|---|---|---|---|---|---|
| VIN (V_batt) | pin 10 | 0,09 | 11,11 | 36,7 V | ✅ mesuré |
| V1 (V_inter) | pin 16 | 0,032 | 31,25 | 103,1 V | ✅ mesuré |
| VOUT (V_HT) | pin 14 | 0,0055 | 181,82 | 600 V | ✅ mesuré |
**Remarques :**
- VIN et V1 tombent exactement sur les valeurs théoriques calculées à la
conception (11,11 et 31,25) — le pont a été monté avec les bonnes valeurs
de résistances.
- VOUT diffère de 1,8 % de la valeur théorique (178,6 → 181,82), imputable
à la tolérance des résistances réelles. Sans conséquence : la pleine
échelle réelle (600 V) reste au-dessus de l'objectif (400 V max), avec
même un peu plus de marge que prévu.
- V1 max visé = 50 V → seulement ~1,6 V sur l'ADC (48 % de la plage), pas
de saturation, résolution confirmée suffisante.
---
## 2. Chaîne de mesure de courant d'entrée (IIN)
Méthode : courant imposé au primaire, lecture de la tension de sortie de
l'ampli de mesure au multimètre.
| Point | Valeur |
|---|---|
| Mesure | 250 mA → 0,208 V en sortie ampli |
| Gain calculé | 0,832 V/A |
| Gain inverse | 1,2019 A/V |
| Pleine échelle (3,3 V) | 3,97 A |
Un seul point mesuré (pas de vérification de linéarité multi-points comme
pour les shunts MOSFET). Cohérent avec le courant d'entrée nominal attendu
(~590 mA pour ~5 W de charge côté HT), bonne marge avant saturation.
**Reste à faire** : idéalement un second point de mesure pour confirmer la
linéarité et détecter un éventuel offset (mesure faite à un seul courant,
donc le calcul suppose une caractéristique passant par zéro — à vérifier).
---
## 3. Chaînes de mesure de courant shunt MOSFET (I1, I2) — protection rapide
⚠️ **Mesures faites avec le MCP6001 monté provisoirement sur le banc de
test.** Le composant final prévu est le TLV9151 (meilleur offset, meilleure
bande passante). Le gain (fixé par le réseau de résistances RF/RG) doit
rester valable après le remplacement ; l'offset devra être entièrement
remesuré.
Méthode : courant imposé dans chaque shunt (0,02 Ω), lecture au
multimètre de la tension aux bornes du shunt (`V_Rshunt`) et de la sortie
ampli-op avant ADC (`V_shunt_adc`), à quatre points (0, 0,5, 1, 2 A).
### Données brutes
**Shunt 1 (étage 1) :**
| I (A) | V_Rshunt (mV) | V_shunt1_adc (V) |
|---|---|---|
| 0 | — | 0,0473 |
| 0,5 | 10,3 | 0,367 |
| 1 | 20,38 | 0,682 |
| 2 | 40,9 | 1,31 |
**Shunt 2 (étage 2) :**
| I (A) | V_Rshunt (mV) | V_shunt2_adc (V) |
|---|---|---|
| 0 | — | 0,0340 |
| 0,5 | 10,48 | 0,357 |
| 1 | 20,58 | 0,674 |
| 2 | 40,76 | 1,31 |
### Résultats (régression linéaire sur les 4 points)
| Voie | Offset (I=0) | Gain (V/A) | Gain équivalent shunt×ampli (V/V) | Gain inverse (A/V) | Pleine échelle (3,3 V) |
|---|---|---|---|---|---|
| I1 (étage 1) | 47,3 mV | 0,631 | ≈ 31,5 | 1,585 | 5,15 A |
| I2 (étage 2) | 34,0 mV | 0,637 | ≈ 31,8 | 1,570 | 5,12 A |
**Formules de conversion (provisoires, MCP6001) :**
```
I1 = (Vadc 0,0473) / 0,631
I2 = (Vadc 0,0340) / 0,637
```
**Remarques :**
- Très bonne linéarité sur les 4 points des deux voies.
- Écart de gain d'environ 1 % entre les deux voies — attribuable à la
tolérance des résistances de gain, propre à chaque canal. Ne pas
utiliser un gain unique moyenné : garder deux jeux de constantes séparés
dans `calib.h`.
- Le gain mesuré (~31,5-31,8) est proche de la valeur visée à la
conception (31), mais **s'écarte de l'hypothèse initiale utilisée pour
calculer le seuil DAC** (qui supposait un gain de 30 pile). Voir §5 pour
la correction.
- Les deux offsets (34-47 mV) sont nettement plus élevés que le Vos
typique du MCP6001 seul à ce gain — probablement une combinaison de
l'offset ampli réel et d'un petit décalage du pont/de la référence.
Sans conséquence pratique : l'auto-zéro logiciel prévu au boot du
firmware absorbe cet écart automatiquement, quelle que soit son origine.
---
## 4. Chaîne de mesure de courant de sortie (IOUT)
**Non mesurée.** Reste à faire — même méthode que pour IIN (courant imposé
côté charge après le switch de sortie, lecture de la tension ampli-op).
Valeur théorique provisoire dans `calib.h` : 3,0 V @ 50 mA → gain
0,016667 A/V (à confirmer/remplacer par une mesure réelle).
---
## 5. Seuil de protection — recalcul avec les gains mesurés
Le calcul initial du code DAC (`DACVAL`) pour le seuil de 3 A avait été
fait en supposant un gain exactement égal à 30 (shunt 0,02 Ω × ampli-op
×30), donnant `Vadc = 1,8 V` à 3 A. **Cette hypothèse est fausse** : le gain
réel mesuré est ~31,5-31,8, donc en réalité `1,8 V` correspondait à un
seuil réel plus proche de 3,3 A que de 3 A. Non dangereux (la protection
aurait juste déclenché un peu tard), mais corrigé avant tout flashage.
**Valeurs recalculées avec les gains et offsets mesurés (§3) :**
```
Seuil 3 A, comparateur 1 (I-shunt1) :
Vadc = 0,0473 + 3 × 0,631 = 1,940 V
DACVAL = round(1,940 × 1023 / 3,3) = 601
Seuil 3 A, comparateur 2 (I-shunt2) :
Vadc = 0,0340 + 3 × 0,637 = 1,945 V
DACVAL = round(1,945 × 1023 / 3,3) = 603
```
**⚠️ Ces deux valeurs sont provisoires (MCP6001).** Elles devront être
recalculées avec les mêmes formules dès que le gain et l'offset seront
remesurés sur le TLV9151. Le firmware ne doit jamais figer `DACVAL` en
dur : le calculer par macro à partir de `SAFETY_ISHUNT_THRESHOLD_A` et des
constantes de gain/offset mesurées, pour que ce remplacement futur ne
touche qu'aux valeurs de `calib.h`, pas au code.
---
## 6. Ce qu'il reste à faire
### Mesures à compléter
- [ ] IOUT : gain réel (un point de mesure minimum, idéalement deux)
- [ ] IIN : second point de mesure pour vérifier la linéarité/l'offset
- [ ] Chaîne NTC (Temp1, Temp2) : vérifier la valeur réelle de la
résistance fixe des ponts diviseurs (10 kΩ supposé) et, si possible,
un point de mesure à température connue pour valider la formule
Steinhart-Hart simplifiée
- [ ] I1, I2 **à refaire intégralement** une fois le TLV9151 monté (gain à
confirmer proche de ~31,5-31,8, offset à mesurer — attendu très
inférieur aux 34-47 mV actuels)
### Matériel restant à monter sur la carte élévateur
- [ ] Composants de puissance (MOSFET IPD60R360, diode SiC STPSC406,
inductances) — carte actuellement à moitié complète
- [ ] Driver de grille : **le UCC27518/19 initialement prévu ne convient
pas** (seuils d'entrée CMOS proportionnels à VDD = 12 V, un signal
3,3 V du F28027 ne les franchira jamais). Remplacer par UCC27517
(seuils CMOS/TTL indépendants de VDD, même boîtier SOT-23-5, même
brochage) — vérifier la polarité (inverseur/non-inverseur) requise
selon le sens voulu sur EN
- [ ] TLV9151 : montage définitif à la place du MCP6001 de banc
### Firmware (TMS320F28027-dualboost, côté Claude Code / CCS)
- [ ] Reporter les gains/offsets mesurés (§1-3) dans `calib.h`, avec la
structure par voie recommandée (gain, offset, seuil) plutôt que des
constantes isolées
- [ ] Vérifier que le calcul de `DACVAL` est bien fait par macro à partir
des constantes, pas en dur
- [ ] Une fois le driver de grille corrigé et les composants de puissance
montés : test matériel de l'étape 2 du firmware (sécurité) avant
toute activation du PWM — critère : `TZFRC` force les sorties à 0,
un breakpoint coupe le PWM (TZ6/OSHT6), le flag ne se réarme pas
seul
### Points de configuration matérielle en attente de confirmation
- [ ] Fréquence de découpage par étage (100 ou 200 kHz)
- [ ] Polarité réelle des LED (bleue/rouge)
- [ ] Valeur d'inductance retenue par étage (47 ou 100 µH)
- [ ] État de la liaison JTAG / UART après le dernier diagnostic (mesure
du 1,8 V sur VDD broches 32/43, recâblage croisé RX/TX) — à
confirmer que tout fonctionne avant de reprendre les tests

BIN
docs/sprui09a.pdf Normal file

Binary file not shown.

BIN
docs/tms320f28027.pdf Normal file

Binary file not shown.

154
src/adc.c Normal file
View File

@ -0,0 +1,154 @@
#include "DSP28x_Project.h"
#include "adc.h"
#include "control.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;
// Note de mesure (instrumentation retiree) : la duree de cette ISR a ete
// mesuree au scope en basculant une broche a l'entree et a la sortie.
// Resultat apres correction des etats d'attente de la flash : 4,9 us pour
// une periode de 15 us, soit 33 % de charge CPU a 66,85 kHz.
// Ne PAS refaire cette mesure sur GPIO32 : c'est HV_EN, la commande de
// l'optocoupleur VOM1271. Utiliser une broche libre (GPIO4, 6 ou 7).
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++;
// Point d'appel cadence de la commande (PROMPT §6 etape 8 : dt constant,
// dans l'ISR ADC ou PWM). Une sequence ADC complete = une decision.
// Doit rester tres court : ni flottant, ni division, ni attente.
control_tick();
AdcRegs.ADCINTFLGCLR.bit.ADCINT1 = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}

46
src/adc.h Normal file
View File

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

28
src/bsp_clock.c Normal file
View File

@ -0,0 +1,28 @@
#include "DSP28x_Project.h"
#include "bsp_clock.h"
#include "calib.h"
void bsp_clock_init(void)
{
EALLOW;
SysCtrlRegs.PLLLOCKPRD = CLK_PLLLOCKPRD;
EDIS;
InitSysCtrl();
#ifdef _FLASH
// Etats d'attente de la flash. Au reset ils valent leur MAXIMUM
// (FBANKWAIT = 0x0F0F, soit RANDWAIT = PAGEWAIT = 15) : chaque acces
// coute alors 16 cycles au lieu des 3 necessaires a 60 MHz, et TOUT le
// code resident en flash tourne environ 4 fois trop lentement.
//
// Mesure avant/apres sur l'ISR ADC : c'etait le facteur dominant, bien
// devant l'optimisation du compilateur.
//
// InitFlash() DOIT s'executer depuis la RAM (elle reconfigure la flash
// sous ses propres pieds) : c'est tout l'objet du memcpy des ramfuncs
// fait en tete de main(), qui existait deja mais ne servait a rien
// puisque la fonction n'etait jamais appelee.
InitFlash();
#endif
}

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

186
src/bsp_gpio.c Normal file
View File

@ -0,0 +1,186 @@
#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 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;
// DISCHARGE = GPIO5 (broche 40)
GpioCtrlRegs.GPAMUX1.bit.GPIO5 = 0;
GpioCtrlRegs.GPADIR.bit.GPIO5 = 1;
EDIS;
// Etat sur avant toute autre configuration. La decharge est laissee
// ACTIVE : au demarrage, le condensateur de sortie peut encore etre
// charge par une session precedente, rien ne justifie de l'inhiber
// avant que la conversion ne soit reellement demandee.
stage_enable_set(STAGE_1, false);
stage_enable_set(STAGE_2, false);
hv_enable_set(false);
hv_discharge_set(true);
}
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 hv_discharge_set(bool active)
{
// Niveau BAS = decharge active (NPN bloque, grille du MOSFET libre).
// Voir bsp_gpio.h pour le detail de la polarite inversee.
if (active)
{
GpioDataRegs.GPACLEAR.bit.GPIO5 = 1;
}
else
{
GpioDataRegs.GPASET.bit.GPIO5 = 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
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;
}
}
}

73
src/bsp_gpio.h Normal file
View File

@ -0,0 +1,73 @@
#ifndef BSP_GPIO_H
#define BSP_GPIO_H
#include <stdbool.h>
typedef enum
{
LED_BLUE = 0,
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);
// 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);
// 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);
// DISCHARGE (GPIO5, broche 40) : circuit de decharge rapide du condensateur
// de sortie, sur la carte de puissance. 47 kOhm en serie avec un MOSFET,
// soit ~2 s pour passer de 500 V a 50 V (tau = 0,94 s).
//
// POLARITE INVERSEE, et c'est le coeur du montage. Le GPIO pilote un NPN
// qui court-circuite la grille du MOSFET :
// niveau HAUT -> NPN conduit -> grille a la masse -> MOSFET bloque
// -> decharge INHIBEE (fonctionnement normal)
// niveau BAS, ou broche non pilotee
// -> NPN bloque -> grille tiree a 12 V (chaine 3x1 MOhm
// depuis le +500 V, ecretee par Zener) -> MOSFET conduit
// -> decharge ACTIVE
//
// GPIO5 est une broche a fonction PWM : ses pull-ups ne sont PAS actives au
// reset (PROMPT §6 etape 3), elle flotte donc. La decharge est par
// consequent active au reset, pendant un plantage, et surtout ALIMENTATION
// COUPEE -- c'est-a-dire exactement le cas ou le firmware ne peut plus rien
// garantir et ou l'operateur croit la carte inoffensive.
void hv_discharge_set(bool active);
// 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

145
src/calib.h Normal file
View File

@ -0,0 +1,145 @@
#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 36)
// Point ouvert §9 du prompt : polarite a confirmer au premier test.
// Observation bring-up (carte 2) : GPIO33 haut = LED rouge visuellement allumee
// -> cablage actif-haut, pas actif-bas comme suppose initialement.
#define LED_ACTIVE_LOW 0
// Valeur de champ AIOMUX1 pour basculer une broche AIOx en mode analogique
#define GPIO_ANALOG_MODE 2
// =====================================================================
// Constantes de calibration des chaines de mesure.
// Source : docs/mesure-cartepuissance.md (mesures reelles sur la carte
// elevateur). Aucune de ces valeurs ne doit apparaitre ailleurs.
// =====================================================================
// ---- Chaines de tension (doc §1) ------------------------------------
// Gain = Vreel / Vadc, soit l'inverse du rapport du pont diviseur.
// VIN et V1 tombent sur les valeurs theoriques ; VOUT s'en ecarte de
// 1,8 % (tolerance des resistances), sans consequence : la pleine
// echelle reelle (600 V) reste au-dessus des 400 V vises.
#define MEAS_VIN_GAIN_V_PER_V 11.11f // broche 10, coef mesure 0,09
#define MEAS_V1_GAIN_V_PER_V 31.25f // broche 16, coef mesure 0,032
#define MEAS_VOUT_GAIN_V_PER_V 181.82f // broche 14, coef mesure 0,0055
// ---- Courant d'entree (doc §2) --------------------------------------
// Un seul point mesure (250 mA -> 0,208 V) : la caracteristique est
// SUPPOSEE passer par zero, offset non verifie. Un second point reste
// a faire.
#define MEAS_IIN_A_PER_V 1.2019f
// ---- Courant de sortie (doc §4) -------------------------------------
// NON MESURE : valeur theorique de conception (3,0 V @ 50 mA).
// A remplacer des qu'une mesure reelle sera disponible.
#define MEAS_IOUT_A_PER_V 0.016667f
// ---- Shunts MOSFET 0,02 ohm (doc §3) --------------------------------
// PROVISOIRE : mesures faites avec les MCP6001 de banc. Le gain, fixe
// par le reseau RF/RG, doit rester valable apres passage au TLV9151 ;
// l'OFFSET en revanche est domine par le Vos de l'ampli et devra etre
// ENTIEREMENT REMESURE. Les deux voies different d'environ 1 % :
// garder deux jeux de constantes, ne jamais moyenner.
#define MEAS_I1_OFFSET_V 0.0473f
#define MEAS_I1_GAIN_V_PER_A 0.631f
#define MEAS_I2_OFFSET_V 0.0340f
#define MEAS_I2_GAIN_V_PER_A 0.637f
// ---- NTC B57451V5103J062 (PROMPT §5) --------------------------------
// Montage : 3,3 V -- NTC -- R_fixe -- 0 V, mesure au point milieu.
// La NTC etant du cote 3,3 V, la tension MONTE avec la temperature :
// R_ntc = R_fixe * (VREF - Vadc) / Vadc
// (le PROMPT §5 donne cette relation inversee ; verifie sur ses propres
// points de repere : 0,75 V a 0 C -> 34,2 kOhm, ce que beta=4000 predit,
// alors que la forme inversee donnerait 2,94 kOhm, donc du chaud.)
// R_fixe suppose a 10 kOhm : reste a confirmer sur la carte (doc §6).
#define MEAS_NTC_R_FIXED_OHM 10000.0f
#define MEAS_NTC_R25_OHM 10000.0f
#define MEAS_NTC_BETA_K 4000.0f
#define MEAS_NTC_T25_K 298.15f
#define MEAS_KELVIN_OFFSET 273.15f
// ---- Seuil de protection rapide (doc §5) ----------------------------
// Le seuil est defini en AMPERES ; le code DAC est calcule par macro a
// partir du gain et de l'offset mesures, jamais ecrit en dur. Ainsi le
// futur passage au TLV9151 ne touchera que les constantes ci-dessus.
//
// Formule TI : V = DACVAL * (VDDA - VSSA) / 1023 -> 1023, pas 4096.
// Seuil nominal (PROMPT §5). Le declenchement a ete valide au banc a 1,5 A
// (courant injectable), puis le seuil remis a sa valeur de service.
#define SAFETY_ISHUNT_THRESHOLD_A 3.0f
#define SAFETY_DAC_VREF_V 3.3f
#define SAFETY_DAC_CODE_FROM_V(v_) \
((uint16_t)((v_) / SAFETY_DAC_VREF_V * 1023.0f + 0.5f))
// Seuil ramene a la sortie de l'ampli : Vadc = offset + I * gain.
#define SAFETY_DAC_CODE_STAGE1 \
SAFETY_DAC_CODE_FROM_V(MEAS_I1_OFFSET_V \
+ SAFETY_ISHUNT_THRESHOLD_A * MEAS_I1_GAIN_V_PER_A)
#define SAFETY_DAC_CODE_STAGE2 \
SAFETY_DAC_CODE_FROM_V(MEAS_I2_OFFSET_V \
+ SAFETY_ISHUNT_THRESHOLD_A * MEAS_I2_GAIN_V_PER_A)
// ---- Protection thermique (logicielle, PROMPT §6 etape 4) -----------
// Contrairement a la surintensite, la thermique est lente : le logiciel
// suffit, aucun chemin materiel n'est requis.
// PROVISOIRE : seuil a confirmer selon la tenue reelle du MOSFET et
// l'implantation des NTC sur la carte.
// L'hysteresis evite que l'etat oscille autour du point de bascule.
#define SAFETY_OVERTEMP_C 80.0f
#define SAFETY_OVERTEMP_HYST_C 10.0f
// ---- Timeout de la liaison ESP32 (PROMPT §6 etape 7) ----------------
// Sans trame $C valide au-dela de ce delai, la liaison est declaree
// perdue. La securite ne depend jamais de l'ESP32.
#define UART_LINK_TIMEOUT_MS 2000U
// 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
// ---- Balayage de caracterisation (BRING-UP, boucle ouverte) ---------
// Rampe montante puis descendante entre ces deux bornes, par pas de 1 LSB
// de CMPA a chaque conversion ADC. Sert a mesurer la vitesse de correction
// atteignable, pas a reguler. A supprimer avec le reste du harnais.
#define CONTROL_SWEEP_MIN_PCT 1.0f
#define CONTROL_SWEEP_MAX_PCT 50.0f
// 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
#define UART_LSPCLK_HZ (CLK_SYSCLKOUT_HZ / 4UL)
// BRR = round(LSPCLK / (8 * baud)) - 1 (TRM SPRUI09A, registre SCIHBAUD:SCILBAUD)
#define UART_SCIBRR \
((uint16_t)(((UART_LSPCLK_HZ + 4UL * UART_BAUD_RATE) / (8UL * UART_BAUD_RATE)) - 1U))
#define UART_RX_RING_SIZE 64U
#define UART_LINE_MAX 200U // limite cote ESP32 (g_lineBuf[200])
#endif

121
src/control.c Normal file
View File

@ -0,0 +1,121 @@
#include "control.h"
#include "pwm.h"
#include "bsp_gpio.h"
#include "calib.h"
// Bornes du balayage, en counts, calculees une fois a l'init depuis la
// periode reelle de chaque etage. Elles different d'un etage a l'autre :
// 300 counts a 200 kHz contre 600 a 100 kHz.
static uint16_t s_min_counts[2];
static uint16_t s_max_counts[2];
static uint16_t s_counts[2];
static bool s_rising[2];
static volatile bool s_armed = false;
// Compteur d'allers-retours complets, lisible au debogueur : permet de
// verifier que le balayage tourne sans avoir a sonder les broches.
static volatile uint32_t s_sweep_cycles = 0;
static uint16_t pct_to_counts(uint16_t period, float pct)
{
uint16_t c = (uint16_t)((pct / 100.0f) * (float)period + 0.5f);
// Au moins 1 LSB : un duty de 0 ne serait pas un balayage mais un arret.
if (c == 0U)
{
c = 1U;
}
if (c > period)
{
c = period;
}
return c;
}
void control_init(void)
{
uint16_t i;
stage_id_t stages[2] = {STAGE_1, STAGE_2};
for (i = 0U; i < 2U; i++)
{
uint16_t period = pwm_get_period_counts(stages[i]);
s_min_counts[i] = pct_to_counts(period, CONTROL_SWEEP_MIN_PCT);
s_max_counts[i] = pct_to_counts(period, CONTROL_SWEEP_MAX_PCT);
s_counts[i] = s_min_counts[i];
s_rising[i] = true;
}
s_armed = false;
s_sweep_cycles = 0;
}
void control_start(void)
{
uint16_t i;
stage_id_t stages[2] = {STAGE_1, STAGE_2};
// Les deux etages repartent ensemble du minimum.
for (i = 0U; i < 2U; i++)
{
s_counts[i] = s_min_counts[i];
s_rising[i] = true;
pwm_set_duty_counts(stages[i], s_counts[i]);
}
s_armed = true;
}
void control_stop(void)
{
s_armed = false;
}
// Appelee en ISR : que des comparaisons, incrementations et decrementations.
// Le tableau des identifiants est en static const, pas en local : une
// initialisation locale serait recopiee sur la pile a chaque appel, soit
// plusieurs milliers de fois par seconde pour rien.
static const stage_id_t k_stages[2] = {STAGE_1, STAGE_2};
void control_tick(void)
{
uint16_t i;
if (!s_armed)
{
return;
}
for (i = 0U; i < 2U; i++)
{
if (s_rising[i])
{
if (s_counts[i] < s_max_counts[i])
{
s_counts[i]++;
}
else
{
s_rising[i] = false;
}
}
else
{
if (s_counts[i] > s_min_counts[i])
{
s_counts[i]--;
}
else
{
s_rising[i] = true;
if (i == 0U)
{
s_sweep_cycles++; // compte les allers-retours de l'etage 1
}
}
}
pwm_set_duty_counts(k_stages[i], s_counts[i]);
}
}

32
src/control.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef CONTROL_H
#define CONTROL_H
#include <stdbool.h>
#include <stdint.h>
// Emplacement de la future regulation (PROMPT §6 etape 8). Pour l'instant
// ce module ne contient PAS de regulation : il execute un balayage de duty
// en boucle OUVERTE, destine a caracteriser la vitesse de correction
// atteignable. Aucune mesure n'influence encore le duty.
//
// Le balayage va de CONTROL_SWEEP_MIN_PCT a CONTROL_SWEEP_MAX_PCT puis
// revient, par pas de 1 LSB de CMPA a chaque conversion ADC -- c'est-a-dire
// le plus petit increment realisable, et la cadence la plus rapide dont on
// dispose sans ajouter d'interruption.
void control_init(void);
// Arme le balayage et repositionne les deux etages au duty minimal.
void control_start(void);
// Fige le balayage. Le duty n'est pas modifie : c'est pwm_enable() et le
// Trip Zone qui coupent reellement les sorties.
void control_stop(void);
// A appeler a CADENCE FIXE depuis l'ISR ADC (PROMPT §6 etape 8 : "prevoir
// l'emplacement d'appel cadence, dt constant"). Ne fait rien si le balayage
// n'est pas arme. Sans flottant ni division : un increment et une ecriture
// de registre par etage.
void control_tick(void);
#endif

337
src/main.c Normal file
View File

@ -0,0 +1,337 @@
#include "DSP28x_Project.h"
#include "bsp_clock.h"
#include "bsp_gpio.h"
#include "safety.h"
#include "uart_link.h"
#include "protocol.h"
#include "pwm.h"
#include "adc.h"
#include "measure.h"
#include "status_led.h"
#include "control.h"
#include "calib.h"
extern uint16_t RamfuncsLoadStart;
extern uint16_t RamfuncsLoadSize;
extern uint16_t RamfuncsRunStart;
interrupt void cpu_timer0_isr(void);
// Dernier CommandState recu, expose au debogueur.
static volatile command_state_t g_last_cmd = {false, false, false};
static volatile bool s_send_telemetry = false;
// Age de la derniere trame $C valide, en ticks de 10 ms. Incremente par
// l'ISR, remis a zero par la boucle principale a chaque commande valide.
// Sature pour ne jamais reboucler.
#define TICK_MS 10U
#define LINK_TIMEOUT_TICKS ((uint16_t)(UART_LINK_TIMEOUT_MS / TICK_MS))
static volatile uint16_t s_ticks_since_cmd = LINK_TIMEOUT_TICKS;
// Surtemperature, suivie PAR VOIE : le protocole distingue les codes 4 (T1)
// et 5 (T2), un simple maximum ne permettrait pas de les separer.
// Hysteresis pour eviter le battement autour du seuil.
static bool s_overtemp_t1 = false;
static bool s_overtemp_t2 = false;
static bool update_one_overtemp(bool current, float temp_c)
{
if (!current)
{
return (temp_c >= SAFETY_OVERTEMP_C);
}
return (temp_c > (SAFETY_OVERTEMP_C - SAFETY_OVERTEMP_HYST_C));
}
static void update_overtemp(void)
{
s_overtemp_t1 =
update_one_overtemp(s_overtemp_t1, measure_temp(adc_get_raw(ADC_CH_T1)));
s_overtemp_t2 =
update_one_overtemp(s_overtemp_t2, measure_temp(adc_get_raw(ADC_CH_T2)));
}
// Sequence de demarrage de la conversion. Appelee au boot ET apres un
// EMUSTOP : dans les deux cas on repart du meme etat connu, on ne reprend
// jamais la conversion la ou elle s'etait arretee.
//
// Ordre impose : la decharge est inhibee AVANT d'autoriser les etages,
// sinon la conversion travaillerait contre le circuit de decharge.
static void start_conversion(void)
{
hv_discharge_set(false);
// Duty a zero d'abord : c'est le point de depart obligatoire, un
// condensateur de sortie vide provoquerait sinon un appel de courant
// destructeur (PROMPT §6 etape 6).
pwm_set_duty(STAGE_1, 0.0f);
pwm_set_duty(STAGE_2, 0.0f);
pwm_enable(STAGE_1, true);
pwm_enable(STAGE_2, true);
stage_enable_set(STAGE_1, true);
stage_enable_set(STAGE_2, true);
// Le duty est desormais pilote par control.c, qui repart du minimum.
// Les deux etages demarrent ensemble.
control_start();
}
// Priorite 2 > 3 > 4 > 5 > 1 : EMUSTOP est le code le moins prioritaire
// malgre son numero, pour ne jamais masquer une surintensite reelle.
static fault_code_t compute_fault_code(const safety_faults_t *f)
{
if (f->overcurrent && f->stage1_fault)
{
return FAULT_OVERCURRENT_I1;
}
if (f->overcurrent && f->stage2_fault)
{
return FAULT_OVERCURRENT_I2;
}
if (s_overtemp_t1)
{
return FAULT_OVERTEMP_T1;
}
if (s_overtemp_t2)
{
return FAULT_OVERTEMP_T2;
}
if (f->emustop)
{
return FAULT_EMUSTOP;
}
return FAULT_NONE;
}
// Etat sur : les deux etages inhibes ET la sortie HT coupee.
//
// Couper HV_EN est indispensable et n'a rien de redondant : un boost a 0 %
// de duty ne donne PAS 0 V en sortie, le chemin Vin -> L -> diode -> Cout
// reste passant en permanence. HV_EN (VOM1271) est le seul organe qui isole
// reellement la charge (PROMPT §6 etape 6).
static void enter_safe_state(void)
{
control_stop();
pwm_enable(STAGE_1, false);
pwm_enable(STAGE_2, false);
stage_enable_set(STAGE_1, false);
stage_enable_set(STAGE_2, false);
hv_enable_set(false);
// Decharge active. Elle vient APRES la coupure de HV_EN : la charge est
// d'abord isolee, puis le condensateur vide en ~2 s. Sans elle, les
// 400 V resteraient presents pres d'une minute sur le seul bleeder de
// 1 MOhm, sans aucune indication une fois l'alimentation coupee.
hv_discharge_set(true);
}
// Priorite : surintensite > surtemperature > EMUSTOP > liaison perdue.
static led_state_t compute_led_state(const safety_faults_t *f)
{
safety_faults_t faults = *f;
if (faults.overcurrent)
{
return LED_STATE_OVERCURRENT;
}
if (s_overtemp_t1 || s_overtemp_t2)
{
return LED_STATE_OVERTEMP;
}
if (faults.emustop)
{
return LED_STATE_EMUSTOP;
}
if (s_ticks_since_cmd >= LINK_TIMEOUT_TICKS)
{
return LED_STATE_LINK_LOST;
}
return LED_STATE_NOMINAL;
}
void main(void)
{
#ifdef _FLASH
memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, (size_t)&RamfuncsLoadSize);
#endif
bsp_clock_init();
bsp_gpio_leds_init();
status_led_init(); // bleu fixe des le depart : "je suis parti"
DINT;
InitPieCtrl();
IER = 0x0000;
IFR = 0x0000;
InitPieVectTable();
bsp_gpio_analog_init();
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();
// Protection complete : comparateurs + Digital Compare + Trip Zone, et
// TZ6/EMUSTOP. Les entrees shunt sont maintenant reellement cablees, le
// comparateur ne declenchera donc pas spontanement.
safety_init();
// control_init() lit les periodes ePWM, donc apres pwm_init(), et avant
// adc_init() dont l'ISR appellera control_tick().
control_init();
// ADC : declenche par ePWM1, donc apres pwm_init().
adc_init();
uart_link_init();
start_conversion();
EALLOW;
PieVectTable.TINT0 = &cpu_timer0_isr;
EDIS;
InitCpuTimers();
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;
PieCtrlRegs.PIEIER1.bit.INTx7 = 1; // TINT0
EINT;
ERTM;
for (;;)
{
command_state_t cmd;
safety_faults_t faults;
if (uart_link_poll(&cmd))
{
g_last_cmd = cmd;
s_ticks_since_cmd = 0U;
}
update_overtemp();
faults = safety_get_fault_flags();
// ARRET GLOBAL : n'importe quel defaut, sur n'importe quel etage,
// arrete l'ensemble. Sur un boost en cascade, laisser tourner
// l'etage 2 alors que l'etage 1 est coupe n'a pas de sens : il
// travaillerait sans tension d'entree.
//
// Les Trip Zones restent independantes en materiel (une surintensite
// etage 2 ne coupe que EPWM2A) : cet arret global est la couche
// systeme qui vient PAR-DESSUS. Le logiciel peut inhiber en plus,
// jamais outrepasser la protection materielle (PROMPT §2).
//
// Aucun redemarrage automatique : rien ne reactive le PWM ensuite,
// meme si la temperature redescend sous l'hysteresis ou si le defaut
// disparait. Il faut un reset, ou une commande explicite (etape 8).
if (faults.overcurrent || s_overtemp_t1 || s_overtemp_t2)
{
// Defaut de PUISSANCE : verrouille jusqu'au cycle d'alimentation
// (PROMPT §8). Aucun acquittement, ni automatique ni par UART.
enter_safe_state();
}
else if (faults.emustop)
{
// EMUSTOP seul : ce n'est pas un defaut de puissance mais un
// artefact du debogueur. On autorise donc la reprise -- mais en
// repassant par la sequence de demarrage complete, jamais en
// reprenant la conversion la ou elle s'etait arretee.
//
// L'effacement ne peut aboutir qu'ici, CPU en marche : tant que
// le coeur est halte, EMUSTOP reste asserte et le drapeau se
// re-verrouille aussitot.
//
// La liaison est relancee aussi : pendant la halte l'ESP32 a
// continue d'emettre, l'anneau de reception a deborde et la
// ligne en cours est tronquee.
uart_link_restart();
safety_clear_faults();
start_conversion();
}
status_led_set_state(compute_led_state(&faults));
if (s_send_telemetry)
{
telemetry_t t;
// Grandeurs physiques reelles (etape 5) : conversion par
// measure.c a partir des coefficients mesures sur la carte
// (docs/mesure-cartepuissance.md).
// FREQ/DUTY restent les valeurs de consigne du bloc de bring-up
// ci-dessus, pas encore une mesure.
t.freq1_hz = (float)PWM_STAGE1_FREQ_HZ;
t.freq2_hz = (float)PWM_STAGE2_FREQ_HZ;
// Duty reellement en sortie, relu depuis CMPA. Pendant le
// balayage la valeur change bien plus vite que la cadence de
// telemetrie : c'est un echantillon, pas un suivi.
t.duty1_pct = pwm_get_duty(STAGE_1) * 100.0f;
t.duty2_pct = pwm_get_duty(STAGE_2) * 100.0f;
t.vin_v = measure_vin(adc_get_raw(ADC_CH_VIN));
t.iin_a = measure_iin(adc_get_raw(ADC_CH_IIN));
t.v1_v = measure_v1(adc_get_raw(ADC_CH_V1));
t.i1_a = measure_i1(adc_get_raw(ADC_CH_I1));
t.t1_c = measure_temp(adc_get_raw(ADC_CH_T1));
t.vout_v = measure_vout(adc_get_raw(ADC_CH_VOUT));
t.i2_a = measure_i2(adc_get_raw(ADC_CH_I2));
t.t2_c = measure_temp(adc_get_raw(ADC_CH_T2));
t.iout_a = measure_iout(adc_get_raw(ADC_CH_IOUT));
t.fault = compute_fault_code(&faults);
uart_link_send_telemetry(&t);
s_send_telemetry = false;
}
// Emission en tache de fond : pousse au plus 4 octets (profondeur de
// la FIFO) puis rend la main. Ne doit jamais retarder la regulation.
uart_link_service_tx();
}
}
// 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.
// Aucune multiplication ni division ici : le rythme de la telemetrie est
// obtenu par un compteur qui reboucle sur comparaison, pas par un modulo
// (division logicielle sur C28x).
#define TELEMETRY_PERIOD_TICKS 30U // 300 ms
interrupt void cpu_timer0_isr(void)
{
static uint16_t telemetry_tick = 0;
CpuTimer0.InterruptCount++;
status_led_tick();
if (s_ticks_since_cmd < LINK_TIMEOUT_TICKS)
{
s_ticks_since_cmd++; // sature au seuil, pas de rebouclage
}
telemetry_tick++;
if (telemetry_tick >= TELEMETRY_PERIOD_TICKS)
{
telemetry_tick = 0U;
s_send_telemetry = true;
}
PieCtrlRegs.PIEACK.all = PIEACK_GROUP1;
}

76
src/measure.c Normal file
View File

@ -0,0 +1,76 @@
#include <math.h>
#include "measure.h"
#include "calib.h"
// Brut -> tension a la broche. Reference interne, pleine echelle 3,3 V.
static float raw_to_volts(uint16_t raw)
{
return (float)raw * ADC_VREF_V / 4096.0f;
}
float measure_vin(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_VIN_GAIN_V_PER_V;
}
float measure_iin(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_IIN_A_PER_V;
}
float measure_v1(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_V1_GAIN_V_PER_V;
}
float measure_vout(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_VOUT_GAIN_V_PER_V;
}
float measure_iout(uint16_t raw)
{
return raw_to_volts(raw) * MEAS_IOUT_A_PER_V;
}
// Vadc = offset + I * gain -> I = (Vadc - offset) / gain.
// Le resultat peut etre legerement negatif a courant nul (bruit autour de
// l'offset) : on ne le sature pas, une valeur negative en telemetrie est un
// indice utile de derive de l'offset apres changement d'ampli.
float measure_i1(uint16_t raw)
{
return (raw_to_volts(raw) - MEAS_I1_OFFSET_V) / MEAS_I1_GAIN_V_PER_A;
}
float measure_i2(uint16_t raw)
{
return (raw_to_volts(raw) - MEAS_I2_OFFSET_V) / MEAS_I2_GAIN_V_PER_A;
}
// Pont 3,3 V -- NTC -- R_fixe -- 0 V, mesure au point milieu.
// R_ntc = R_fixe * (VREF - Vadc) / Vadc
// T(K) = 1 / ( 1/T25 + ln(R_ntc / R25) / beta )
//
// Precision attendue : la tolerance de +/-3 % sur beta donne environ
// +/-2 degC vers 100 degC sans calibration individuelle. Suffisant pour de
// la protection thermique, pas pour de la mesure fine (PROMPT §5).
float measure_temp(uint16_t raw)
{
float vadc = raw_to_volts(raw);
float r_ntc;
float inv_t;
// Bornes : hors de cette plage le pont est en butee (broche en l'air,
// NTC en court-circuit ou coupee) et le calcul n'a plus de sens.
if (vadc <= 0.01f || vadc >= (ADC_VREF_V - 0.01f))
{
return 0.0f;
}
r_ntc = MEAS_NTC_R_FIXED_OHM * (ADC_VREF_V - vadc) / vadc;
inv_t = 1.0f / MEAS_NTC_T25_K
+ logf(r_ntc / MEAS_NTC_R25_OHM) / MEAS_NTC_BETA_K;
return (1.0f / inv_t) - MEAS_KELVIN_OFFSET;
}

30
src/measure.h Normal file
View File

@ -0,0 +1,30 @@
#ifndef MEASURE_H
#define MEASURE_H
#include <stdint.h>
// Conversion des valeurs brutes de l'ADC (0..4095) vers les grandeurs
// physiques, a partir des seules constantes de calib.h (elles-memes issues
// de docs/mesure-cartepuissance.md).
//
// Ces fonctions utilisent du flottant et, pour les NTC, un logarithme :
// a n'appeler QUE depuis la boucle principale, jamais depuis une ISR
// (PROMPT §7). La protection rapide ne passe pas par ici.
float measure_vin(uint16_t raw); // V - tension d'entree batterie
float measure_iin(uint16_t raw); // A - courant d'entree
float measure_v1(uint16_t raw); // V - tension intermediaire (etage 1)
float measure_vout(uint16_t raw); // V - tension de sortie HT
float measure_iout(uint16_t raw); // A - courant de sortie HT
// Courant shunt MOSFET. Deux fonctions distinctes et non une seule
// parametree : les deux voies ont leur propre gain ET leur propre offset,
// mesures separement.
float measure_i1(uint16_t raw); // A - shunt etage 1
float measure_i2(uint16_t raw); // A - shunt etage 2
// Temperature NTC, en degres Celsius. Meme chaine de mesure sur les deux
// etages, donc une seule fonction.
float measure_temp(uint16_t raw); // degC
#endif

54
src/protocol.h Normal file
View File

@ -0,0 +1,54 @@
#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).
// Codes du champ FAULT de la trame $T (docs/ESP32-UART.md).
//
// Attention a la priorite d'affichage quand plusieurs defauts coexistent :
// elle est 2 > 3 > 4 > 5 > 1, donc EMUSTOP a la priorite la PLUS BASSE
// malgre son numero. C'est voulu : en developpement EMUSTOP se declenche a
// chaque halte du debogueur et ne doit jamais masquer une surintensite.
//
// Il n'existe volontairement AUCUN code de court-circuit : un court-circuit
// franchit le meme comparateur et le meme seuil qu'une surintensite, le
// firmware ne dispose d'aucune information permettant de les distinguer.
typedef enum
{
FAULT_NONE = 0,
FAULT_EMUSTOP = 1,
FAULT_OVERCURRENT_I1 = 2,
FAULT_OVERCURRENT_I2 = 3,
FAULT_OVERTEMP_T1 = 4,
FAULT_OVERTEMP_T2 = 5
} fault_code_t;
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;
fault_code_t fault;
} telemetry_t;
typedef struct
{
bool ht_enabled;
bool pwm1_enabled;
bool pwm2_enabled;
} command_state_t;
#endif

185
src/pwm.c Normal file
View File

@ -0,0 +1,185 @@
#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);
}
float pwm_get_duty(stage_id_t stage)
{
volatile struct EPWM_REGS *p = pwm_regs(stage);
uint16_t period = (uint16_t)(p->TBPRD + 1U);
if (period == 0U)
{
return 0.0f;
}
return (float)p->CMPA.half.CMPA / (float)period;
}
uint16_t pwm_get_period_counts(stage_id_t stage)
{
return (uint16_t)(pwm_regs(stage)->TBPRD + 1U);
}
void pwm_set_duty_counts(stage_id_t stage, uint16_t counts)
{
volatile struct EPWM_REGS *p = pwm_regs(stage);
uint16_t period = (uint16_t)(p->TBPRD + 1U);
if (counts > period)
{
counts = period;
}
p->CMPA.half.CMPA = counts;
// PAS de mise a jour de la consigne flottante ici : cette fonction est
// appelee depuis l'ISR ADC a plusieurs dizaines de kHz, et le F28027
// n'a pas d'unite flottante -- une division y coutait plusieurs
// centaines de cycles et effondrait la cadence de l'ISR.
// pwm_get_duty() relit CMPA, la telemetrie reste donc juste.
}
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;
}

40
src/pwm.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef PWM_H
#define PWM_H
#include <stdbool.h>
#include <stdint.h>
#include "bsp_gpio.h" // stage_id_t
// ePWM1 -> etage 1 (EPWM1A, GPIO0, broche 29)
// ePWM2 -> etage 2 (EPWM2A, GPIO2, broche 37)
//
// Initialise les deux etages a leur frequence nominale (calib.h), duty a 0
// et sorties inhibees. PROMPT §6 etape 6 : ne jamais demarrer au duty
// nominal, le condensateur de sortie vide provoquerait un appel de courant
// destructeur. La rampe de demarrage (soft-start) est du ressort de
// l'appelant, pas de ce module.
void pwm_init(void);
void pwm_set_freq(stage_id_t stage, uint32_t hz);
// duty exprime en 0..1. Sature hors bornes.
void pwm_set_duty(stage_id_t stage, float duty_0_1);
// Duty courant (0..1), reconstruit depuis CMPA : reflete donc ce qui sort
// reellement, y compris apres un reglage en counts.
float pwm_get_duty(stage_id_t stage);
// ---- Reglage en pas de compteur --------------------------------------
// Le pas minimal realisable est 1 LSB de CMPA, et il DEPEND DE L'ETAGE :
// 1/300 = 0,333 % a 200 kHz, 1/600 = 0,167 % a 100 kHz. Une consigne en
// flottant ne permet pas d'exprimer "le plus petit pas possible" ; ces
// deux fonctions si.
uint16_t pwm_get_period_counts(stage_id_t stage); // TBPRD + 1
void pwm_set_duty_counts(stage_id_t stage, uint16_t counts);
// 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

224
src/safety.c Normal file
View File

@ -0,0 +1,224 @@
#include "DSP28x_Project.h"
#include "safety.h"
#include "bsp_gpio.h"
#include "calib.h"
static volatile bool s_stage1_fault = false;
static volatile bool s_stage2_fault = false;
static volatile bool s_overcurrent = false;
static volatile bool s_emustop = false;
interrupt void epwm1_tzint_isr(void);
interrupt void epwm2_tzint_isr(void);
void safety_init(void)
{
EALLOW;
// Le comparateur partage la reference bandgap de l'ADC : elle doit etre
// alimentee meme si l'ADC lui-meme n'est pas encore utilise (etape 4).
SysCtrlRegs.PCLKCR0.bit.ADCENCLK = 1;
SysCtrlRegs.PCLKCR3.bit.COMP1ENCLK = 1;
SysCtrlRegs.PCLKCR3.bit.COMP2ENCLK = 1;
AdcRegs.ADCCTL1.bit.ADCBGPWD = 1;
EDIS;
DELAY_US(1000L);
EALLOW;
// Comparateur 1 : shunt etage 1 sur COMP1A (entree non-inverseuse fixe),
// seuil DAC sur l'entree inverseuse (COMPSOURCE=0).
Comp1Regs.COMPCTL.bit.COMPDACEN = 1;
Comp1Regs.COMPCTL.bit.COMPSOURCE = 0;
Comp1Regs.COMPCTL.bit.SYNCSEL = 0; // asynchrone -> ~30 ns vers le Trip Zone
Comp1Regs.COMPCTL.bit.CMPINV = 1; // driver actif haut : 1=OK, 0=defaut
Comp1Regs.COMPCTL.bit.QUALSEL = 0; // pas de qualification pour l'instant
// Seuil propre a l'etage 1 : les deux chaines de mesure different d'environ
// 1 % en gain et n'ont pas le meme offset (docs/mesure-cartepuissance.md).
Comp1Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE_STAGE1;
GpioCtrlRegs.GPAMUX1.bit.GPIO1 = 3; // COMP1OUT
// Comparateur 2 : idem, shunt etage 2 sur COMP2A.
Comp2Regs.COMPCTL.bit.COMPDACEN = 1;
Comp2Regs.COMPCTL.bit.COMPSOURCE = 0;
Comp2Regs.COMPCTL.bit.SYNCSEL = 0;
Comp2Regs.COMPCTL.bit.CMPINV = 1;
Comp2Regs.COMPCTL.bit.QUALSEL = 0;
Comp2Regs.DACVAL.bit.DACVAL = SAFETY_DAC_CODE_STAGE2;
GpioCtrlRegs.GPAMUX1.bit.GPIO3 = 3; // COMP2OUT
// Digital Compare : COMPxOUT -> DCAEVT1 -> Trip Zone one-shot (latche)
EPwm1Regs.DCTRIPSEL.bit.DCAHCOMPSEL = DC_COMP1OUT;
EPwm1Regs.TZDCSEL.bit.DCAEVT1 = TZ_DCAH_LOW;
EPwm1Regs.TZSEL.bit.DCAEVT1 = 1;
EPwm1Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
EPwm1Regs.TZEINT.bit.OST = 1;
// TZ6 = EMUSTOP (signal cable en dur depuis le CPU, TRM SPRUI09A section
// 3.2.7) : coupe le PWM des que le debugger arrete le coeur. FREE_SOFT
// seul ne suffit pas, il ne fait que geler le compteur de base de temps,
// la broche reste figee dans son dernier etat.
EPwm1Regs.TZSEL.bit.OSHT6 = 1;
EPwm1Regs.TBCTL.bit.FREE_SOFT = 0;
EPwm2Regs.DCTRIPSEL.bit.DCAHCOMPSEL = DC_COMP2OUT;
EPwm2Regs.TZDCSEL.bit.DCAEVT1 = TZ_DCAH_LOW;
EPwm2Regs.TZSEL.bit.DCAEVT1 = 1;
EPwm2Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
EPwm2Regs.TZEINT.bit.OST = 1;
EPwm2Regs.TZSEL.bit.OSHT6 = 1;
EPwm2Regs.TBCTL.bit.FREE_SOFT = 0;
// Efface un trip herite d'AVANT ce demarrage. TZFLG.OST est latche dans
// le materiel : il survit a un reset logiciel comme a un simple
// rechargement de programme, et sans ce clear le PWM resterait coupe
// sans raison visible.
//
// Sans danger : a ce stade pwm_init() a laisse les deux sorties forcees
// a l'etat bas (AQCSFRC), donc rien ne demarre. Et si la condition de
// defaut est toujours presente, le trip se re-verrouille immediatement
// -- on n'efface qu'un etat perime, jamais un defaut reel.
EPwm1Regs.TZCLR.bit.DCAEVT1 = 1;
EPwm1Regs.TZCLR.bit.OST = 1;
EPwm1Regs.TZCLR.bit.INT = 1;
EPwm2Regs.TZCLR.bit.DCAEVT1 = 1;
EPwm2Regs.TZCLR.bit.OST = 1;
EPwm2Regs.TZCLR.bit.INT = 1;
PieVectTable.EPWM1_TZINT = &epwm1_tzint_isr;
PieVectTable.EPWM2_TZINT = &epwm2_tzint_isr;
EDIS;
IER |= M_INT2;
PieCtrlRegs.PIEIER2.bit.INTx1 = 1; // EPWM1_TZINT
PieCtrlRegs.PIEIER2.bit.INTx2 = 1; // EPWM2_TZINT
}
void safety_arm_emustop_only(void)
{
EALLOW;
// TZ6 = EMUSTOP, cable en dur depuis le CPU (TRM SPRUI09A section 3.2.7).
// One-shot : le flag latche, il ne se rearme pas seul.
EPwm1Regs.TZSEL.bit.OSHT6 = 1;
EPwm1Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
EPwm1Regs.TZEINT.bit.OST = 1;
EPwm1Regs.TBCTL.bit.FREE_SOFT = 0;
EPwm2Regs.TZSEL.bit.OSHT6 = 1;
EPwm2Regs.TZCTL.bit.TZA = TZ_FORCE_LO;
EPwm2Regs.TZEINT.bit.OST = 1;
EPwm2Regs.TBCTL.bit.FREE_SOFT = 0;
PieVectTable.EPWM1_TZINT = &epwm1_tzint_isr;
PieVectTable.EPWM2_TZINT = &epwm2_tzint_isr;
EDIS;
IER |= M_INT2;
PieCtrlRegs.PIEIER2.bit.INTx1 = 1; // EPWM1_TZINT
PieCtrlRegs.PIEIER2.bit.INTx2 = 1; // EPWM2_TZINT
}
safety_faults_t safety_get_fault_flags(void)
{
safety_faults_t f;
f.stage1_fault = s_stage1_fault;
f.stage2_fault = s_stage2_fault;
f.overcurrent = s_overcurrent;
f.emustop = s_emustop;
return f;
}
void safety_clear_faults(void)
{
EALLOW;
// OST libere la sortie PWM, DCAEVT1 remet a zero la cause (sinon le
// prochain trip serait attribue a une surintensite meme s'il vient
// d'EMUSTOP), INT autorise a nouveau la generation d'interruptions, et
// TZEINT.OST rearme la notification desarmee par l'ISR.
EPwm1Regs.TZCLR.bit.DCAEVT1 = 1;
EPwm1Regs.TZCLR.bit.OST = 1;
EPwm1Regs.TZCLR.bit.INT = 1;
EPwm1Regs.TZEINT.bit.OST = 1;
EPwm2Regs.TZCLR.bit.DCAEVT1 = 1;
EPwm2Regs.TZCLR.bit.OST = 1;
EPwm2Regs.TZCLR.bit.INT = 1;
EPwm2Regs.TZEINT.bit.OST = 1;
EDIS;
s_stage1_fault = false;
s_stage2_fault = false;
s_overcurrent = false;
s_emustop = false;
// Les LED ne sont plus pilotees ici : c'est status_led.c qui detient
// l'affichage, a partir de ces drapeaux.
}
void safety_force_trip_test(void)
{
EALLOW;
EPwm1Regs.TZFRC.bit.OST = 1;
EPwm2Regs.TZFRC.bit.OST = 1;
EDIS;
}
// Diagnostic uniquement : la coupure est deja faite en materiel (PROMPT §6
// etape 2 point 7). On se contente de relever l'origine du trip.
//
// Deux subtilites, decouvertes au banc :
//
// 1. TZFLG.INT est le drapeau GLOBAL d'interruption Trip Zone. Tant qu'il
// n'est pas efface via TZCLR.INT, plus AUCUNE interruption TZ n'est
// generee. Sans ce clear, seul le tout premier trip de la session etait
// signale au logiciel : les suivants coupaient bien le PWM en materiel,
// mais en silence, sans lever de drapeau.
//
// 2. Effacer INT alors que la condition de trip persiste (surintensite
// toujours presente) regenere l'evenement immediatement -> tempete
// d'interruptions qui affamerait la boucle principale. On desarme donc
// TZEINT.OST ici : une seule notification par episode de defaut. Le
// rearmement est fait par safety_clear_faults(), c'est-a-dire jamais
// automatiquement (PROMPT §8).
//
// Le drapeau DCAEVT1 n'est volontairement PAS efface ici : il porte la
// cause du defaut et doit rester lisible jusqu'a l'effacement explicite.
interrupt void epwm1_tzint_isr(void)
{
s_stage1_fault = true;
if (EPwm1Regs.TZFLG.bit.DCAEVT1)
{
s_overcurrent = true;
}
else
{
s_emustop = true;
}
EALLOW;
EPwm1Regs.TZEINT.bit.OST = 0;
EPwm1Regs.TZCLR.bit.INT = 1;
EDIS;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP2;
}
interrupt void epwm2_tzint_isr(void)
{
s_stage2_fault = true;
if (EPwm2Regs.TZFLG.bit.DCAEVT1)
{
s_overcurrent = true;
}
else
{
s_emustop = true;
}
EALLOW;
EPwm2Regs.TZEINT.bit.OST = 0;
EPwm2Regs.TZCLR.bit.INT = 1;
EDIS;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP2;
}

41
src/safety.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef SAFETY_H
#define SAFETY_H
#include <stdbool.h>
typedef struct
{
bool stage1_fault;
bool stage2_fault;
// Origine du trip, relevee dans TZFLG au moment ou l'ISR s'execute.
// TZFLG.DCAEVT1 (bit 3) est distinct de TZFLG.OST (bit 2), ce qui permet
// de separer une vraie surintensite d'un arret du debugger -- les deux
// n'appellent pas du tout la meme reaction.
bool overcurrent; // DCAEVT1 : comparateur, surintensite reelle
bool emustop; // OST sans DCAEVT1 : EMUSTOP (ou TZFRC de test)
} 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
// 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

132
src/status_led.c Normal file
View File

@ -0,0 +1,132 @@
#include <stdbool.h>
#include <stdint.h>
#include "status_led.h"
#include "bsp_gpio.h"
// Durees exprimees en ticks de 10 ms. Ce sont des constantes de
// presentation, pas de calibration materielle : elles restent ici plutot
// que dans calib.h, qui decrit la carte.
#define TICKS_PER_100MS 10U
#define STARTUP_MIN_TICKS 100U // 1 s : rend l'etat de demarrage visible
#define NOMINAL_PERIOD 200U // 2,0 s
#define NOMINAL_ON 20U // 0,2 s
#define LINK_LOST_PERIOD 40U // 0,4 s
#define LINK_LOST_ON 20U // 0,2 s
#define EMUSTOP_PERIOD 100U // 1,0 s
#define EMUSTOP_HALF 50U // 0,5 s bleu puis 0,5 s rouge
#define OVERTEMP_PERIOD 100U // 1,0 s
#define OVERTEMP_ON 50U // 0,5 s
static led_state_t s_state = LED_STATE_STARTUP;
static uint16_t s_phase = 0; // position dans le motif courant
static uint16_t s_startup_hold = STARTUP_MIN_TICKS;
void status_led_init(void)
{
s_state = LED_STATE_STARTUP;
s_phase = 0;
s_startup_hold = STARTUP_MIN_TICKS;
led_set(LED_BLUE, true);
led_set(LED_RED, false);
}
void status_led_set_state(led_state_t state)
{
if (s_startup_hold != 0U)
{
return; // duree minimale de l'etat de demarrage non ecoulee
}
if (state != s_state)
{
s_state = state;
s_phase = 0; // le motif repart proprement a son debut
}
}
// Appelee en ISR : aucune multiplication ni division. Le compteur de phase
// reboucle par COMPARAISON, jamais par modulo -- sur C28x un "%" par une
// valeur qui n'est pas une puissance de deux est une division logicielle,
// donc plusieurs dizaines de cycles.
void status_led_tick(void)
{
uint16_t period;
uint16_t on_time;
bool blue = false;
bool red = false;
if (s_startup_hold != 0U)
{
s_startup_hold--;
}
// Periode et duree du niveau actif du motif courant.
switch (s_state)
{
case LED_STATE_NOMINAL:
period = NOMINAL_PERIOD;
on_time = NOMINAL_ON;
break;
case LED_STATE_LINK_LOST:
period = LINK_LOST_PERIOD;
on_time = LINK_LOST_ON;
break;
case LED_STATE_EMUSTOP:
period = EMUSTOP_PERIOD;
on_time = EMUSTOP_HALF;
break;
case LED_STATE_OVERTEMP:
period = OVERTEMP_PERIOD;
on_time = OVERTEMP_ON;
break;
default: // STARTUP et OVERCURRENT : niveau fixe, pas de motif
period = 1U;
on_time = 1U;
break;
}
s_phase++;
if (s_phase >= period)
{
s_phase = 0U;
}
switch (s_state)
{
case LED_STATE_NOMINAL:
case LED_STATE_LINK_LOST:
blue = (s_phase < on_time);
break;
case LED_STATE_EMUSTOP:
blue = (s_phase < on_time);
red = !blue;
break;
case LED_STATE_OVERTEMP:
red = (s_phase < on_time);
break;
case LED_STATE_OVERCURRENT:
red = true;
break;
case LED_STATE_STARTUP:
default:
blue = true;
break;
}
led_set(LED_BLUE, blue);
led_set(LED_RED, red);
}

41
src/status_led.h Normal file
View File

@ -0,0 +1,41 @@
#ifndef STATUS_LED_H
#define STATUS_LED_H
// Affichage d'etat sur la LED bicolore (bleue = GPIO12, rouge = GPIO33).
// Les motifs sont volontairement tous distinguables a l'oeil :
//
// STARTUP bleu fixe
// NOMINAL bleu 0,2 s ON / 1,8 s OFF (battement discret)
// LINK_LOST bleu 0,2 s ON / 0,2 s OFF (clignotement rapide)
// EMUSTOP alternance bleu/rouge 0,5 s / 0,5 s
// OVERTEMP rouge 0,5 s ON / 0,5 s OFF
// OVERCURRENT rouge fixe
//
// L'ordre de l'enumeration EST l'ordre de priorite croissante : quand
// plusieurs conditions sont vraies, on affiche la plus grande valeur.
// La surintensite passe donc devant EMUSTOP -- pendant le developpement
// EMUSTOP se declenche en permanence et ne doit jamais masquer un vrai
// defaut de puissance.
typedef enum
{
LED_STATE_STARTUP = 0,
LED_STATE_NOMINAL,
LED_STATE_LINK_LOST,
LED_STATE_EMUSTOP,
LED_STATE_OVERTEMP,
LED_STATE_OVERCURRENT
} led_state_t;
void status_led_init(void);
// Etat souhaite, calcule dans la boucle principale. Tout changement
// redemarre le motif a zero. Les demandes de sortie de STARTUP sont
// ignorees tant que la duree minimale d'affichage n'est pas ecoulee,
// sans quoi cet etat ne serait jamais visible (l'init dure quelques ms).
void status_led_set_state(led_state_t state);
// A appeler a chaque tick de 10 ms. Ne fait qu'ecrire GPASET/GPACLEAR,
// donc utilisable depuis une ISR.
void status_led_tick(void);
#endif

487
src/uart_link.c Normal file
View File

@ -0,0 +1,487 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "DSP28x_Project.h"
#include "uart_link.h"
#include "calib.h"
static volatile uint8_t s_rx_ring[UART_RX_RING_SIZE];
static volatile uint16_t s_rx_head = 0; // ecrit par l'ISR uniquement
static uint16_t s_rx_tail = 0; // ecrit par uart_link_poll() uniquement
static char s_line[UART_LINE_MAX];
static uint16_t s_line_len = 0;
interrupt void scia_rx_isr(void);
// Profondeur de la FIFO d'emission du SCI (TRM SPRUI09A) : on ne pousse
// jamais au-dela, ce qui garantit que uart_link_service_tx() rend la main
// immediatement au lieu d'attendre le debit de la ligne.
#define SCI_TX_FIFO_DEPTH 4U
static uint8_t checksum_of(const char *s, int len)
{
uint8_t cs = 0;
int i;
for (i = 0; i < len; i++)
{
cs ^= (uint8_t)s[i];
}
return cs;
}
void uart_link_init(void)
{
EALLOW;
GpioCtrlRegs.GPAPUD.bit.GPIO28 = 0; // pull-up sur RX
GpioCtrlRegs.GPAPUD.bit.GPIO29 = 1; // pas de pull-up sur TX (sortie)
GpioCtrlRegs.GPAQSEL2.bit.GPIO28 = 3; // RX asynchrone, pas de qualification
GpioCtrlRegs.GPAMUX2.bit.GPIO28 = 1; // GPIO28 -> SCIRXDA
GpioCtrlRegs.GPAMUX2.bit.GPIO29 = 1; // GPIO29 -> SCITXDA
EDIS;
// 1 stop bit, pas de parite, 8 bits, mode async, protocole idle-line
SciaRegs.SCICCR.all = 0x0007;
// TX/RX actives, horloge SCI interne
SciaRegs.SCICTL1.all = 0x0003;
SciaRegs.SCICTL2.bit.TXINTENA = 0; // TX en polling, pas d'interruption
SciaRegs.SCICTL2.bit.RXBKINTENA = 1;
SciaRegs.SCIHBAUD = (UART_SCIBRR >> 8) & 0xFF;
SciaRegs.SCILBAUD = UART_SCIBRR & 0xFF;
SciaRegs.SCIFFTX.all = 0xE040; // FIFO active, TX en polling (pas d'IT FIFO)
SciaRegs.SCIFFRX.all = 0x2021; // FIFO active, RXFFIENA=1, seuil=1 octet
SciaRegs.SCIFFCT.all = 0x0;
SciaRegs.SCICTL1.all = 0x0023; // sort le SCI de son reset local
EALLOW;
PieVectTable.SCIRXINTA = &scia_rx_isr;
EDIS;
IER |= M_INT9;
PieCtrlRegs.PIEIER9.bit.INTx1 = 1; // SCIRXINTA
}
// Buffer d'emission statique et non local : evite 160 mots de pile a chaque
// envoi (la pile ne fait que 1024 mots et sprintf en consomme deja beaucoup).
static char s_tx_frame[UART_LINE_MAX];
static uint16_t s_tx_len = 0; // longueur de la trame en attente d'emission
static uint16_t s_tx_pos = 0; // prochain octet a pousser dans la FIFO
static uint16_t s_tx_dropped = 0; // trames abandonnees (ligne trop lente)
static uint16_t s_tx_truncated = 0; // trames ayant perdu des champs (borne)
// Place a reserver en fin de buffer : "*XX\n" + le NUL ecrit par snprintf.
#define UART_TX_TAIL_LEN 5
// Garde-fou sur les valeurs formatees : un flottant aberrant (capteur non
// encore cable, ADC non initialise, NaN) rendrait le cast en int32
// indefini, donc un champ de longueur arbitraire.
#define UART_FIELD_ABS_MAX 999999.0f
// Formatage decimal a la main, SANS "%f". Sur C28x le support flottant de
// printf passe par du long double 64 bits emule (frexpl/scalbnl/L$$DIV) :
// tres couteux en pile et en temps, et constate au bring-up comme faisant
// partir le CPU dans le decor (PC=0) des le premier envoi de telemetrie.
// Voir aussi PROMPT §7 etape 7, qui prevoyait deja ce repli.
//
// `room` = octets disponibles pour ce champ, NUL compris. Renvoie 0 (champ
// entierement abandonne, rien d'ecrit) s'il ne tient pas : le protocole
// autorise les champs manquants, l'ESP32 garde alors sa derniere valeur
// (docs/ESP32-UART.md). Mieux vaut une trame courte mais valide qu'un
// debordement de s_tx_frame.
static int append_field(char *out, int room, const char *tag, float value,
uint16_t decimals)
{
static const int32_t k_pow10[3] = {1L, 10L, 100L};
const char *sign = "";
int32_t scaled;
int32_t ip;
int32_t fp;
int n;
if (room <= 1)
{
return 0;
}
// value != value n'est vrai que pour un NaN.
if (value != value)
{
value = 0.0f;
}
else if (value < -UART_FIELD_ABS_MAX)
{
value = -UART_FIELD_ABS_MAX;
}
else if (value > UART_FIELD_ABS_MAX)
{
value = UART_FIELD_ABS_MAX;
}
scaled = (int32_t)(value * (float)k_pow10[decimals]
+ ((value >= 0.0f) ? 0.5f : -0.5f));
if (scaled < 0)
{
scaled = -scaled;
sign = "-";
}
ip = scaled / k_pow10[decimals];
fp = scaled % k_pow10[decimals];
if (decimals == 0)
{
n = snprintf(out, (size_t)room, ",%s=%s%ld", tag, sign, ip);
}
else if (decimals == 1)
{
n = snprintf(out, (size_t)room, ",%s=%s%ld.%01ld", tag, sign, ip, fp);
}
else
{
n = snprintf(out, (size_t)room, ",%s=%s%ld.%02ld", tag, sign, ip, fp);
}
if (n < 0 || n >= room)
{
out[0] = '\0'; // tronque : on annule le champ au lieu de le laisser coupe
return 0;
}
return n;
}
void uart_link_restart(void)
{
// Reset logiciel du SCI : vide les FIFO et efface les etats d'erreur.
SciaRegs.SCICTL1.bit.SWRESET = 0;
SciaRegs.SCICTL1.bit.SWRESET = 1;
SciaRegs.SCIFFRX.bit.RXFIFORESET = 0;
SciaRegs.SCIFFRX.bit.RXFIFORESET = 1;
SciaRegs.SCIFFRX.bit.RXFFOVRCLR = 1;
SciaRegs.SCIFFRX.bit.RXFFINTCLR = 1;
SciaRegs.SCIFFTX.bit.TXFIFOXRESET = 0;
SciaRegs.SCIFFTX.bit.TXFIFOXRESET = 1;
// On jette le contenu de l'anneau en avancant seulement la QUEUE.
// s_rx_head appartient a l'ISR : ne pas y toucher evite toute course.
s_rx_tail = s_rx_head;
s_line_len = 0;
// Trame d'emission en cours abandonnee : elle date d'avant l'arret.
s_tx_len = 0;
s_tx_pos = 0;
}
// Champ entier (FAULT). Meme convention de bornage que append_field :
// renvoie 0 et n'ecrit rien si le champ ne tient pas.
static int append_field_int(char *out, int room, const char *tag, int value)
{
int n;
if (room <= 1)
{
return 0;
}
n = snprintf(out, (size_t)room, ",%s=%d", tag, value);
if (n < 0 || n >= room)
{
out[0] = '\0';
return 0;
}
return n;
}
// Construit la trame et la met en attente. NE BLOQUE PAS : l'emission reelle
// se fait ensuite par uart_link_service_tx(), appelee quand la boucle
// principale a du temps disponible. Renvoie false si la trame precedente
// n'est pas encore partie (nouvelle trame abandonnee, la telemetrie est par
// nature perissable : mieux vaut la suivante que du retard accumule).
bool uart_link_send_telemetry(const telemetry_t *t)
{
int n;
int fields_dropped;
uint8_t cs;
if (uart_link_tx_busy())
{
s_tx_dropped++;
return false;
}
s_tx_frame[0] = '$';
s_tx_frame[1] = 'T';
n = 2;
fields_dropped = 0;
#define TX_ROOM() ((int)UART_LINE_MAX - UART_TX_TAIL_LEN - n)
#define TX_ADD(tag_, val_, dec_) \
do { \
int added_ = append_field(&s_tx_frame[n], TX_ROOM(), (tag_), (val_), \
(dec_)); \
if (added_ == 0) { fields_dropped++; } else { n += added_; } \
} while (0)
TX_ADD("FREQ1", t->freq1_hz, 0);
TX_ADD("FREQ2", t->freq2_hz, 0);
TX_ADD("DUTY1", t->duty1_pct, 1);
TX_ADD("DUTY2", t->duty2_pct, 1);
TX_ADD("VIN", t->vin_v, 1);
TX_ADD("IIN", t->iin_a, 2);
TX_ADD("V1", t->v1_v, 1);
TX_ADD("I1", t->i1_a, 2);
TX_ADD("T1", t->t1_c, 1);
TX_ADD("VOUT", t->vout_v, 1);
TX_ADD("I2", t->i2_a, 2);
TX_ADD("T2", t->t2_c, 1);
TX_ADD("IOUT", t->iout_a, 2);
{
int added_ = append_field_int(&s_tx_frame[n], TX_ROOM(), "FAULT",
(int)t->fault);
if (added_ == 0) { fields_dropped++; } else { n += added_; }
}
#undef TX_ADD
#undef TX_ROOM
if (fields_dropped != 0)
{
s_tx_truncated++;
}
// Checksum XOR sur le corps seul, entre '$' et '*' (docs/ESP32-UART.md).
// La place a ete reservee par UART_TX_TAIL_LEN, l'ecriture tient toujours.
cs = checksum_of(&s_tx_frame[1], n - 1);
n += snprintf(&s_tx_frame[n], (size_t)((int)UART_LINE_MAX - n),
"*%02X\n", cs);
s_tx_len = (uint16_t)n;
s_tx_pos = 0;
return true;
}
bool uart_link_tx_busy(void)
{
return (s_tx_pos < s_tx_len);
}
void uart_link_service_tx(void)
{
while ((s_tx_pos < s_tx_len)
&& (SciaRegs.SCIFFTX.bit.TXFFST < SCI_TX_FIFO_DEPTH))
{
SciaRegs.SCITXBUF = (uint16_t)(uint8_t)s_tx_frame[s_tx_pos];
s_tx_pos++;
}
}
// body pointe juste apres "C," ; len = nb de caracteres avant le '*'.
static bool parse_command(const char *body, command_state_t *cmd)
{
const char *p;
bool ht_found = false, pwm1_found = false, pwm2_found = false;
p = strstr(body, "HT=");
if (p != NULL)
{
cmd->ht_enabled = (p[3] == '1');
ht_found = true;
}
p = strstr(body, "PWM1=");
if (p != NULL)
{
cmd->pwm1_enabled = (p[5] == '1');
pwm1_found = true;
}
p = strstr(body, "PWM2=");
if (p != NULL)
{
cmd->pwm2_enabled = (p[5] == '1');
pwm2_found = true;
}
return ht_found && pwm1_found && pwm2_found;
}
// Decode EXACTEMENT deux chiffres hexadecimaux. Renvoie false si l'un des
// deux n'en est pas un.
//
// Remplace strtol, qui etait doublement piegeux ici :
// - s_line n'est jamais terminee par un '\0', donc strtol lisait au-dela
// de la trame, dans les residus de la ligne precedente ;
// - sur C28x, uint8_t fait 16 bits (pas d'adressage par octet), donc le
// cast (uint8_t) ne tronquait pas le resultat. Un "7D" suivi d'un "7D"
// residuel donnait 0x7D7D, retenu tel quel, et toute trame etait
// rejetee. Sur une architecture a octets le bug serait passe inapercu.
static bool parse_hex2(const char *s, uint8_t *out)
{
uint16_t value = 0U;
uint16_t i;
for (i = 0U; i < 2U; i++)
{
char c = s[i];
uint16_t digit;
if ((c >= '0') && (c <= '9'))
{
digit = (uint16_t)(c - '0');
}
else if ((c >= 'A') && (c <= 'F'))
{
digit = (uint16_t)(c - 'A' + 10);
}
else if ((c >= 'a') && (c <= 'f'))
{
digit = (uint16_t)(c - 'a' + 10);
}
else
{
return false;
}
value = (uint16_t)((value << 4) | digit);
}
*out = (uint8_t)(value & 0xFFU);
return true;
}
// s_line contient une ligne complete (sans le '\n' terminal).
static bool process_line(command_state_t *cmd)
{
int len = (int)s_line_len;
int i;
int star = -1;
uint8_t cs_calc, cs_recv;
if (len > 0 && s_line[len - 1] == '\r')
{
len--;
}
if (len < 4 || s_line[0] != '$')
{
return false;
}
for (i = 1; i < len; i++)
{
if (s_line[i] == '*')
{
star = i;
break;
}
}
if (star < 0 || (len - star) < 3)
{
return false; // pas de checksum complet
}
cs_calc = checksum_of(&s_line[1], star - 1);
if (!parse_hex2(&s_line[star + 1], &cs_recv))
{
return false; // checksum malforme -> trame ignoree
}
if (cs_calc != cs_recv)
{
return false; // trame corrompue -> ignoree silencieusement
}
if (s_line[1] != 'C' || s_line[2] != ',')
{
return false; // pas une trame de commande
}
s_line[star] = '\0';
return parse_command(&s_line[3], cmd);
}
// Compteur de recuperations, expose au debogueur : une valeur qui grimpe
// signale un probleme physique sur la ligne (baud, masse, niveaux), pas un
// simple alea.
static uint16_t s_rx_error_recoveries = 0;
// Sur C28x, un FE / OE / PE / BRKDT positionne SCIRXST.RXERROR, qui est
// LATCHE : le recepteur reste bloque tant qu'on ne fait pas un SW RESET du
// SCI (TRM SPRUI09A, SCICTL1.SWRESET) ou un reset systeme. Sans ce
// traitement, une seule perturbation sur la ligne arrete definitivement la
// reception -- constate au bring-up (RXERROR+FE+BRKDT latches, plus aucune
// trame $C recue ensuite).
static void scia_recover_if_rx_error(void)
{
if (SciaRegs.SCIRXST.bit.RXERROR == 0)
{
return;
}
SciaRegs.SCICTL1.bit.SWRESET = 0;
SciaRegs.SCICTL1.bit.SWRESET = 1;
SciaRegs.SCIFFRX.bit.RXFIFORESET = 0;
SciaRegs.SCIFFRX.bit.RXFIFORESET = 1;
SciaRegs.SCIFFRX.bit.RXFFOVRCLR = 1;
SciaRegs.SCIFFRX.bit.RXFFINTCLR = 1;
// La ligne en cours d'assemblage est forcement tronquee : on repart propre.
s_line_len = 0;
s_rx_error_recoveries++;
}
bool uart_link_poll(command_state_t *cmd)
{
bool got_command = false;
scia_recover_if_rx_error();
while (s_rx_tail != s_rx_head)
{
uint8_t c = s_rx_ring[s_rx_tail];
s_rx_tail = (uint16_t)((s_rx_tail + 1U) % UART_RX_RING_SIZE);
if (c == '\n')
{
if (process_line(cmd))
{
got_command = true;
}
s_line_len = 0;
}
else if (s_line_len < (UART_LINE_MAX - 1U))
{
s_line[s_line_len++] = (char)c;
}
else
{
// Ligne trop longue (> limite cote ESP32) : abandonnee.
s_line_len = 0;
}
}
return got_command;
}
interrupt void scia_rx_isr(void)
{
uint16_t next_head = (uint16_t)((s_rx_head + 1U) % UART_RX_RING_SIZE);
if (next_head != s_rx_tail)
{
s_rx_ring[s_rx_head] = (uint8_t)SciaRegs.SCIRXBUF.bit.RXDT;
s_rx_head = next_head;
}
else
{
(void)SciaRegs.SCIRXBUF.bit.RXDT; // anneau plein : octet jete
}
SciaRegs.SCIFFRX.bit.RXFFOVRCLR = 1;
SciaRegs.SCIFFRX.bit.RXFFINTCLR = 1;
PieCtrlRegs.PIEACK.all = PIEACK_GROUP9;
}

39
src/uart_link.h Normal file
View File

@ -0,0 +1,39 @@
#ifndef UART_LINK_H
#define UART_LINK_H
#include <stdbool.h>
#include "protocol.h"
void uart_link_init(void);
// Remet la liaison a plat sans reconfigurer le baud ni les broches :
// reset logiciel du SCI, vidage des FIFO, abandon de la ligne en cours
// d'assemblage et de la trame en attente d'emission.
//
// A appeler apres une interruption prolongee du CPU (EMUSTOP) : pendant la
// halte l'ESP32 a continue d'emettre, l'anneau de reception a deborde et
// la ligne en cours est tronquee. Repartir propre evite de traiter des
// fragments anterieurs a l'arret.
void uart_link_restart(void);
// Construit une trame $T,...*XX et la met en attente (docs/ESP32-UART.md).
// NE BLOQUE PAS : rien n'est emis ici, c'est uart_link_service_tx() qui
// alimente la ligne ensuite. Renvoie false si la trame precedente n'est pas
// encore partie -- la nouvelle est alors abandonnee.
bool uart_link_send_telemetry(const telemetry_t *t);
// Pousse dans la FIFO du SCI ce qui y rentre, puis rend la main
// immediatement. A appeler depuis la boucle principale, en tache de fond :
// l'emission ne doit jamais passer avant la regulation.
void uart_link_service_tx(void);
// true tant qu'une trame reste a emettre.
bool uart_link_tx_busy(void);
// A appeler dans la boucle principale (jamais en ISR). Si une trame
// $C,...*XX complete et de checksum valide a ete recue depuis le dernier
// appel, decode son contenu dans *cmd et renvoie true. Les trames
// corrompues sont ignorees silencieusement (voir docs/ESP32-UART.md).
bool uart_link_poll(command_state_t *cmd);
#endif