From e2b005a2187286aa1716bd75d07243f91e637830 Mon Sep 17 00:00:00 2001 From: Junior Deogracias Date: Sun, 8 Nov 2026 10:28:22 +0000 Subject: [PATCH 1/3] DC motors: the dc_motor primitive and Pololu's 50:1 micro metal gearmotor M7's robot begins with its motors. dc_motor (core/analog/motor.c) is a brushed DC motor in the analog engine: the winding's resistance and inductance in series with the back EMF ke w, the rotor's inertia driven by kt i against friction and a load torque through a gearbox's ratio and efficiency, the current and the speed two extra unknowns integrated by the island's method with their truncation errors in its step control. At the operating point the rotor is at rest, so a motor powered at t = 0 starts at its stall current and spins up. emblink/pololu_3073_gearmotor takes its constants from Pololu's specifications at 6 V (650 rpm and 0.15 A unloaded, 1.5 A and 0.74 kg cm stalled; the best-efficiency point they give lands within 6 % of Pololu's); its four fixtures check no load, the start, a stall and reverse. --- CHANGELOG.md | 14 + .../pololu_3073_gearmotor/component.json | 261 +++++++++++++++++ .../pololu_3073_gearmotor/symbol.svg | 9 + .../pololu_3073_gearmotor/tests/no-load.json | 51 ++++ .../pololu_3073_gearmotor/tests/reverse.json | 46 +++ .../pololu_3073_gearmotor/tests/stall.json | 59 ++++ .../pololu_3073_gearmotor/tests/start.json | 51 ++++ core/analog/devices.c | 8 +- core/analog/motor.c | 263 ++++++++++++++++++ core/analog/named.h | 3 + docs/STATUS.md | 20 ++ .../SPEC-004-analog-engine-and-bridges.md | 18 ++ 12 files changed, 801 insertions(+), 2 deletions(-) create mode 100644 catalog/components/pololu_3073_gearmotor/component.json create mode 100644 catalog/components/pololu_3073_gearmotor/symbol.svg create mode 100644 catalog/components/pololu_3073_gearmotor/tests/no-load.json create mode 100644 catalog/components/pololu_3073_gearmotor/tests/reverse.json create mode 100644 catalog/components/pololu_3073_gearmotor/tests/stall.json create mode 100644 catalog/components/pololu_3073_gearmotor/tests/start.json create mode 100644 core/analog/motor.c diff --git a/CHANGELOG.md b/CHANGELOG.md index d035e1722..8632e4671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ versions follow semantic versioning. There is no release yet. ## Unreleased +### DC motors: the first of the robot's parts (M7) + +- Added the analog primitive `dc_motor` (`core/analog/motor.c`, SPEC-004): a + brushed DC motor whose winding (resistance, inductance, back EMF) and + rotor (inertia, friction, a gearbox's ratio and efficiency, a load torque + on the shaft) are solved with the circuit as two extra unknowns, so a + motor draws its stall current at start, spins up, slows under load and + generates when turned. +- Added `emblink/pololu_3073_gearmotor`, Pololu's 50:1 Micro Metal + Gearmotor HPCB 6V with extended shaft, its constants from Pololu's + specifications (650 rpm and 0.15 A unloaded, 1.5 A and 0.74 kg cm + stalled at 6 V; best efficiency within 6 % of Pololu's), with four + fixtures (no load, start, stall, reverse). + ### A library sorted the way an EDA sorts it - Parts are filed by category and sub-category as Proteus's device picker diff --git a/catalog/components/pololu_3073_gearmotor/component.json b/catalog/components/pololu_3073_gearmotor/component.json new file mode 100644 index 000000000..bf2ddc435 --- /dev/null +++ b/catalog/components/pololu_3073_gearmotor/component.json @@ -0,0 +1,261 @@ +{ + "schema_version": 1, + "kind": "component", + "id": "emblink/pololu_3073_gearmotor", + "version": "1.0.0", + "name": "Pololu 50:1 Micro Metal Gearmotor HPCB 6V (3073)", + "category": "electromechanical", + "subcategory": "motors_dc", + "description": "Pololu's 50:1 Micro Metal Gearmotor HPCB 6V with extended motor shaft (item 3073): a brushed DC motor with carbon brushes behind a 51.45:1 metal spur gearbox, 650 rpm and 0.15 A unloaded at 6 V, 1.5 A and 0.74 kg cm stalled. Its winding (4.0 ohm in series with the back EMF) and its rotor (inertia, friction, the gearbox's efficiency) are solved with the circuit, so it draws its stall current at start, spins up, slows and draws more under the shaft's load, and generates when turned. The extended shaft takes Pololu's magnetic encoder.", + "keywords": [ + "motor", + "DC motor", + "gearmotor", + "micro metal gearmotor", + "Pololu", + "3073", + "robot", + "N20", + "brushed" + ], + "pins": [ + { + "name": "+", + "number": 1, + "model": { + "kind": "passive" + }, + "doc": "Motor terminal; current in at + turns the output shaft positive" + }, + { + "name": "-", + "number": 2, + "model": { + "kind": "passive" + }, + "doc": "Motor terminal" + } + ], + "params": { + "r": { + "type": "number", + "unit": "ohm", + "default": 4.0, + "min": 0.1, + "max": 1000, + "doc": "Terminal resistance: 6 V over the 1.5 A stall current", + "advanced": true + }, + "ke": { + "type": "number", + "unit": "V s/rad", + "default": 0.001542, + "min": 1e-06, + "max": 1, + "doc": "Back-EMF constant at the rotor: (6 V - 4.0 ohm x 0.15 A) over 650 rpm x 51.45", + "advanced": true + }, + "kt": { + "type": "number", + "unit": "N m/A", + "default": 0.001542, + "min": 1e-06, + "max": 1, + "doc": "Torque constant at the rotor (equal to ke in SI units)", + "advanced": true + }, + "tf": { + "type": "number", + "unit": "N m", + "default": 0.0002313, + "min": 0, + "max": 1, + "doc": "Brush and bearing friction at the rotor: kt x the 0.15 A no-load current", + "advanced": true + }, + "eta": { + "type": "number", + "default": 0.678, + "min": 0.05, + "max": 1, + "doc": "The gearbox's efficiency: the 0.74 kg cm stall torque over the rotor's stall torque x 51.45", + "advanced": true + }, + "ratio": { + "type": "number", + "default": 51.45, + "min": 1, + "max": 10000, + "doc": "Gear ratio (the product's 50:1 is 51.45:1)", + "advanced": true + }, + "l": { + "type": "number", + "unit": "H", + "default": "1m", + "min": "1u", + "max": 1, + "doc": "Winding inductance: not published; an estimate for a motor of this size (L/R 0.25 ms)", + "advanced": true + }, + "j": { + "type": "number", + "unit": "kg m^2", + "default": 8e-09, + "min": 1e-12, + "max": 1, + "doc": "Rotor inertia: not published; an estimate from the armature's size (an 8 mm, 1 g rotor), giving a 13 ms mechanical time constant", + "advanced": true + } + }, + "inputs": [ + { + "name": "load", + "type": "number", + "unit": "N m", + "min": -1, + "max": 1, + "default": 0, + "doc": "Torque on the output shaft against its positive direction (a wheel's rolling resistance, a stalled hand; 0.0726 N m stalls it at 6 V)" + } + ], + "outputs": [ + { + "name": "current", + "type": "number", + "unit": "A" + }, + { + "name": "voltage", + "type": "number", + "unit": "V" + }, + { + "name": "rpm", + "type": "number", + "unit": "rpm", + "doc": "The output shaft's speed" + }, + { + "name": "speed", + "type": "number", + "unit": "rad/s", + "doc": "The output shaft's speed" + }, + { + "name": "rotor_speed", + "type": "number", + "unit": "rad/s" + }, + { + "name": "angle", + "type": "number", + "unit": "deg", + "doc": "The output shaft's angle since reset" + }, + { + "name": "torque", + "type": "number", + "unit": "N m", + "doc": "The torque the motor makes at the output shaft beyond its friction: turning the load, or accelerating the rotor while it spins up" + }, + { + "name": "power", + "type": "number", + "unit": "W", + "doc": "Electrical power in" + }, + { + "name": "mechanical_power", + "type": "number", + "unit": "W", + "doc": "Power at the output shaft" + } + ], + "model": { + "primitive": "dc_motor", + "bind": { + "r": "r", + "ke": "ke", + "kt": "kt", + "tf": "tf", + "eta": "eta", + "ratio": "ratio", + "l": "l", + "j": "j" + } + }, + "symbol": { + "svg": "symbol.svg", + "width": 60, + "height": 40, + "pins": { + "+": { + "x": 0, + "y": 20, + "side": "left" + }, + "-": { + "x": 60, + "y": 20, + "side": "right" + } + } + }, + "fidelity": { + "functional": null, + "timed": { + "claims": "As electrical.", + "omissions": [] + }, + "electrical": { + "claims": "A brushed DC motor solved with the circuit (the primitive dc_motor): 4.0 ohm and the winding's inductance in series with the back EMF ke w; the rotor's inertia driven by kt i against the brushes' friction and the load through the 51.45:1 gearbox at 67.8 % efficiency. The constants come from Pololu's specifications at 6 V: unloaded 650 rpm and 0.15 A, stalled 1.5 A and 0.74 kg cm; at the 0.42 A of best efficiency it gives 520 rpm, 0.148 kg cm and 31 % (Pololu: 490 rpm, 0.16 kg cm, 32 %). A motor powered at t = 0 starts from rest at its stall current; reversed it turns backwards; turned by its load it generates.", + "omissions": [ + "the winding's inductance and the rotor's inertia, which Pololu does not publish (estimates: 1 mH, 8e-9 kg m^2)", + "the commutator's current ripple and the cogging torque", + "the split of the no-load drag between Coulomb and viscous friction (all Coulomb: the no-load current does not rise with speed)", + "the gearbox's backlash and its different efficiency when back-driven", + "heating and the winding's resistance rising with it; damage from a long stall (the specifications warn that stalls could damage it)", + "the ±20 % speed and ±50 % no-load current spread between parts" + ], + "validation": [ + "tests/*.json" + ] + } + }, + "provenance": { + "author": "EmbSIM", + "license": "Apache-2.0", + "sources": [ + { + "title": "Pololu 50:1 Micro Metal Gearmotor HPCB 6V with Extended Motor Shaft, specifications", + "url": "https://www.pololu.com/product/3073/specs" + } + ] + }, + "tests": [ + "tests/*.json" + ], + "i18n": { + "fr": { + "source": "3d66bab36e38", + "description": "Le motoréducteur Micro Metal 50:1 HPCB 6V de Pololu à arbre moteur prolongé (référence 3073) : un moteur à courant continu à balais en carbone derrière un réducteur métallique à engrenages droits de 51,45:1, 650 tr/min et 0,15 A à vide sous 6 V, 1,5 A et 0,74 kg cm bloqué. Son bobinage (4,0 ohm en série avec la force contre-électromotrice) et son rotor (inertie, frottement, rendement du réducteur) sont résolus avec le circuit : il tire son courant de blocage au démarrage, prend de la vitesse, ralentit et tire davantage sous la charge de l'arbre, et génère quand on l'entraîne. L'arbre prolongé reçoit le codeur magnétique de Pololu.", + "fidelity": { + "timed": { + "claims": "Comme en électrique." + }, + "electrical": { + "claims": "Un moteur à courant continu à balais résolu avec le circuit (la primitive dc_motor) : 4,0 ohm et l'inductance du bobinage en série avec la force contre-électromotrice ke w ; l'inertie du rotor entraînée par kt i contre le frottement des balais et la charge, à travers le réducteur de 51,45:1 au rendement de 67,8 %. Les constantes viennent des spécifications de Pololu sous 6 V : à vide 650 tr/min et 0,15 A, bloqué 1,5 A et 0,74 kg cm ; aux 0,42 A du meilleur rendement il donne 520 tr/min, 0,148 kg cm et 31 % (Pololu : 490 tr/min, 0,16 kg cm, 32 %). Un moteur alimenté à t = 0 part du repos à son courant de blocage ; inversé il tourne en sens contraire ; entraîné par sa charge il génère.", + "omissions": [ + "l'inductance du bobinage et l'inertie du rotor, que Pololu ne publie pas (estimations : 1 mH, 8e-9 kg m^2)", + "l'ondulation du courant due au collecteur et le couple de détente", + "la répartition de la traînée à vide entre frottement sec et visqueux (tout en frottement sec : le courant à vide ne croît pas avec la vitesse)", + "le jeu du réducteur et son rendement différent quand il est entraîné par la charge", + "l'échauffement et la résistance du bobinage qui augmente avec lui ; les dégâts d'un blocage prolongé (les spécifications préviennent qu'un blocage peut endommager le moteur ou le réducteur)", + "la dispersion de ±20 % de la vitesse et de ±50 % du courant à vide d'une pièce à l'autre" + ] + } + } + } + } +} diff --git a/catalog/components/pololu_3073_gearmotor/symbol.svg b/catalog/components/pololu_3073_gearmotor/symbol.svg new file mode 100644 index 000000000..1409df225 --- /dev/null +++ b/catalog/components/pololu_3073_gearmotor/symbol.svg @@ -0,0 +1,9 @@ + + + + + + M + + + − + diff --git a/catalog/components/pololu_3073_gearmotor/tests/no-load.json b/catalog/components/pololu_3073_gearmotor/tests/no-load.json new file mode 100644 index 000000000..1db242216 --- /dev/null +++ b/catalog/components/pololu_3073_gearmotor/tests/no-load.json @@ -0,0 +1,51 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "no-load", + "description": "6 V and no load: the datasheet's 650 rpm and 0.15 A once spun up.", + "policy": "electrical", + "until": "400ms", + "circuit": { + "components": [ + { + "id": "V1", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "DUT", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VIN": [ + "V1.+", + "DUT.+" + ], + "GND": [ + "V1.-", + "DUT.-", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.rpm": { + "at": "300ms", + "min": 640, + "max": 660 + }, + "output:DUT.current": { + "at": "300ms", + "min": 0.145, + "max": 0.155 + } + } +} \ No newline at end of file diff --git a/catalog/components/pololu_3073_gearmotor/tests/reverse.json b/catalog/components/pololu_3073_gearmotor/tests/reverse.json new file mode 100644 index 000000000..a8876b345 --- /dev/null +++ b/catalog/components/pololu_3073_gearmotor/tests/reverse.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "reverse", + "description": "-6 V: the same 650 rpm the other way.", + "policy": "electrical", + "until": "400ms", + "circuit": { + "components": [ + { + "id": "V1", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": -6.0 + } + }, + { + "id": "DUT", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VIN": [ + "V1.+", + "DUT.+" + ], + "GND": [ + "V1.-", + "DUT.-", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.rpm": { + "at": "300ms", + "min": -660, + "max": -640 + } + } +} \ No newline at end of file diff --git a/catalog/components/pololu_3073_gearmotor/tests/stall.json b/catalog/components/pololu_3073_gearmotor/tests/stall.json new file mode 100644 index 000000000..c7df7853a --- /dev/null +++ b/catalog/components/pololu_3073_gearmotor/tests/stall.json @@ -0,0 +1,59 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "stall", + "description": "At 6 V with 0.0726 N m (0.74 kg cm) on the shaft: stalled, drawing 1.5 A.", + "policy": "electrical", + "until": "600ms", + "circuit": { + "components": [ + { + "id": "V1", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "DUT", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VIN": [ + "V1.+", + "DUT.+" + ], + "GND": [ + "V1.-", + "DUT.-", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.rpm": { + "at": "500ms", + "min": -15, + "max": 15 + }, + "output:DUT.current": { + "at": "500ms", + "min": 1.4, + "max": 1.5 + } + }, + "inputs": [ + { + "at": "0s", + "target": "DUT", + "input": "load", + "value": 0.0726 + } + ] +} \ No newline at end of file diff --git a/catalog/components/pololu_3073_gearmotor/tests/start.json b/catalog/components/pololu_3073_gearmotor/tests/start.json new file mode 100644 index 000000000..dc662a5f0 --- /dev/null +++ b/catalog/components/pololu_3073_gearmotor/tests/start.json @@ -0,0 +1,51 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "start", + "description": "Powered at t = 0 from rest it draws its 1.5 A stall current, and spins up on its mechanical time constant: past half speed within 20 ms, near its 650 rpm by 100 ms.", + "policy": "electrical", + "until": "150ms", + "circuit": { + "components": [ + { + "id": "V1", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "DUT", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VIN": [ + "V1.+", + "DUT.+" + ], + "GND": [ + "V1.-", + "DUT.-", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.current": { + "at": "10us", + "min": 1.3, + "max": 1.5 + }, + "output:DUT.rpm": { + "at": "100ms", + "min": 620, + "max": 660 + } + } +} \ No newline at end of file diff --git a/core/analog/devices.c b/core/analog/devices.c index 003006ade..3b72e49fa 100644 --- a/core/analog/devices.c +++ b/core/analog/devices.c @@ -56,7 +56,7 @@ const char *const *esk_adev_pin_names(const esk_adev_ops *ops, int *n) { "nmos", mos, 3 }, { "pmos", mos, 3 }, { "opamp", amp, 5 }, { "regulator", reg, 3 }, { "pull", one, 1 }, { "usb_meter", reg, 3 }, { "instrument_input", src, 2 }, { "power_profiler", ppk, 3 }, { "oscilloscope", scope, 5 }, { "multimeter", dmm, 4 }, { "transformer", xfmr, 4 }, { "transformer_ct", xfmr_ct, 5 }, - { "triac", triac, 3 }, { "diac", diac, 2 }, + { "triac", triac, 3 }, { "diac", diac, 2 }, { "dc_motor", src, 2 }, }; size_t i; for (i = 0; ops && i < sizeof table / sizeof table[0]; i++) @@ -3053,7 +3053,7 @@ static const esk_adev_ops *const device_table[] = { &esk_comparator_ops, &esk_temperature_ops, &esk_timer555_ops, &esk_environmental_ops, &esk_photodiode_ops, &esk_optocoupler_ops, &esk_relay_contacts_ops, &esk_spdt_ops, &esk_button4_ops, &esk_ptc_fuse_ops, &esk_usb_meter_ops, &instrument_ops, &esk_power_profiler_ops, &esk_oscilloscope_ops, - &esk_multimeter_ops, + &esk_multimeter_ops, &esk_dc_motor_ops, }; void esk_adev_on_time(esk_adev *d, es_time_t now) @@ -3088,6 +3088,10 @@ void esk_adev_on_input(esk_adev *d, const char *input, es_time_t now) esk_led_on_input(d, input, now); return; } + if (d && input && d->ops == &esk_dc_motor_ops) { + esk_motor_on_input(d, now); + return; + } if (!d || !input || strcmp(input, "run")) return; if (d->ops == &esk_oscilloscope_ops) diff --git a/core/analog/motor.c b/core/analog/motor.c new file mode 100644 index 000000000..0293f104e --- /dev/null +++ b/core/analog/motor.c @@ -0,0 +1,263 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Copyright 2026 Junior Deogracias */ +/* motor.c -- a permanent-magnet brushed DC motor, with or without a gearbox, + * as its datasheet's constants describe it (SPEC-004's devices, M7's robot). + * + * Electrically the motor is its winding's resistance r and inductance l in + * series with the back EMF ke w, w the rotor's speed (rad/s): v = r i + + * l di/dt + ke w across pins "+" and "-", the current flowing from + to - + * inside. The rotor is a mass of inertia j driven by kt i, held back by + * viscous damping b w, by the brushes' and bearings' friction tf (Coulomb, + * smoothed through tanh(w / w_s) so the Newton iteration sees a slope) and + * by the load: j dw/dt = kt i - b w - tf tanh(w / w_s) - load / (ratio eta), + * the input `load` being the torque on the output shaft against its + * positive direction (the gearbox, ratio:1 at efficiency eta, reflects it to + * the rotor). Positive current turns it positive; driven backwards it is a + * generator, its EMF pushing current into the circuit. + * + * Both equations are part of the island's system: the current and the speed + * are two extra unknowns, integrated by the island's own method (the + * winding's flux l i and the rotor's momentum j w through the integrator's + * companion, their truncation errors in the step control); the speed's row + * is divided by kt, so its residual is in amperes as the circuit's are. At + * the operating point the rotor holds its initial speed `w0` (0: at rest, + * so a motor powered at t = 0 draws its stall current and spins up, as on + * the bench). + * + * Outputs: the current, the rotor's and the output shaft's speed (rad/s and + * rpm), the output shaft's angle (degrees, from the speed integrated at each + * accepted step and on to now while the island sleeps at a steady speed), + * the torque the shaft delivers, and the electrical and mechanical power. + * + * Inside: the commutator's ripple (the current's dips as the brushes pass + * between segments) and the cogging torque are not modelled; the gearbox is + * an ideal ratio with one efficiency both ways (no backlash, no back-driving + * limit); the friction is smoothed below w_s; the winding's resistance does + * not heat. */ +#include "analog/named.h" +#include + +#define MOTOR_PI 3.14159265358979323846 + +typedef struct motor_state { + double r, l, ke, kt, j, b, tf, ws, ratio, eta, w0; + /* history of the winding's flux and of the rotor's momentum, and of + * their derivatives (the inductor's voltage, the inertial torque) */ + double phi_prev, vl_prev, vl_prev2, h_prev, t_prev, t_prev2; + double i_trial, w_trial, vl_trial, t_trial; + int nhist, trial_first, trial_negligible; + double angle; /* the output shaft's angle at t_acc, rad */ + double w_acc; /* the rotor's speed at t_acc, rad/s */ + double i_acc; /* the current at t_acc, A */ + es_time_t t_acc; /* the last accepted step's time */ + int have_acc; +} motor_state; + +#define MOTOR_LTE_I 1e-3 /* the current's error is held to lte_tol mA */ +#define MOTOR_LTE_W 1e-2 /* and the rotor's speed to lte_tol x 10 mrad/s */ + +static double par(const esk_adev *d, const char *name, double fallback) +{ + double v; + return d->comp && esk_comp_param_double(d->comp, name, &v) == 0 ? v : fallback; +} + +static double load_rotor(const esk_adev *d) +{ + const motor_state *s = (const motor_state *)d->state; + double t = d->comp ? esk_comp_input_double(d->comp, "load", 0.0) : 0.0; + return t / (s->ratio * s->eta); +} + +static double clampd(double v, double lo, double hi) { return v < lo ? lo : v > hi ? hi : v; } + +static es_status motor_setup(esk_adev *d) +{ + motor_state *s = (motor_state *)d->state; + s->r = par(d, "r", 3.75); + s->l = par(d, "l", 1e-3); + s->ke = par(d, "ke", 1e-3); + s->kt = par(d, "kt", s->ke); + s->j = par(d, "j", 1e-9); + s->b = par(d, "b", 0.0); + s->tf = par(d, "tf", 0.0); + s->ws = par(d, "w_s", 1.0); + s->ratio = par(d, "ratio", 1.0); + s->eta = par(d, "eta", 1.0); + s->w0 = par(d, "w0", 0.0); + if (!(s->r > 0.0)) + s->r = 1.0; + if (!(s->l > 0.0)) + s->l = 1e-6; + if (!(s->ke > 0.0)) + s->ke = 1e-3; + if (!(s->kt > 0.0)) + s->kt = s->ke; + if (!(s->j > 0.0)) + s->j = 1e-9; + if (s->b < 0.0) + s->b = 0.0; + if (s->tf < 0.0) + s->tf = 0.0; + if (!(s->ws > 0.0)) + s->ws = 1.0; + if (!(s->ratio > 0.0)) + s->ratio = 1.0; + s->eta = clampd(s->eta, 0.01, 1.0); + d->nbranch = 2; /* the current, the rotor's speed */ + return ES_OK; +} + +/* the friction and damping torque at speed w, and its slope */ +static void drag(const motor_state *s, double w, double *t, double *dt) +{ + double x = clampd(w / s->ws, -40.0, 40.0), th = tanh(x); + *t = s->b * w + s->tf * th; + *dt = s->b + s->tf / s->ws * (1.0 - th * th); +} + +/* KCL: the current leaves "+" into the motor and returns at "-" */ +static void stamp_kcl(esk_amatrix *m, int n1, int n2, int bi, double i) +{ + esk_am_stamp_j(m, n1, bi, 1.0); + esk_am_stamp_j(m, n2, bi, -1.0); + esk_am_stamp_f(m, n1, i); + esk_am_stamp_f(m, n2, -i); +} + +static void motor_load_dc(esk_adev *d, esk_amatrix *m) +{ + motor_state *s = (motor_state *)d->state; + int n1 = d->node[0], n2 = d->node[1], bi = d->branch[0], bw = d->branch[1]; + double i = esk_am_x(m, bi), w = esk_am_x(m, bw); + stamp_kcl(m, n1, n2, bi, i); + /* the winding: v - r i - ke w = 0 (the inductance a short at DC) */ + esk_am_stamp_j(m, bi, n1, 1.0); + esk_am_stamp_j(m, bi, n2, -1.0); + esk_am_stamp_j(m, bi, bi, -s->r); + esk_am_stamp_j(m, bi, bw, -s->ke); + esk_am_stamp_f(m, bi, esk_am_v(m, n1, n2) - s->r * i - s->ke * w); + /* the rotor holds its initial speed */ + esk_am_stamp_j(m, bw, bw, 1.0); + esk_am_stamp_f(m, bw, w - s->w0); + s->i_trial = i; + s->w_trial = w; + s->vl_trial = 0.0; + s->t_trial = 0.0; + s->trial_first = 1; + s->trial_negligible = 0; +} + +static void motor_load_tr(esk_adev *d, esk_amatrix *m, const esk_integ *in) +{ + motor_state *s = (motor_state *)d->state; + int n1 = d->node[0], n2 = d->node[1], bi = d->branch[0], bw = d->branch[1]; + double i = esk_am_x(m, bi), w = esk_am_x(m, bw); + double req, vl, geq, tin, td, dtd; + esk_integ_companion(in, s->l * i, s->phi_prev, s->vl_prev, s->l, &req, &vl); + esk_integ_companion(in, s->j * w, s->h_prev, s->t_prev, s->j, &geq, &tin); + stamp_kcl(m, n1, n2, bi, i); + /* the winding: v - r i - l di/dt - ke w = 0 */ + esk_am_stamp_j(m, bi, n1, 1.0); + esk_am_stamp_j(m, bi, n2, -1.0); + esk_am_stamp_j(m, bi, bi, -(s->r + req)); + esk_am_stamp_j(m, bi, bw, -s->ke); + esk_am_stamp_f(m, bi, esk_am_v(m, n1, n2) - s->r * i - vl - s->ke * w); + /* the rotor, divided by kt: (j dw/dt + drag + load) / kt - i = 0 */ + drag(s, w, &td, &dtd); + esk_am_stamp_j(m, bw, bw, (geq + dtd) / s->kt); + esk_am_stamp_j(m, bw, bi, -1.0); + esk_am_stamp_f(m, bw, (tin + td + load_rotor(d)) / s->kt - i); + s->i_trial = i; + s->w_trial = w; + s->vl_trial = vl; + s->t_trial = tin; + s->trial_first = esk_integ_first_order(in); + s->trial_negligible = esk_integ_negligible(in); +} + +static void motor_accept(esk_adev *d, es_time_t t) +{ + motor_state *s = (motor_state *)d->state; + if (s->trial_first) { + s->nhist = 1; + s->vl_prev2 = s->vl_prev; + s->t_prev2 = s->t_prev; + } else if (s->trial_negligible && s->nhist >= 1) { + /* replace the newest point: the spacing to the one before stays usable */ + } else { + if (s->nhist < 2) + s->nhist++; + s->vl_prev2 = s->vl_prev; + s->t_prev2 = s->t_prev; + } + s->vl_prev = s->vl_trial; + s->t_prev = s->t_trial; + s->phi_prev = s->l * s->i_trial; + s->h_prev = s->j * s->w_trial; + if (s->have_acc && t > s->t_acc) + s->angle += 0.5 * (s->w_acc + s->w_trial) * ((double)(t - s->t_acc) * 1e-12) / s->ratio; + s->w_acc = s->w_trial; + s->i_acc = s->i_trial; + s->t_acc = t; + s->have_acc = 1; + d->i_last[0] = s->i_trial; + d->i_last[1] = -s->i_trial; +} + +/* a reactive state's truncation error from its derivative's history: + * trapezoidal h^3/12 |x'''|, backward Euler h^2/2 |x''| */ +static double hist_lte(const esk_integ *in, double now, double prev, double prev2, int nhist) +{ + double h = esk_integ_h(in), hp = esk_integ_h_prev(in); + if (!(h > 0.0) || nhist < 1) + return 0.0; + if (esk_integ_first_order(in) || nhist < 2 || !(hp > 0.0)) + return 0.5 * h * fabs(now - prev); + { + double d1 = (now - prev) / h, d0 = (prev - prev2) / hp; + return h * h * h / 12.0 * fabs(2.0 * (d1 - d0) / (h + hp)); + } +} + +static double motor_lte(esk_adev *d, const esk_integ *in) +{ + const motor_state *s = (const motor_state *)d->state; + double ei = hist_lte(in, s->vl_trial, s->vl_prev, s->vl_prev2, s->nhist) / (s->l * MOTOR_LTE_I); + double ew = hist_lte(in, s->t_trial, s->t_prev, s->t_prev2, s->nhist) / (s->j * MOTOR_LTE_W); + return ei > ew ? ei : ew; +} + +static void motor_observe(esk_adev *d, esk_json *out) +{ + const motor_state *s = (const motor_state *)d->state; + esk_alloc *a = d->isl->a; + double w = s->w_acc, i = s->i_acc, angle = s->angle, td, dtd, v, wout, tout; + es_time_t now = esk_engine_now(d->isl->e); + if (s->have_acc && now > s->t_acc) /* asleep at a steady speed: on to now */ + angle += w * ((double)(now - s->t_acc) * 1e-12) / s->ratio; + drag(s, w, &td, &dtd); + v = esk_island_node_v(d->isl, d->node[0]) - esk_island_node_v(d->isl, d->node[1]); + wout = w / s->ratio; + /* the torque the output shaft delivers at a steady speed: the rotor's + * net torque through the gearbox */ + tout = (s->kt * i - td) * s->ratio * (s->kt * i - td >= 0.0 ? s->eta : 1.0 / s->eta); + esk_json_set_number(a, out, "current", i); + esk_json_set_number(a, out, "voltage", v); + esk_json_set_number(a, out, "rotor_speed", w); + esk_json_set_number(a, out, "speed", wout); + esk_json_set_number(a, out, "rpm", wout * 60.0 / (2.0 * MOTOR_PI)); + esk_json_set_number(a, out, "angle", angle * 180.0 / MOTOR_PI); + esk_json_set_number(a, out, "torque", tout); + esk_json_set_number(a, out, "power", v * i); + esk_json_set_number(a, out, "mechanical_power", tout * wout); +} + +void esk_motor_on_input(esk_adev *d, es_time_t now) +{ + esk_island_touch(d->isl, now); +} + +const esk_adev_ops esk_dc_motor_ops = { "dc_motor", sizeof(motor_state), motor_setup, motor_load_dc, + motor_load_tr, motor_accept, NULL, NULL, motor_lte, motor_observe, + NULL }; diff --git a/core/analog/named.h b/core/analog/named.h index a01710e0c..e62f66821 100644 --- a/core/analog/named.h +++ b/core/analog/named.h @@ -44,6 +44,9 @@ void esk_power_profiler_on_input(esk_adev *d, es_time_t now); void esk_oscilloscope_free(esk_adev *d); /* a PTC fuse's look at its temperatures (its component's wake) */ void esk_ptc_on_time(esk_adev *d, es_time_t now); +/* a brushed DC motor (motor.c): its load torque changed */ +extern const esk_adev_ops esk_dc_motor_ops; +void esk_motor_on_input(esk_adev *d, es_time_t now); void esk_environmental_on_time(esk_adev *d, es_time_t now); void esk_environmental_on_input(esk_adev *d, es_time_t now); #endif diff --git a/docs/STATUS.md b/docs/STATUS.md index 8bb7ecc85..0ea4c7404 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -5,6 +5,26 @@ maintained by hand until evidence tooling exists. It is not a generated support matrix. Specifications define target contracts; their acceptance does not mean the described behaviour exists. +## DC motors, the robot's first parts (2026-11-08) + +M7's robot on the floor begins with its motors. `dc_motor` +(`core/analog/motor.c`, SPEC-004) is a brushed DC motor in the analog +engine: the winding's resistance and inductance in series with the back +EMF, the rotor's inertia, friction and gearbox, the current and the speed +two extra unknowns of the island, a load torque on the output shaft as an +input. `emblink/pololu_3073_gearmotor` is Pololu's 50:1 Micro Metal +Gearmotor HPCB 6V (item 3073, the extended shaft that takes Pololu's +encoder) with the constants its specifications give at 6 V: 4.0 ohm, +ke = kt = 1.542 mN m/A at the rotor, the no-load current's friction, a +67.8 % efficient 51.45:1 gearbox. Evidence: its fixtures (`no-load`: 650 +rpm and 0.15 A; `start`: 1.43 A at power-on, 407 rpm, 63 % of full speed, +at its 13 ms mechanical time constant; `stall`: 1.5 A under 0.74 kg cm; +`reverse`), and the best-efficiency point the constants give, 520 rpm, +0.148 kg cm, 0.79 W, 31 % at 0.42 A, against Pololu's 490 rpm, 0.16 kg +cm, 0.80 W, 32 %. Not yet: the winding's inductance and rotor's inertia +measured (Pololu publishes neither: estimates), the commutator's ripple, +heating, a motor driver, the encoder, the robot itself. + ## The library's categories (2026-11-03) Parts are filed by category and sub-category as Proteus's device picker and diff --git a/docs/specs/SPEC-004-analog-engine-and-bridges.md b/docs/specs/SPEC-004-analog-engine-and-bridges.md index e8f6fd9dd..af3fd3987 100644 --- a/docs/specs/SPEC-004-analog-engine-and-bridges.md +++ b/docs/specs/SPEC-004-analog-engine-and-bridges.md @@ -190,6 +190,24 @@ has `A`, `K`, `C`, `E`, an LED knee and nominal CTR capped by collector saturati resistance; no galvanic input-output stamp is introduced. These are low-frequency approximations with per-entry limits, not full device or optical models. +`dc_motor` (pins `+`, `-`) is a permanent-magnet brushed DC motor, with an +optional gearbox: +- **Electrical.** The winding's resistance `r` and inductance `l` in series + with the back EMF `ke` w: v = r i + l di/dt + ke w, the current flowing + from `+` to `-` inside. +- **Mechanical.** The rotor's inertia `j` driven by `kt` i against viscous + damping `b`, Coulomb friction `tf` (smoothed by tanh(w / `w_s`)) and the + input `load`, the output shaft's torque reflected through `ratio` at + efficiency `eta`. +- **Unknowns.** The current and the rotor's speed are two extra unknowns, + integrated by the island's method through the integrator's companion, their + truncation errors in the step control; the speed's row is divided by `kt` + so its residual is in amperes. At the operating point the rotor holds `w0` + (0: a motor powered at t = 0 starts from rest). +- **Outputs.** The current, the rotor's and output shaft's speeds, the output + shaft's angle (integrated at accepted steps and on to now while the island + sleeps), its torque and the electrical and mechanical power. + `ptc_fuse` (pins `1`, `2`) is a polymeric PTC resettable fuse, a resistance that heats itself: - **Thermal model.** Two thermal nodes: the polymer core (`c_core`, `r_core` From 8709e31dcd875576efe3592ff4d74c2778250e55 Mon Sep 17 00:00:00 2001 From: Junior Deogracias Date: Sun, 8 Nov 2026 16:22:11 +0000 Subject: [PATCH 2/3] Motor drivers: the h_bridge primitive and TI's DRV8833 One bridge of an integrated motor driver solved with the circuit (core/analog/hbridge.c, SPEC-004): FETs from VM and to the sense return as on-resistances that follow VM, their body diodes, logic inputs read as voltages through Schmitt thresholds and pulldowns, the DRV8833, TB6612FNG and L293D truth tables, deglitch, propagation delay, dead time, nSLEEP and its wake, undervoltage lockout, slow-decay current chopping and overcurrent shutdown with an open-drain fault. emblink/h_bridge is the primitive as a part; emblink/drv8833 is two bridges with the datasheet's values (SLVSAR1E), with fixtures for the drive, timing, UVLO, a gearmotor, chopping, overcurrent, sleep and awake currents. An analog device takes up to 16 terminals, and a composite's pin declared nc needs no port. --- CHANGELOG.md | 18 +- catalog/components/drv8833/component.json | 395 +++++++++++ catalog/components/drv8833/symbol.svg | 21 + catalog/components/drv8833/tests/awake.json | 69 ++ .../components/drv8833/tests/chopping.json | 95 +++ catalog/components/drv8833/tests/motor.json | 90 +++ .../components/drv8833/tests/overcurrent.json | 126 ++++ catalog/components/drv8833/tests/sleep.json | 56 ++ catalog/components/h_bridge/component.json | 652 ++++++++++++++++++ catalog/components/h_bridge/symbol.svg | 15 + .../components/h_bridge/tests/forward.json | 94 +++ catalog/components/h_bridge/tests/timing.json | 168 +++++ catalog/components/h_bridge/tests/uvlo.json | 114 +++ core/analog/analog.h | 7 +- core/analog/devices.c | 15 +- core/analog/hbridge.c | 587 ++++++++++++++++ core/analog/mna.c | 4 +- core/analog/named.h | 7 + core/engine/engine.c | 6 +- core/project/loader.c | 4 +- docs/STATUS.md | 26 +- .../SPEC-004-analog-engine-and-bridges.md | 29 + 22 files changed, 2583 insertions(+), 15 deletions(-) create mode 100644 catalog/components/drv8833/component.json create mode 100644 catalog/components/drv8833/symbol.svg create mode 100644 catalog/components/drv8833/tests/awake.json create mode 100644 catalog/components/drv8833/tests/chopping.json create mode 100644 catalog/components/drv8833/tests/motor.json create mode 100644 catalog/components/drv8833/tests/overcurrent.json create mode 100644 catalog/components/drv8833/tests/sleep.json create mode 100644 catalog/components/h_bridge/component.json create mode 100644 catalog/components/h_bridge/symbol.svg create mode 100644 catalog/components/h_bridge/tests/forward.json create mode 100644 catalog/components/h_bridge/tests/timing.json create mode 100644 catalog/components/h_bridge/tests/uvlo.json create mode 100644 core/analog/hbridge.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 8632e4671..39c99b91f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ versions follow semantic versioning. There is no release yet. ## Unreleased -### DC motors: the first of the robot's parts (M7) +### DC motors and their driver: the first of the robot's parts (M7) - Added the analog primitive `dc_motor` (`core/analog/motor.c`, SPEC-004): a brushed DC motor whose winding (resistance, inductance, back EMF) and @@ -18,6 +18,22 @@ versions follow semantic versioning. There is no release yet. specifications (650 rpm and 0.15 A unloaded, 1.5 A and 0.74 kg cm stalled at 6 V; best efficiency within 6 % of Pololu's), with four fixtures (no load, start, stall, reverse). +- Added the analog primitive `h_bridge` (`core/analog/hbridge.c`, SPEC-004): + one bridge of a motor driver. Its FETs drive from VM through their + on-resistance, with body diodes, so a motor slows as its supply sags. + - logic inputs read as voltages through Schmitt thresholds and pulldowns + - the DRV8833, TB6612FNG and L293D truth tables + - deglitch, propagation delay and dead time + - nSLEEP and its wake time, undervoltage lockout + - current chopping on a sense resistor + - overcurrent shutdown with retry and an open-drain fault output +- Added `emblink/h_bridge` (the primitive as a part, the DRV8833's values) + and `emblink/drv8833`, Texas Instruments' DRV8833 dual H-bridge driver, + as two bridges with the datasheet's values, and eight fixtures (forward, + timing, UVLO; a gearmotor driven, current chopping at 0.4 A, overcurrent + and nFAULT, sleep and awake supply currents). +- An analog primitive takes up to 16 terminals (8 before), and a + composite's pin declared `nc` needs no port. ### A library sorted the way an EDA sorts it diff --git a/catalog/components/drv8833/component.json b/catalog/components/drv8833/component.json new file mode 100644 index 000000000..921f6ddd3 --- /dev/null +++ b/catalog/components/drv8833/component.json @@ -0,0 +1,395 @@ +{ + "schema_version": 1, + "kind": "component", + "id": "emblink/drv8833", + "version": "1.0.0", + "name": "DRV8833 dual H-bridge motor driver", + "category": "analog_ics", + "subcategory": "drivers", + "description": "Texas Instruments DRV8833 dual H-bridge motor driver (HTSSOP-16) for two brushed DC motors or one bipolar stepper, 2.7 to 10.8 V. Each bridge follows its xIN1/xIN2 pair (00 coast, 10 forward, 01 reverse, 11 brake) 1.1 us later, its outputs driving from VM through 200 + 160 mohm of FETs with their body diodes, so a motor slows as the supply sags and recirculates through the diodes while coasting. nSLEEP sleeps the device (1.6 uA) and wakes it in 1 ms; a sense resistor on xISEN limits the current to 200 mV / R by slow-decay chopping at 50 kHz; overcurrent shuts a bridge down for 1.35 ms with nFAULT low; VM below 2.6 V locks it out.", + "keywords": [ + "DRV8833", + "motor driver", + "h-bridge", + "dual h-bridge", + "dc motor", + "stepper", + "Texas Instruments", + "robot" + ], + "part": { + "manufacturer": "Texas Instruments", + "mpn": "DRV8833PWPR", + "package": "HTSSOP-16", + "datasheet": "https://www.ti.com/lit/ds/symlink/drv8833.pdf" + }, + "pins": [ + { + "name": "nSLEEP", + "number": 1, + "model": { + "kind": "passive" + }, + "doc": "Sleep mode input: high enables the device, low sleeps it and resets its logic; 500 kohm pulldown, clamped at 6.5 V" + }, + { + "name": "AOUT1", + "number": 2, + "model": { + "kind": "passive" + }, + "doc": "Bridge A output 1", + "required": false + }, + { + "name": "AISEN", + "number": 3, + "model": { + "kind": "passive" + }, + "doc": "Bridge A ground / current sense: to a sense resistor, or GND", + "required": false + }, + { + "name": "AOUT2", + "number": 4, + "model": { + "kind": "passive" + }, + "doc": "Bridge A output 2", + "required": false + }, + { + "name": "BOUT2", + "number": 5, + "model": { + "kind": "passive" + }, + "doc": "Bridge B output 2", + "required": false + }, + { + "name": "BISEN", + "number": 6, + "model": { + "kind": "passive" + }, + "doc": "Bridge B ground / current sense: to a sense resistor, or GND", + "required": false + }, + { + "name": "BOUT1", + "number": 7, + "model": { + "kind": "passive" + }, + "doc": "Bridge B output 1", + "required": false + }, + { + "name": "nFAULT", + "number": 8, + "model": { + "kind": "passive" + }, + "doc": "Fault output, open drain: low while a bridge is shut down by overcurrent", + "required": false + }, + { + "name": "BIN1", + "number": 9, + "model": { + "kind": "passive" + }, + "doc": "Bridge B input 1; 150 kohm pulldown", + "required": false + }, + { + "name": "BIN2", + "number": 10, + "model": { + "kind": "passive" + }, + "doc": "Bridge B input 2; 150 kohm pulldown", + "required": false + }, + { + "name": "VCP", + "number": 11, + "model": { + "kind": "nc" + }, + "doc": "High-side gate drive voltage: a 0.01 uF capacitor to VM (not modelled)", + "required": false + }, + { + "name": "VM", + "number": 12, + "model": { + "kind": "passive" + }, + "doc": "Device power supply, 2.7 to 10.8 V: a 10 uF bypass capacitor to GND" + }, + { + "name": "GND", + "number": 13, + "model": { + "kind": "passive" + }, + "doc": "Device ground (and the PowerPAD)" + }, + { + "name": "VINT", + "number": 14, + "model": { + "kind": "nc" + }, + "doc": "Internal supply bypass: a 2.2 uF capacitor to GND (not modelled)", + "required": false + }, + { + "name": "AIN2", + "number": 15, + "model": { + "kind": "passive" + }, + "doc": "Bridge A input 2; 150 kohm pulldown", + "required": false + }, + { + "name": "AIN1", + "number": 16, + "model": { + "kind": "passive" + }, + "doc": "Bridge A input 1; 150 kohm pulldown", + "required": false + } + ], + "params": {}, + "model": { + "composite": { + "components": [ + { + "id": "A", + "component": "emblink/h_bridge", + "params": { + "mode": "in_in", + "sleep": true, + "vtrip": 0.2, + "iocp": 3.3, + "rpd": 150000.0, + "vil": 0.7, + "vih": 2.0, + "vhys": 0.4, + "en_vil": 0.5, + "en_vih": 2.5, + "en_vhys": 0.4, + "rpd_en": 500000.0, + "en_clamp": 6.5, + "iq": 0.0017, + "iq_sleep": 1.6e-06 + } + }, + { + "id": "B", + "component": "emblink/h_bridge", + "params": { + "mode": "in_in", + "sleep": true, + "vtrip": 0.2, + "iocp": 3.3, + "rpd": 150000.0, + "vil": 0.7, + "vih": 2.0, + "vhys": 0.4, + "en_vil": 0.5, + "en_vih": 2.5, + "en_vhys": 0.4, + "rpd_en": 0, + "en_clamp": 0, + "iq": 0, + "iq_sleep": 0 + } + } + ], + "nets": { + "SLEEP": [ + "A.EN", + "B.EN" + ], + "VM": [ + "A.VM", + "B.VM" + ], + "GND": [ + "A.GND", + "B.GND", + "A.PWM", + "B.PWM" + ], + "FAULT": [ + "A.FAULT", + "B.FAULT" + ] + }, + "ports": { + "nSLEEP": "A.EN", + "AOUT1": "A.OUT1", + "AISEN": "A.SEN", + "AOUT2": "A.OUT2", + "BOUT2": "B.OUT2", + "BISEN": "B.SEN", + "BOUT1": "B.OUT1", + "nFAULT": "A.FAULT", + "BIN1": "B.IN1", + "BIN2": "B.IN2", + "VM": "A.VM", + "GND": "A.GND", + "AIN2": "A.IN2", + "AIN1": "A.IN1" + } + } + }, + "symbol": { + "svg": "symbol.svg", + "width": 100, + "height": 110, + "pins": { + "nSLEEP": { + "x": 0, + "y": 20, + "side": "left" + }, + "AIN1": { + "x": 0, + "y": 30, + "side": "left" + }, + "AIN2": { + "x": 0, + "y": 40, + "side": "left" + }, + "BIN1": { + "x": 0, + "y": 50, + "side": "left" + }, + "BIN2": { + "x": 0, + "y": 60, + "side": "left" + }, + "nFAULT": { + "x": 0, + "y": 80, + "side": "left" + }, + "AOUT1": { + "x": 100, + "y": 20, + "side": "right" + }, + "AOUT2": { + "x": 100, + "y": 30, + "side": "right" + }, + "BOUT1": { + "x": 100, + "y": 50, + "side": "right" + }, + "BOUT2": { + "x": 100, + "y": 60, + "side": "right" + }, + "VM": { + "x": 40, + "y": 0, + "side": "top" + }, + "VCP": { + "x": 60, + "y": 0, + "side": "top" + }, + "VINT": { + "x": 80, + "y": 0, + "side": "top" + }, + "GND": { + "x": 30, + "y": 110, + "side": "bottom" + }, + "AISEN": { + "x": 50, + "y": 110, + "side": "bottom" + }, + "BISEN": { + "x": 70, + "y": 110, + "side": "bottom" + } + } + }, + "fidelity": { + "functional": null, + "timed": { + "claims": "As electrical.", + "omissions": [] + }, + "electrical": { + "claims": "Two bridges solved with the circuit (the primitive h_bridge, by way of emblink/h_bridge): the datasheet's H-bridge logic, RDS(on) of 200 mohm high side and 160 mohm low side at 5 V (250 and 200 at 2.7 V), the body diodes, input thresholds (VIL 0.7, VIH 2 V; nSLEEP 0.5 and 2.5 V), 0.4 V hysteresis and pulldowns (150 kohm; nSLEEP 500 kohm and its 6.5 V clamp), the 450 ns deglitch, 1.1 us propagation delay and 450 ns dead time, sleep at 1.6 uA and the 1 ms wake, 1.7 mA awake, UVLO at 2.6 V with 90 mV hysteresis, 200 mV current chopping in slow decay on a 50 kHz clock with 3.75 us blanking, and per-bridge overcurrent shutdown at 3.3 A after 4 us with a 1.35 ms retry and nFAULT low.", + "omissions": [ + "VINT and VCP (the internal regulator and the charge pump: their capacitors are not needed, nor checked)", + "the die's temperature and thermal shutdown (TSD)", + "the FETs' rise and fall times (instant switching)", + "the FETs' analog current limiting during the overcurrent deglitch", + "an input's or the sense comparator's crossing between two of the circuit's steps is taken at the second" + ], + "validation": [ + "tests/*.json" + ] + } + }, + "provenance": { + "author": "EmbSIM", + "license": "Apache-2.0", + "sources": [ + { + "title": "Texas Instruments DRV8833 Dual H-Bridge Motor Driver datasheet (SLVSAR1E, July 2015)", + "url": "https://www.ti.com/lit/ds/symlink/drv8833.pdf" + } + ] + }, + "tests": [ + "tests/*.json" + ], + "i18n": { + "fr": { + "source": "c51b55f9cfbf", + "description": "Pilote de moteur double pont en H DRV8833 de Texas Instruments (HTSSOP-16) pour deux moteurs à courant continu à balais ou un moteur pas à pas bipolaire, de 2,7 à 10,8 V. Chaque pont suit sa paire xIN1/xIN2 (00 roue libre, 10 avant, 01 arrière, 11 frein) 1,1 µs plus tard, ses sorties alimentées depuis VM à travers 200 + 160 mΩ de MOSFET avec leurs diodes de structure : un moteur ralentit quand l'alimentation faiblit et son courant recircule par les diodes en roue libre. nSLEEP met le circuit en veille (1,6 µA) et le réveille en 1 ms ; une résistance de mesure sur xISEN limite le courant à 200 mV / R par découpage en décroissance lente à 50 kHz ; une surintensité coupe un pont pendant 1,35 ms avec nFAULT bas ; VM sous 2,6 V le verrouille.", + "fidelity": { + "timed": { + "claims": "Comme en électrique." + }, + "electrical": { + "claims": "Deux ponts résolus avec le circuit (la primitive h_bridge, par emblink/h_bridge) : la logique de pont en H de la fiche technique, RDS(on) de 200 mΩ côté haut et 160 mΩ côté bas à 5 V (250 et 200 à 2,7 V), les diodes de structure, les seuils d'entrée (VIL 0,7, VIH 2 V ; nSLEEP 0,5 et 2,5 V), 0,4 V d'hystérésis et les résistances de rappel (150 kΩ ; nSLEEP 500 kΩ et sa limitation à 6,5 V), le filtre anti-parasites de 450 ns, le délai de propagation de 1,1 µs et le temps mort de 450 ns, la veille à 1,6 µA et le réveil en 1 ms, 1,7 mA éveillé, le verrouillage en sous-tension à 2,6 V avec 90 mV d'hystérésis, le découpage du courant à 200 mV en décroissance lente sur une horloge de 50 kHz avec 3,75 µs de masquage, et la coupure en surintensité par pont à 3,3 A après 4 µs avec une reprise après 1,35 ms et nFAULT bas.", + "omissions": [ + "VINT et VCP (le régulateur interne et la pompe de charge : leurs condensateurs ne sont ni nécessaires ni vérifiés)", + "la température de la puce et la coupure thermique (TSD)", + "les temps de montée et de descente des MOSFET (commutation instantanée)", + "la limitation analogique du courant des MOSFET pendant le filtrage de la surintensité", + "le passage d'un seuil par une entrée ou par le comparateur de mesure entre deux pas du circuit est pris au second" + ] + } + } + } + } +} diff --git a/catalog/components/drv8833/symbol.svg b/catalog/components/drv8833/symbol.svg new file mode 100644 index 000000000..d3397924a --- /dev/null +++ b/catalog/components/drv8833/symbol.svg @@ -0,0 +1,21 @@ + + + + nSLEEP + AIN1 + AIN2 + BIN1 + BIN2 + nFAULT + AOUT1 + AOUT2 + BOUT1 + BOUT2 + VM + VCP + VINT + GND + AISEN + BISEN + DRV8833 + diff --git a/catalog/components/drv8833/tests/awake.json b/catalog/components/drv8833/tests/awake.json new file mode 100644 index 000000000..2934a8b13 --- /dev/null +++ b/catalog/components/drv8833/tests/awake.json @@ -0,0 +1,69 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "awake", + "description": "nSLEEP high and every input low: awake and coasting, 1.7 mA from VM at 5 V.", + "policy": "electrical", + "until": "3ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 5.0 + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/drv8833" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.nSLEEP" + ], + "GND": [ + "VM.-", + "VL.-", + "DUT.GND", + "DUT.AISEN", + "DUT.BISEN", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.A.asleep": { + "at": "2ms", + "equals": false + }, + "output:DUT.A.state": { + "at": "2ms", + "equals": "coast" + }, + "output:DUT.A.supply_current": { + "at": "2ms", + "min": 0.0016, + "max": 0.0018 + } + } +} \ No newline at end of file diff --git a/catalog/components/drv8833/tests/chopping.json b/catalog/components/drv8833/tests/chopping.json new file mode 100644 index 000000000..3924224a8 --- /dev/null +++ b/catalog/components/drv8833/tests/chopping.json @@ -0,0 +1,95 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "chopping", + "description": "A 0.5 ohm sense resistor on AISEN sets the chopping current to 200 mV / 0.5 ohm = 0.4 A: the gearmotor starting at 6 V, which would draw 1.38 A, is held near 0.4 A by slow-decay chopping on the 50 kHz clock while it spins up.", + "policy": "electrical", + "until": "8ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/drv8833" + }, + { + "id": "M", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "RS", + "component": "emblink/resistor", + "params": { + "resistance": 0.5 + } + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.AIN1", + "DUT.nSLEEP" + ], + "A": [ + "DUT.AOUT1", + "M.+" + ], + "B": [ + "DUT.AOUT2", + "M.-" + ], + "SEN": [ + "DUT.AISEN", + "RS.1" + ], + "GND": [ + "VM.-", + "VL.-", + "DUT.GND", + "RS.2", + "DUT.BISEN", + "DUT.AIN2", + "DUT.BIN1", + "DUT.BIN2", + "G.GND" + ] + } + }, + "expect": { + "output:M.current": { + "between": [ + "2ms", + "7ms" + ], + "min": 0.3, + "max": 0.45 + }, + "output:M.rpm": { + "at": "7ms", + "min": 5 + } + } +} \ No newline at end of file diff --git a/catalog/components/drv8833/tests/motor.json b/catalog/components/drv8833/tests/motor.json new file mode 100644 index 000000000..4226bc5a5 --- /dev/null +++ b/catalog/components/drv8833/tests/motor.json @@ -0,0 +1,90 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "motor", + "description": "A Pololu 50:1 micro metal gearmotor on bridge A at VM = 6 V, AIN1 high from the MCU's 3.3 V: after the 1 ms wake its current rises on the winding's L/R toward its stall current less the bridge's 0.36 ohm (6 / 4.36 = 1.38 A) and it spins up to about 640 rpm, a little under the 650 rpm it reaches straight from the supply; bridge B, its inputs low, coasts.", + "policy": "electrical", + "until": "150ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/drv8833" + }, + { + "id": "M", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.AIN1", + "DUT.nSLEEP" + ], + "A": [ + "DUT.AOUT1", + "M.+" + ], + "B": [ + "DUT.AOUT2", + "M.-" + ], + "GND": [ + "VM.-", + "VL.-", + "DUT.GND", + "DUT.AISEN", + "DUT.BISEN", + "DUT.AIN2", + "DUT.BIN1", + "DUT.BIN2", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.A.state": { + "at": "0.5ms", + "equals": "coast" + }, + "output:M.current": { + "at": "1.8ms", + "min": 1.15, + "max": 1.38 + }, + "output:M.rpm": { + "at": "140ms", + "min": 620, + "max": 650 + }, + "output:DUT.B.state": { + "at": "140ms", + "equals": "coast" + } + } +} \ No newline at end of file diff --git a/catalog/components/drv8833/tests/overcurrent.json b/catalog/components/drv8833/tests/overcurrent.json new file mode 100644 index 000000000..ba2e51fb0 --- /dev/null +++ b/catalog/components/drv8833/tests/overcurrent.json @@ -0,0 +1,126 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "overcurrent", + "description": "Bridge A's outputs shorted through 0.1 ohm (13 A from 6 V) trip its overcurrent protection 4 us after the 1 ms wake: the bridge turns off and nFAULT, pulled up to 3.3 V, goes low; 1.35 ms later it retries, nFAULT rises, and the short trips it again. Bridge B drives its 10 ohm load throughout.", + "policy": "electrical", + "until": "3ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/drv8833" + }, + { + "id": "RSH", + "component": "emblink/resistor", + "params": { + "resistance": 0.1 + } + }, + { + "id": "RL", + "component": "emblink/resistor", + "params": { + "resistance": 10 + } + }, + { + "id": "RP", + "component": "emblink/resistor", + "params": { + "resistance": 10000 + } + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.AIN1", + "DUT.BIN1", + "DUT.nSLEEP", + "RP.1" + ], + "A1": [ + "DUT.AOUT1", + "RSH.1" + ], + "A2": [ + "DUT.AOUT2", + "RSH.2" + ], + "B1": [ + "DUT.BOUT1", + "RL.1" + ], + "B2": [ + "DUT.BOUT2", + "RL.2" + ], + "NFAULT": [ + "DUT.nFAULT", + "RP.2" + ], + "GND": [ + "VM.-", + "VL.-", + "DUT.GND", + "DUT.AISEN", + "DUT.BISEN", + "DUT.AIN2", + "DUT.BIN2", + "G.GND" + ] + } + }, + "expect": { + "net:NFAULT": { + "at": "0.9ms", + "min_v": 3.2 + }, + "output:DUT.A.fault": { + "at": "1.5ms", + "equals": true + }, + "output:DUT.A.state": { + "at": "1.5ms", + "equals": "coast" + }, + "output:DUT.B.state": { + "at": "1.5ms", + "equals": "forward" + }, + "output:DUT.A.chopping": { + "at": "2.5ms", + "equals": false + }, + "output:DUT.A.current": { + "at": "2.356ms", + "min": 10 + } + } +} \ No newline at end of file diff --git a/catalog/components/drv8833/tests/sleep.json b/catalog/components/drv8833/tests/sleep.json new file mode 100644 index 000000000..87c24e562 --- /dev/null +++ b/catalog/components/drv8833/tests/sleep.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "sleep", + "description": "nSLEEP left low (its 500 kohm pulldown): both bridges asleep, outputs off, 1.6 uA from VM at 5 V. Bridge A's quiescent current is the device's.", + "policy": "electrical", + "until": "1ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 5.0 + } + }, + { + "id": "DUT", + "component": "emblink/drv8833" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "GND": [ + "VM.-", + "DUT.GND", + "DUT.AISEN", + "DUT.BISEN", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.A.asleep": { + "at": "0.5ms", + "equals": true + }, + "output:DUT.B.state": { + "at": "0.5ms", + "equals": "coast" + }, + "output:DUT.A.supply_current": { + "at": "0.5ms", + "min": 1.5e-06, + "max": 1.8e-06 + } + } +} \ No newline at end of file diff --git a/catalog/components/h_bridge/component.json b/catalog/components/h_bridge/component.json new file mode 100644 index 000000000..20fc42df0 --- /dev/null +++ b/catalog/components/h_bridge/component.json @@ -0,0 +1,652 @@ +{ + "schema_version": 1, + "kind": "component", + "id": "emblink/h_bridge", + "version": "1.0.0", + "name": "H-bridge (motor driver bridge)", + "category": "analog_ics", + "subcategory": "drivers", + "description": "One H-bridge of an integrated motor driver: two half-bridges of FETs with their body diodes between VM and the low-side return, driven by logic inputs read as voltages through Schmitt thresholds and pulldowns, with the datasheet's truth table (DRV8833 IN/IN, TB6612FNG IN/IN/PWM, or L293D half-H with enable), deglitch, propagation delay, dead time, sleep and wake, undervoltage lockout, current chopping on a sense resistor and overcurrent shutdown with an open-drain fault output. The outputs drive from VM through the FETs' on-resistance, so a motor slows as its supply sags. Defaults are the DRV8833's.", + "keywords": [ + "h-bridge", + "motor driver", + "half-bridge", + "dc motor", + "DRV8833", + "TB6612FNG" + ], + "pins": [ + { + "name": "IN1", + "number": 1, + "model": { + "kind": "passive" + }, + "doc": "Logic input 1", + "required": false + }, + { + "name": "IN2", + "number": 2, + "model": { + "kind": "passive" + }, + "doc": "Logic input 2", + "required": false + }, + { + "name": "PWM", + "number": 3, + "model": { + "kind": "passive" + }, + "doc": "PWM input (mode in_in_pwm)", + "required": false + }, + { + "name": "EN", + "number": 4, + "model": { + "kind": "passive" + }, + "doc": "nSLEEP (sleep true) or enable/standby", + "required": false + }, + { + "name": "OUT1", + "number": 5, + "model": { + "kind": "passive" + }, + "doc": "Output 1", + "required": false + }, + { + "name": "OUT2", + "number": 6, + "model": { + "kind": "passive" + }, + "doc": "Output 2", + "required": false + }, + { + "name": "VM", + "number": 7, + "model": { + "kind": "passive" + }, + "doc": "Motor supply" + }, + { + "name": "GND", + "number": 8, + "model": { + "kind": "passive" + }, + "doc": "Logic ground" + }, + { + "name": "SEN", + "number": 9, + "model": { + "kind": "passive" + }, + "doc": "Low-side return: the sense resistor, or GND", + "required": false + }, + { + "name": "FAULT", + "number": 10, + "model": { + "kind": "passive" + }, + "doc": "Fault output, open drain, low while a fault is asserted", + "required": false + } + ], + "params": { + "mode": { + "type": "enum", + "enum": [ + "in_in", + "in_in_pwm", + "half" + ], + "default": "in_in", + "doc": "The truth table: in_in (DRV8833: 00 coast, 10 forward, 01 reverse, 11 brake), in_in_pwm (TB6612FNG: PWM low or 11 short brake, 00 off), half (L293D: each output follows its input while EN is high)" + }, + "sleep": { + "type": "boolean", + "default": true, + "doc": "EN is nSLEEP: low sleeps the bridge and resets its logic, t_wake passes after it rises; otherwise EN is a logic input of the table" + }, + "ron_hs": { + "type": "number", + "unit": "ohm", + "default": 0.2, + "min": 0.001, + "max": 100, + "doc": "High-side FET on-resistance at vm_hi", + "advanced": true + }, + "ron_ls": { + "type": "number", + "unit": "ohm", + "default": 0.16, + "min": 0.001, + "max": 100, + "doc": "Low-side FET on-resistance at vm_hi", + "advanced": true + }, + "ron_hs_low": { + "type": "number", + "unit": "ohm", + "default": 0.25, + "min": 0.001, + "max": 100, + "doc": "High-side FET on-resistance at vm_lo", + "advanced": true + }, + "ron_ls_low": { + "type": "number", + "unit": "ohm", + "default": 0.2, + "min": 0.001, + "max": 100, + "doc": "Low-side FET on-resistance at vm_lo", + "advanced": true + }, + "vm_hi": { + "type": "number", + "unit": "V", + "default": 5.0, + "min": 0.1, + "max": 100, + "doc": "VM of the on-resistances ron_hs and ron_ls", + "advanced": true + }, + "vm_lo": { + "type": "number", + "unit": "V", + "default": 2.7, + "min": 0, + "max": 100, + "doc": "VM of the on-resistances ron_hs_low and ron_ls_low (between the two points they follow VM)", + "advanced": true + }, + "roff": { + "type": "number", + "unit": "ohm", + "default": 100000000.0, + "min": 1000.0, + "max": 1000000000000.0, + "doc": "Off-state leakage resistance of each FET (the datasheet's IOFF is at most 1 uA)", + "advanced": true + }, + "bd_vf": { + "type": "number", + "unit": "V", + "default": 0.8, + "min": 0.1, + "max": 3, + "doc": "Body diode forward voltage at bd_if", + "advanced": true + }, + "bd_if": { + "type": "number", + "unit": "A", + "default": 1.0, + "min": 1e-06, + "max": 100, + "doc": "Current of bd_vf", + "advanced": true + }, + "bd_n": { + "type": "number", + "default": 1.2, + "min": 0.5, + "max": 5, + "doc": "Body diode emission coefficient", + "advanced": true + }, + "vil": { + "type": "number", + "unit": "V", + "default": 0.7, + "min": 0, + "max": 50, + "doc": "Logic input low threshold (VIL max)", + "advanced": true + }, + "vih": { + "type": "number", + "unit": "V", + "default": 2.0, + "min": 0, + "max": 50, + "doc": "Logic input high threshold (VIH min)", + "advanced": true + }, + "vhys": { + "type": "number", + "unit": "V", + "default": 0.4, + "min": 0, + "max": 10, + "doc": "Logic input hysteresis about the thresholds' mean", + "advanced": true + }, + "en_vil": { + "type": "number", + "unit": "V", + "default": 0.5, + "min": 0, + "max": 50, + "doc": "EN input low threshold", + "advanced": true + }, + "en_vih": { + "type": "number", + "unit": "V", + "default": 2.5, + "min": 0, + "max": 50, + "doc": "EN input high threshold", + "advanced": true + }, + "en_vhys": { + "type": "number", + "unit": "V", + "default": 0.4, + "min": 0, + "max": 10, + "doc": "EN input hysteresis", + "advanced": true + }, + "rpd": { + "type": "number", + "unit": "ohm", + "default": 150000.0, + "min": 0, + "max": 1000000000000.0, + "doc": "Pulldown of IN1, IN2 and PWM (0: none)", + "advanced": true + }, + "rpd_en": { + "type": "number", + "unit": "ohm", + "default": 500000.0, + "min": 0, + "max": 1000000000000.0, + "doc": "Pulldown of EN (0: none)", + "advanced": true + }, + "en_clamp": { + "type": "number", + "unit": "V", + "default": 0, + "min": 0, + "max": 100, + "doc": "EN's clamping Zener voltage (0: none)", + "advanced": true + }, + "iq": { + "type": "number", + "unit": "A", + "default": 0.0017, + "min": 0, + "max": 1, + "doc": "VM supply current while awake", + "advanced": true + }, + "iq_sleep": { + "type": "number", + "unit": "A", + "default": 1.6e-06, + "min": 0, + "max": 1, + "doc": "VM supply current asleep", + "advanced": true + }, + "uvlo": { + "type": "number", + "unit": "V", + "default": 2.6, + "min": 0, + "max": 100, + "doc": "VM undervoltage lockout, falling", + "advanced": true + }, + "uvlo_hys": { + "type": "number", + "unit": "V", + "default": 0.09, + "min": 0, + "max": 10, + "doc": "VM undervoltage lockout hysteresis", + "advanced": true + }, + "vtrip": { + "type": "number", + "unit": "V", + "default": 0, + "min": 0, + "max": 10, + "doc": "SEN trip voltage of the current chopping (0: none)", + "advanced": true + }, + "f_chop": { + "type": "number", + "unit": "Hz", + "default": 50000.0, + "min": 1, + "max": 100000000.0, + "doc": "Current chopping clock", + "advanced": true + }, + "t_blank": { + "type": "number", + "unit": "s", + "default": 3.75e-06, + "min": 0, + "max": 1, + "doc": "Current sense blanking time", + "advanced": true + }, + "iocp": { + "type": "number", + "unit": "A", + "default": 0, + "min": 0, + "max": 1000, + "doc": "Overcurrent trip level of each FET (0: none)", + "advanced": true + }, + "t_ocp_deg": { + "type": "number", + "unit": "s", + "default": 4e-06, + "min": 0, + "max": 1, + "doc": "Overcurrent deglitch time", + "advanced": true + }, + "t_ocp": { + "type": "number", + "unit": "s", + "default": 0.00135, + "min": 0, + "max": 10, + "doc": "Overcurrent retry period", + "advanced": true + }, + "fault_r": { + "type": "number", + "unit": "ohm", + "default": 100, + "min": 1, + "max": 1000000.0, + "doc": "FAULT pull-down resistance while asserted", + "advanced": true + }, + "t_deg": { + "type": "number", + "unit": "s", + "default": 4.5e-07, + "min": 0, + "max": 1, + "doc": "Input deglitch time: shorter pulses are lost", + "advanced": true + }, + "t_prop": { + "type": "number", + "unit": "s", + "default": 1.1e-06, + "min": 0, + "max": 1, + "doc": "Propagation delay from an input to the outputs (at least t_deg)", + "advanced": true + }, + "t_dead": { + "type": "number", + "unit": "s", + "default": 4.5e-07, + "min": 0, + "max": 1, + "doc": "Dead time of an output passing between high and low", + "advanced": true + }, + "t_wake": { + "type": "number", + "unit": "s", + "default": 0.001, + "min": 0, + "max": 10, + "doc": "Start-up time from nSLEEP high (sleep true)", + "advanced": true + } + }, + "outputs": [ + { + "name": "state", + "type": "enum", + "enum": [ + "forward", + "reverse", + "brake", + "coast", + "half" + ], + "doc": "forward OUT1 high OUT2 low, reverse the other way, brake both low, coast both off; half for any other pair" + }, + { + "name": "out1", + "type": "enum", + "enum": [ + "high", + "low", + "off" + ] + }, + { + "name": "out2", + "type": "enum", + "enum": [ + "high", + "low", + "off" + ] + }, + { + "name": "current", + "type": "number", + "unit": "A", + "doc": "Out of OUT1 into the load" + }, + { + "name": "vout", + "type": "number", + "unit": "V", + "doc": "OUT1 - OUT2" + }, + { + "name": "supply_current", + "type": "number", + "unit": "A", + "doc": "Into VM" + }, + { + "name": "power", + "type": "number", + "unit": "W", + "doc": "Dissipated in the bridge" + }, + { + "name": "fault", + "type": "boolean" + }, + { + "name": "asleep", + "type": "boolean" + }, + { + "name": "chopping", + "type": "boolean" + }, + { + "name": "undervoltage", + "type": "boolean" + } + ], + "model": { + "primitive": "h_bridge", + "bind": { + "mode": "mode", + "sleep": "sleep", + "ron_hs": "ron_hs", + "ron_ls": "ron_ls", + "ron_hs_low": "ron_hs_low", + "ron_ls_low": "ron_ls_low", + "vm_hi": "vm_hi", + "vm_lo": "vm_lo", + "roff": "roff", + "bd_vf": "bd_vf", + "bd_if": "bd_if", + "bd_n": "bd_n", + "vil": "vil", + "vih": "vih", + "vhys": "vhys", + "en_vil": "en_vil", + "en_vih": "en_vih", + "en_vhys": "en_vhys", + "rpd": "rpd", + "rpd_en": "rpd_en", + "en_clamp": "en_clamp", + "iq": "iq", + "iq_sleep": "iq_sleep", + "uvlo": "uvlo", + "uvlo_hys": "uvlo_hys", + "vtrip": "vtrip", + "f_chop": "f_chop", + "t_blank": "t_blank", + "iocp": "iocp", + "t_ocp_deg": "t_ocp_deg", + "t_ocp": "t_ocp", + "fault_r": "fault_r", + "t_deg": "t_deg", + "t_prop": "t_prop", + "t_dead": "t_dead", + "t_wake": "t_wake" + } + }, + "symbol": { + "svg": "symbol.svg", + "width": 80, + "height": 100, + "pins": { + "IN1": { + "x": 0, + "y": 20, + "side": "left" + }, + "IN2": { + "x": 0, + "y": 30, + "side": "left" + }, + "PWM": { + "x": 0, + "y": 40, + "side": "left" + }, + "EN": { + "x": 0, + "y": 50, + "side": "left" + }, + "FAULT": { + "x": 0, + "y": 70, + "side": "left" + }, + "OUT1": { + "x": 80, + "y": 30, + "side": "right" + }, + "OUT2": { + "x": 80, + "y": 60, + "side": "right" + }, + "VM": { + "x": 40, + "y": 0, + "side": "top" + }, + "GND": { + "x": 30, + "y": 100, + "side": "bottom" + }, + "SEN": { + "x": 50, + "y": 100, + "side": "bottom" + } + } + }, + "fidelity": { + "functional": null, + "timed": { + "claims": "As electrical.", + "omissions": [] + }, + "electrical": { + "claims": "Solved with the circuit (the primitive h_bridge): each output's high-side FET from VM and low-side FET to SEN as on-resistances set from VM between two datasheet points, their body diodes, the inputs' thresholds, hysteresis and pulldowns, the truth table of its mode, the deglitch filter, propagation delay and dead time, nSLEEP's wake time, the undervoltage lockout, slow-decay current chopping on the f_chop clock with blanking, and the overcurrent shutdown with its retry and the open-drain FAULT.", + "omissions": [ + "the FETs' rise and fall times (instant switching)", + "the FETs' analog current limiting during the overcurrent deglitch", + "the die's temperature and thermal shutdown", + "an input's crossing between two of the island's steps is taken at the second" + ], + "validation": [ + "tests/*.json" + ] + } + }, + "provenance": { + "author": "EmbSIM", + "license": "Apache-2.0", + "sources": [ + { + "title": "Texas Instruments DRV8833 Dual H-Bridge Motor Driver datasheet (SLVSAR1E)", + "url": "https://www.ti.com/lit/ds/symlink/drv8833.pdf" + } + ] + }, + "tests": [ + "tests/*.json" + ], + "i18n": { + "fr": { + "source": "b3764521b87b", + "description": "Un pont en H d'un pilote de moteur intégré : deux demi-ponts de MOSFET avec leurs diodes de structure entre VM et le retour bas, commandés par des entrées logiques lues en tensions à travers des seuils à hystérésis et des résistances de rappel, avec la table de vérité de la fiche technique (DRV8833 IN/IN, TB6612FNG IN/IN/PWM ou L293D demi-pont avec validation), le filtre anti-parasites, le délai de propagation, le temps mort, la veille et le réveil, le verrouillage en sous-tension, le découpage du courant sur une résistance de mesure et la coupure en surintensité avec une sortie de défaut à drain ouvert. Les sorties sont alimentées depuis VM à travers la résistance à l'état passant des MOSFET : un moteur ralentit quand son alimentation faiblit. Valeurs par défaut : celles du DRV8833.", + "fidelity": { + "timed": { + "claims": "Comme en électrique." + }, + "electrical": { + "claims": "Résolu avec le circuit (la primitive h_bridge) : le MOSFET haut de chaque sortie depuis VM et le MOSFET bas vers SEN, en résistances à l'état passant fixées selon VM entre deux points de la fiche technique, leurs diodes de structure, les seuils, l'hystérésis et les résistances de rappel des entrées, la table de vérité de son mode, le filtre anti-parasites, le délai de propagation et le temps mort, le temps de réveil de nSLEEP, le verrouillage en sous-tension, le découpage du courant en décroissance lente sur l'horloge f_chop avec masquage, et la coupure en surintensité avec sa reprise et la sortie FAULT à drain ouvert.", + "omissions": [ + "les temps de montée et de descente des MOSFET (commutation instantanée)", + "la limitation analogique du courant des MOSFET pendant le filtrage de la surintensité", + "la température de la puce et la coupure thermique", + "le passage d'un seuil par une entrée entre deux pas de l'îlot est pris au second" + ] + } + } + } + } +} diff --git a/catalog/components/h_bridge/symbol.svg b/catalog/components/h_bridge/symbol.svg new file mode 100644 index 000000000..b1793c7ac --- /dev/null +++ b/catalog/components/h_bridge/symbol.svg @@ -0,0 +1,15 @@ + + + + IN1 + IN2 + PWM + EN + FAULT + OUT1 + OUT2 + VM + GND + SEN + H-bridge + diff --git a/catalog/components/h_bridge/tests/forward.json b/catalog/components/h_bridge/tests/forward.json new file mode 100644 index 000000000..09659ea38 --- /dev/null +++ b/catalog/components/h_bridge/tests/forward.json @@ -0,0 +1,94 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "forward", + "description": "IN1 high, IN2 low, nSLEEP high: after the 1 ms wake the bridge drives a 10 ohm load forward from VM through both on-resistances (0.2 + 0.16 ohm at 5 V): 4.83 V across it, 483 mA.", + "policy": "electrical", + "until": "3ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 5.0 + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/h_bridge" + }, + { + "id": "RL", + "component": "emblink/resistor", + "params": { + "resistance": 10 + } + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.IN1", + "DUT.EN" + ], + "A": [ + "DUT.OUT1", + "RL.1" + ], + "B": [ + "DUT.OUT2", + "RL.2" + ], + "GND": [ + "VM.-", + "VL.-", + "DUT.GND", + "DUT.SEN", + "DUT.IN2", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.out1": { + "at": "0.5ms", + "equals": "off" + }, + "output:DUT.asleep": { + "at": "0.5ms", + "equals": true + }, + "output:DUT.state": { + "at": "2ms", + "equals": "forward" + }, + "output:DUT.current": { + "at": "2ms", + "min": 0.478, + "max": 0.488 + }, + "output:DUT.vout": { + "at": "2ms", + "min": 4.78, + "max": 4.88 + } + } +} \ No newline at end of file diff --git a/catalog/components/h_bridge/tests/timing.json b/catalog/components/h_bridge/tests/timing.json new file mode 100644 index 000000000..660bc6b96 --- /dev/null +++ b/catalog/components/h_bridge/tests/timing.json @@ -0,0 +1,168 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "timing", + "description": "Input timing at VM = 5 V into 10 ohm: IN1 rising at 2 ms reaches OUT1 1.1 us later (still off at 1.05 us, forward at 1.2 us); a 300 ns pulse on IN2 at 2.5 ms is shorter than the 450 ns deglitch and is lost; reversing at 3 ms turns both outputs off for the 450 ns dead time before they cross, then 483 mA flows backwards.", + "policy": "electrical", + "until": "4ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 5.0 + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "V1", + "component": "emblink/vsource", + "params": { + "kind": "pwl", + "points": [ + [ + 0, + 0 + ], + [ + 0.002, + 0 + ], + [ + 0.00200001, + 3.3 + ], + [ + 0.003, + 3.3 + ], + [ + 0.00300001, + 0 + ] + ] + } + }, + { + "id": "V2", + "component": "emblink/vsource", + "params": { + "kind": "pwl", + "points": [ + [ + 0, + 0 + ], + [ + 0.0025, + 0 + ], + [ + 0.00250001, + 3.3 + ], + [ + 0.00250031, + 3.3 + ], + [ + 0.00250032, + 0 + ], + [ + 0.003, + 0 + ], + [ + 0.00300001, + 3.3 + ] + ] + } + }, + { + "id": "DUT", + "component": "emblink/h_bridge" + }, + { + "id": "RL", + "component": "emblink/resistor", + "params": { + "resistance": 10 + } + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.EN" + ], + "I1": [ + "V1.+", + "DUT.IN1" + ], + "I2": [ + "V2.+", + "DUT.IN2" + ], + "A": [ + "DUT.OUT1", + "RL.1" + ], + "B": [ + "DUT.OUT2", + "RL.2" + ], + "GND": [ + "VM.-", + "VL.-", + "V1.-", + "V2.-", + "DUT.GND", + "DUT.SEN", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.out1": { + "at": "2.00105ms", + "equals": "off" + }, + "output:DUT.state": { + "at": "2.0012ms", + "equals": "forward" + }, + "output:DUT.out2": { + "at": "2.5015ms", + "equals": "low" + }, + "output:DUT.vout": { + "at": "3.0013ms", + "min": -0.05, + "max": 0.05 + }, + "output:DUT.current": { + "at": "3.002ms", + "min": -0.488, + "max": -0.478 + } + } +} \ No newline at end of file diff --git a/catalog/components/h_bridge/tests/uvlo.json b/catalog/components/h_bridge/tests/uvlo.json new file mode 100644 index 000000000..e9d765286 --- /dev/null +++ b/catalog/components/h_bridge/tests/uvlo.json @@ -0,0 +1,114 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "uvlo", + "description": "VM sagging from 5 V to 2.5 V (2 to 3 ms) passes the 2.6 V lockout: the bridge turns off and its logic resets; back at 5 V (from 5 ms) it wakes again 1 ms later and drives the 10 ohm load.", + "policy": "electrical", + "until": "7ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "pwl", + "points": [ + [ + 0, + 5 + ], + [ + 0.002, + 5 + ], + [ + 0.003, + 2.5 + ], + [ + 0.005, + 2.5 + ], + [ + 0.00501, + 5 + ] + ] + } + }, + { + "id": "VL", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/h_bridge" + }, + { + "id": "RL", + "component": "emblink/resistor", + "params": { + "resistance": 10 + } + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM" + ], + "LOGIC": [ + "VL.+", + "DUT.EN", + "DUT.IN1" + ], + "A": [ + "DUT.OUT1", + "RL.1" + ], + "B": [ + "DUT.OUT2", + "RL.2" + ], + "GND": [ + "VM.-", + "VL.-", + "DUT.GND", + "DUT.SEN", + "DUT.IN2", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.state": { + "at": "1.9ms", + "equals": "forward" + }, + "output:DUT.undervoltage": { + "at": "4ms", + "equals": true + }, + "output:DUT.out1": { + "at": "4ms", + "equals": "off" + }, + "output:DUT.asleep": { + "at": "5.5ms", + "equals": true + }, + "output:DUT.vout": { + "at": "6.5ms", + "min": 4.78, + "max": 4.88 + } + } +} \ No newline at end of file diff --git a/core/analog/analog.h b/core/analog/analog.h index eea571020..791b94ace 100644 --- a/core/analog/analog.h +++ b/core/analog/analog.h @@ -40,17 +40,20 @@ struct esk_adev_ops { void (*set_logic)(esk_adev *d, esk_logic v, es_time_t at); }; +/* the most terminals one analog device has (a dual H-bridge's sixteen) */ +#define ESK_ADEV_MAX_PINS 16 + struct esk_adev { esk_island *isl; esk_comp *comp; /* owning component (NULL for bridges owned by a pin) */ esk_pin *pin; /* for bridges: the digital pin */ const esk_adev_ops *ops; void *state; - int node[8]; /* nodes of the device's analog pins in pin order (0 = ground) */ + int node[ESK_ADEV_MAX_PINS]; /* nodes of the device's analog pins in pin order (0 = ground) */ int nnodes; int branch[4]; /* branch unknown indices allocated in setup */ int nbranch; - double i_last[8]; /* current into each pin at the last accepted step (observe) */ + double i_last[ESK_ADEV_MAX_PINS]; /* current into each pin at the last accepted step (observe) */ }; typedef struct esk_island_params { diff --git a/core/analog/devices.c b/core/analog/devices.c index 3b72e49fa..1d9e8d688 100644 --- a/core/analog/devices.c +++ b/core/analog/devices.c @@ -46,7 +46,8 @@ const char *const *esk_adev_pin_names(const esk_adev_ops *ops, int *n) *const dmm[] = { "COM", "V", "MA", "A" }, *const xfmr[] = { "P1", "P2", "S1", "S2" }, *const xfmr_ct[] = { "P1", "P2", "S1", "CT", "S2" }, - *const triac[] = { "MT2", "MT1", "G" }, *const diac[] = { "A1", "A2" }; + *const triac[] = { "MT2", "MT1", "G" }, *const diac[] = { "A1", "A2" }, + *const hbr[] = { "IN1", "IN2", "PWM", "EN", "OUT1", "OUT2", "VM", "GND", "SEN", "FAULT" }; static const struct { const char *name; const char *const *pins; int n; } table[] = { { "environment_resistor", env, 2 }, { "ptc_fuse", two, 2 }, { "photodiode", dio, 2 }, { "optocoupler", opto, 4 }, { "relay_contacts", relay, 5 }, { "spdt_contact", spdt, 3 }, @@ -56,7 +57,7 @@ const char *const *esk_adev_pin_names(const esk_adev_ops *ops, int *n) { "nmos", mos, 3 }, { "pmos", mos, 3 }, { "opamp", amp, 5 }, { "regulator", reg, 3 }, { "pull", one, 1 }, { "usb_meter", reg, 3 }, { "instrument_input", src, 2 }, { "power_profiler", ppk, 3 }, { "oscilloscope", scope, 5 }, { "multimeter", dmm, 4 }, { "transformer", xfmr, 4 }, { "transformer_ct", xfmr_ct, 5 }, - { "triac", triac, 3 }, { "diac", diac, 2 }, { "dc_motor", src, 2 }, + { "triac", triac, 3 }, { "diac", diac, 2 }, { "dc_motor", src, 2 }, { "h_bridge", hbr, 10 }, }; size_t i; for (i = 0; ops && i < sizeof table / sizeof table[0]; i++) @@ -78,7 +79,8 @@ int esk_adev_preserves_open_pins(const esk_adev_ops *ops) !strcmp(ops->name, "oscilloscope") || /* a channel with no probe on it */ !strcmp(ops->name, "multimeter") || /* a jack with no lead in it */ !strcmp(ops->name, "transformer") || !strcmp(ops->name, "transformer_ct") || /* a lead left free (an unused centre tap) */ - !strcmp(ops->name, "optocoupler")); /* an LED or a phototransistor not yet wired */ + !strcmp(ops->name, "optocoupler") || /* an LED or a phototransistor not yet wired */ + !strcmp(ops->name, "h_bridge")); /* an input, SEN or FAULT a part does not have */ } static double pd(const esk_adev *d, const char *name, double def) @@ -217,6 +219,11 @@ static double vcrit_of(double is, double nvt) return nvt * log(nvt / (sqrt(2.0) * is)); } +/* the junction helpers for the devices outside this file (named.h) */ +void esk_pn_eval(double v, double is, double nvt, double *id, double *gd) { pn_eval(v, is, nvt, id, gd); } +double esk_pnjlim(double vnew, double vold, double vt, double vcrit) { return pnjlim(vnew, vold, vt, vcrit); } +double esk_vcrit(double is, double nvt) { return vcrit_of(is, nvt); } + /* LTE of a reactive element's state (charge or flux) from the history of its * derivative (current for a capacitor, voltage for an inductor): trapezoidal * h^3/12 |q'''|, backward Euler h^2/2 |q''|, by divided differences. The @@ -3053,7 +3060,7 @@ static const esk_adev_ops *const device_table[] = { &esk_comparator_ops, &esk_temperature_ops, &esk_timer555_ops, &esk_environmental_ops, &esk_photodiode_ops, &esk_optocoupler_ops, &esk_relay_contacts_ops, &esk_spdt_ops, &esk_button4_ops, &esk_ptc_fuse_ops, &esk_usb_meter_ops, &instrument_ops, &esk_power_profiler_ops, &esk_oscilloscope_ops, - &esk_multimeter_ops, &esk_dc_motor_ops, + &esk_multimeter_ops, &esk_dc_motor_ops, &esk_h_bridge_ops, }; void esk_adev_on_time(esk_adev *d, es_time_t now) diff --git a/core/analog/hbridge.c b/core/analog/hbridge.c new file mode 100644 index 000000000..ff57f87d1 --- /dev/null +++ b/core/analog/hbridge.c @@ -0,0 +1,587 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Copyright 2026 Junior Deogracias */ +/* hbridge.c -- one H-bridge of an integrated motor driver (DRV8833, + * TB6612FNG, L293D's pairs), as its datasheet describes it (SPEC-004's + * devices, M7's robot). + * + * Terminals: the logic inputs IN1, IN2, PWM and EN, the outputs OUT1 and + * OUT2, the motor supply VM, the logic ground GND, the low-side return SEN + * (the sense resistor's top, or GND) and the open-drain FAULT output. Each + * output is a half-bridge: a high-side FET from VM (ron_hs) and a low-side + * FET to SEN (ron_ls), each with its body diode (OUT to VM, SEN to OUT), so + * a motor's current recirculates through the diodes while both FETs are off + * (fast decay) and its back EMF, driven, pumps current back into VM. An + * output's high level is therefore VM less the drop of the current it + * carries, as on the bench: a sagging battery slows the motor. + * + * The inputs are read as voltages to GND through Schmitt thresholds (the + * datasheet's VIL and VIH, the hysteresis vhys about their mean), each with + * its pulldown (rpd; EN's rpd_en and, when en_clamp > 0, its clamping + * Zener). The logic is the datasheet's, by `mode`: + * in_in (DRV8833) IN1 IN2: 00 coast, 10 forward, 01 reverse, 11 brake; + * in_in_pwm (TB6612FNG) IN1 IN2 PWM: 00 off (coast), 10/01 with PWM high + * forward/reverse, PWM low or 11 short brake; + * half (L293D) each output follows its input while EN is high, + * both off while low. + * With `sleep` 1, EN is nSLEEP: low sleeps the bridge (outputs off, the + * logic reset, iq_sleep drawn) and t_wake passes after it rises before the + * outputs follow; otherwise EN is a logic input of the table above + * (TB6612FNG's STBY: low turns both outputs off). + * + * Timing: an input change is taken after it holds t_deg (the deglitch + * filter; shorter pulses are lost) and reaches the outputs t_prop after it + * began; an output passing between high and low is off for t_dead (the + * break-before-make). VM below uvlo (falling; uvlo + uvlo_hys rising) + * disables the bridge and resets its logic. + * + * Current control (vtrip > 0): while the bridge drives, SEN above vtrip + * (I x Rsense) switches it to slow decay (both low sides on) until the + * next edge of the f_chop clock; the comparator is then ignored for + * t_blank. Overcurrent (iocp > 0): a FET carrying more than iocp for + * t_ocp_deg disables the bridge and pulls FAULT low for t_ocp, then it + * retries. + * + * Inside: the comparator, the overcurrent detector and the Schmitt inputs + * are sampled at the island's accepted steps, which stop at every timer of + * this model (deglitch, delay, dead time, wake, chop clock, retry) but not + * at an input's crossing between them (an MCU's edge reaches the island as + * a step of its own). The FETs switch instantly (no rise or fall time, no + * gate charge), each a resistance set from VM between its datasheet's + * points (ron at vm_hi, ron_low at vm_lo); the FETs' analog current + * limiting during the overcurrent deglitch, the die's temperature (thermal + * shutdown) and the charge pump's and regulator's pins are not modelled. */ +#include "analog/named.h" +#include +#include + +enum { HB_IN1, HB_IN2, HB_PWM, HB_EN, HB_OUT1, HB_OUT2, HB_VM, HB_GND, HB_SEN, HB_FAULT, HB_NPINS }; +enum { HB_Z, HB_H, HB_L }; +enum { HB_MODE_IN_IN, HB_MODE_IN_IN_PWM, HB_MODE_HALF }; + +#define HB_Q 8 /* commands in flight through the propagation delay */ +#define HB_NIN 4 /* IN1, IN2, PWM, EN */ +#define HB_VT 0.025852 /* kT/q at 27 C */ + +typedef struct hb_cmd { + es_time_t at; + int out[2]; +} hb_cmd; + +typedef struct hb_state { + int mode, sleep; + double ron_hs_hi, ron_ls_hi, ron_hs_lo, ron_ls_lo, vm_lo, vm_hi, ron_hs, ron_ls, goff; + double bd_is, bd_nvt, bd_vcrit; + double rpd, rpd_en, vt_rise[HB_NIN], vt_fall[HB_NIN]; + double en_clamp, z_is, z_nvt, z_vcrit; + double iq, iq_sleep, uvlo, uvlo_hys, vtrip, iocp, fault_r; + es_time_t t_deg, t_prop, t_dead, t_wake, t_blank, t_chop, t_ocp_deg, t_ocp; + /* the inputs: as seen at the last accepted step, and as taken */ + int raw[HB_NIN], filt[HB_NIN]; + int pend[HB_NIN]; /* raw differs from filt: taken at deg_at */ + es_time_t deg_at[HB_NIN]; + hb_cmd q[HB_Q]; + int qn; + int out[2]; /* what the logic commands after the delay */ + int want[2]; /* after sleep, faults and chopping */ + int eff[2]; /* on the FETs (after the dead time) */ + es_time_t dead_until[2]; + int awake, waking; + es_time_t wake_at; + int uvlo_on, chop, over, fault, powered; + es_time_t chop_end, blank_end, over_since, fault_end; + double vlim[4], vz_old; /* junction limiting: the body diodes and EN's clamp */ +} hb_state; + +static double par(const esk_adev *d, const char *name, double fallback) +{ + double v; + return d->comp && esk_comp_param_double(d->comp, name, &v) == 0 ? v : fallback; +} + +static es_time_t par_t(const esk_adev *d, const char *name, double fallback_s) +{ + double s = par(d, name, fallback_s); + return s > 0.0 && s < 1e6 ? (es_time_t)(s * 1e12 + 0.5) : 0; +} + +static double node_v(const esk_adev *d, int k) +{ + return esk_island_node_v(d->isl, d->node[k]); +} + +static void bad_param(esk_adev *d, const char *name, const char *why) +{ + if (d->comp) + esk_comp_diag(d->comp, ES_SEV_ERROR, "analog.param_out_of_range", "%s: %s", name, why); +} + +static es_status hb_setup(esk_adev *d) +{ + hb_state *s = (hb_state *)d->state; + const char *mode = d->comp ? esk_comp_param_str(d->comp, "mode", "in_in") : "in_in"; + double vil = par(d, "vil", 0.7), vih = par(d, "vih", 2.0), hys = par(d, "vhys", 0.4); + double en_vil = par(d, "en_vil", vil), en_vih = par(d, "en_vih", vih), en_hys = par(d, "en_vhys", hys); + double bd_vf = par(d, "bd_vf", 0.8), bd_if = par(d, "bd_if", 1.0); + int k; + if (d->nnodes < HB_NPINS) { + bad_param(d, "pins", "an h_bridge has ten terminals"); + return ES_EINVAL; + } + s->mode = !strcmp(mode, "in_in_pwm") ? HB_MODE_IN_IN_PWM : !strcmp(mode, "half") ? HB_MODE_HALF : HB_MODE_IN_IN; + if (strcmp(mode, "in_in") && strcmp(mode, "in_in_pwm") && strcmp(mode, "half")) + bad_param(d, "mode", "in_in, in_in_pwm or half"); + s->sleep = par(d, "sleep", s->mode == HB_MODE_IN_IN ? 1.0 : 0.0) != 0.0; + s->ron_hs_hi = par(d, "ron_hs", 0.2); + s->ron_ls_hi = par(d, "ron_ls", 0.16); + s->ron_hs_lo = par(d, "ron_hs_low", s->ron_hs_hi); + s->ron_ls_lo = par(d, "ron_ls_low", s->ron_ls_hi); + s->vm_hi = par(d, "vm_hi", 5.0); + s->vm_lo = par(d, "vm_lo", 2.7); + s->goff = 1.0 / par(d, "roff", 1e8); + s->bd_nvt = par(d, "bd_n", 1.2) * HB_VT; + s->rpd = par(d, "rpd", 150e3); + s->rpd_en = par(d, "rpd_en", 500e3); + s->en_clamp = par(d, "en_clamp", 0.0); + s->iq = par(d, "iq", 1.7e-3); + s->iq_sleep = par(d, "iq_sleep", 1.6e-6); + s->uvlo = par(d, "uvlo", 2.6); + s->uvlo_hys = par(d, "uvlo_hys", 0.09); + s->vtrip = par(d, "vtrip", 0.0); + s->iocp = par(d, "iocp", 0.0); + s->fault_r = par(d, "fault_r", 100.0); + s->t_deg = par_t(d, "t_deg", 450e-9); + s->t_prop = par_t(d, "t_prop", 1.1e-6); + s->t_dead = par_t(d, "t_dead", 450e-9); + s->t_wake = par_t(d, "t_wake", s->sleep ? 1e-3 : 0.0); + s->t_blank = par_t(d, "t_blank", 3.75e-6); + s->t_chop = par_t(d, "t_chop", 1.0 / par(d, "f_chop", 50e3)); + s->t_ocp_deg = par_t(d, "t_ocp_deg", 4e-6); + s->t_ocp = par_t(d, "t_ocp", 1.35e-3); + if (!(s->ron_hs_hi > 0.0) || !(s->ron_ls_hi > 0.0) || !(s->ron_hs_lo > 0.0) || !(s->ron_ls_lo > 0.0) || + !(s->vm_hi > s->vm_lo) || !(s->goff > 0.0) || !(s->bd_nvt > 0.0) || !(bd_vf > 0.0) || !(bd_if > 0.0) || + s->rpd < 0.0 || s->rpd_en < 0.0 || s->iq < 0.0 || s->iq_sleep < 0.0 || s->en_clamp < 0.0 || + s->vtrip < 0.0 || s->iocp < 0.0 || !(s->fault_r > 0.0) || !(vih > vil) || !(en_vih > en_vil) || + hys < 0.0 || en_hys < 0.0 || s->t_prop < s->t_deg || !s->t_chop) { + bad_param(d, "h_bridge", "resistances, VM points, junction and thresholds out of range"); + return ES_EINVAL; + } + s->ron_hs = s->ron_hs_hi; + s->ron_ls = s->ron_ls_hi; + s->bd_is = bd_if / (exp(bd_vf / s->bd_nvt) - 1.0); + s->bd_vcrit = esk_vcrit(s->bd_is, s->bd_nvt); + for (k = 0; k < HB_NIN; k++) { + double lo = k == HB_EN ? en_vil : vil, hi = k == HB_EN ? en_vih : vih, h = k == HB_EN ? en_hys : hys; + double mid = 0.5 * (lo + hi); + s->vt_rise[k] = mid + 0.5 * h; + s->vt_fall[k] = mid - 0.5 * h; + } + /* the clamp: 100 uA at en_clamp, n = 1.5 */ + s->z_nvt = 1.5 * HB_VT; + s->z_is = 1e-14; + s->z_vcrit = esk_vcrit(s->z_is, s->z_nvt); + s->out[0] = s->out[1] = s->want[0] = s->want[1] = s->eff[0] = s->eff[1] = HB_Z; + return ES_OK; +} + +/* the clamp's junction voltage for a pin voltage v: 100 uA at en_clamp */ +static double hb_zx(const hb_state *s, double v) +{ + return v - s->en_clamp + s->z_nvt * log(1e-4 / s->z_is); +} + +/* conductance or nonlinear current from pin a to pin b (into a, out of b) */ +static void stamp_nl(esk_amatrix *m, int na, int nb, double g, double i, double vl) +{ + esk_am_stamp_g(m, na, nb, g); + esk_am_stamp_i(m, na, nb, i - g * vl); +} + +static void hb_diode(esk_adev *d, esk_amatrix *m, int a, int c, double *vold, double is, double nvt, double vcrit, double xofs) +{ + double v = esk_am_v(m, d->node[a], d->node[c]), x = v + xofs, xl = esk_pnjlim(x, *vold, nvt, vcrit), i, g; + if (fabs(xl - x) > 1e-12) + esk_am_noncon(m); + *vold = xl; + esk_pn_eval(xl, is, nvt, &i, &g); + stamp_nl(m, d->node[a], d->node[c], g + 1e-12, i, xl - xofs); +} + +/* the supply's quiescent current, fading out below 0.5 V so an unpowered + * VM is not pulled negative */ +static void hb_iq(double iq, double v, double *i, double *g) +{ + if (v <= 0.0) { + *i = 0.0; + *g = 0.0; + } else { + double e = exp(-v / 0.5); + *i = iq * (1.0 - e); + *g = iq * e / 0.5; + } +} + +static void hb_load(esk_adev *d, esk_amatrix *m) +{ + hb_state *s = (hb_state *)d->state; + int vm = d->node[HB_VM], gnd = d->node[HB_GND], sen = d->node[HB_SEN], k; + double i, g, v; + for (k = 0; k < 2; k++) { + int o = d->node[HB_OUT1 + k]; + if (s->eff[k] == HB_H) + esk_am_stamp_g(m, vm, o, 1.0 / s->ron_hs); + else if (s->eff[k] == HB_L) + esk_am_stamp_g(m, o, sen, 1.0 / s->ron_ls); + esk_am_stamp_g(m, vm, o, s->goff); + esk_am_stamp_g(m, o, sen, s->goff); + hb_diode(d, m, HB_OUT1 + k, HB_VM, &s->vlim[2 * k], s->bd_is, s->bd_nvt, s->bd_vcrit, 0.0); + hb_diode(d, m, HB_SEN, HB_OUT1 + k, &s->vlim[2 * k + 1], s->bd_is, s->bd_nvt, s->bd_vcrit, 0.0); + } + for (k = 0; k < HB_NIN; k++) { + double r = k == HB_EN ? s->rpd_en : s->rpd; + if (r > 0.0) + esk_am_stamp_g(m, d->node[k], gnd, 1.0 / r); + } + if (s->en_clamp > 0.0) + hb_diode(d, m, HB_EN, HB_GND, &s->vz_old, s->z_is, s->z_nvt, s->z_vcrit, hb_zx(s, 0.0)); + v = esk_am_v(m, vm, gnd); + hb_iq(s->awake || !s->sleep ? s->iq : s->iq_sleep, v, &i, &g); + stamp_nl(m, vm, gnd, g + 1e-12, i, v); + esk_am_stamp_g(m, d->node[HB_FAULT], gnd, s->fault ? 1.0 / s->fault_r : 1e-9); +} + +static void hb_load_tr(esk_adev *d, esk_amatrix *m, const esk_integ *in) +{ + (void)in; + hb_load(d, m); +} + +/* the table: the outputs the taken inputs command */ +static void hb_table(const hb_state *s, const int *in, int *o) +{ + int a = in[HB_IN1], b = in[HB_IN2]; + switch (s->mode) { + case HB_MODE_HALF: + if (!in[HB_EN]) { + o[0] = o[1] = HB_Z; + } else { + o[0] = a ? HB_H : HB_L; + o[1] = b ? HB_H : HB_L; + } + return; + case HB_MODE_IN_IN_PWM: + if (!s->sleep && !in[HB_EN]) { /* standby */ + o[0] = o[1] = HB_Z; + } else if (!a && !b) { + o[0] = o[1] = HB_Z; + } else if (a && b) { + o[0] = o[1] = HB_L; + } else if (!in[HB_PWM]) { + o[0] = o[1] = HB_L; + } else { + o[0] = a ? HB_H : HB_L; + o[1] = a ? HB_L : HB_H; + } + return; + default: + if (!s->sleep && !in[HB_EN]) { + o[0] = o[1] = HB_Z; + } else if (!a && !b) { + o[0] = o[1] = HB_Z; + } else if (a && b) { + o[0] = o[1] = HB_L; + } else { + o[0] = a ? HB_H : HB_L; + o[1] = a ? HB_L : HB_H; + } + return; + } +} + +static int hb_driving(const int *o) +{ + return (o[0] == HB_H && o[1] == HB_L) || (o[0] == HB_L && o[1] == HB_H); +} + +/* the logic is reset (sleep, UVLO): nothing in flight, no fault, no chop */ +static void hb_logic_reset(hb_state *s) +{ + s->qn = 0; + s->out[0] = s->out[1] = HB_Z; + s->chop = s->over = s->fault = 0; +} + +/* the FET currents at the accepted solution: high side k from VM, low side k to SEN */ +static double hb_fet_i(const esk_adev *d, const hb_state *s, int k) +{ + double vo = node_v(d, HB_OUT1 + k); + if (s->eff[k] == HB_H) + return (node_v(d, HB_VM) - vo) / s->ron_hs; + if (s->eff[k] == HB_L) + return (vo - node_v(d, HB_SEN)) / s->ron_ls; + return 0.0; +} + +static void hb_currents(esk_adev *d) +{ + hb_state *s = (hb_state *)d->state; + double *il = d->i_last, i, g; + double vgnd = node_v(d, HB_GND), vm = node_v(d, HB_VM), vsen = node_v(d, HB_SEN); + int k; + for (k = 0; k < HB_NPINS; k++) + il[k] = 0.0; +#define FLOW(a, b, cur) do { double c_ = (cur); il[a] += c_; il[b] -= c_; } while (0) + for (k = 0; k < 2; k++) { + int o = HB_OUT1 + k; + double vo = node_v(d, o); + if (s->eff[k] == HB_H) + FLOW(HB_VM, o, (vm - vo) / s->ron_hs); + else if (s->eff[k] == HB_L) + FLOW(o, HB_SEN, (vo - vsen) / s->ron_ls); + FLOW(HB_VM, o, (vm - vo) * s->goff); + FLOW(o, HB_SEN, (vo - vsen) * s->goff); + esk_pn_eval(vo - vm, s->bd_is, s->bd_nvt, &i, &g); + FLOW(o, HB_VM, i); + esk_pn_eval(vsen - vo, s->bd_is, s->bd_nvt, &i, &g); + FLOW(HB_SEN, o, i); + } + for (k = 0; k < HB_NIN; k++) { + double r = k == HB_EN ? s->rpd_en : s->rpd; + if (r > 0.0) + FLOW(k, HB_GND, (node_v(d, k) - vgnd) / r); + } + if (s->en_clamp > 0.0) { + esk_pn_eval(hb_zx(s, node_v(d, HB_EN) - vgnd), s->z_is, s->z_nvt, &i, &g); + FLOW(HB_EN, HB_GND, i); + } + hb_iq(s->awake || !s->sleep ? s->iq : s->iq_sleep, vm - vgnd, &i, &g); + FLOW(HB_VM, HB_GND, i); + FLOW(HB_FAULT, HB_GND, (node_v(d, HB_FAULT) - vgnd) * (s->fault ? 1.0 / s->fault_r : 1e-9)); +#undef FLOW +} + +static void hb_accept(esk_adev *d, es_time_t t) +{ + hb_state *s = (hb_state *)d->state; + double vgnd = node_v(d, HB_GND), vm = node_v(d, HB_VM) - vgnd; + int before[2]; + int k, taken = 0; + before[0] = s->eff[0]; + before[1] = s->eff[1]; + /* the FETs' resistances follow VM between the datasheet's points */ + { + double x = (vm - s->vm_lo) / (s->vm_hi - s->vm_lo); + x = x < 0.0 ? 0.0 : x > 1.0 ? 1.0 : x; + s->ron_hs = s->ron_hs_lo + (s->ron_hs_hi - s->ron_hs_lo) * x; + s->ron_ls = s->ron_ls_lo + (s->ron_ls_hi - s->ron_ls_lo) * x; + } + /* undervoltage lockout: below it everything is off and reset */ + if (!s->powered || s->uvlo_on) { + if (vm > s->uvlo + s->uvlo_hys) { + s->powered = 1; + s->uvlo_on = 0; + hb_logic_reset(s); + for (k = 0; k < HB_NIN; k++) + s->pend[k] = 0; + s->awake = 0; + s->waking = 0; + for (k = 0; k < HB_NIN; k++) + s->filt[k] = s->raw[k] = 0; + } + } else if (vm < s->uvlo) { + s->uvlo_on = 1; + s->awake = s->waking = 0; + hb_logic_reset(s); + } + /* the inputs: Schmitt triggers, then the deglitch filter */ + for (k = 0; k < HB_NIN; k++) { + double v = node_v(d, k) - vgnd; + int r = s->raw[k]; + if (!r && v > s->vt_rise[k]) + r = 1; + else if (r && v < s->vt_fall[k]) + r = 0; + if (r != s->raw[k]) { + s->raw[k] = r; + s->pend[k] = r != s->filt[k]; + s->deg_at[k] = t + s->t_deg; + } + } + if (s->powered && !s->uvlo_on) { + es_time_t when = 0; + for (k = 0; k < HB_NIN; k++) + if (s->pend[k] && s->deg_at[k] <= t) { + if (s->deg_at[k] > when) + when = s->deg_at[k]; + s->filt[k] = s->raw[k]; + s->pend[k] = 0; + taken = 1; + if (k == HB_EN && s->sleep) { + if (s->filt[k]) { + s->waking = 1; + s->wake_at = when + s->t_wake; + } else { + s->awake = s->waking = 0; + hb_logic_reset(s); + } + } + } + if (!s->sleep) + s->awake = 1; + else if (!s->awake && !s->waking && s->filt[HB_EN]) { + /* powered up (or out of UVLO) with nSLEEP high: it wakes */ + s->waking = 1; + s->wake_at = t + s->t_wake; + } + if (s->waking && t >= s->wake_at) { + s->waking = 0; + s->awake = 1; + s->qn = 0; + hb_table(s, s->filt, s->out); + taken = 0; + } + if (taken && s->awake) { + hb_cmd c; + hb_table(s, s->filt, c.out); + c.at = when + (s->t_prop - s->t_deg); + if (s->qn == HB_Q) { + memmove(&s->q[0], &s->q[1], (HB_Q - 1) * sizeof s->q[0]); + s->qn--; + } + s->q[s->qn++] = c; + } + while (s->qn && s->q[0].at <= t) { + s->out[0] = s->q[0].out[0]; + s->out[1] = s->q[0].out[1]; + memmove(&s->q[0], &s->q[1], (size_t)(s->qn - 1) * sizeof s->q[0]); + s->qn--; + } + } + /* overcurrent: a FET over iocp for t_ocp_deg disables the bridge for t_ocp */ + if (s->fault && t >= s->fault_end) + s->fault = 0; + if (s->iocp > 0.0 && !s->fault) { + int over = fabs(hb_fet_i(d, s, 0)) > s->iocp || fabs(hb_fet_i(d, s, 1)) > s->iocp; + if (!over) + s->over = 0; + else if (!s->over) { + s->over = 1; + s->over_since = t; + } + if (s->over && t >= s->over_since + s->t_ocp_deg) { + s->over = 0; + s->fault = 1; + s->fault_end = t + s->t_ocp; + } + } + /* current chopping: slow decay to the next clock edge, then blanking */ + if (s->chop && t >= s->chop_end) { + s->chop = 0; + s->blank_end = t + s->t_blank; + } + { + int active = s->awake && !s->uvlo_on && s->powered && !s->fault; + int w[2]; + if (!active || !hb_driving(s->out)) + s->chop = 0; + if (active && !s->chop && s->vtrip > 0.0 && hb_driving(s->eff) && t >= s->blank_end && + node_v(d, HB_SEN) - vgnd > s->vtrip) { + s->chop = 1; + s->chop_end = (t / s->t_chop + 1) * s->t_chop; + } + if (!active) + w[0] = w[1] = HB_Z; + else if (s->chop) + w[0] = w[1] = HB_L; + else { + w[0] = s->out[0]; + w[1] = s->out[1]; + } + if (!hb_driving(s->want) && hb_driving(w)) + s->blank_end = t + s->t_blank; + for (k = 0; k < 2; k++) { + if ((s->want[k] == HB_H && w[k] == HB_L) || (s->want[k] == HB_L && w[k] == HB_H)) + s->dead_until[k] = t + s->t_dead; + s->want[k] = w[k]; + s->eff[k] = t < s->dead_until[k] ? HB_Z : w[k]; + } + } + if (s->eff[0] != before[0] || s->eff[1] != before[1]) + esk_island_touch(d->isl, t); + hb_currents(d); +} + +static void hb_min(es_time_t *best, es_time_t at, es_time_t after) +{ + if (at > after && at < *best) + *best = at; +} + +static int hb_next_breakpoint(esk_adev *d, es_time_t after, es_time_t *at) +{ + const hb_state *s = (const hb_state *)d->state; + es_time_t best = ES_TIME_MAX; + int k; + for (k = 0; k < HB_NIN; k++) + if (s->pend[k]) + hb_min(&best, s->deg_at[k], after); + if (s->qn) + hb_min(&best, s->q[0].at, after); + if (s->waking) + hb_min(&best, s->wake_at, after); + for (k = 0; k < 2; k++) + hb_min(&best, s->dead_until[k], after); + if (s->chop) + hb_min(&best, s->chop_end, after); + if (s->fault) + hb_min(&best, s->fault_end, after); + if (s->over) + hb_min(&best, s->over_since + s->t_ocp_deg, after); + if (best == ES_TIME_MAX) + return 0; + *at = best; + return 1; +} + +static const char *hb_half_name(int o) +{ + return o == HB_H ? "high" : o == HB_L ? "low" : "off"; +} + +static void hb_observe(esk_adev *d, esk_json *o) +{ + const hb_state *s = (const hb_state *)d->state; + esk_alloc *a = d->isl->a; + const char *state; + if (s->eff[0] == HB_H && s->eff[1] == HB_L) + state = "forward"; + else if (s->eff[0] == HB_L && s->eff[1] == HB_H) + state = "reverse"; + else if (s->eff[0] == HB_L && s->eff[1] == HB_L) + state = "brake"; + else if (s->eff[0] == HB_Z && s->eff[1] == HB_Z) + state = "coast"; + else + state = "half"; + esk_json_set_string(a, o, "state", state); + esk_json_set_string(a, o, "out1", hb_half_name(s->eff[0])); + esk_json_set_string(a, o, "out2", hb_half_name(s->eff[1])); + /* the load current, out of OUT1 (into the motor) */ + esk_json_set_number(a, o, "current", -d->i_last[HB_OUT1]); + esk_json_set_number(a, o, "supply_current", d->i_last[HB_VM]); + esk_json_set_number(a, o, "vout", node_v(d, HB_OUT1) - node_v(d, HB_OUT2)); + esk_json_set_bool(a, o, "fault", s->fault != 0); + esk_json_set_bool(a, o, "asleep", s->sleep && !s->awake); + esk_json_set_bool(a, o, "chopping", s->chop != 0); + esk_json_set_bool(a, o, "undervoltage", s->uvlo_on || !s->powered); + { + double p = 0.0; + int k; + for (k = 0; k < HB_NPINS; k++) + p += node_v(d, k) * d->i_last[k]; + esk_json_set_number(a, o, "power", p); + } +} + +const esk_adev_ops esk_h_bridge_ops = {"h_bridge", sizeof(hb_state), hb_setup, hb_load, + hb_load_tr, hb_accept, NULL, hb_next_breakpoint, + NULL, hb_observe, NULL}; diff --git a/core/analog/mna.c b/core/analog/mna.c index f5f575a4f..d20988c4c 100644 --- a/core/analog/mna.c +++ b/core/analog/mna.c @@ -898,7 +898,7 @@ es_status esk_island_add_device(esk_island *isl, esk_comp *c, const esk_adev_ops mna *p = (mna *)isl->priv; esk_adev *d; int k; - if (!ops || npins < 0 || npins > 8 || p->started) + if (!ops || npins < 0 || npins > ESK_ADEV_MAX_PINS || p->started) return ES_EINVAL; d = dev_new(isl, ops); if (!d) @@ -965,7 +965,7 @@ static es_status setup_devices(esk_island *isl) if (d->state) memset(d->state, 0, d->ops->state_size); d->nbranch = 0; - for (k = 0; k < 8; k++) + for (k = 0; k < ESK_ADEV_MAX_PINS; k++) d->i_last[k] = 0.0; if (d->ops->setup) { es_status s2 = d->ops->setup(d); diff --git a/core/analog/named.h b/core/analog/named.h index e62f66821..1532cdb74 100644 --- a/core/analog/named.h +++ b/core/analog/named.h @@ -47,6 +47,13 @@ void esk_ptc_on_time(esk_adev *d, es_time_t now); /* a brushed DC motor (motor.c): its load torque changed */ extern const esk_adev_ops esk_dc_motor_ops; void esk_motor_on_input(esk_adev *d, es_time_t now); +/* Shockley junction helpers shared by the devices (devices.c): a junction's + * current and slope at a voltage, SPICE's pnjlim, and its critical voltage */ +void esk_pn_eval(double v, double is, double nvt, double *id, double *gd); +double esk_pnjlim(double vnew, double vold, double vt, double vcrit); +double esk_vcrit(double is, double nvt); +/* an H-bridge motor driver's bridge (hbridge.c) */ +extern const esk_adev_ops esk_h_bridge_ops; void esk_environmental_on_time(esk_adev *d, es_time_t now); void esk_environmental_on_input(esk_adev *d, es_time_t now); #endif diff --git a/core/engine/engine.c b/core/engine/engine.c index 402ef80f6..fe9ee4f99 100644 --- a/core/engine/engine.c +++ b/core/engine/engine.c @@ -1323,14 +1323,14 @@ es_status esk_engine_partition(esk_engine *e) /* devices and bridges */ for (i = 0; i < e->comps.len && st == ES_OK; i++) { esk_comp *c = e->comps.data[i]; - esk_pin *pins[8]; + esk_pin *pins[ESK_ADEV_MAX_PINS]; int np = 0; esk_island *isl = NULL; for (k = 0; k < c->npins; k++) { esk_pin *p = &c->pins[k]; if (p->net && p->net->island) { if (comp_is_electrical(c)) { - if (np < 8) pins[np++] = p; + if (np < ESK_ADEV_MAX_PINS) pins[np++] = p; isl = p->net->island; p->analog_node = p->net->node; } else if (p->model.kind == ESK_PIN_DIGITAL_OUT || p->model.kind == ESK_PIN_DIGITAL_IN || @@ -1345,7 +1345,7 @@ es_status esk_engine_partition(esk_engine *e) * a description may list them in any order */ int nnames, all = 1, named = 0; const char *const *names = esk_adev_pin_names(c->ops->adev, &nnames); - esk_pin *byname[8]; + esk_pin *byname[ESK_ADEV_MAX_PINS]; for (k = 0; names && k < (size_t)nnames; k++) { esk_pin *p = esk_comp_pin(c, names[k]); all = all && p && p->net && p->net->island == isl; diff --git a/core/project/loader.c b/core/project/loader.c index 08e068dc2..b4cec2bf2 100644 --- a/core/project/loader.c +++ b/core/project/loader.c @@ -460,7 +460,9 @@ static es_status expand_composite(es_session *s, graph *g, const char *id, const names[i] = esk_json_get_str(esk_json_at(pins, i), "name", "?"); esk_pin_model_family(NULL, &models[i]); models[i].kind = ESK_PIN_NC; - if (!esk_json_get(ports, names[i])) + /* a pin declared nc (a charge pump's capacitor, not modelled) needs no port */ + if (!esk_json_get(ports, names[i]) && + !esk_str_eq(esk_json_get_str(esk_json_get(esk_json_at(pins, i), "model"), "kind", ""), "nc")) ld_diag(s, ES_SEV_WARNING, "catalog.port_missing", id, "composite %s has no port for pin %s", item->id, names[i]); } st = esk_engine_add_comp(&s->eng, id, item->id, &composite_shell_ops, names, models, (uint32_t)np, params, item->desc, out); diff --git a/docs/STATUS.md b/docs/STATUS.md index 0ea4c7404..5c11f65fa 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -5,7 +5,7 @@ maintained by hand until evidence tooling exists. It is not a generated support matrix. Specifications define target contracts; their acceptance does not mean the described behaviour exists. -## DC motors, the robot's first parts (2026-11-08) +## DC motors and their driver, the robot's first parts (2026-11-08) M7's robot on the floor begins with its motors. `dc_motor` (`core/analog/motor.c`, SPEC-004) is a brushed DC motor in the analog @@ -23,7 +23,29 @@ at its 13 ms mechanical time constant; `stall`: 1.5 A under 0.74 kg cm; 0.148 kg cm, 0.79 W, 31 % at 0.42 A, against Pololu's 490 rpm, 0.16 kg cm, 0.80 W, 32 %. Not yet: the winding's inductance and rotor's inertia measured (Pololu publishes neither: estimates), the commutator's ripple, -heating, a motor driver, the encoder, the robot itself. +heating, the encoder, the robot itself. + +The motors' driver: `h_bridge` (`core/analog/hbridge.c`) is one bridge of +an integrated driver. Its FETs drive from VM through their on-resistance, +so a motor slows as the battery sags, and coast through their body diodes. +Its logic inputs are read as voltages through the datasheet's thresholds, +with deglitch, propagation delay, dead time, sleep and wake, undervoltage +lockout, current chopping and overcurrent shutdown. `emblink/drv8833` is +Texas Instruments' DRV8833, two such bridges with its datasheet's values. + +Evidence, its eight fixtures: +- 4.83 V across 10 ohm from 5 V (200 + 160 mohm); +- OUT1 following IN1 1.1 us later, a 300 ns pulse lost to the deglitch, + and the 450 ns dead time on reversal; +- the 2.6 V lockout, and the wake after recovery; +- the gearmotor at 6 V spinning up to 640 rpm; +- 0.5 ohm on AISEN holding it at 0.38 to 0.40 A; +- a short tripping the overcurrent protection, with nFAULT low and the + 1.35 ms retry; +- 1.6 uA asleep and 1.7 mA awake. + +Not yet: the thermal shutdown, switching edges, the TB6612FNG and L293D +parts on the primitive. ## The library's categories (2026-11-03) diff --git a/docs/specs/SPEC-004-analog-engine-and-bridges.md b/docs/specs/SPEC-004-analog-engine-and-bridges.md index af3fd3987..f582190a5 100644 --- a/docs/specs/SPEC-004-analog-engine-and-bridges.md +++ b/docs/specs/SPEC-004-analog-engine-and-bridges.md @@ -208,6 +208,35 @@ optional gearbox: shaft's angle (integrated at accepted steps and on to now while the island sleeps), its torque and the electrical and mechanical power. +`h_bridge` (pins `IN1`, `IN2`, `PWM`, `EN`, `OUT1`, `OUT2`, `VM`, `GND`, +`SEN`, `FAULT`) is one H-bridge of an integrated motor driver: +- **Power stage.** Each output is a half-bridge: a high-side FET from `VM` + and a low-side FET to `SEN` (the sense resistor's top, or ground), each a + resistance set from VM between two datasheet points, each with its body + diode. An output driven high therefore sits at VM less its FET's drop, and + a motor coasting recirculates its current through the diodes. +- **Logic.** The inputs are node voltages to `GND` through Schmitt + thresholds, with their pulldowns (and `EN`'s clamp). `mode` picks the + truth table: `in_in` (DRV8833), `in_in_pwm` (TB6612FNG), `half` (L293D). + With `sleep`, `EN` is nSLEEP: low resets the logic, and `t_wake` passes + after it rises. +- **Timing.** The logic state changes only on accepted steps, as the + thyristors' does. An input is taken `t_deg` after it changes (the + deglitch; a shorter pulse is lost) and reaches the outputs `t_prop` after + it began. An output crossing between high and low is off for `t_dead`. + Each of these times, the wake, the chopping clock and the overcurrent + timers is a breakpoint of the device, and a change of the FETs' state + touches the island. +- **Protection.** Undervoltage lockout on VM resets the logic. With `vtrip`, + current chopping goes to slow decay (both low sides on) when `SEN` passes + `vtrip`, until the next edge of the `f_chop` clock, then blanks the + comparator for `t_blank`. With `iocp`, a FET carrying more than `iocp` for + `t_ocp_deg` shuts the bridge down for `t_ocp`, its open-drain `FAULT` + pulled low meanwhile. +- **Inside.** An input's or the comparator's crossing between two accepted + steps is taken at the second. The FETs switch instantly; the die's + temperature is not modelled. + `ptc_fuse` (pins `1`, `2`) is a polymeric PTC resettable fuse, a resistance that heats itself: - **Thermal model.** Two thermal nodes: the polymer core (`c_core`, `r_core` From c0c49dc66e16985752dc92f33ea15b40d91ed44e Mon Sep 17 00:00:00 2001 From: Junior Deogracias Date: Sun, 8 Nov 2026 16:29:21 +0000 Subject: [PATCH 3/3] Motor drivers: the h_bridge VCC terminal and Toshiba's TB6612FNG The primitive takes a logic supply on VCC (vcc_ref): input thresholds as fractions of it, the lockout on it and the quiescent current from it; a separate low-to-high dead time; boolean parameters are read as such. emblink/tb6612fng is the TB6612FNG on it, with the datasheet's control table, 0.5 ohm outputs, 1 V diodes, 50/230 ns dead times, 200 kohm pulldowns, the 1.9/2.2 V VCC lockout and 1 uA standby, and four fixtures. --- CHANGELOG.md | 5 + catalog/components/h_bridge/component.json | 95 ++- catalog/components/h_bridge/symbol.svg | 1 + catalog/components/tb6612fng/component.json | 539 ++++++++++++++++++ catalog/components/tb6612fng/symbol.svg | 30 + catalog/components/tb6612fng/tests/drive.json | 96 ++++ .../components/tb6612fng/tests/pwm_brake.json | 80 +++ .../components/tb6612fng/tests/standby.json | 71 +++ .../tb6612fng/tests/vcc_thresholds.json | 130 +++++ core/analog/devices.c | 4 +- core/analog/hbridge.c | 80 ++- docs/STATUS.md | 9 +- .../SPEC-004-analog-engine-and-bridges.md | 6 +- 13 files changed, 1101 insertions(+), 45 deletions(-) create mode 100644 catalog/components/tb6612fng/component.json create mode 100644 catalog/components/tb6612fng/symbol.svg create mode 100644 catalog/components/tb6612fng/tests/drive.json create mode 100644 catalog/components/tb6612fng/tests/pwm_brake.json create mode 100644 catalog/components/tb6612fng/tests/standby.json create mode 100644 catalog/components/tb6612fng/tests/vcc_thresholds.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 39c99b91f..f8bc16530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,11 @@ versions follow semantic versioning. There is no release yet. as two bridges with the datasheet's values, and eight fixtures (forward, timing, UVLO; a gearmotor driven, current chopping at 0.4 A, overcurrent and nFAULT, sleep and awake supply currents). +- Added `emblink/tb6612fng`, Toshiba's TB6612FNG dual motor driver: the + primitive's VCC terminal gives it thresholds at 0.3/0.7 VCC and a VCC + lockout (1.9/2.2 V), with 0.5 ohm outputs, 50/230 ns dead times and + standby at 1 uA, and four fixtures (a gearmotor driven, PWM-low brake, + standby, VCC-relative thresholds and lockout). - An analog primitive takes up to 16 terminals (8 before), and a composite's pin declared `nc` needs no port. diff --git a/catalog/components/h_bridge/component.json b/catalog/components/h_bridge/component.json index 20fc42df0..d48d60fe7 100644 --- a/catalog/components/h_bridge/component.json +++ b/catalog/components/h_bridge/component.json @@ -103,6 +103,15 @@ }, "doc": "Fault output, open drain, low while a fault is asserted", "required": false + }, + { + "name": "VCC", + "number": 11, + "model": { + "kind": "passive" + }, + "doc": "Logic supply (vcc_ref true); otherwise unused", + "required": false } ], "params": { @@ -425,6 +434,37 @@ "max": 10, "doc": "Start-up time from nSLEEP high (sleep true)", "advanced": true + }, + "vcc_ref": { + "type": "boolean", + "default": false, + "doc": "The logic has its own supply VCC (TB6612FNG): thresholds are fractions of VCC, the lockout watches VCC, and the quiescent current is drawn from it", + "advanced": true + }, + "vil_frac": { + "type": "number", + "default": 0.3, + "min": 0, + "max": 1, + "doc": "Input low threshold as a fraction of VCC (vcc_ref)", + "advanced": true + }, + "vih_frac": { + "type": "number", + "default": 0.7, + "min": 0, + "max": 1, + "doc": "Input high threshold as a fraction of VCC (vcc_ref)", + "advanced": true + }, + "t_dead_lh": { + "type": "number", + "unit": "s", + "default": 4.5e-07, + "min": 0, + "max": 1, + "doc": "Dead time of an output passing from low to high (t_dead is high to low)", + "advanced": true } }, "outputs": [ @@ -476,6 +516,12 @@ "unit": "A", "doc": "Into VM" }, + { + "name": "logic_current", + "type": "number", + "unit": "A", + "doc": "Into VCC (vcc_ref)" + }, { "name": "power", "type": "number", @@ -537,7 +583,11 @@ "t_deg": "t_deg", "t_prop": "t_prop", "t_dead": "t_dead", - "t_wake": "t_wake" + "t_wake": "t_wake", + "vcc_ref": "vcc_ref", + "vil_frac": "vil_frac", + "vih_frac": "vih_frac", + "t_dead_lh": "t_dead_lh" } }, "symbol": { @@ -594,6 +644,11 @@ "x": 50, "y": 100, "side": "bottom" + }, + "VCC": { + "x": 60, + "y": 0, + "side": "top" } } }, @@ -629,24 +684,24 @@ "tests": [ "tests/*.json" ], - "i18n": { - "fr": { - "source": "b3764521b87b", - "description": "Un pont en H d'un pilote de moteur intégré : deux demi-ponts de MOSFET avec leurs diodes de structure entre VM et le retour bas, commandés par des entrées logiques lues en tensions à travers des seuils à hystérésis et des résistances de rappel, avec la table de vérité de la fiche technique (DRV8833 IN/IN, TB6612FNG IN/IN/PWM ou L293D demi-pont avec validation), le filtre anti-parasites, le délai de propagation, le temps mort, la veille et le réveil, le verrouillage en sous-tension, le découpage du courant sur une résistance de mesure et la coupure en surintensité avec une sortie de défaut à drain ouvert. Les sorties sont alimentées depuis VM à travers la résistance à l'état passant des MOSFET : un moteur ralentit quand son alimentation faiblit. Valeurs par défaut : celles du DRV8833.", - "fidelity": { - "timed": { - "claims": "Comme en électrique." - }, - "electrical": { - "claims": "Résolu avec le circuit (la primitive h_bridge) : le MOSFET haut de chaque sortie depuis VM et le MOSFET bas vers SEN, en résistances à l'état passant fixées selon VM entre deux points de la fiche technique, leurs diodes de structure, les seuils, l'hystérésis et les résistances de rappel des entrées, la table de vérité de son mode, le filtre anti-parasites, le délai de propagation et le temps mort, le temps de réveil de nSLEEP, le verrouillage en sous-tension, le découpage du courant en décroissance lente sur l'horloge f_chop avec masquage, et la coupure en surintensité avec sa reprise et la sortie FAULT à drain ouvert.", - "omissions": [ - "les temps de montée et de descente des MOSFET (commutation instantanée)", - "la limitation analogique du courant des MOSFET pendant le filtrage de la surintensité", - "la température de la puce et la coupure thermique", - "le passage d'un seuil par une entrée entre deux pas de l'îlot est pris au second" - ] - } - } + "i18n": { + "fr": { + "source": "b3764521b87b", + "description": "Un pont en H d'un pilote de moteur intégré : deux demi-ponts de MOSFET avec leurs diodes de structure entre VM et le retour bas, commandés par des entrées logiques lues en tensions à travers des seuils à hystérésis et des résistances de rappel, avec la table de vérité de la fiche technique (DRV8833 IN/IN, TB6612FNG IN/IN/PWM ou L293D demi-pont avec validation), le filtre anti-parasites, le délai de propagation, le temps mort, la veille et le réveil, le verrouillage en sous-tension, le découpage du courant sur une résistance de mesure et la coupure en surintensité avec une sortie de défaut à drain ouvert. Les sorties sont alimentées depuis VM à travers la résistance à l'état passant des MOSFET : un moteur ralentit quand son alimentation faiblit. Valeurs par défaut : celles du DRV8833.", + "fidelity": { + "timed": { + "claims": "Comme en électrique." + }, + "electrical": { + "claims": "Résolu avec le circuit (la primitive h_bridge) : le MOSFET haut de chaque sortie depuis VM et le MOSFET bas vers SEN, en résistances à l'état passant fixées selon VM entre deux points de la fiche technique, leurs diodes de structure, les seuils, l'hystérésis et les résistances de rappel des entrées, la table de vérité de son mode, le filtre anti-parasites, le délai de propagation et le temps mort, le temps de réveil de nSLEEP, le verrouillage en sous-tension, le découpage du courant en décroissance lente sur l'horloge f_chop avec masquage, et la coupure en surintensité avec sa reprise et la sortie FAULT à drain ouvert.", + "omissions": [ + "les temps de montée et de descente des MOSFET (commutation instantanée)", + "la limitation analogique du courant des MOSFET pendant le filtrage de la surintensité", + "la température de la puce et la coupure thermique", + "le passage d'un seuil par une entrée entre deux pas de l'îlot est pris au second" + ] } + } } -} + } +} \ No newline at end of file diff --git a/catalog/components/h_bridge/symbol.svg b/catalog/components/h_bridge/symbol.svg index b1793c7ac..5e58841da 100644 --- a/catalog/components/h_bridge/symbol.svg +++ b/catalog/components/h_bridge/symbol.svg @@ -11,5 +11,6 @@ VM GND SEN + VCC H-bridge diff --git a/catalog/components/tb6612fng/component.json b/catalog/components/tb6612fng/component.json new file mode 100644 index 000000000..87d6f2f46 --- /dev/null +++ b/catalog/components/tb6612fng/component.json @@ -0,0 +1,539 @@ +{ + "schema_version": 1, + "kind": "component", + "id": "emblink/tb6612fng", + "version": "1.0.0", + "name": "TB6612FNG dual DC motor driver", + "category": "analog_ics", + "subcategory": "drivers", + "description": "Toshiba TB6612FNG dual H-bridge driver (SSOP24) for two brushed DC motors, VM up to 15 V and logic from VCC 2.7 to 5.5 V. Each channel's IN1/IN2/PWM select CW, CCW, short brake or stop (PWM low brakes), STBY low puts both in standby; the LDMOS outputs drive from VM through 0.5 ohm (upper + lower), with 1 V regenerative diodes, 50/230 ns dead time and inputs at 0.3/0.7 VCC with 200 kohm pulldowns; VCC below 1.9 V turns it off until 2.2 V.", + "keywords": [ + "TB6612FNG", + "TB6612", + "motor driver", + "h-bridge", + "dual h-bridge", + "dc motor", + "Toshiba", + "robot" + ], + "part": { + "manufacturer": "Toshiba", + "mpn": "TB6612FNG", + "package": "SSOP24", + "datasheet": "https://toshiba.semicon-storage.com/info/docget.jsp?did=10660&prodName=TB6612FNG" + }, + "pins": [ + { + "name": "AO1", + "number": 1, + "model": { + "kind": "passive" + }, + "doc": "Channel A output 1", + "required": false + }, + { + "name": "AO1_2", + "number": 2, + "model": { + "kind": "passive" + }, + "doc": "Channel A output 1 (second pin, joined to pin 1)", + "required": false + }, + { + "name": "PGND1", + "number": 3, + "model": { + "kind": "passive" + }, + "doc": "Power ground 1", + "required": false + }, + { + "name": "PGND1_2", + "number": 4, + "model": { + "kind": "passive" + }, + "doc": "Power ground 1 (second pin, joined to pin 3)", + "required": false + }, + { + "name": "AO2", + "number": 5, + "model": { + "kind": "passive" + }, + "doc": "Channel A output 2", + "required": false + }, + { + "name": "AO2_2", + "number": 6, + "model": { + "kind": "passive" + }, + "doc": "Channel A output 2 (second pin, joined to pin 5)", + "required": false + }, + { + "name": "BO2", + "number": 7, + "model": { + "kind": "passive" + }, + "doc": "Channel B output 2", + "required": false + }, + { + "name": "BO2_2", + "number": 8, + "model": { + "kind": "passive" + }, + "doc": "Channel B output 2 (second pin, joined to pin 7)", + "required": false + }, + { + "name": "PGND2", + "number": 9, + "model": { + "kind": "passive" + }, + "doc": "Power ground 2", + "required": false + }, + { + "name": "PGND2_2", + "number": 10, + "model": { + "kind": "passive" + }, + "doc": "Power ground 2 (second pin, joined to pin 9)", + "required": false + }, + { + "name": "BO1", + "number": 11, + "model": { + "kind": "passive" + }, + "doc": "Channel B output 1", + "required": false + }, + { + "name": "BO1_2", + "number": 12, + "model": { + "kind": "passive" + }, + "doc": "Channel B output 1 (second pin, joined to pin 11)", + "required": false + }, + { + "name": "VM2", + "number": 13, + "model": { + "kind": "passive" + }, + "doc": "Motor supply (joined to VM1 and VM3)", + "required": false + }, + { + "name": "VM3", + "number": 14, + "model": { + "kind": "passive" + }, + "doc": "Motor supply", + "required": false + }, + { + "name": "PWMB", + "number": 15, + "model": { + "kind": "passive" + }, + "doc": "Channel B PWM input; 200 kohm pulldown", + "required": false + }, + { + "name": "BIN2", + "number": 16, + "model": { + "kind": "passive" + }, + "doc": "Channel B input 2; 200 kohm pulldown", + "required": false + }, + { + "name": "BIN1", + "number": 17, + "model": { + "kind": "passive" + }, + "doc": "Channel B input 1; 200 kohm pulldown", + "required": false + }, + { + "name": "GND", + "number": 18, + "model": { + "kind": "passive" + }, + "doc": "Small signal ground" + }, + { + "name": "STBY", + "number": 19, + "model": { + "kind": "passive" + }, + "doc": "Low: standby (outputs off); 200 kohm pulldown", + "required": false + }, + { + "name": "VCC", + "number": 20, + "model": { + "kind": "passive" + }, + "doc": "Small signal supply, 2.7 to 5.5 V" + }, + { + "name": "AIN1", + "number": 21, + "model": { + "kind": "passive" + }, + "doc": "Channel A input 1; 200 kohm pulldown", + "required": false + }, + { + "name": "AIN2", + "number": 22, + "model": { + "kind": "passive" + }, + "doc": "Channel A input 2; 200 kohm pulldown", + "required": false + }, + { + "name": "PWMA", + "number": 23, + "model": { + "kind": "passive" + }, + "doc": "Channel A PWM input; 200 kohm pulldown", + "required": false + }, + { + "name": "VM1", + "number": 24, + "model": { + "kind": "passive" + }, + "doc": "Motor supply, up to 15 V" + } + ], + "params": {}, + "model": { + "composite": { + "components": [ + { + "id": "A", + "component": "emblink/h_bridge", + "params": { + "mode": "in_in_pwm", + "sleep": true, + "vcc_ref": true, + "vil_frac": 0.3, + "vih_frac": 0.7, + "vhys": 0, + "en_vhys": 0, + "ron_hs": 0.25, + "ron_ls": 0.25, + "ron_hs_low": 0.25, + "ron_ls_low": 0.25, + "bd_vf": 1.0, + "bd_if": 1.0, + "rpd": 200000.0, + "uvlo": 1.9, + "uvlo_hys": 0.3, + "vtrip": 0, + "iocp": 0, + "t_deg": 0, + "t_prop": 0, + "t_dead": 5e-08, + "t_dead_lh": 2.3e-07, + "t_wake": 0, + "rpd_en": 200000.0, + "iq": 0.0011, + "iq_sleep": 1e-06 + } + }, + { + "id": "B", + "component": "emblink/h_bridge", + "params": { + "mode": "in_in_pwm", + "sleep": true, + "vcc_ref": true, + "vil_frac": 0.3, + "vih_frac": 0.7, + "vhys": 0, + "en_vhys": 0, + "ron_hs": 0.25, + "ron_ls": 0.25, + "ron_hs_low": 0.25, + "ron_ls_low": 0.25, + "bd_vf": 1.0, + "bd_if": 1.0, + "rpd": 200000.0, + "uvlo": 1.9, + "uvlo_hys": 0.3, + "vtrip": 0, + "iocp": 0, + "t_deg": 0, + "t_prop": 0, + "t_dead": 5e-08, + "t_dead_lh": 2.3e-07, + "t_wake": 0, + "rpd_en": 0, + "iq": 0, + "iq_sleep": 0 + } + } + ], + "nets": { + "STBY": [ + "A.EN", + "B.EN" + ], + "VM": [ + "A.VM", + "B.VM" + ], + "GND": [ + "A.GND", + "B.GND" + ], + "VCC": [ + "A.VCC", + "B.VCC" + ] + }, + "ports": { + "AO1": "A.OUT1", + "AO1_2": "A.OUT1", + "PGND1": "A.SEN", + "PGND1_2": "A.SEN", + "AO2": "A.OUT2", + "AO2_2": "A.OUT2", + "BO2": "B.OUT2", + "BO2_2": "B.OUT2", + "PGND2": "B.SEN", + "PGND2_2": "B.SEN", + "BO1": "B.OUT1", + "BO1_2": "B.OUT1", + "VM1": "A.VM", + "VM2": "A.VM", + "VM3": "A.VM", + "PWMB": "B.PWM", + "BIN2": "B.IN2", + "BIN1": "B.IN1", + "GND": "A.GND", + "STBY": "A.EN", + "VCC": "A.VCC", + "AIN1": "A.IN1", + "AIN2": "A.IN2", + "PWMA": "A.PWM" + } + } + }, + "symbol": { + "svg": "symbol.svg", + "width": 100, + "height": 120, + "pins": { + "STBY": { + "x": 0, + "y": 20, + "side": "left" + }, + "AIN1": { + "x": 0, + "y": 30, + "side": "left" + }, + "AIN2": { + "x": 0, + "y": 40, + "side": "left" + }, + "PWMA": { + "x": 0, + "y": 50, + "side": "left" + }, + "BIN1": { + "x": 0, + "y": 70, + "side": "left" + }, + "BIN2": { + "x": 0, + "y": 80, + "side": "left" + }, + "PWMB": { + "x": 0, + "y": 90, + "side": "left" + }, + "AO1": { + "x": 100, + "y": 20, + "side": "right" + }, + "AO1_2": { + "x": 100, + "y": 30, + "side": "right" + }, + "AO2": { + "x": 100, + "y": 40, + "side": "right" + }, + "AO2_2": { + "x": 100, + "y": 50, + "side": "right" + }, + "BO1": { + "x": 100, + "y": 70, + "side": "right" + }, + "BO1_2": { + "x": 100, + "y": 80, + "side": "right" + }, + "BO2": { + "x": 100, + "y": 90, + "side": "right" + }, + "BO2_2": { + "x": 100, + "y": 100, + "side": "right" + }, + "VCC": { + "x": 20, + "y": 0, + "side": "top" + }, + "VM1": { + "x": 40, + "y": 0, + "side": "top" + }, + "VM2": { + "x": 60, + "y": 0, + "side": "top" + }, + "VM3": { + "x": 80, + "y": 0, + "side": "top" + }, + "GND": { + "x": 20, + "y": 120, + "side": "bottom" + }, + "PGND1": { + "x": 40, + "y": 120, + "side": "bottom" + }, + "PGND1_2": { + "x": 50, + "y": 120, + "side": "bottom" + }, + "PGND2": { + "x": 70, + "y": 120, + "side": "bottom" + }, + "PGND2_2": { + "x": 80, + "y": 120, + "side": "bottom" + } + } + }, + "fidelity": { + "functional": null, + "timed": { + "claims": "As electrical.", + "omissions": [] + }, + "electrical": { + "claims": "Two bridges solved with the circuit (the primitive h_bridge, by way of emblink/h_bridge): the datasheet's H-SW control table (IN1 IN2 PWM STBY), the output saturation of 0.5 V at 1 A (0.5 ohm upper + lower), the 1 V regenerative diodes, input thresholds at 0.3 and 0.7 VCC with 200 kohm pulldowns, standby at 1 uA, 1.1 mA from VCC when on, the 50 ns (high to low) and 230 ns (low to high) dead times, and the VCC lockout at 1.9 V released at 2.2 V.", + "omissions": [ + "the split of the 0.5 ohm output saturation between the upper and lower FETs (0.25 ohm each assumed) and its change with VM", + "the propagation delay and input hysteresis, which the datasheet does not give (none modelled)", + "the output rise and fall times (24 and 41 ns; instant switching)", + "the die's temperature and thermal shutdown (175 C)", + "the supply current's rise with VCC (1.1 mA at 3 V, 1.5 mA at 5.5 V: 1.1 mA modelled)", + "an input's crossing between two of the circuit's steps is taken at the second" + ], + "validation": [ + "tests/*.json" + ] + } + }, + "provenance": { + "author": "EmbSIM", + "license": "Apache-2.0", + "sources": [ + { + "title": "Toshiba TB6612FNG Driver IC for Dual DC motor datasheet", + "url": "https://toshiba.semicon-storage.com/info/docget.jsp?did=10660&prodName=TB6612FNG" + } + ] + }, + "tests": [ + "tests/*.json" + ], + "i18n": { + "fr": { + "source": "d5d7def10a53", + "description": "Pilote double pont en H TB6612FNG de Toshiba (SSOP24) pour deux moteurs à courant continu à balais, VM jusqu'à 15 V et logique alimentée par VCC de 2,7 à 5,5 V. Les entrées IN1/IN2/PWM de chaque canal choisissent sens horaire, sens antihoraire, frein court ou arrêt (PWM bas freine) ; STBY bas met les deux canaux en veille ; les sorties LDMOS sont alimentées depuis VM à travers 0,5 Ω (haut + bas), avec des diodes de récupération de 1 V, un temps mort de 50/230 ns et des entrées à 0,3/0,7 VCC avec des résistances de rappel de 200 kΩ ; VCC sous 1,9 V l'éteint jusqu'à 2,2 V.", + "fidelity": { + "timed": { + "claims": "Comme en électrique." + }, + "electrical": { + "claims": "Deux ponts résolus avec le circuit (la primitive h_bridge, par emblink/h_bridge) : la table de commande H-SW de la fiche technique (IN1 IN2 PWM STBY), la tension de saturation de sortie de 0,5 V à 1 A (0,5 Ω haut + bas), les diodes de récupération de 1 V, les seuils d'entrée à 0,3 et 0,7 VCC avec des résistances de rappel de 200 kΩ, la veille à 1 µA, 1,1 mA tiré de VCC en marche, les temps morts de 50 ns (haut vers bas) et 230 ns (bas vers haut), et le verrouillage de VCC à 1,9 V relâché à 2,2 V.", + "omissions": [ + "la répartition des 0,5 Ω de saturation entre MOSFET haut et bas (0,25 Ω chacun supposé) et sa variation avec VM", + "le délai de propagation et l'hystérésis des entrées, que la fiche technique ne donne pas (non modélisés)", + "les temps de montée et de descente des sorties (24 et 41 ns ; commutation instantanée)", + "la température de la puce et la coupure thermique (175 °C)", + "la hausse du courant d'alimentation avec VCC (1,1 mA à 3 V, 1,5 mA à 5,5 V : 1,1 mA modélisé)", + "le passage d'un seuil par une entrée entre deux pas du circuit est pris au second" + ] + } + } + } + } +} diff --git a/catalog/components/tb6612fng/symbol.svg b/catalog/components/tb6612fng/symbol.svg new file mode 100644 index 000000000..19aced340 --- /dev/null +++ b/catalog/components/tb6612fng/symbol.svg @@ -0,0 +1,30 @@ + + + + STBY + AIN1 + AIN2 + PWMA + BIN1 + BIN2 + PWMB + AO1 + AO1 + AO2 + AO2 + BO1 + BO1 + BO2 + BO2 + VCC + VM1 + VM2 + VM3 + + + + + + GNDPGND1PGND2 + TB6612FNG + diff --git a/catalog/components/tb6612fng/tests/drive.json b/catalog/components/tb6612fng/tests/drive.json new file mode 100644 index 000000000..6dbb53312 --- /dev/null +++ b/catalog/components/tb6612fng/tests/drive.json @@ -0,0 +1,96 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "drive", + "description": "VCC 3.3 V, VM 6 V, STBY high. Channel A (AIN1 high, AIN2 low, PWMA high) turns a Pololu 50:1 gearmotor clockwise: about 1.33 A at start through 0.5 ohm of LDMOS, then about 640 rpm. Channel B with BIN1 and BIN2 high is in short brake.", + "policy": "electrical", + "until": "150ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VC", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/tb6612fng" + }, + { + "id": "M", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM1", + "DUT.VM2", + "DUT.VM3" + ], + "VCC": [ + "VC.+", + "DUT.VCC", + "DUT.STBY", + "DUT.AIN1", + "DUT.PWMA", + "DUT.BIN1", + "DUT.BIN2" + ], + "A": [ + "DUT.AO1", + "DUT.AO1_2", + "M.+" + ], + "B": [ + "DUT.AO2", + "DUT.AO2_2", + "M.-" + ], + "GND": [ + "VM.-", + "VC.-", + "DUT.GND", + "DUT.PGND1", + "DUT.PGND2", + "G.GND", + "DUT.AIN2" + ] + } + }, + "expect": { + "output:DUT.A.state": { + "at": "1ms", + "equals": "forward" + }, + "output:M.current": { + "at": "0.5ms", + "min": 1.1, + "max": 1.34 + }, + "output:M.rpm": { + "at": "140ms", + "min": 620, + "max": 650 + }, + "output:DUT.B.state": { + "at": "1ms", + "equals": "brake" + } + } +} \ No newline at end of file diff --git a/catalog/components/tb6612fng/tests/pwm_brake.json b/catalog/components/tb6612fng/tests/pwm_brake.json new file mode 100644 index 000000000..11aadd88e --- /dev/null +++ b/catalog/components/tb6612fng/tests/pwm_brake.json @@ -0,0 +1,80 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "pwm_brake", + "description": "PWMA low with AIN1 high and AIN2 low is a short brake: both of channel A's outputs low, the motor's terminals shorted through the lower FETs.", + "policy": "electrical", + "until": "1ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VC", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/tb6612fng" + }, + { + "id": "M", + "component": "emblink/pololu_3073_gearmotor" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM1" + ], + "VCC": [ + "VC.+", + "DUT.VCC", + "DUT.STBY", + "DUT.AIN1" + ], + "A": [ + "DUT.AO1", + "M.+" + ], + "B": [ + "DUT.AO2", + "M.-" + ], + "GND": [ + "VM.-", + "VC.-", + "DUT.GND", + "DUT.PGND1", + "DUT.PGND2", + "G.GND", + "DUT.AIN2", + "DUT.PWMA" + ] + } + }, + "expect": { + "output:DUT.A.state": { + "at": "0.5ms", + "equals": "brake" + }, + "output:DUT.A.out1": { + "at": "0.5ms", + "equals": "low" + } + } +} \ No newline at end of file diff --git a/catalog/components/tb6612fng/tests/standby.json b/catalog/components/tb6612fng/tests/standby.json new file mode 100644 index 000000000..61c4fc495 --- /dev/null +++ b/catalog/components/tb6612fng/tests/standby.json @@ -0,0 +1,71 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "standby", + "description": "STBY low (its 200 kohm pulldown): both channels off whatever their inputs, under 1 uA from VCC.", + "policy": "electrical", + "until": "1ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VC", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/tb6612fng" + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM1" + ], + "VCC": [ + "VC.+", + "DUT.VCC", + "DUT.AIN1", + "DUT.PWMA" + ], + "GND": [ + "VM.-", + "VC.-", + "DUT.GND", + "DUT.PGND1", + "DUT.PGND2", + "G.GND" + ] + } + }, + "expect": { + "output:DUT.A.state": { + "at": "0.5ms", + "equals": "coast" + }, + "output:DUT.A.asleep": { + "at": "0.5ms", + "equals": true + }, + "output:DUT.A.logic_current": { + "at": "0.5ms", + "min": 9e-07, + "max": 1.1e-06 + } + } +} \ No newline at end of file diff --git a/catalog/components/tb6612fng/tests/vcc_thresholds.json b/catalog/components/tb6612fng/tests/vcc_thresholds.json new file mode 100644 index 000000000..c144f09ca --- /dev/null +++ b/catalog/components/tb6612fng/tests/vcc_thresholds.json @@ -0,0 +1,130 @@ +{ + "schema_version": 1, + "kind": "component_test", + "name": "vcc_thresholds", + "description": "The inputs' thresholds follow VCC: at VCC = 5 V an input at 2.0 V is low (channel A stops) while 3.3 V is high (channel B drives a 10 ohm load); VCC falling to 1.8 V from 3 ms passes the 1.9 V lockout and turns both off.", + "policy": "electrical", + "until": "5ms", + "circuit": { + "components": [ + { + "id": "VM", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 6.0 + } + }, + { + "id": "VC", + "component": "emblink/vsource", + "params": { + "kind": "pwl", + "points": [ + [ + 0, + 5 + ], + [ + 0.003, + 5 + ], + [ + 0.0031, + 1.8 + ] + ] + } + }, + { + "id": "V2", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 2.0 + } + }, + { + "id": "V3", + "component": "emblink/vsource", + "params": { + "kind": "dc", + "dc": 3.3 + } + }, + { + "id": "DUT", + "component": "emblink/tb6612fng" + }, + { + "id": "RL", + "component": "emblink/resistor", + "params": { + "resistance": 10 + } + }, + { + "id": "G", + "component": "emblink/gnd" + } + ], + "nets": { + "VM": [ + "VM.+", + "DUT.VM1" + ], + "VCC": [ + "VC.+", + "DUT.VCC", + "DUT.STBY", + "DUT.PWMA", + "DUT.PWMB" + ], + "TWO": [ + "V2.+", + "DUT.AIN1" + ], + "THREE": [ + "V3.+", + "DUT.BIN1" + ], + "B1": [ + "DUT.BO1", + "RL.1" + ], + "B2": [ + "DUT.BO2", + "RL.2" + ], + "GND": [ + "VM.-", + "VC.-", + "DUT.GND", + "DUT.PGND1", + "DUT.PGND2", + "G.GND", + "V2.-", + "V3.-" + ] + } + }, + "expect": { + "output:DUT.A.state": { + "at": "2ms", + "equals": "coast" + }, + "output:DUT.B.state": { + "at": "2ms", + "equals": "forward" + }, + "output:DUT.B.vout": { + "at": "2ms", + "min": 5.65, + "max": 5.75 + }, + "output:DUT.B.undervoltage": { + "at": "4ms", + "equals": true + } + } +} \ No newline at end of file diff --git a/core/analog/devices.c b/core/analog/devices.c index 1d9e8d688..2d70b5e39 100644 --- a/core/analog/devices.c +++ b/core/analog/devices.c @@ -47,7 +47,7 @@ const char *const *esk_adev_pin_names(const esk_adev_ops *ops, int *n) *const xfmr[] = { "P1", "P2", "S1", "S2" }, *const xfmr_ct[] = { "P1", "P2", "S1", "CT", "S2" }, *const triac[] = { "MT2", "MT1", "G" }, *const diac[] = { "A1", "A2" }, - *const hbr[] = { "IN1", "IN2", "PWM", "EN", "OUT1", "OUT2", "VM", "GND", "SEN", "FAULT" }; + *const hbr[] = { "IN1", "IN2", "PWM", "EN", "OUT1", "OUT2", "VM", "GND", "SEN", "FAULT", "VCC" }; static const struct { const char *name; const char *const *pins; int n; } table[] = { { "environment_resistor", env, 2 }, { "ptc_fuse", two, 2 }, { "photodiode", dio, 2 }, { "optocoupler", opto, 4 }, { "relay_contacts", relay, 5 }, { "spdt_contact", spdt, 3 }, @@ -57,7 +57,7 @@ const char *const *esk_adev_pin_names(const esk_adev_ops *ops, int *n) { "nmos", mos, 3 }, { "pmos", mos, 3 }, { "opamp", amp, 5 }, { "regulator", reg, 3 }, { "pull", one, 1 }, { "usb_meter", reg, 3 }, { "instrument_input", src, 2 }, { "power_profiler", ppk, 3 }, { "oscilloscope", scope, 5 }, { "multimeter", dmm, 4 }, { "transformer", xfmr, 4 }, { "transformer_ct", xfmr_ct, 5 }, - { "triac", triac, 3 }, { "diac", diac, 2 }, { "dc_motor", src, 2 }, { "h_bridge", hbr, 10 }, + { "triac", triac, 3 }, { "diac", diac, 2 }, { "dc_motor", src, 2 }, { "h_bridge", hbr, 11 }, }; size_t i; for (i = 0; ops && i < sizeof table / sizeof table[0]; i++) diff --git a/core/analog/hbridge.c b/core/analog/hbridge.c index ff57f87d1..4329a9f58 100644 --- a/core/analog/hbridge.c +++ b/core/analog/hbridge.c @@ -6,7 +6,8 @@ * * Terminals: the logic inputs IN1, IN2, PWM and EN, the outputs OUT1 and * OUT2, the motor supply VM, the logic ground GND, the low-side return SEN - * (the sense resistor's top, or GND) and the open-drain FAULT output. Each + * (the sense resistor's top, or GND), the open-drain FAULT output and the + * logic supply VCC (used with vcc_ref, else left open). Each * output is a half-bridge: a high-side FET from VM (ron_hs) and a low-side * FET to SEN (ron_ls), each with its body diode (OUT to VM, SEN to OUT), so * a motor's current recirculates through the diodes while both FETs are off @@ -31,8 +32,14 @@ * Timing: an input change is taken after it holds t_deg (the deglitch * filter; shorter pulses are lost) and reaches the outputs t_prop after it * began; an output passing between high and low is off for t_dead (the - * break-before-make). VM below uvlo (falling; uvlo + uvlo_hys rising) - * disables the bridge and resets its logic. + * break-before-make; t_dead_lh for low to high when it differs). VM below + * uvlo (falling; uvlo + uvlo_hys rising) disables the bridge and resets + * its logic. + * + * With vcc_ref (TB6612FNG), the logic has its own supply VCC: the input + * thresholds are the fractions vil_frac and vih_frac of it (vhys about + * their mean, in volts), the lockout watches VCC instead of VM, and the + * quiescent currents are drawn from VCC. * * Current control (vtrip > 0): while the bridge drives, SEN above vtrip * (I x Rsense) switches it to slow decay (both low sides on) until the @@ -54,7 +61,7 @@ #include #include -enum { HB_IN1, HB_IN2, HB_PWM, HB_EN, HB_OUT1, HB_OUT2, HB_VM, HB_GND, HB_SEN, HB_FAULT, HB_NPINS }; +enum { HB_IN1, HB_IN2, HB_PWM, HB_EN, HB_OUT1, HB_OUT2, HB_VM, HB_GND, HB_SEN, HB_FAULT, HB_VCC, HB_NPINS }; enum { HB_Z, HB_H, HB_L }; enum { HB_MODE_IN_IN, HB_MODE_IN_IN_PWM, HB_MODE_HALF }; @@ -68,13 +75,14 @@ typedef struct hb_cmd { } hb_cmd; typedef struct hb_state { - int mode, sleep; + int mode, sleep, vcc_ref; + double vil_frac, vih_frac, hys_v[HB_NIN]; double ron_hs_hi, ron_ls_hi, ron_hs_lo, ron_ls_lo, vm_lo, vm_hi, ron_hs, ron_ls, goff; double bd_is, bd_nvt, bd_vcrit; double rpd, rpd_en, vt_rise[HB_NIN], vt_fall[HB_NIN]; double en_clamp, z_is, z_nvt, z_vcrit; double iq, iq_sleep, uvlo, uvlo_hys, vtrip, iocp, fault_r; - es_time_t t_deg, t_prop, t_dead, t_wake, t_blank, t_chop, t_ocp_deg, t_ocp; + es_time_t t_deg, t_prop, t_dead, t_dead_lh, t_wake, t_blank, t_chop, t_ocp_deg, t_ocp; /* the inputs: as seen at the last accepted step, and as taken */ int raw[HB_NIN], filt[HB_NIN]; int pend[HB_NIN]; /* raw differs from filt: taken at deg_at */ @@ -98,6 +106,18 @@ static double par(const esk_adev *d, const char *name, double fallback) return d->comp && esk_comp_param_double(d->comp, name, &v) == 0 ? v : fallback; } +/* a boolean parameter (true/false, or a number) */ +static int par_b(const esk_adev *d, const char *name, int fallback) +{ + int b; + double v; + if (d->comp && esk_comp_param_bool(d->comp, name, &b) == 0) + return b != 0; + if (d->comp && esk_comp_param_double(d->comp, name, &v) == 0) + return v != 0.0; + return fallback; +} + static es_time_t par_t(const esk_adev *d, const char *name, double fallback_s) { double s = par(d, name, fallback_s); @@ -124,13 +144,16 @@ static es_status hb_setup(esk_adev *d) double bd_vf = par(d, "bd_vf", 0.8), bd_if = par(d, "bd_if", 1.0); int k; if (d->nnodes < HB_NPINS) { - bad_param(d, "pins", "an h_bridge has ten terminals"); + bad_param(d, "pins", "an h_bridge has eleven terminals"); return ES_EINVAL; } s->mode = !strcmp(mode, "in_in_pwm") ? HB_MODE_IN_IN_PWM : !strcmp(mode, "half") ? HB_MODE_HALF : HB_MODE_IN_IN; if (strcmp(mode, "in_in") && strcmp(mode, "in_in_pwm") && strcmp(mode, "half")) bad_param(d, "mode", "in_in, in_in_pwm or half"); - s->sleep = par(d, "sleep", s->mode == HB_MODE_IN_IN ? 1.0 : 0.0) != 0.0; + s->sleep = par_b(d, "sleep", s->mode == HB_MODE_IN_IN); + s->vcc_ref = par_b(d, "vcc_ref", 0); + s->vil_frac = par(d, "vil_frac", 0.3); + s->vih_frac = par(d, "vih_frac", 0.7); s->ron_hs_hi = par(d, "ron_hs", 0.2); s->ron_ls_hi = par(d, "ron_ls", 0.16); s->ron_hs_lo = par(d, "ron_hs_low", s->ron_hs_hi); @@ -152,6 +175,7 @@ static es_status hb_setup(esk_adev *d) s->t_deg = par_t(d, "t_deg", 450e-9); s->t_prop = par_t(d, "t_prop", 1.1e-6); s->t_dead = par_t(d, "t_dead", 450e-9); + s->t_dead_lh = par_t(d, "t_dead_lh", (double)s->t_dead * 1e-12); s->t_wake = par_t(d, "t_wake", s->sleep ? 1e-3 : 0.0); s->t_blank = par_t(d, "t_blank", 3.75e-6); s->t_chop = par_t(d, "t_chop", 1.0 / par(d, "f_chop", 50e3)); @@ -161,7 +185,7 @@ static es_status hb_setup(esk_adev *d) !(s->vm_hi > s->vm_lo) || !(s->goff > 0.0) || !(s->bd_nvt > 0.0) || !(bd_vf > 0.0) || !(bd_if > 0.0) || s->rpd < 0.0 || s->rpd_en < 0.0 || s->iq < 0.0 || s->iq_sleep < 0.0 || s->en_clamp < 0.0 || s->vtrip < 0.0 || s->iocp < 0.0 || !(s->fault_r > 0.0) || !(vih > vil) || !(en_vih > en_vil) || - hys < 0.0 || en_hys < 0.0 || s->t_prop < s->t_deg || !s->t_chop) { + hys < 0.0 || en_hys < 0.0 || !(s->vih_frac > s->vil_frac) || s->vil_frac < 0.0 || s->vih_frac > 1.0 || s->t_prop < s->t_deg || !s->t_chop) { bad_param(d, "h_bridge", "resistances, VM points, junction and thresholds out of range"); return ES_EINVAL; } @@ -174,6 +198,7 @@ static es_status hb_setup(esk_adev *d) double mid = 0.5 * (lo + hi); s->vt_rise[k] = mid + 0.5 * h; s->vt_fall[k] = mid - 0.5 * h; + s->hys_v[k] = h; } /* the clamp: 100 uA at en_clamp, n = 1.5 */ s->z_nvt = 1.5 * HB_VT; @@ -243,9 +268,12 @@ static void hb_load(esk_adev *d, esk_amatrix *m) } if (s->en_clamp > 0.0) hb_diode(d, m, HB_EN, HB_GND, &s->vz_old, s->z_is, s->z_nvt, s->z_vcrit, hb_zx(s, 0.0)); - v = esk_am_v(m, vm, gnd); - hb_iq(s->awake || !s->sleep ? s->iq : s->iq_sleep, v, &i, &g); - stamp_nl(m, vm, gnd, g + 1e-12, i, v); + { + int sup = s->vcc_ref ? d->node[HB_VCC] : vm; + v = esk_am_v(m, sup, gnd); + hb_iq(s->awake || !s->sleep ? s->iq : s->iq_sleep, v, &i, &g); + stamp_nl(m, sup, gnd, g + 1e-12, i, v); + } esk_am_stamp_g(m, d->node[HB_FAULT], gnd, s->fault ? 1.0 / s->fault_r : 1e-9); } @@ -353,8 +381,11 @@ static void hb_currents(esk_adev *d) esk_pn_eval(hb_zx(s, node_v(d, HB_EN) - vgnd), s->z_is, s->z_nvt, &i, &g); FLOW(HB_EN, HB_GND, i); } - hb_iq(s->awake || !s->sleep ? s->iq : s->iq_sleep, vm - vgnd, &i, &g); - FLOW(HB_VM, HB_GND, i); + { + int sup = s->vcc_ref ? HB_VCC : HB_VM; + hb_iq(s->awake || !s->sleep ? s->iq : s->iq_sleep, node_v(d, sup) - vgnd, &i, &g); + FLOW(sup, HB_GND, i); + } FLOW(HB_FAULT, HB_GND, (node_v(d, HB_FAULT) - vgnd) * (s->fault ? 1.0 / s->fault_r : 1e-9)); #undef FLOW } @@ -363,6 +394,7 @@ static void hb_accept(esk_adev *d, es_time_t t) { hb_state *s = (hb_state *)d->state; double vgnd = node_v(d, HB_GND), vm = node_v(d, HB_VM) - vgnd; + double vcc = node_v(d, HB_VCC) - vgnd, vsup = s->vcc_ref ? vcc : vm; int before[2]; int k, taken = 0; before[0] = s->eff[0]; @@ -376,7 +408,7 @@ static void hb_accept(esk_adev *d, es_time_t t) } /* undervoltage lockout: below it everything is off and reset */ if (!s->powered || s->uvlo_on) { - if (vm > s->uvlo + s->uvlo_hys) { + if (vsup > s->uvlo + s->uvlo_hys) { s->powered = 1; s->uvlo_on = 0; hb_logic_reset(s); @@ -387,7 +419,7 @@ static void hb_accept(esk_adev *d, es_time_t t) for (k = 0; k < HB_NIN; k++) s->filt[k] = s->raw[k] = 0; } - } else if (vm < s->uvlo) { + } else if (vsup < s->uvlo) { s->uvlo_on = 1; s->awake = s->waking = 0; hb_logic_reset(s); @@ -396,9 +428,15 @@ static void hb_accept(esk_adev *d, es_time_t t) for (k = 0; k < HB_NIN; k++) { double v = node_v(d, k) - vgnd; int r = s->raw[k]; - if (!r && v > s->vt_rise[k]) + double rise = s->vt_rise[k], fall = s->vt_fall[k]; + if (s->vcc_ref) { + double mid = 0.5 * (s->vil_frac + s->vih_frac) * vcc; + rise = mid + 0.5 * s->hys_v[k]; + fall = mid - 0.5 * s->hys_v[k]; + } + if (!r && v > rise) r = 1; - else if (r && v < s->vt_fall[k]) + else if (r && v < fall) r = 0; if (r != s->raw[k]) { s->raw[k] = r; @@ -499,8 +537,10 @@ static void hb_accept(esk_adev *d, es_time_t t) if (!hb_driving(s->want) && hb_driving(w)) s->blank_end = t + s->t_blank; for (k = 0; k < 2; k++) { - if ((s->want[k] == HB_H && w[k] == HB_L) || (s->want[k] == HB_L && w[k] == HB_H)) + if (s->want[k] == HB_H && w[k] == HB_L) s->dead_until[k] = t + s->t_dead; + else if (s->want[k] == HB_L && w[k] == HB_H) + s->dead_until[k] = t + s->t_dead_lh; s->want[k] = w[k]; s->eff[k] = t < s->dead_until[k] ? HB_Z : w[k]; } @@ -568,6 +608,8 @@ static void hb_observe(esk_adev *d, esk_json *o) /* the load current, out of OUT1 (into the motor) */ esk_json_set_number(a, o, "current", -d->i_last[HB_OUT1]); esk_json_set_number(a, o, "supply_current", d->i_last[HB_VM]); + if (s->vcc_ref) + esk_json_set_number(a, o, "logic_current", d->i_last[HB_VCC]); esk_json_set_number(a, o, "vout", node_v(d, HB_OUT1) - node_v(d, HB_OUT2)); esk_json_set_bool(a, o, "fault", s->fault != 0); esk_json_set_bool(a, o, "asleep", s->sleep && !s->awake); diff --git a/docs/STATUS.md b/docs/STATUS.md index 5c11f65fa..27cb6173d 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -44,8 +44,13 @@ Evidence, its eight fixtures: 1.35 ms retry; - 1.6 uA asleep and 1.7 mA awake. -Not yet: the thermal shutdown, switching edges, the TB6612FNG and L293D -parts on the primitive. +`emblink/tb6612fng` is Toshiba's TB6612FNG on the same primitive, its logic +referred to its own VCC supply. Its fixtures cover the gearmotor at 6 V +(640 rpm), PWM low braking, standby under 1 uA, and inputs and lockout +following VCC. + +Not yet: the thermal shutdown, switching edges, the L293D on the +primitive. ## The library's categories (2026-11-03) diff --git a/docs/specs/SPEC-004-analog-engine-and-bridges.md b/docs/specs/SPEC-004-analog-engine-and-bridges.md index f582190a5..87cc187d7 100644 --- a/docs/specs/SPEC-004-analog-engine-and-bridges.md +++ b/docs/specs/SPEC-004-analog-engine-and-bridges.md @@ -209,7 +209,7 @@ optional gearbox: sleeps), its torque and the electrical and mechanical power. `h_bridge` (pins `IN1`, `IN2`, `PWM`, `EN`, `OUT1`, `OUT2`, `VM`, `GND`, -`SEN`, `FAULT`) is one H-bridge of an integrated motor driver: +`SEN`, `FAULT`, `VCC`) is one H-bridge of an integrated motor driver: - **Power stage.** Each output is a half-bridge: a high-side FET from `VM` and a low-side FET to `SEN` (the sense resistor's top, or ground), each a resistance set from VM between two datasheet points, each with its body @@ -219,7 +219,9 @@ optional gearbox: thresholds, with their pulldowns (and `EN`'s clamp). `mode` picks the truth table: `in_in` (DRV8833), `in_in_pwm` (TB6612FNG), `half` (L293D). With `sleep`, `EN` is nSLEEP: low resets the logic, and `t_wake` passes - after it rises. + after it rises. With `vcc_ref`, the logic has its own supply on `VCC`: + the thresholds are fractions of it, the lockout watches it and the + quiescent current is drawn from it (TB6612FNG). - **Timing.** The logic state changes only on accepted steps, as the thyristors' does. An input is taken `t_deg` after it changes (the deglitch; a shorter pulse is lost) and reaches the outputs `t_prop` after