"""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", pwm_rapport_cyclique), Figure("m03-servo-50hz", servo_50hz), Figure("m03-montage-variateur", montage_variateur), Figure("m03-adc-quantification", adc_quantification), ]