Files
formation-arduino/schemas/m03_pwm_adc.py
T
ArtemiaandClaude Opus 5.5 343a557332 Ajoute le générateur de schémas (schemdraw + matplotlib) et les figures du module 3
Charte commune (style.py), générateur SVG/PNG (generer.py) et 4 figures pour le module PWM & ADC : rapport cyclique, servo 50 Hz, montage du variateur, quantification ADC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 14:38:29 +02:00

154 lines
7.0 KiB
Python

"""Module 3 — Signaux analogiques : PWM & ADC."""
import numpy as np
import matplotlib.pyplot as plt
import schemdraw
import schemdraw.elements as elm
from style import BLEU, ORANGE, GRIS, ENCRE, VERT, CM, Figure
schemdraw.use("matplotlib")
def _signal_carre(rapport, periodes=3, points=2000):
t = np.linspace(0, periodes, points, endpoint=False)
return t, np.where((t % 1) < rapport, 3.3, 0.0)
def pwm_rapport_cyclique():
fig, axes = plt.subplots(3, 1, figsize=(16 * CM, 11 * CM), sharex=True)
for ax, rapport in zip(axes, (0.25, 0.50, 0.75)):
t, v = _signal_carre(rapport)
moyenne = 3.3 * rapport
ax.fill_between(t, v, step=None, color=BLEU, alpha=0.12)
ax.plot(t, v, color=BLEU)
ax.axhline(moyenne, color=ORANGE, ls="--", lw=1.8)
ax.text(3.04, moyenne, f"moyenne\n{round(moyenne + 1e-9, 2):.2f} V".replace(".", ","),
color=ORANGE, va="center", fontsize=9, fontweight="bold")
ax.set_ylim(-0.3, 4.1)
ax.set_yticks([0, 3.3], ["0 V", "3,3 V"])
ax.set_title(f"Rapport cyclique {int(rapport * 100)} % → analogWrite(pin, {round(rapport * 255)})",
loc="left", fontsize=10, pad=16 if rapport == 0.25 else 6)
ax.grid(axis="x")
# Cotes T et t_on sur le premier graphe
ax = axes[0]
ax.annotate("", xy=(1, 3.75), xytext=(2, 3.75),
arrowprops=dict(arrowstyle="<->", color=ENCRE, lw=1))
ax.text(1.5, 3.85, "période T", ha="center", va="bottom", fontsize=8.5, color=ENCRE)
ax.annotate("", xy=(0, 3.75), xytext=(0.25, 3.75),
arrowprops=dict(arrowstyle="<->", color=ENCRE, lw=1))
ax.text(0.125, 3.85, "t$_{on}$", ha="center", va="bottom", fontsize=8.5, color=ENCRE)
axes[-1].set_xticks([0, 1, 2, 3], ["0", "T", "2T", "3T"])
axes[-1].set_xlabel("temps (ex. T = 0,2 ms à 5 kHz)")
fig.tight_layout()
return fig
def servo_50hz():
fig, axes = plt.subplots(3, 1, figsize=(16 * CM, 9.5 * CM), sharex=True)
cas = ((1.0, "0°"), (1.5, "90°"), (2.0, "180°"))
t = np.linspace(0, 40, 4000, endpoint=False)
for ax, (largeur, angle) in zip(axes, cas):
v = np.where((t % 20) < largeur, 3.3, 0.0)
ax.plot(t, v, color=BLEU, lw=1.8)
ax.fill_between(t, v, color=BLEU, alpha=0.12)
ax.set_ylim(-0.3, 4.3)
ax.set_yticks([0, 3.3], ["0 V", "3,3 V"])
ax.text(22.8, 2.0, f"impulsion {str(largeur).replace('.', ',')} ms → bras à {angle}",
fontsize=9, color=ENCRE, va="center")
ax.grid(axis="x")
ax = axes[0]
ax.annotate("", xy=(0, 3.85), xytext=(20, 3.85),
arrowprops=dict(arrowstyle="<->", color=ORANGE, lw=1.2))
ax.text(10, 3.95, "période = 20 ms (50 Hz)", ha="center", va="bottom",
fontsize=9, color=ORANGE, fontweight="bold")
axes[-1].set_xticks([0, 5, 10, 15, 20, 25, 30, 35, 40])
axes[-1].set_xlabel("temps (ms)")
fig.tight_layout()
return fig
def montage_variateur():
d = schemdraw.Drawing(show=False)
d.config(fontsize=11, unit=2.5, color=ENCRE)
ic = elm.Ic(
pins=[
# schemdraw empile les broches de gauche de bas en haut
elm.IcPin(name="GND", side="left", anchorname="GNDL"),
elm.IcPin(name="GPIO1 (ADC1)", side="left", anchorname="ADC"),
elm.IcPin(name="3V3", side="left", anchorname="V33"),
elm.IcPin(name="GPIO4 (PWM)", side="right", anchorname="PWM", slot="1/3"),
],
size=(6.5, 4.2), pinfontsize=10, lblsize=13,
).label("ESP32-S3", loc="top", fontsize=14).color(BLEU)
d += ic
# Potentiomètre 10 kΩ entre 3V3 et GND, curseur vers GPIO1
haut = ic.V33
bas = ic.GNDL
d += elm.Line().at(haut).left().length(2.2)
p_haut = d.here
d += elm.Line().at(bas).left().length(2.2)
p_bas = d.here
d += (pot := elm.Potentiometer().at(p_haut).down().toy(p_bas).label("10 kΩ", loc="top").color(VERT))
d += elm.Wire("-").at(pot.tap).to((ic.ADC[0], pot.tap[1])).color(VERT)
d += elm.Ground().at(p_bas)
# Sortie PWM : résistance 220 Ω puis LED vers GND
d += elm.Line().at(ic.PWM).right().length(1)
d += elm.Resistor().right().label("220 Ω").color(ORANGE)
d += elm.LED().down().label("LED", loc="bottom").color(ORANGE)
d += elm.Ground()
return d.draw(show=False)
def adc_quantification():
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16 * CM, 7.5 * CM),
gridspec_kw={"width_ratios": [1, 1.25]})
# (a) principe de la quantification, illustré sur 3 bits
v = np.linspace(0, 3.3, 1000)
niveaux = 8
code = np.minimum(np.floor(v / 3.3 * niveaux), niveaux - 1)
ax1.plot(v, v / 3.3 * (niveaux - 1), color=GRIS, ls=":", lw=1.3, label="tension réelle")
ax1.step(v, code, where="post", color=BLEU, label="valeur numérique")
ax1.set_xlim(0, 3.3)
ax1.set_yticks(range(niveaux))
ax1.set_xlabel("tension d'entrée (V)")
ax1.set_ylabel("valeur lue")
ax1.set_title("(a) Principe : la tension est\ndécoupée en paliers", loc="left", fontsize=10)
ax1.text(2.35, 0.6, "illustré avec 8 paliers (3 bits)\nl'ESP32-S3 en a 4096 (12 bits)",
fontsize=7.5, color=GRIS, ha="center")
ax1.legend(fontsize=7.5, loc="upper left", frameon=False)
# (b) chaîne de conversion utilisée dans l'exercice
ax2.plot([0, 3.3], [0, 4095], color=BLEU)
ax2.set_xlim(0, 3.3)
ax2.set_ylim(0, 4095)
ax2.set_yticks([0, 1024, 2048, 3072, 4095])
ax2.set_xlabel("tension d'entrée (V)")
ax2.set_ylabel("analogRead()", color=BLEU)
sec = ax2.secondary_yaxis("right", functions=(lambda x: x * 255 / 4095, lambda y: y * 4095 / 255))
sec.set_ylabel("map() → PWM", color=ORANGE)
sec.set_yticks([0, 85, 170, 255])
sec.tick_params(colors=ORANGE)
sec.spines["right"].set_visible(True)
sec.spines["right"].set_color(ORANGE)
ax2.plot([1.65, 1.65, 3.3], [0, 2048, 2048], color=ORANGE, ls="--", lw=1.2)
ax2.plot(1.65, 2048, "o", color=ORANGE)
ax2.text(1.75, 1150, "1,65 V → 2048\nmap() → 127", color=ORANGE, fontsize=8.5, fontweight="bold")
ax2.set_title("(b) Lecture puis mise à l'échelle", loc="left", fontsize=10)
fig.tight_layout()
return fig
FIGURES = [
Figure("m03-pwm-rapport-cyclique",
"Le rapport cyclique fixe la tension moyenne perçue par la charge (LED, moteur)",
"1. Qu'est-ce que le PWM ?", pwm_rapport_cyclique),
Figure("m03-servo-50hz",
"Commande d'un servomoteur : la largeur de l'impulsion (1 à 2 ms) fixe l'angle",
"2.2 Approche explicite : API LEDC (contrôle fin)", servo_50hz, largeur_cm=15),
Figure("m03-montage-variateur",
"Câblage de l'exercice : potentiomètre sur GPIO1 (ADC1), LED sur GPIO4 (PWM)",
"3. Exercice : variateur de luminosité avec potentiomètre", montage_variateur, largeur_cm=14),
Figure("m03-adc-quantification",
"Conversion analogique-numérique puis mise à l'échelle avec map()",
"4. Le convertisseur analogique-numérique (ADC)", adc_quantification),
]