diff --git a/.github/workflows/calculos-lpu.yml b/.github/workflows/calculos-lpu.yml new file mode 100644 index 0000000..9cced42 --- /dev/null +++ b/.github/workflows/calculos-lpu.yml @@ -0,0 +1,95 @@ +name: Cálculos LPU (Magnet Driver Abejorro) + +# Recalcula los cálculos de diseño del Magnet Driver Abejorro (carpeta H12/LPU) cuando cambian +# sus entradas o su motor de cálculo. No se ejecuta con los cambios de otras carpetas del repositorio. +# +# - push a la rama principal: guarda el informe general y el de cambios en H12/LPU/docs/calculos +# y los deja descargables en la ejecución; +# - pull request: solo calcula y deja los informes descargables (no escribe en el repositorio); +# - ejecución manual (Actions > Cálculos LPU > Run workflow): igual que un push. +# +# Si la carpeta del proyecto cambia de sitio, hay que actualizar PROY, los filtros «paths» y la ruta +# del paso «Dejar los informes descargables». +# Si la rama principal no es «main», hay que cambiarla en «branches». +# Si la rama principal está protegida y la Action no puede escribir, ponga PUBLICAR: "no": +# entonces solo genera las descargas y los informes se guardan a mano desde la vista previa local. +on: + push: + branches: [main] + paths: + - "H12/LPU/calculos/**" + - "H12/LPU/tests/**" + - ".github/workflows/calculos-lpu.yml" + pull_request: + paths: + - "H12/LPU/calculos/**" + - "H12/LPU/tests/**" + workflow_dispatch: + +env: + PROY: H12/LPU + PUBLICAR: "si" + +permissions: + contents: write + +concurrency: + group: calculos-lpu-${{ github.ref }} + cancel-in-progress: false + +jobs: + calcular: + runs-on: ubuntu-latest + steps: + - name: Descargar el repositorio + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Preparar Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + + - name: Instalar dependencias + run: pip install -r "$PROY/requirements.txt" + + - name: Pruebas del motor de cálculo + run: python -m pytest -q -p no:cacheprovider "$PROY/tests" + + - name: Calcular y generar los informes + run: | + if [ "${{ github.event_name }}" = "pull_request" ] || [ "$PUBLICAR" != "si" ]; then + python "$PROY/calculos/calcular.py" --commit "${{ github.event.pull_request.head.sha || github.sha }}" + else + python "$PROY/calculos/calcular.py" --publicar --commit "${{ github.sha }}" + fi + + - name: Fecha para el nombre de la descarga + id: fecha + run: echo "valor=$(TZ=Europe/Madrid date +%Y-%m-%d_%H%M)" >> "$GITHUB_OUTPUT" + + - name: Dejar los informes descargables + uses: actions/upload-artifact@v6 + with: + name: informes-lpu-${{ steps.fecha.outputs.valor }} + path: H12/LPU/salida/ + retention-days: 90 + + - name: Guardar los informes en el repositorio + if: github.event_name != 'pull_request' + run: | + if [ "$PUBLICAR" != "si" ]; then + echo "PUBLICAR = $PUBLICAR: los informes solo quedan como descarga." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "$PROY/docs/calculos" + if git diff --cached --quiet; then + echo "Los informes del repositorio no cambian." + exit 0 + fi + git commit -m "LPU: informes de cálculo actualizados (${GITHUB_SHA::7}) [skip ci]" + git pull --rebase origin "${GITHUB_REF_NAME}" + git push origin "HEAD:${GITHUB_REF_NAME}" diff --git a/H12/LPU/.gitignore b/H12/LPU/.gitignore new file mode 100644 index 0000000..8de3fb4 --- /dev/null +++ b/H12/LPU/.gitignore @@ -0,0 +1,11 @@ +# Vista previa local de los informes +salida/ + +# Python +__pycache__/ +*.pyc +.pytest_cache/ +.venv/ + +# El repositorio del equipo ignora los .csv; en esta carpeta sí hacen falta +!*.csv diff --git a/H12/LPU/.vscode/extensions.json b/H12/LPU/.vscode/extensions.json new file mode 100644 index 0000000..f4abf97 --- /dev/null +++ b/H12/LPU/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "yzhang.markdown-all-in-one", + "ms-python.python" + ] +} diff --git a/H12/LPU/.vscode/tasks.json b/H12/LPU/.vscode/tasks.json new file mode 100644 index 0000000..33cea3d --- /dev/null +++ b/H12/LPU/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Cálculos: vista previa", + "detail": "Recalcula y deja el informe general y el de cambios en salida/ (no modifica docs/)", + "type": "shell", + "command": "python calculos/calcular.py", + "windows": { "command": "py calculos\\calcular.py" }, + "group": { "kind": "build", "isDefault": true }, + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "Cálculos: pruebas del motor", + "detail": "Comprueba que las fórmulas siguen dando los resultados de referencia", + "type": "shell", + "command": "python -m pytest -q -p no:cacheprovider tests", + "windows": { "command": "py -m pytest -q -p no:cacheprovider tests" }, + "group": "test", + "problemMatcher": [] + } + ] +} diff --git a/H12/LPU/README.md b/H12/LPU/README.md new file mode 100644 index 0000000..e8ac163 --- /dev/null +++ b/H12/LPU/README.md @@ -0,0 +1,24 @@ +# Magnet Driver Abejorro (H12/LPU) + +Convertidor DC/DC en puente en H con control local en FPGA para los electroimanes de levitación y guiado del prototipo H12. Primera generación con FPGA; sustituye a la LPU anterior. + +Todo el proyecto vive en esta carpeta del repositorio de Hardware, salvo el fichero de la GitHub Action, que tiene que estar en la raíz del repositorio (`.github/workflows/calculos-lpu.yml`) porque GitHub solo ejecuta los workflows que están ahí. + +## Estructura + +| Carpeta | Contenido | +|---|---| +| `docs/requisitos/` | Documento de requisitos y arquitectura (borrador v0.2) | +| `docs/calculos/` | Informe de cálculos de diseño **generado automáticamente**, sus resultados en JSON y el historial de informes de cambios. No se edita a mano | +| `calculos/entradas/` | **Entradas de los cálculos en Markdown**: parámetros, datos de componentes y tablas de diseño | +| `calculos/normas/` | Tablas de normas (IPC-2221B, IEC 60664-1, PoE) en CSV | +| `calculos/motor/` | Motor de cálculo en Python (fórmulas) | +| `tests/` | Pruebas del motor con un juego de entradas fijo | +| `salida/` | Vista previa local de los informes (no se sube al repositorio) | + +## Cálculos de diseño en dos líneas + +1. Edita `calculos/entradas/parametros.md` (o los otros ficheros de entradas) y sube el cambio. +2. La Action recalcula, actualiza `docs/calculos/informe.md`, añade un informe de cambios en `docs/calculos/cambios/` y deja ambos como descarga de la ejecución. + +Manual completo: [`calculos/README.md`](calculos/README.md). Último informe: [`docs/calculos/informe.md`](docs/calculos/informe.md). diff --git a/H12/LPU/calculos/README.md b/H12/LPU/calculos/README.md new file mode 100644 index 0000000..e46fcc7 --- /dev/null +++ b/H12/LPU/calculos/README.md @@ -0,0 +1,147 @@ +# Cálculos de diseño: manual de uso + +Los cálculos del Magnet Driver Abejorro (potencia, térmico, pistas, aislamiento, integridad de señal, sensado, gate driving y alimentación de BT) se definen con ficheros Markdown y se recalculan automáticamente. Sustituyen a la hoja Excel v0.1, con las mismas fórmulas y las mismas 75 comprobaciones. + +## 1. Cómo funciona + +``` +H12/LPU/calculos/entradas/*.md ──► motor (Python) ──► H12/LPU/docs/calculos/informe.md (informe general) +H12/LPU/calculos/normas/*.csv ──┘ ──► H12/LPU/docs/calculos/cambios/.md (informe de cambios) + ──► H12/LPU/docs/calculos/resultados.json (estado para comparar) +``` + +Todas las rutas de este manual son relativas a `H12/LPU`, salvo la de la Action: su fichero está en `.github/workflows/calculos-lpu.yml`, en la raíz del repositorio de Hardware, porque GitHub solo ejecuta los workflows que están ahí. La Action solo se dispara con cambios de esta carpeta. + +1. Se edita un fichero de `calculos/entradas/` y se sube el cambio a la rama principal. +2. La GitHub Action **Cálculos LPU (Magnet Driver Abejorro)**: + - ejecuta las pruebas del motor; + - recalcula todo; + - compara con el último estado publicado (`docs/calculos/resultados.json`); + - guarda en el repositorio el informe general y, si ha cambiado algo, un informe de cambios con fecha y commit; + - deja los dos informes como **descarga** de la ejecución. +3. Si una entrada está mal escrita, la Action falla e indica el fichero, la línea y el problema. GitHub avisa por correo. + +## 2. Qué fichero editar + +| Fichero | Contenido | Cuándo se edita | +|---|---|---| +| `entradas/parametros.md` | Parámetros escalares con fuente, estado y requisito | Normalmente, es el único que se toca | +| `entradas/componentes.md` | Hojas de datos: MOSFET candidatos, driver, fuente aislada, sensores, componentes que cruzan la barrera | Al añadir o cambiar un componente | +| `entradas/diseno.md` | Pistas de potencia, interfaces de señal, distancias de aislamiento previstas y consumos de BT | Al avanzar el layout | +| `normas/*.csv` | Tablas de IPC-2221B, IEC 60664-1 y PoE | Al verificar las normas con el texto oficial | + +### Reglas de formato + +- En las tablas de parámetros se cambian «Valor», «Estado», «Fuente» y «Nota». **La columna «Nombre» (o «Clave») la usan las fórmulas**: no la cambie. +- Decimales con coma o con punto (`1,92` o `1.92`), sin separador de miles. Signo menos: `-` o `−`. +- «Estado»: `OK`, `AC` o `TBD`. Los TBD llevan un valor provisional y el informe los lista en el anexo C. +- Las unidades se convierten solas. Un valor en `%` se interpreta como fracción. Unidades admitidas: + `V mV kV · A mA µA · Ω mΩ µΩ kΩ MΩ · H mH µH nH · F mF µF nF pF · C µC nC · J mJ µJ · W mW kW · s ms µs ns ps · Hz kHz MHz · m mm µm mm² · °C K K/W W/(m·K) · L/min · oz · bit · kV/µs · % · –`. + Se puede escribir `u` en lugar de `µ` (por ejemplo, `uF`). +- Los parámetros de texto (unidad `–`) solo admiten los valores indicados en su descripción: `Encadenado`/`Estrella`, `Unipolar`/`Bipolar`, `Básico`/`Reforzado`, y la referencia de un MOSFET de `componentes.md` en `MOSFET_sel`. +- No use el carácter `|` dentro de una celda. +- La extensión recomendada *Markdown All in One* alinea las tablas con **Alt + Mayús + F**. + +## 3. Ver el resultado antes de subir el cambio + +Desde la raíz del repositorio: + +```bash +python H12/LPU/calculos/calcular.py # en Windows: py H12\LPU\calculos\calcular.py +``` + +En VS Code hay dos tareas preparadas («Cálculos: vista previa», que es la tarea de compilación por defecto, y «Cálculos: pruebas del motor»). Están en `H12/LPU/.vscode/tasks.json`, así que aparecen si abres **la carpeta `H12/LPU`** como espacio de trabajo (Archivo > Abrir carpeta) y se lanzan con Ctrl + Mayús + B. + +Si prefieres abrir el repositorio entero, puedes usar el comando de arriba en el terminal integrado o copiar las dos tareas de [`extras/tareas-para-la-raiz.json`](../extras/tareas-para-la-raiz.json) en el `.vscode/tasks.json` de la raíz (ya llevan las rutas con el prefijo `H12/LPU`). + +Genera en `salida/` (carpeta no versionada): + +- `informe_.md`: informe general, con la fecha, el commit y la instantánea de todos los parámetros (anexo B). Es el que conviene descargar para saber «en qué parámetros me he quedado»; +- `cambios_.md`: cambios respecto al último informe publicado en `docs/calculos/`; +- `resultados.json`. + +Si hay cambios sin confirmar en `calculos/`, el informe lo indica en «Versión de las entradas». Para ver los informes con formato, abra el fichero y pulse **Ctrl + Mayús + V**. + +Las pruebas se lanzan con `python -m pytest -q -p no:cacheprovider H12/LPU/tests` (el `-p no:cacheprovider` evita que pytest cree una carpeta de caché en la raíz del repositorio de Hardware). + +Requisitos: Python 3.9 o posterior. Para las pruebas: `pip install -r H12/LPU/requirements.txt`. En Windows, `tzdata` hace falta para que la fecha salga en hora de Madrid. + +## 4. Dónde están los informes + +| Qué | Dónde | +|---|---| +| Último informe general | `docs/calculos/informe.md` (en GitHub se lee con formato) | +| Historial de informes de cambios | `docs/calculos/cambios/README.md` | +| Informes de una ejecución concreta | GitHub > Actions > Cálculos LPU > la ejecución > «Artifacts» > `informes-lpu-` (se conservan 90 días) | +| Informe a fecha de hoy sin cambiar nada | GitHub > Actions > Cálculos LPU > **Run workflow** | +| Estado de cualquier fecha pasada | Historial de git de `docs/calculos/informe.md` | + +El informe general solo se reescribe en el repositorio cuando cambia algo más que la fecha; el informe de cambios solo se crea cuando cambia alguna entrada o algún resultado. + +## 5. Tareas habituales + +### Añadir un MOSFET candidato + +Rellene la columna «Candidato 2» (o añada «Candidato 4», «Candidato 5»…) de `componentes.md`. Todas las filas son obligatorias salvo «Fabricante», «Encapsulado», «VGS máxima absoluta (negativa)», «Tj máxima», «Coss», «Qrr» y «RG de ensayo»; un candidato incompleto se ignora y el informe dice qué le falta. Para usarlo en el resto de cálculos, ponga su referencia en `MOSFET_sel`. + +### Añadir una pista, una interfaz o un consumo + +Añada una fila a la tabla correspondiente de `diseno.md` con una «Clave» nueva (letras, números y `_`). La comprobación aparecerá en el informe como `PIS-`, `SI-`, etc. + +### Añadir un parámetro o cambiar una fórmula + +Las fórmulas están en `motor/`, un fichero por apartado (`potencia.py`, `termico.py`, etc.). Cada magnitud se escribe como: + +```python +s.res("I_bus_pk", "Corriente media de bus a Ipk, Vbus_min", x.P_coil_pk / (x.eta_pwr * x.Vbus_min), "A", dec=2, + expr="P_coil_pk/(eta_pwr·Vbus_min)") +``` + +- `x.` da el valor en SI de un parámetro o de una magnitud ya calculada. +- Los valores se guardan en SI. `esc` y `dec` solo afectan a cómo se muestran. +- Las comprobaciones se escriben con `s.chk(id, etiqueta, valor, límite, "<=" o ">=", ...)`. Con `blando=True`, un fallo aparece como REVISAR. + +Si añade un parámetro, añada también su fila en `parametros.md` **y en `tests/datos_v0.2/entradas/parametros.md`**, porque las pruebas usan esa copia fija. Si cambia una fórmula a propósito, las pruebas fallarán hasta que regenere los resultados de referencia: + +```bash +python H12/LPU/tests/regenera_esperado.py +``` + +## 6. Ajustes de la Action + +El fichero `.github/workflows/calculos-lpu.yml` (raíz del repositorio) tiene tres cosas que puede hacer falta tocar: + +| Qué | Dónde | Cuándo | +|---|---|---| +| Ruta del proyecto | `env: PROY: H12/LPU` y los filtros `paths:` | Si la carpeta cambia de sitio o de nombre | +| Rama principal | `on: push: branches: [main]` | Si la rama principal no se llama `main` | +| `env: PUBLICAR: "si"` | Ponga `"no"` | Si la rama principal está protegida y la Action no puede escribir en el repositorio: entonces solo genera las descargas, y los informes se suben a mano desde la vista previa local | + +Si el paso «Guardar los informes en el repositorio» falla con un error de permisos, hay dos opciones: activar «Read and write permissions» en Settings > Actions > General (lo decide quien administre el repositorio) o poner `PUBLICAR: "no"`. + +Con pull requests, la Action calcula y deja los informes descargables en el propio pull request; los guarda en el repositorio al fusionar en la rama principal. + +## 7. Pruebas + +`tests/` contiene una copia fija de las entradas de la versión v0.2 y los resultados de referencia. Esos resultados se verificaron frente a la hoja Excel v0.1: 75 comprobaciones con los mismos valores y resultados. Las pruebas no dependen de los parámetros del proyecto, así que cambiar `parametros.md` nunca las rompe; solo detectan cambios no intencionados en las fórmulas. + +## 8. Limitaciones + +- Son cálculos de primer orden: L constante en cada punto de trabajo, escalado lineal de las pérdidas de conmutación, modelo térmico unidimensional en régimen permanente, fórmula de IPC-2221 para las pistas, fórmulas de IPC-2141A y AN-905 para la impedancia. +- Las tablas de normas vienen de fuentes secundarias: ver [`normas/README.md`](normas/README.md). +- El caso A del MOSFET supone Ipk en régimen permanente. Con picos de pocos segundos es muy conservador; hace falta la duración del pico (EM-C-01) y un modelo térmico transitorio. + +## 9. Fuentes + +| Dato | Fuente | +|---|---| +| Requisitos y valores de la sección 6 | `H12/LPU/docs/requisitos/01_requisitos_arquitectura_v0.2.md` | +| SCT012H90G3AG | [ST, hoja de datos](https://www.st.com/resource/en/datasheet/sct012h90g3ag.pdf) | +| ADuM4146 | [Analog Devices, hoja de datos](https://www.analog.com/media/en/technical-documentation/data-sheets/adum4146.pdf) | +| MGJ2D052005BSC | [Murata, hoja de datos MGJ2](https://docs.rs-online.com/4d1d/A700000007023131.pdf) | +| AMC1302 | [TI, hoja de datos](https://www.ti.com/lit/ds/symlink/amc1302.pdf) | +| AMC1306 | [TI, hoja de datos](https://www.ti.com/lit/ds/symlink/amc1306m05.pdf) | +| GA10K4A1IA | [TE, página de producto](https://www.te.com/en/product-GA10K4A1IA.html) | +| IPC-2221B, tabla 6-1 | [ProtoExpress](https://www.protoexpress.com/blog/ipc-2221-circuit-board-design/) y [Siemens EDA](https://blogs.sw.siemens.com/electronic-systems-design/2025/04/29/pcb-high-voltage-spacing-what-every-engineer-should-know/) | +| IEC 60664-1 | [TI SLUAAR5](https://www.ti.com/lit/pdf/sluaar5) y [Vishay](https://www.vishay.com/docs/28962/clearandcreepconsidcostsandspacefilmres.pdf) (parcial) | +| EtherCAT P | [Beckhoff](https://infosys.beckhoff.com/content/1033/ek1300/2268718219.html) | diff --git a/H12/LPU/calculos/calcular.py b/H12/LPU/calculos/calcular.py new file mode 100644 index 0000000..9e96658 --- /dev/null +++ b/H12/LPU/calculos/calcular.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Calcula el Magnet Driver Abejorro y genera los informes. + +Uso: + python calculos/calcular.py Vista previa: escribe en salida/ (no toca docs/) + python calculos/calcular.py --publicar Además actualiza docs/calculos/ (lo usa la GitHub Action) + +Opciones: + --commit SHA Commit al que corresponden las entradas (por defecto, el de git si está disponible) + --base FICHERO resultados.json con el que comparar (por defecto, docs/calculos/resultados.json) + --salida DIR Carpeta de la vista previa (por defecto, salida/) +""" +import argparse +import json +import os +import subprocess +import sys +from datetime import datetime + +AQUI = os.path.dirname(os.path.abspath(__file__)) +RAIZ = os.path.dirname(AQUI) +sys.path.insert(0, AQUI) + +from motor.calculo import ejecuta # noqa: E402 +from motor.cambios import compara # noqa: E402 +from motor.informe import informe, resultados_json # noqa: E402 +from motor.lector import ErrorEntradas # noqa: E402 + +try: + from zoneinfo import ZoneInfo + TZ = ZoneInfo("Europe/Madrid") +except Exception: # pragma: no cover + TZ = None + + +def git(*args): + try: + return subprocess.run(["git", *args], cwd=RAIZ, capture_output=True, text=True, check=True).stdout.strip() + except Exception: + return None + + +def meta_ejecucion(commit_arg): + ahora = datetime.now(TZ) if TZ else datetime.now() + commit = commit_arg or git("rev-parse", "HEAD") + sucio = bool(git("status", "--porcelain", "--", "calculos")) + if commit: + version = f"commit `{commit[:7]}`" + (" + cambios locales sin confirmar en `calculos/`" if sucio and not commit_arg else "") + else: + version = "sin control de versiones" + return { + "fecha_iso": ahora.isoformat(timespec="seconds"), + "fecha_texto": ahora.strftime("%d/%m/%Y %H:%M") + (" (hora de Madrid)" if TZ else ""), + "fecha_fichero": ahora.strftime("%Y-%m-%d_%H%M"), + "commit": commit, + "version_texto": version, + } + + +def escribe(ruta, texto): + os.makedirs(os.path.dirname(ruta), exist_ok=True) + with open(ruta, "w", encoding="utf-8", newline="\n") as f: + f.write(texto) + + +def sin_cabecera(texto): + """Informe sin las líneas de fecha y versión, para no reescribirlo solo por la fecha.""" + return "\n".join(l for l in texto.splitlines() + if not l.startswith("| Fecha |") and not l.startswith("| Versión de las entradas |")) + + +def indice_cambios(dir_cambios): + ficheros = sorted((f for f in os.listdir(dir_cambios) if f.endswith(".md") and f != "README.md"), reverse=True) + L = ["# Historial de informes de cambios", "", + "Un informe por cada actualización de las entradas que modifica algún resultado, del más reciente al más antiguo.", ""] + L += [f"- [{f[:-3]}]({f})" for f in ficheros] + escribe(os.path.join(dir_cambios, "README.md"), "\n".join(L) + "\n") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--publicar", action="store_true") + ap.add_argument("--commit") + ap.add_argument("--base", default=os.path.join(RAIZ, "docs", "calculos", "resultados.json")) + ap.add_argument("--salida", default=os.path.join(RAIZ, "salida")) + a = ap.parse_args() + + try: + ctx = ejecuta(os.path.join(AQUI, "entradas"), os.path.join(AQUI, "normas")) + except ErrorEntradas as e: + print("ERROR en las entradas:", file=sys.stderr) + for m in e.errores: + print(" -", m, file=sys.stderr) + return 1 + except Exception as e: # errores de cálculo con mensaje legible + print(f"ERROR en el cálculo: {e}", file=sys.stderr) + return 1 + + meta = meta_ejecucion(a.commit) + txt_informe = informe(ctx, meta) + datos = resultados_json(ctx, meta) + datos["fecha_texto"] = meta["fecha_texto"] + anterior = None + if os.path.exists(a.base): + with open(a.base, encoding="utf-8") as f: + anterior = json.load(f) + txt_cambios, hay_cambios = compara(anterior, datos, meta) + js = json.dumps(datos, ensure_ascii=False, indent=1, sort_keys=True) + "\n" + + ff = meta["fecha_fichero"] + escribe(os.path.join(a.salida, f"informe_{ff}.md"), txt_informe) + escribe(os.path.join(a.salida, f"cambios_{ff}.md"), txt_cambios) + escribe(os.path.join(a.salida, "resultados.json"), js) + rc = ctx.recuento() + print(f"Cálculo correcto: {rc['CUMPLE']} CUMPLE, {rc['NO CUMPLE']} NO CUMPLE, {rc['REVISAR']} REVISAR.") + print(f"Vista previa: {os.path.relpath(os.path.join(a.salida, f'informe_{ff}.md'), RAIZ)}") + print(f"Cambios: {os.path.relpath(os.path.join(a.salida, f'cambios_{ff}.md'), RAIZ)}" + + ("" if hay_cambios else " (sin cambios)")) + + if a.publicar: + docs = os.path.join(RAIZ, "docs", "calculos") + ruta_inf = os.path.join(docs, "informe.md") + previo = open(ruta_inf, encoding="utf-8").read() if os.path.exists(ruta_inf) else None + if hay_cambios or previo is None or sin_cabecera(previo) != sin_cabecera(txt_informe): + escribe(ruta_inf, txt_informe) + if hay_cambios: + sha = (meta["commit"] or "local")[:7] + dir_c = os.path.join(docs, "cambios") + escribe(os.path.join(dir_c, f"{ff}_{sha}.md"), txt_cambios) + indice_cambios(dir_c) + escribe(os.path.join(docs, "resultados.json"), js) + print("Publicado en docs/calculos/ (con informe de cambios).") + else: + print("Sin cambios en los resultados: docs/calculos/resultados.json no se modifica.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/H12/LPU/calculos/entradas/componentes.md b/H12/LPU/calculos/entradas/componentes.md new file mode 100644 index 0000000..c380b27 --- /dev/null +++ b/H12/LPU/calculos/entradas/componentes.md @@ -0,0 +1,106 @@ +# Datos de componentes + +Datos de hoja de datos de los componentes candidatos. Se editan al añadir o cambiar un candidato; las referencias están en «Fuentes» de `calculos/README.md`. + +Las mismas reglas de formato que en `parametros.md`: decimales con coma o punto, sin `|` dentro de las celdas. + +## 1. MOSFET: candidatos + +Una columna por candidato (se pueden añadir columnas «Candidato N»; una columna vacía se ignora). La columna «Clave» la usan las fórmulas y no debe cambiarse. El candidato activo se elige con `MOSFET_sel` en `parametros.md`, por su referencia. + +| Parámetro | Clave | Unidad | Candidato 1 | Candidato 2 | Candidato 3 | Fuente / nota | +|---|---|---|---|---|---|---| +| Fabricante | fab | – | STMicroelectronics | | | | +| Referencia | ref | – | SCT012H90G3AG | | | Candidato del TFG, sin ensayar (PWR-02) | +| Encapsulado | pkg | – | H²PAK-7 (SMD) | | | ST, hoja de datos SCT012H90G3AG | +| VDS máxima | VDS | V | 900 | | | ST, hoja de datos | +| VGS máxima absoluta (positiva) | VGSmax | V | 22 | | | ST, hoja de datos | +| VGS máxima absoluta (negativa) | VGSmin | V | -10 | | | ST, hoja de datos | +| VGS recomendada en ON (máxima) | VGSrec_on | V | 18 | | | ST: rango recomendado −5 a 18 V | +| VGS recomendada en OFF (mínima) | VGSrec_off | V | -5 | | | ST, hoja de datos | +| VGS(th) mínima | Vth_min | V | 1,8 | | | ST (típ. 3,1 V; máx. 4,2 V) | +| ID continua (Tc = 25 °C) | ID | A | 110 | | | ST (110 A también a Tc = 100 °C) | +| ID pulsada | IDM | A | 454 | | | ST, hoja de datos | +| Tj máxima | Tjmax | °C | 175 | | | ST, hoja de datos | +| Rth unión-cápsula | Rthjc | K/W | 0,24 | | | ST, hoja de datos | +| RDS(on) típica a 25 °C | R25 | mΩ | 12 | | | ST: VGS = 18 V, ID = 60 A | +| RDS(on) máxima a 25 °C | R25max | mΩ | 15,8 | | | ST: VGS = 18 V, ID = 60 A | +| RDS(on) típica a temperatura alta | Rhot | mΩ | 18,5 | | | ST: VGS = 18 V, ID = 60 A | +| Temperatura de la RDS(on) anterior | Thot | °C | 175 | | | ST, hoja de datos | +| Tensión del diodo interno | VSD | V | 2,8 | | | ST: ISD = 60 A, VGS = 0 V | +| Carga de recuperación inversa | Qrr | nC | 335 | | | ST: ISD = 60 A | +| Carga total de puerta | Qg | nC | 138 | | | ST: VDD = 600 V, ID = 60 A | +| Ciss | Ciss | pF | 3880 | | | ST: VDS = 600 V, f = 1 MHz | +| Coss | Coss | pF | 244 | | | ST: VDS = 600 V | +| Crss | Crss | pF | 20 | | | ST: VDS = 600 V (a VDS bajas es mayor) | +| Resistencia interna de puerta | Rgint | Ω | 1,8 | | | ST, hoja de datos | +| Energía de encendido | Eon | µJ | 708 | | | ST: condiciones de ensayo abajo | +| Energía de apagado | Eoff | µJ | 889 | | | ST, hoja de datos | +| Tensión de ensayo de Eon/Eoff | Vtest | V | 600 | | | ST, hoja de datos | +| Corriente de ensayo de Eon/Eoff | Itest | A | 60 | | | ST, hoja de datos | +| RG de ensayo de Eon/Eoff y de los tiempos | Rgtest | Ω | 15 | | | ST: VGS = −5 a 18 V | +| td(on) | tdon | ns | 37 | | | ST, hoja de datos | +| tr | tr | ns | 47 | | | ST, hoja de datos | +| td(off) | tdoff | ns | 82 | | | ST, hoja de datos | +| tf | tf | ns | 30 | | | ST, hoja de datos | + +## 2. Gate driver: ADuM4146 + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| D_Isrc | Corriente de cortocircuito de fuente | 11 | A | ADI | OK | GD-01 | | +| D_Isnk | Corriente de cortocircuito de sumidero | 9 | A | ADI | OK | GD-01 | | +| D_Rp | RDS(on) del PMOS de salida (máx.) | 0,975 | Ω | ADI | OK | GD-01 | | +| D_Rn | RDS(on) del NMOS de salida (máx.) | 0,807 | Ω | ADI | OK | GD-01 | | +| D_skew | Desfase de propagación entre dispositivos (máx.) | 25 | ns | ADI | OK | PWR-04 | | +| D_desat | Umbral DESAT (grado B) | 9,2 | V | ADI | OK | GD-01 | Grados A y C: 3,5 V sin fuente de corriente interna | +| D_Idesat | Corriente de carga DESAT (grado B, típ.) | 527 | µA | ADI | OK | GD-01 | | +| D_mask | Tiempo de enmascaramiento DESAT (máx.) | 340 | ns | ADI | OK | GD-01 | 260 a 340 ns | +| D_Rclamp | Resistencia del Miller clamp (máx.) | 2,75 | Ω | ADI | OK | GD-01 | | +| D_CMTI | CMTI | 100 | kV/µs | ADI | OK | GD-01 | | +| D_IDD2 | Corriente de VDD2 (máx.) | 6,5 | mA | ADI | OK | AUX-02 | | +| D_UVLO | UVLO de VDD2, umbral de subida (grados B/C) | 11,5 | V | ADI | AC | GD-02 | Grado A: 14,5 V. Grado del TFG por confirmar | + +## 3. Fuente aislada de puerta: MGJ2D052005BSC + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| G_Vin | Tensión de entrada nominal | 5 | V | Murata | OK | AUX-02 | 4,5 a 5,5 V | +| G_P | Potencia nominal | 2 | W | Murata | OK | GD-01 | | +| G_eta | Rendimiento mínimo a plena carga | 74 | % | Murata | OK | AUX-02 | Típ. 78,5 % | +| G_Ipos | Corriente máxima de la salida +20 V | 80 | mA | Murata | OK | GD-01 | | +| G_Ineg | Corriente máxima de la salida −5 V | 40 | mA | Murata | OK | GD-01 | | +| G_CMTI | CMTI | 200 | kV/µs | Murata | OK | GD-01 | > 200 kV/µs | + +## 4. Sensores aislados: AMC1302 y AMC1306 + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Sa_Vlin | AMC1302: entrada diferencial lineal | 50 | mV | TI | OK | SNS-01 | ±50 mV | +| Sa_Vclip | AMC1302: entrada de recorte | 64 | mV | TI | OK | SNS-01 | ±64 mV | +| Sa_G | AMC1302: ganancia fija | 41 | – | TI | OK | SNS-01 | | +| Sa_BW | AMC1302: ancho de banda (mínimo) | 220 | kHz | TI | OK | SNS-05 | 220 a 280 kHz | +| Sa_td | AMC1302: retardo de señal 50–90 % (máximo) | 3 | µs | TI | OK | SNS-05 | | +| Sd_Vlin | AMC1306x05: entrada diferencial lineal | 50 | mV | TI | OK | SNS-01 | x25: ±250 mV | +| Sd_fmin | AMC1306: frecuencia de reloj mínima | 5 | MHz | TI | OK | FW-01 | | +| Sd_fmax | AMC1306: frecuencia de reloj máxima | 21 | MHz | TI | OK | FW-01 | A 5 V | +| Sd_ENOB | AMC1306: ENOB con sinc3 y OSR = 256 | 14 | bit | TI | AC | FW-01 | ≈ 14 bits; con otro OSR es distinto | +| S_CMTI | CMTI mínimo de los sensores aislados | 100 | kV/µs | TI | OK | SNS-01 | AMC1302: 100 kV/µs mín.; AMC1306: 100 kV/µs típ. | + +## 5. Componentes que cruzan la barrera HV–BT + +Distancias de fuga y tensiones de trabajo del encapsulado. La «Clave» identifica la comprobación en el informe. + +| Clave | Componente | Fuga (mm) | Tensión de trabajo (V) | Fuente / nota | +|---|---|--:|--:|---| +| ADUM4146 | ADuM4146 (gate driver) | 8,3 | 2150 | ADI: 8,3 mm mín.; VIORM 2150 V pico | +| AMC1302 | AMC1302 (amplificador aislado) | 8,5 | 2121 | TI: ≥ 8,5 mm; VIORM 2121 V pico | +| AMC1306 | AMC1306 (modulador ΔΣ aislado) | 8,5 | 2120 | TI: ≥ 8,5 mm; VIORM 2120 V pico | +| MGJ2 | MGJ2D052005BSC (fuente aislada de puerta) | 2 | 2400 | Murata: 2 mm en el apartado de la aprobación UL60950; 2400 V de tensión continua de barrera. Medir la distancia real entre pines en el footprint | + +## 6. EtherCAT P + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| ECP_V | Tensión de alimentación de EtherCAT P | 24 | V | Beckhoff | OK | AUX-01 | | +| ECP_Imax | Corriente máxima por alimentación (US o UP) | 3 | A | Beckhoff | OK | AUX-01 | Solo aplica si se elige EtherCAT P (ME-I-02) | diff --git a/H12/LPU/calculos/entradas/diseno.md b/H12/LPU/calculos/entradas/diseno.md new file mode 100644 index 0000000..117951c --- /dev/null +++ b/H12/LPU/calculos/entradas/diseno.md @@ -0,0 +1,60 @@ +# Tablas de diseño + +Datos del layout y de las interfaces. Casi todos son **provisionales** hasta tener el diseño de la PCB. Se pueden añadir filas; la columna «Clave» identifica cada fila en el informe y debe ser única. + +## 1. Pistas de potencia + +«Corriente» admite un valor en amperios (`12,5`) o el nombre de una magnitud calculada: `I_in_first` (entrada de la primera placa a Ipk), `I_in_first_cont` (ídem en continuo), `I_bridge_rms` (DC-link ↔ puente), `Ipk`, `Irms_cont`. «Capa»: `Ext` o `Int`. + +| Clave | Red | Corriente | Capa | Capas en paralelo | Ancho previsto (mm) | Longitud (mm) | Estado | Nota | +|---|---|---|---|--:|--:|--:|---|---| +| entrada_pk | Entrada HV (1.ª placa), Ipk | I_in_first | Ext | 2 | 35 | 60 | TBD | PROVISIONAL | +| entrada_cont | Entrada HV (1.ª placa), continuo | I_in_first_cont | Ext | 2 | 10 | 60 | TBD | PROVISIONAL | +| puente_rms | DC-link ↔ puente, eficaz a Ipk | I_bridge_rms | Ext | 2 | 15 | 20 | TBD | PROVISIONAL | +| bobina_pk | Puente → bobina, Ipk | Ipk | Ext | 2 | 15 | 80 | TBD | PROVISIONAL | +| bobina_cont | Puente → bobina, continuo | Irms_cont | Ext | 2 | 5 | 80 | TBD | PROVISIONAL | + +## 2. Interfaces de señal + +«Tipo»: `SE` (simple) o `Dif` (diferencial). «Capa»: `MS` (microstrip, capa externa) o `SL` (stripline, capa interna). La separación solo se usa en los pares diferenciales. + +| Clave | Interfaz | Tipo | Capa | Z objetivo (Ω) | Ancho (mm) | Separación (mm) | tr (ns) | Longitud (mm) | Estado | Nota | +|---|---|---|---|--:|--:|--:|--:|--:|---|---| +| mdi_in | EtherCAT IN (MDI) | Dif | MS | 100 | 0,2 | 0,2 | 3 | 30 | TBD | 100 Ω diferencial en el MDI de Ethernet | +| mdi_out | EtherCAT OUT (MDI) | Dif | MS | 100 | 0,2 | 0,2 | 3 | 30 | TBD | | +| reloj_phy | Reloj FPGA–PHY | SE | SL | 50 | 0,12 | | 1 | 60 | TBD | | +| datos_phy | Datos FPGA–PHY | SE | SL | 50 | 0,12 | | 1 | 60 | TBD | | +| reloj_ds | Reloj de los moduladores ΔΣ | SE | SL | 50 | 0,12 | | 1 | 80 | TBD | | +| datos_ds | Datos de los moduladores ΔΣ | SE | SL | 50 | 0,12 | | 2 | 80 | TBD | | +| pwm | PWM a los drivers | SE | MS | 50 | 0,29 | | 2 | 100 | TBD | | +| jtag_tck | JTAG TCK | SE | MS | 50 | 0,29 | | 2 | 100 | TBD | | + +## 3. Distancias de aislamiento previstas + +Distancia mínima medida en el layout para cada caso. Las claves son fijas: `HVLV_coat`, `HVHV_coat`, `HVHV_int`, `HVHV_bare`, `WB`. + +| Clave | Caso | Previsto (mm) | Estado | Nota | +|---|---|--:|---|---| +| HVLV_coat | HV–BT, superficie con coating | 4,5 | TBD | PROVISIONAL | +| HVHV_coat | HV–HV, superficie con coating | 2,5 | TBD | PROVISIONAL | +| HVHV_int | HV–HV, capas internas | 2,5 | TBD | PROVISIONAL | +| HVHV_bare | HV–HV, terminales sin recubrir (conectores, etc.) | 3,5 | TBD | PROVISIONAL | +| WB | Taladros del waterblock – cualquier conductor | 4,5 | TBD | PROVISIONAL | + +## 4. Consumos de la alimentación auxiliar (BT) + +«Corriente» admite un valor en mA o el nombre de una magnitud calculada (`I_iso_in`: entrada de cada fuente aislada de puerta). «Rendimiento» es el del convertidor que genera ese raíl. + +| Clave | Consumidor | Raíl (V) | Corriente (mA) | Nº | Rendimiento (%) | Fuente | Estado | Nota | +|---|---|--:|---|--:|--:|---|---|---| +| fpga_core | FPGA MachXO5-NX: núcleo | 1 | 150 | 1 | 85 | — | TBD | PROVISIONAL (FW-02) | +| fpga_io | FPGA MachXO5-NX: bancos de E/S | 3,3 | 30 | 1 | 85 | — | TBD | PROVISIONAL (FW-02) | +| phy | PHY Ethernet | 3,3 | 60 | 2 | 85 | — | TBD | PROVISIONAL (FW-06); 2 puertos (COM-03) | +| osc | Oscilador | 3,3 | 10 | 1 | 85 | — | TBD | PROVISIONAL | +| eeprom | EEPROM SII | 3,3 | 1 | 1 | 85 | — | TBD | PROVISIONAL (COM-06) | +| ds_lv | Moduladores ΔΣ, lado BT (DVDD) | 3,3 | 6 | 3 | 85 | TI AMC1306 | AC | Máx. de la hoja de datos; rendimiento provisional | +| ds_hv | Moduladores ΔΣ, lado HV (AVDD) vía fuente aislada | 5 | 9,8 | 3 | 70 | TI AMC1306 | AC | Máx. de la hoja de datos; rendimiento de la fuente aislada PROVISIONAL | +| drv_lv | Gate drivers ADuM4146, lado BT (VDD1) | 5 | 2,17 | 4 | 85 | ADI ADuM4146 | AC | Máx. de la hoja de datos; rendimiento provisional | +| iso_gd | Fuentes aisladas de puerta (entrada a 5 V) | 5 | I_iso_in | 4 | 85 | Cálculo | AC | Corriente calculada en el apartado de gate driving | +| logica | Lógica cableada y comparadores | 3,3 | 10 | 1 | 85 | — | TBD | PROVISIONAL (HW-01) | +| leds | LEDs | 3,3 | 2 | 4 | 85 | — | TBD | PROVISIONAL | diff --git a/H12/LPU/calculos/entradas/parametros.md b/H12/LPU/calculos/entradas/parametros.md new file mode 100644 index 0000000..4082b3b --- /dev/null +++ b/H12/LPU/calculos/entradas/parametros.md @@ -0,0 +1,175 @@ +# Parámetros de diseño + +Parámetros escalares de los cálculos del Magnet Driver Abejorro. **Este es el fichero que se edita normalmente.** + +- Cambie solo las columnas «Valor», «Estado», «Fuente» y «Nota». La columna «Nombre» es la que usan las fórmulas: no la cambie. +- Decimales con coma o con punto (`1,92` o `1.92`), sin separador de miles. Signo menos con `-` o `−`. +- Unidades admitidas: ver `calculos/README.md`. Un valor en `%` se interpreta como fracción (2 % → 0,02). +- «Estado»: `OK` (confirmado), `AC` (a confirmar) o `TBD` (desconocido: el valor es **provisional** y solo sirve para calcular). +- «Fuente»: `U` (equipo), `TFG` (TFG de L. Navarro), `C` (cálculo del documento de requisitos), `P` (propuesta) o el documento o fabricante citado. +- No use el carácter `|` dentro de las celdas. + + +## 1. Bus DC + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Vbus_min | Tensión mínima del bus de HV | 320 | V | U | OK | DCB-01 | Revisar con la nueva batería (ME-I-07) | +| Vbus_max | Tensión máxima del bus de HV | 400 | V | U | OK | DCB-01 | Revisar con la nueva batería (ME-I-07) | +| V_cont | Tensión continua que deben soportar componentes y aislamiento | 600 | V | U | OK | DCB-02 · ISO-06 | | +| N_MD | Número de Magnet Driver conectados al bus | 10 | – | U | OK | DCB-03 · SYS-01 | 4 HEMS + 6 EMS | +| Bus_topo | Distribución del bus: «Encadenado» o «Estrella» | Encadenado | – | TFG | TBD | DCB-05 · ME-I-03 | Encadenado: la primera placa conduce la corriente de todas | +| dVdc_pct | Rizado máximo de tensión en el DC-link (fracción de Vbus_min) | 2 | % | TFG | AC | DCB-07 · HW-02 | 2 % de 400 V = 8 V según el TFG | +| eta_pwr | Rendimiento de la etapa de potencia (para la corriente media de bus) | 97 | % | P | TBD | — | PROVISIONAL. Ajustar con las pérdidas del puente del informe cuando se elija componente | + +## 2. Carga: electroimanes (prototipo H11) + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Rcoil | Resistencia de la bobina | 1,2 | Ω | U | OK | LOAD-01 | Medida en la bobina construida. Nuevas bobinas: EM-D-04 | +| Ipk | Corriente de pico (primeros segundos de la levitación) | 60 | A | U | OK | LOAD-03 | | +| t_pk | Duración del pico de corriente | 2 | s | — | TBD | LOAD-03 · EM-C-01 | PROVISIONAL. «Primeros segundos» | +| Irms_cont | Corriente eficaz en régimen permanente (dimensionado térmico) | 5 | A | TFG | TBD | LOAD-05 · EM-C-01 | Valor mencionado en el TFG, sin revalidar | +| L_EMS_min | Inductancia incremental mínima del EMS | 1,92 | mH | R5 | AC | LOAD-01 · EM-D-01/02 | Sección 6.1.1 | +| L_EMS_max | Inductancia incremental máxima del EMS | 117 | mH | R5 | AC | LOAD-01 · EM-D-01/02 | Sección 6.1.1 | +| L_HEMS_min | Inductancia incremental mínima del HEMS (dψ/di derivada) | 6,2 | mH | C | OK | LOAD-01 | Sección 6.1.2 (derivado del Excel R7) | +| L_HEMS_max | Inductancia incremental máxima del HEMS (dψ/di derivada) | 11,2 | mH | C | OK | LOAD-01 | Sección 6.1.2 | +| L_EMS_0 | Inductancia del EMS a I = 0 (valor citado, columnas 5–6) | 26,7 | mH | R5 | AC | 6.1.1 | Solo para la cota pesimista de energía (L constante) | +| E_EMS | Energía devuelta al desmagnetizar el EMS desde 55 A (mapa H11) | 15,6 | J | C | AC | 6.1.3 · EM-D-03 | Mapa hasta ±55 A | +| E_HEMS | Energía devuelta al desmagnetizar el HEMS desde −55 A, 6 mm (mapa H11) | 16,7 | J | C | AC | 6.1.3 · EM-D-03 | Peor caso del H11 | +| I_map | Corriente máxima cubierta por los mapas de inductancia | 55 | A | R7 | OK | EM-D-03 | | +| dI_pp_max | Rizado de corriente pico a pico máximo admisible | 2 | A | — | TBD | LOAD-07 · EM-C-03 | PROVISIONAL. | +| T_coil_max | Temperatura máxima de la bobina (umbral de protección) | 120 | °C | — | TBD | SAF-01 · sin pendiente | PROVISIONAL. No hay cuestión abierta que lo resuelva: proponer a EM-D | + +## 3. Conmutación y MOSFET + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| fsw | Frecuencia de conmutación | 20 | kHz | TFG | TBD | PWR-03 · FW-08 | | +| Modulacion | Modulación: «Unipolar» o «Bipolar» | Unipolar | – | TFG | TBD | PWR-03 · FW-08 | | +| t_dead | Tiempo muerto | 100 | ns | TFG | AC | PWR-04 · FW-08 | Red RC del TFG; pasa a ser configurable en la FPGA | +| MOSFET_sel | Referencia del MOSFET usado en el resto de cálculos (una de las columnas de componentes.md) | SCT012H90G3AG | – | P | AC | PWR-02 · HW-02 | | +| D_worst | Fracción del periodo que conduce el MOSFET más cargado | 1 | – | P | OK | SAF-05 | 1 = rueda libre permanente por los low-side (conservador). Con PWM en régimen, el más cargado conduce (1 + m)/2 | +| Tj_assumed | Tj supuesta para evaluar RDS(on) | 125 | °C | P | AC | — | Debe ser ≥ Tj calculada (comprobación en el apartado MOSFET) | +| Tj_lim | Tj máxima de diseño (criterio) | 150 | °C | P | AC | — | Margen sobre los 175 °C del SCT012H90G3AG | +| t_dead_mrg | Margen adicional en el cálculo del tiempo muerto mínimo | 20 | ns | P | TBD | PWR-04 | PROVISIONAL. | + +## 4. DC-link y protección del bus + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Cdc | Capacidad del DC-link en placa | 105 | µF | TFG | AC | DCB-07 · HW-02 · HW-03 | | +| V_film | Tensión nominal de los condensadores del DC-link | 500 | V | TFG | AC | DCB-02 · HW-02 | No cubre los 600 V de DCB-02 | +| ESR_dc | ESR equivalente del DC-link a 2·fsw | 2 | mΩ | — | TBD | HW-02 | PROVISIONAL. | +| N_dc_abs | Número de DC-link que absorben la energía en el caso de fallo | 1 | – | P | AC | DCB-08 · HW-03 | 1 = solo la propia placa (conservador) | +| V_chop_on | Tensión de actuación del clamp o chopper (si se usa) | 550 | V | P | TBD | HW-03 | PROVISIONAL. | +| k_chop_mrg | Margen mínimo de V_chop_on sobre Vbus_max (criterio) | 1,1 | – | P | TBD | HW-03 | PROVISIONAL | +| I_fuse | Corriente nominal del fusible en placa | 15 | A | TFG | AC | DCB-06 · HW-02 | 0ADKC9150-BE | +| V_fuse | Tensión nominal DC del fusible | 500 | V | TFG | AC | DCB-06 · HW-02 | 0ADKC9150-BE | +| k_fuse | Factor de utilización máximo del fusible (criterio) | 0,75 | – | P | TBD | DCB-06 | PROVISIONAL. Confirmar con la curva de derating del fabricante | + +## 5. Gate driving + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Vgs_on | Tensión de puerta en ON | 20 | V | TFG | AC | GD-01 · HW-02 | | +| Vgs_off | Tensión de puerta en OFF | -5 | V | TFG | AC | GD-01 · HW-02 | | +| Rg_on | Resistencia de puerta externa de encendido | 5 | Ω | — | TBD | HW-02 | PROVISIONAL. | +| Rg_off | Resistencia de puerta externa de apagado | 5 | Ω | — | TBD | HW-02 | PROVISIONAL. | +| N_d_desat | Número de diodos en serie en el circuito DESAT | 1 | – | — | TBD | HW-01 | PROVISIONAL. | +| Vf_desat | Caída de tensión de cada diodo DESAT | 0,7 | V | — | TBD | HW-01 | PROVISIONAL. | +| R_desat | Resistencia serie del circuito DESAT | 1 | kΩ | — | TBD | HW-01 | PROVISIONAL. | +| C_blank | Condensador de blanking del DESAT | 47 | pF | — | TBD | HW-01 | PROVISIONAL. | +| t_desat_max | Tiempo de respuesta máximo de la protección DESAT | 2 | µs | TFG | AC | GD-01 | | + +## 6. Sensado + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| R_shunt | Resistencia del shunt de corriente | 500 | µΩ | TFG | AC | SNS-01 · HW-02 | | +| P_sh_rat | Potencia nominal del shunt | 3 | W | — | TBD | HW-02 | PROVISIONAL. Referencia del shunt del TFG no documentada aquí | +| I_margin | Margen mínimo fondo de escala / Ipk (criterio) | 1,2 | – | P | AC | SNS-01 | | +| I_OC | Umbral de sobrecorriente por hardware | 80 | A | — | TBD | SAF-04 · HW-01 | PROVISIONAL. | +| f_clk_ds | Frecuencia de reloj de los moduladores ΔΣ | 20 | MHz | P | TBD | SNS-05 · FW-01 | PROVISIONAL. | +| OSR | Relación de sobremuestreo del filtro sinc | 128 | – | P | TBD | SNS-05 · FW-01 | PROVISIONAL. | +| N_sinc | Orden del filtro sinc | 3 | – | P | TBD | FW-01 | sinc3 es el habitual con moduladores de 2.º orden | +| N_adc | Resolución del ADC (opción amplificador aislado + ADC) | 12 | bit | — | TBD | SNS-05 · FW-01 | PROVISIONAL. | +| V_meas_FS | Fondo de escala deseado de la medida de Vbus | 700 | V | P | TBD | SNS-02 | PROVISIONAL. Debe cubrir la tensión del caso de fallo | +| V_sens_FS | Entrada lineal del sensor aislado de Vbus | 0,25 | V | — | TBD | SNS-02 · HW-02 | PROVISIONAL. Ejemplo: AMC1306x25 (±250 mV) | +| R_sens_in | Impedancia de entrada del sensor de Vbus | 22 | kΩ | — | TBD | HW-02 | AMC1306x25: 22 kΩ diferencial (hoja de datos) | +| R_div_bot | Resistencia inferior del divisor de Vbus | 1 | kΩ | — | TBD | SNS-02 | PROVISIONAL. | +| V_res_rat | Tensión máxima de trabajo de cada resistencia del divisor | 200 | V | — | TBD | HW-02 | PROVISIONAL. Depende del encapsulado elegido | +| P_res_rat | Potencia nominal de cada resistencia del divisor | 0,25 | W | — | TBD | HW-02 | PROVISIONAL. | +| k_res | Factor de utilización de potencia de las resistencias (criterio) | 0,5 | – | P | TBD | — | PROVISIONAL. | +| V_OV | Umbral de sobretensión de bus por hardware | 450 | V | — | TBD | SAF-01 · HW-01 | PROVISIONAL. | +| NTC_R25 | Resistencia de la NTC a 25 °C | 10 | kΩ | TFG | AC | LOAD-08 · HW-02 | GA10K4A1IA (TE) | +| NTC_beta | Constante β(25/85) de la NTC | 3694 | K | TE | AC | LOAD-08 | Página de producto TE; contrastar con la tabla R-T | +| R_ntc_pu | Resistencia de polarización de la NTC | 10 | kΩ | — | TBD | SNS-03 | PROVISIONAL. | +| V_ntc_ref | Tensión de referencia del divisor de la NTC | 5 | V | — | TBD | SNS-03 | PROVISIONAL. | + +## 7. Refrigeración y entorno + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| T_amb | Temperatura ambiente máxima | 40 | °C | — | TBD | MEC-06 · ME-C-02 | PROVISIONAL. El TFG asumía ≥ 25 °C | +| T_cool | Temperatura del refrigerante a la entrada | 40 | °C | — | TBD | ME-C-02 | PROVISIONAL. | +| Q_cool | Caudal de refrigerante | 2 | L/min | — | TBD | ME-C-02 | PROVISIONAL. | +| N_cool_ser | Placas en serie en el circuito de agua (la última es la peor) | 1 | – | — | TBD | ME-C-01 · ME-C-02 | PROVISIONAL. | +| Rth_wb | Resistencia térmica cara inferior de la PCB → agua, por MOSFET | 0,3 | K/W | — | TBD | PWR-06 · ME-C-01 | PROVISIONAL. | +| t_TIM | Espesor del material de interfaz térmica | 0,5 | mm | — | TBD | ME-C-01 | PROVISIONAL. | +| k_TIM | Conductividad del material de interfaz térmica | 3 | W/(m·K) | — | TBD | ME-C-01 | PROVISIONAL. | +| V_TIM_bd | Rigidez dieléctrica del material de interfaz (tensión de ruptura) | 4000 | V | — | TBD | ISO-04 · ME-C-01 | PROVISIONAL. El pad de drenador está en HV | +| k_spread | Área efectiva bajo el TIM / área del pad (ensanchamiento) | 2 | – | P | TBD | — | PROVISIONAL. | +| A_pad | Área del pad de drenador del MOSFET | 80 | mm² | — | TBD | HW-02 | PROVISIONAL. Comprobar con el footprint H2PAK-7 | +| d_via | Diámetro de taladro de las vías térmicas | 0,3 | mm | — | TBD | PT-01 | PROVISIONAL. | +| t_plat | Espesor del metalizado de las vías | 25 | µm | — | TBD | PT-01 | PROVISIONAL. | +| N_via | Número de vías térmicas bajo cada MOSFET | 40 | – | — | TBD | — | PROVISIONAL. | +| t_solder | Espesor de la soldadura bajo el pad | 0,1 | mm | — | TBD | PT-02 | PROVISIONAL. | +| k_solder | Conductividad de la soldadura | 50 | W/(m·K) | — | AC | — | Orden de magnitud de aleaciones SAC | +| k_Cu | Conductividad del cobre | 385 | W/(m·K) | — | OK | — | Valor físico | +| k_FR4 | Conductividad del FR-4 en el eje z | 0,3 | W/(m·K) | — | AC | PT-01 | Orden de magnitud; confirmar con el laminado | + +## 8. PCB: stackup, pistas y aislamiento + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| t_pcb | Espesor total de la PCB | 1,6 | mm | — | TBD | MEC-03 · PT-01 | PROVISIONAL. | +| oz_ext | Cobre en capas externas | 2 | oz | TFG | AC | MEC-03 · PT-01 | 1 oz ≈ 35 µm | +| oz_int | Cobre en capas internas | 2 | oz | TFG | AC | MEC-03 · PT-01 | El documento indica 2 oz sin distinguir capas | +| dT_trace | Incremento de temperatura admisible en pistas (criterio) | 20 | °C | P | AC | — | | +| er_pcb | Permitividad relativa del dieléctrico | 4,3 | – | — | TBD | PT-01 | PROVISIONAL. | +| h_ms | Dieléctrico entre capa externa y su plano de referencia (microstrip) | 0,2 | mm | — | TBD | PT-01 | PROVISIONAL | +| b_sl | Distancia entre planos de referencia de la stripline | 0,5 | mm | — | TBD | PT-01 | PROVISIONAL | +| tol_Z | Tolerancia admisible de impedancia (criterio) | 10 | % | P | AC | PT-01 | Tolerancia habitual de fabricación; confirmar con el fabricante | +| Ins_type | Tipo de aislamiento HV–BT: «Básico» o «Reforzado» | Básico | – | — | TBD | ISO-05 · HW-04 | PROVISIONAL. | +| PD_coat | Grado de contaminación bajo el coating (IEC 60664-3) | 1 | – | P | AC | ISO-02 · HW-04 | | +| PD_bare | Grado de contaminación en superficies sin coating | 2 | – | P | TBD | HW-04 | PROVISIONAL. | +| d_HV_LV | Separación mínima HV–BT con coating (regla de la competición) | 4 | mm | U | OK | ISO-02 | | +| d_HV_HV | Separación mínima entre redes de HV | 2 | mm | TFG | AC | ISO-03 · HW-04 | | +| d_WB | Separación mínima taladros del waterblock – conductores | 4 | mm | TFG | AC | ISO-04 · ME-C-01 | | + +## 9. Alimentación auxiliar (BT) + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| V_BT_max | Tensión máxima de la batería de BT | 24 | V | U | OK | AUX-01 | | +| V_BT_min | Tensión mínima de la batería de BT | 18 | V | — | TBD | AUX-03 · ME-I-02 | PROVISIONAL. | +| P_BT_lim | Consumo máximo permitido por placa | 15 | W | — | TBD | AUX-03 · ME-I-02 | PROVISIONAL. | +| eta_BT | Rendimiento de la etapa de entrada BT | 85 | % | — | TBD | AUX-02 · HW-05 | PROVISIONAL. | +| P_margin | Margen sobre el consumo total estimado (criterio) | 20 | % | P | AC | — | | + +## 10. Integridad de señal: presupuesto de tiempos (interfaz MII/RMII u otra síncrona) + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| f_bus_clk | Frecuencia de reloj de la interfaz FPGA–PHY | 50 | MHz | P | TBD | COM-02 · FW-06 | PROVISIONAL. RMII: 50 MHz; MII: 25 MHz | +| tco_max | Retardo de salida máximo del emisor (clock-to-out) | 10 | ns | — | TBD | FW-06 | PROVISIONAL. Hoja de datos del PHY / FPGA | +| tco_min | Retardo de salida mínimo del emisor | 2 | ns | — | TBD | FW-06 | PROVISIONAL. | +| t_su | Tiempo de setup del receptor | 4 | ns | — | TBD | FW-06 | PROVISIONAL. | +| t_h | Tiempo de hold del receptor | 2 | ns | — | TBD | FW-06 | PROVISIONAL. | +| t_jit | Jitter y otras incertidumbres del reloj | 0,5 | ns | — | TBD | FW-06 | PROVISIONAL. | +| dL_clk | Diferencia de longitud del reloj entre emisor y receptor | 10 | mm | — | TBD | — | PROVISIONAL | +| skew_pair | Desfase admisible dentro de un par diferencial | 5 | ps | — | TBD | COM-03 | PROVISIONAL. Criterio del fabricante del PHY o de los magnéticos | +| SI_if_datos | Interfaz de datos usada en el presupuesto de tiempos (clave de diseno.md) | datos_phy | – | P | TBD | COM-02 · FW-06 | | +| SI_if_reloj | Interfaz de reloj usada en el presupuesto de tiempos (clave de diseno.md) | reloj_phy | – | P | TBD | COM-02 · FW-06 | | +| SI_if_par | Par diferencial usado para la igualación de longitudes (clave de diseno.md) | mdi_in | – | P | TBD | COM-03 | | diff --git a/H12/LPU/calculos/motor/__init__.py b/H12/LPU/calculos/motor/__init__.py new file mode 100644 index 0000000..27d0989 --- /dev/null +++ b/H12/LPU/calculos/motor/__init__.py @@ -0,0 +1,7 @@ +"""Motor de cálculo del Magnet Driver Abejorro. + +Lee las entradas en Markdown (calculos/entradas), calcula todos los bloques +y genera el informe general y el informe de cambios en Markdown. +""" + +VERSION_MOTOR = "1.0" diff --git a/H12/LPU/calculos/motor/aislamiento.py b/H12/LPU/calculos/motor/aislamiento.py new file mode 100644 index 0000000..20af3f7 --- /dev/null +++ b/H12/LPU/calculos/motor/aislamiento.py @@ -0,0 +1,111 @@ +"""Aislamiento: IPC-2221B, IEC 60664-1 y reglas del proyecto.""" +from .unidades import fmt + +CLAVES = { + "HVLV_coat": "HV–BT, superficie con coating", + "HVHV_coat": "HV–HV, superficie con coating", + "HVHV_int": "HV–HV, capas internas", + "HVHV_bare": "HV–HV, terminales sin recubrir", + "WB": "Taladros del waterblock – conductores", +} + + +def ipc(ent, V, col): + """Separación IPC-2221B (mm) para la tensión V.""" + if V <= 500: + fila = [f for f in ent.ipc_filas if f["desde"] <= V][-1] + return fila[col] + base = [f for f in ent.ipc_filas if f["hasta"] == 500][0][col] + return base + (V - 500) * ent.ipc_por_voltio[col] + + +def iec(ent, V, pd, superior=False): + """Línea de fuga IEC 60664-1 en PCB (mm), interpolada o de la fila superior. None si V > tabla.""" + col = f"PD{int(pd)}" + filas = ent.iec_filas + if V > filas[-1]["V"]: + return None + if V <= filas[0]["V"]: + return filas[0][col] + for a, b in zip(filas, filas[1:]): + if a["V"] <= V <= b["V"]: + if V == a["V"]: + return a[col] + if superior: + return b[col] + return a[col] + (V - a["V"]) * (b[col] - a[col]) / (b["V"] - a["V"]) + return None + + +def calcula(ctx): + x, ent = ctx.x, ctx.ent + s = ctx.seccion(5, "Aislamiento", + "Reglas del proyecto (ISO-02 a ISO-04) frente a IPC-2221B (tabla 6-1) e IEC 60664-1 (líneas de fuga en circuito impreso). " + "Las tablas de las normas proceden de fuentes secundarias (`calculos/normas/README.md`) y deben contrastarse con el texto oficial.") + if x.Ins_type not in ("Básico", "Reforzado"): + raise ValueError("Ins_type debe ser «Básico» o «Reforzado»") + if int(x.PD_coat) not in (1, 2) or int(x.PD_bare) not in (1, 2): + raise ValueError("PD_coat y PD_bare deben ser 1 o 2 (la tabla incluida solo cubre PD1 y PD2)") + tensiones = [("V_cont", x.V_cont), ("V_fault_max", x.V_fault_max)] + filas, reg = [], {} + for nom, V in tensiones: + vals = [ipc(ent, V, c) for c in ent.ipc_cols] + filas.append([f"{nom} = {fmt(V, 0)} V"] + [fmt(v, 2) for v in vals]) + for c, v in zip(ent.ipc_cols, vals): + reg[f"ipc.{nom}.{c}"] = (v, fmt(v, 2), "mm", f"IPC-2221B {c} a {nom}") + s.tabla(["Tensión"] + ent.ipc_cols, filas, registro=reg, + intro="IPC-2221B, separación mínima entre conductores (mm). B1: internas · B2/B3: externas sin recubrir (≤/> 3050 m) · " + "B4: recubrimiento permanente · A5: conformal coating · A6/A7: terminales sin/con coating.") + k_ins = 2.0 if x.Ins_type == "Reforzado" else 1.0 + filas, reg = [], {} + for nom, V in tensiones: + for pd in (1, 2): + vi, vs = iec(ent, V, pd), iec(ent, V, pd, True) + filas.append([f"{nom} = {fmt(V, 0)} V", f"PD{pd}", fmt(vi, 2) if vi is not None else "fuera de tabla", + fmt(vs, 2) if vs is not None else "fuera de tabla"]) + reg[f"iec.{nom}.PD{pd}"] = (vi, fmt(vi, 2), "mm", f"IEC 60664-1 PD{pd} a {nom}") + s.tabla(["Tensión", "Grado de contaminación", "Interpolada (mm)", "Fila superior (mm)"], filas, registro=reg, + intro=f"IEC 60664-1, aislamiento funcional/básico en circuito impreso. Tipo de aislamiento HV–BT: **{x.Ins_type}** " + f"(factor {fmt(k_ins, 0)}). PD1 aplica bajo un recubrimiento conforme a IEC 60664-3.") + V = x.V_cont + mm = lambda v: None if v is None else v * 1e-3 + iec_c, iec_b = iec(ent, V, x.PD_coat), iec(ent, V, x.PD_bare) + req = { + "HVLV_coat": (x.d_HV_LV, mm(ipc(ent, V, "A5")), mm(None if iec_c is None else k_ins * iec_c), + "IPC A5 · IEC PD_coat × factor de aislamiento", "ISO-02 · ISO-05"), + "HVHV_coat": (x.d_HV_HV, mm(ipc(ent, V, "A5")), mm(iec_c), "IPC A5 · IEC PD_coat, básico", "ISO-03"), + "HVHV_int": (x.d_HV_HV, mm(ipc(ent, V, "B1")), None, "IPC B1 (la línea de fuga no aplica en capas internas)", "ISO-03"), + "HVHV_bare": (x.d_HV_HV, mm(ipc(ent, V, "A6")), mm(iec_b), "IPC A6 · IEC PD_bare, básico", "ISO-03"), + "WB": (x.d_WB, None, None, "Regla del proyecto", "ISO-04"), + } + dist = {f["Clave"]: f for f in ent.tablas.get("distancias", [])} + for clave in dist: + if clave not in CLAVES: + raise ValueError(f"{dist[clave]['_origen']}: clave «{clave}» no reconocida ({', '.join(CLAVES)})") + filas, reg, pend = [], {}, [] + for clave, (regla, v_ipc, v_iec, crit, rq) in req.items(): + nec = max(v for v in (regla, v_ipc, v_iec) if v is not None) + f = dist.get(clave) + prev = ctx.resuelve(f["Previsto"], 1e-3, f["_origen"]) if f else None + ft = lambda v: fmt(None if v is None else v * 1e3, 2) + filas.append([CLAVES[clave], ft(regla), ft(v_ipc), ft(v_iec), ft(nec), crit]) + reg[f"aisl.{clave}.requerido"] = (nec, ft(nec), "mm", f"Distancia requerida: {CLAVES[clave]}") + pend.append((clave, prev, nec, rq, (f.get("Estado") or "") if f else "")) + if clave == "HVLV_coat": + ctx.define("d_req_HVLV", nec) + s.tabla(["Caso", "Regla del proyecto (mm)", "IPC-2221B (mm)", "IEC 60664-1 (mm)", "Requerido (mm)", "Criterio"], + filas, registro=reg, + intro=f"Distancias requeridas a V_cont = {fmt(V, 0)} V: el máximo entre la regla del proyecto y las normas aplicables.") + for clave, prev, nec, rq, est in pend: + s.chk(f"AIS-{clave}", f"Distancia prevista: {CLAVES[clave]}", prev, nec, ">=", "mm", 1e-3, 2, req=rq, + deps="HW-04, PT-01 (layout pendiente)", nota=f"Valor previsto en `diseno.md` (estado {est})" if est else "") + s.sub("5.1 Componentes que cruzan la barrera HV–BT", + "La línea de fuga del encapsulado debe ser ≥ la requerida HV–BT; la tensión de trabajo de la barrera, ≥ V_cont.") + for f in ent.tablas.get("barrera", []): + o = f["_origen"] + fuga = ctx.resuelve(f["Fuga"], 1e-3, o) + vt = ctx.resuelve(f["Tensión de trabajo"], 1.0, o) + s.chk(f"AIS-F-{f['Clave']}", f"Línea de fuga del encapsulado: {f['Componente']}", fuga, x.d_req_HVLV, ">=", "mm", + 1e-3, 1, req="ISO-01 · ISO-02", deps="HW-04, HW-05", blando=True, nota=f.get("Fuente / nota", "")) + s.chk(f"AIS-V-{f['Clave']}", f"Tensión de trabajo de la barrera: {f['Componente']}", vt, x.V_cont, ">=", "V", + dec=0, req="ISO-06") diff --git a/H12/LPU/calculos/motor/alim_bt.py b/H12/LPU/calculos/motor/alim_bt.py new file mode 100644 index 0000000..6a9468f --- /dev/null +++ b/H12/LPU/calculos/motor/alim_bt.py @@ -0,0 +1,38 @@ +"""Alimentación auxiliar de BT: presupuesto de consumo, PoE y EtherCAT P.""" +from .unidades import fmt + + +def calcula(ctx): + x = ctx.x + s = ctx.seccion(9, "Alimentación auxiliar (BT)", + "Consumo por placa desde la batería de BT: P = V·I·N/(η_raíl·eta_BT). Consumos de `diseno.md`, tabla 4.") + filas, reg, total = [], {}, 0.0 + for f in ctx.ent.tablas.get("consumos", []): + o = f["_origen"] + V = ctx.resuelve(f["Raíl"], 1.0, o) + I = ctx.resuelve(f["Corriente"], 1e-3, o) + n = ctx.resuelve(f["Nº"], 1.0, o) + eta = ctx.resuelve(f["Rendimiento"], 1.0, o) + p_carga = V * I * n + p_bt = p_carga / eta / x.eta_BT + total += p_bt + filas.append([f["Consumidor"], fmt(V, 1), fmt(I * 1e3, 2), fmt(n, 0), fmt(p_carga, 3), fmt(eta * 100, 0), + fmt(p_bt, 3), f.get("Estado") or ""]) + reg[f"bt.{f['Clave']}"] = (p_bt, fmt(p_bt, 3), "W", f"Consumo desde BT: {f['Consumidor']}") + s.tabla(["Consumidor", "Raíl (V)", "I (mA)", "Nº", "P en carga (W)", "η raíl (%)", "P desde BT (W)", "Estado"], + filas, registro=reg) + s.res("P_BT_sub", "Subtotal desde BT", total, "W", dec=2, expr="Σ P desde BT") + s.res("P_BT_total", "Total por placa con margen", x.P_BT_sub * (1 + x.P_margin), "W", dec=2, + expr="P_BT_sub·(1 + P_margin)", nota="Se suma a las pérdidas en el balance térmico") + s.res("I_BT_min", "Corriente de entrada a V_BT_min", x.P_BT_total / x.V_BT_min, "A", dec=3, expr="P_BT_total/V_BT_min") + s.res("I_BT_max", "Corriente de entrada a V_BT_max", x.P_BT_total / x.V_BT_max, "A", dec=3, expr="P_BT_total/V_BT_max") + s.res("P_BT_todas", "Consumo de las N_MD placas", x.N_MD * x.P_BT_total, "W", dec=1, expr="N_MD·P_BT_total", + nota="Dato para ME-I-02") + s.chk("BT-01", "Consumo por placa frente al permitido", x.P_BT_total, x.P_BT_lim, "<=", "W", dec=2, + req="AUX-03", deps="ME-I-02, HW-05", expr="P_BT_total ≤ P_BT_lim") + s.chk("BT-02", "EtherCAT P: corriente de la línea completa", x.N_MD * x.P_BT_total / x.ECP_V, x.ECP_Imax, "<=", "A", + dec=2, req="AUX-01", deps="ME-I-02", blando=True, expr="N_MD·P_BT_total/ECP_V ≤ ECP_Imax", + nota="Solo si se elige EtherCAT P: la primera conexión alimenta a todas las placas") + filas = [[n, fmt(p, 2), "Sí" if p >= x.P_BT_total else "No"] for n, p in ctx.ent.poe] + s.tabla(["Tipo de PoE", "Potencia en el equipo alimentado (W)", "¿Suficiente para una placa?"], filas, + intro="PoE es un enlace punto a punto y no encaja directamente con EtherCAT en línea (ME-I-02).") diff --git a/H12/LPU/calculos/motor/calculo.py b/H12/LPU/calculos/motor/calculo.py new file mode 100644 index 0000000..7c615b2 --- /dev/null +++ b/H12/LPU/calculos/motor/calculo.py @@ -0,0 +1,23 @@ +"""Orden de cálculo de los bloques.""" +from . import aislamiento, alim_bt, mosfet, pistas, potencia, puerta, sensado, si, termico +from .lector import Entradas +from .nucleo import Ctx + + +def ejecuta(dir_entradas, dir_normas): + ent = Entradas(dir_entradas, dir_normas) + ctx = Ctx(ent) + mosfet.selecciona(ctx) + potencia.calcula(ctx) + puerta.calcula(ctx) + alim_bt.calcula(ctx) + mosfet.perdidas(ctx) + termico.cadena(ctx) + mosfet.temperatura(ctx) + termico.union(ctx) + pistas.calcula(ctx) + aislamiento.calcula(ctx) + si.calcula(ctx) + sensado.calcula(ctx) + ctx.secciones.sort(key=lambda s: s.num) + return ctx diff --git a/H12/LPU/calculos/motor/cambios.py b/H12/LPU/calculos/motor/cambios.py new file mode 100644 index 0000000..4fd3658 --- /dev/null +++ b/H12/LPU/calculos/motor/cambios.py @@ -0,0 +1,98 @@ +"""Informe de cambios entre dos ejecuciones (resultados.json).""" +from .informe import tabla_md, estado_md + + +def _ab(a, b): + return b if a == b else f"{a} → {b}" + + +def _num(seccion): + try: + return int(seccion.split(".")[0]) + except ValueError: + return 99 + + +def compara(ant, act, meta): + """Devuelve (texto Markdown, hay_cambios).""" + L = ["# Magnet Driver Abejorro · Informe de cambios en los cálculos", ""] + if ant is None: + L += [f"Primera ejecución ({meta['fecha_texto']}): no hay un estado anterior con el que comparar.", ""] + return "\n".join(L), True + L += [tabla_md(["Dato", "Estado anterior", "Estado actual"], [ + ["Fecha", ant.get("fecha_texto") or ant.get("generado", "—"), meta["fecha_texto"]], + ["Versión de las entradas", f"commit `{(ant.get('commit') or '—')[:7]}`", meta["version_texto"]], + ["Motor de cálculo", ant.get("version_motor", "—"), act.get("version_motor", "—")], + ]), ""] + hay = False + + # 1. Entradas + ea, eb = ant.get("entradas", {}), act.get("entradas", {}) + filas = [] + for k in sorted(set(ea) | set(eb)): + a, b = ea.get(k), eb.get(k) + if a is None: + filas.append([b["etiqueta"], "(no existía)", b["texto"], b.get("estado", ""), b.get("fuente", "")]) + elif b is None: + filas.append([a["etiqueta"], a["texto"], "(eliminado)", "", ""]) + elif any(a.get(c) != b.get(c) for c in ("texto", "estado", "fuente")): + dif = lambda c: (f"{a.get(c) or '—'} → {b.get(c) or '—'}" if a.get(c) != b.get(c) else (b.get(c) or "")) + filas.append([f"{b['etiqueta']} (`{k.split('.', 1)[1]}`)", a["texto"], b["texto"], dif("estado"), dif("fuente")]) + L += ["## 1. Entradas modificadas", ""] + if filas: + hay = True + L += [tabla_md(["Entrada", "Antes", "Ahora", "Estado", "Fuente"], filas), ""] + else: + L += ["Ninguna.", ""] + + # 2. Comprobaciones + ca, cb = ant.get("comprobaciones", {}), act.get("comprobaciones", {}) + cambio_estado, cambio_valor = [], [] + for k in list(cb) + [k for k in ca if k not in cb]: + a, b = ca.get(k), cb.get(k) + if a is None: + cambio_estado.append([k, b["etiqueta"], "(nueva)", estado_md(b["estado"]), f"{b['valor']} {b['unidad']}".strip()]) + elif b is None: + cambio_estado.append([k, a["etiqueta"], estado_md(a["estado"]), "(eliminada)", ""]) + elif a["estado"] != b["estado"]: + cambio_estado.append([k, b["etiqueta"], estado_md(a["estado"]), estado_md(b["estado"]), + f"{_ab(a['valor'], b['valor'])} {b['unidad']} (límite {_ab(a['limite'], b['limite'])})"]) + elif a["valor"] != b["valor"] or a["limite"] != b["limite"]: + cambio_valor.append([k, b["etiqueta"], _ab(a["valor"], b["valor"]), _ab(a["limite"], b["limite"]), + b["unidad"], estado_md(b["estado"])]) + L += ["## 2. Comprobaciones que cambian de resultado", ""] + if cambio_estado: + hay = True + L += [tabla_md(["ID", "Comprobación", "Antes", "Ahora", "Valor"], cambio_estado), ""] + else: + L += ["Ninguna.", ""] + L += ["## 3. Comprobaciones con el mismo resultado pero otros valores", ""] + if cambio_valor: + hay = True + L += [tabla_md(["ID", "Comprobación", "Valor", "Límite", "Unidad", "Resultado"], cambio_valor), ""] + else: + L += ["Ninguna.", ""] + + # 4. Resultados + ra, rb = ant.get("resultados", {}), act.get("resultados", {}) + filas = [] + for k in sorted(set(ra) | set(rb), key=lambda k: (_num((rb.get(k) or ra.get(k)).get("seccion", "")), k)): + a, b = ra.get(k), rb.get(k) + if a is None or b is None: + r = b or a + filas.append([r.get("seccion", ""), r["etiqueta"], "(no existía)" if a is None else a["texto"], + "(eliminado)" if b is None else b["texto"], r["unidad"], ""]) + elif a["texto"] != b["texto"]: + var = "" + if isinstance(a["valor"], (int, float)) and isinstance(b["valor"], (int, float)) and a["valor"]: + var = f"{(b['valor'] - a['valor']) / abs(a['valor']) * 100:+.1f} %".replace(".", ",").replace("-", "−") + filas.append([b.get("seccion", ""), b["etiqueta"], a["texto"], b["texto"], b["unidad"], var]) + L += ["## 4. Magnitudes calculadas que cambian", ""] + if filas: + hay = True + L += [tabla_md(["Apartado", "Magnitud", "Antes", "Ahora", "Unidad", "Variación"], filas), ""] + else: + L += ["Ninguna.", ""] + if not hay: + L += ["**Sin cambios respecto al estado anterior.**", ""] + return "\n".join(L), hay diff --git a/H12/LPU/calculos/motor/informe.py b/H12/LPU/calculos/motor/informe.py new file mode 100644 index 0000000..5cdb872 --- /dev/null +++ b/H12/LPU/calculos/motor/informe.py @@ -0,0 +1,152 @@ +"""Informe general en Markdown y resultados en JSON.""" +from . import VERSION_MOTOR +from .nucleo import CUMPLE, NO_CUMPLE, REVISAR +from .unidades import UNIDADES_TEXTO + +AVISO = ("> **Aviso.** Cálculos de primer orden para orientar el diseño: no sustituyen a las calculadoras especializadas, " + "a la simulación ni a las normas originales. Los parámetros en estado **TBD** tienen valores provisionales, y todo " + "resultado que dependa de ellos es provisional.") + + +def celda(t): + return str("" if t is None else t).replace("|", "\\|").replace("\n", " ") + + +def tabla_md(cab, filas): + out = ["| " + " | ".join(celda(c) for c in cab) + " |", "|" + "|".join("---" for _ in cab) + "|"] + for f in filas: + out.append("| " + " | ".join(celda(c) for c in f) + " |") + return "\n".join(out) + + +def estado_md(e): + return f"**{e}**" if e in (NO_CUMPLE, REVISAR) else e + + +def _grupo(items): + con_chk = any(t == "chk" for t, _ in items) + cab = ["Magnitud", "Valor", "Unidad"] + (["Límite", "Resultado"] if con_chk else []) + ["Expresión", "Nota"] + filas = [] + for t, d in items: + if t == "res": + f = [d["etiqueta"], d["texto"], d["unidad"]] + (["", ""] if con_chk else []) + else: + f = [f"{d['etiqueta']} ({d['id']})", d["texto_valor"], d["unidad"], d["texto_limite"], estado_md(d["estado"])] + f += [f"`{d['expr']}`" if d.get("expr") and not d["expr"].startswith("`") else d.get("expr", ""), d.get("nota", "")] + filas.append(f) + return tabla_md(cab, filas) + + +def seccion_md(s): + out = [f"## {s.num}. {s.titulo}", ""] + if s.intro: + out += [s.intro, ""] + grupo = [] + + def vacia(): + if grupo: + out.extend([_grupo(grupo), ""]) + grupo.clear() + for it in s.items: + if it[0] in ("res", "chk"): + grupo.append(it) + continue + vacia() + if it[0] == "sub": + out += [f"### {it[1]}", ""] + if it[2]: + out += [it[2], ""] + elif it[0] == "nota": + out += [f"*{it[1]}*", ""] + elif it[0] == "tabla": + _, cab, filas, intro = it + if intro: + out += [intro, ""] + out += [tabla_md(cab, filas), ""] + vacia() + return "\n".join(out) + + +def ancla(texto): + import re + t = texto.lower().strip() + t = re.sub(r"[^\w\- ]", "", t) + return t.replace(" ", "-") + + +def informe(ctx, meta): + ent = ctx.ent + rc = ctx.recuento() + params = ent.params + tbd = [p for p in params.values() if p.estado == "TBD"] + ac = [p for p in params.values() if p.estado == "AC"] + L = [] + L += ["# Magnet Driver Abejorro · Informe de cálculos de diseño", ""] + L += [tabla_md(["Dato", "Valor"], [ + ["Fecha", meta["fecha_texto"]], + ["Versión de las entradas", meta["version_texto"]], + ["Motor de cálculo", VERSION_MOTOR], + ["Documento base", "Requisitos y arquitectura, borrador v0.2 (`docs/requisitos`)"], + ["MOSFET seleccionado", ctx.x.MOSFET_sel], + ["Parámetros", f"{len(params)} en total: {len(tbd)} TBD, {len(ac)} a confirmar"], + ]), ""] + L += [AVISO, ""] + L += ["## Índice", ""] + L += ["- [Resumen](#resumen)"] + for s in ctx.secciones: + t = f"{s.num}. {s.titulo}" + L.append(f"- [{t}](#{ancla(t)})") + L += ["- [Anexo A. Todas las comprobaciones](#anexo-a-todas-las-comprobaciones)", + "- [Anexo B. Parámetros usados](#anexo-b-parámetros-usados)", + "- [Anexo C. Parámetros pendientes](#anexo-c-parámetros-pendientes)", ""] + L += ["## Resumen", ""] + L += [f"El cálculo incluye {len(ctx.comprobaciones)} comprobaciones. «REVISAR» indica un criterio blando o un dato que " + "requiere un análisis más detallado.", ""] + L += [tabla_md(["Resultado", "Número"], [[CUMPLE, rc.get(CUMPLE, 0)], [NO_CUMPLE, rc.get(NO_CUMPLE, 0)], + [REVISAR, rc.get(REVISAR, 0)]]), ""] + malas = [c for c in ctx.comprobaciones if c["estado"] in (NO_CUMPLE, REVISAR)] + if malas: + L += ["Comprobaciones que no cumplen o que hay que revisar:", ""] + L += [tabla_md(["ID", "Comprobación", "Valor", "Límite", "Unidad", "Resultado", "Requisito", "Depende de"], + [[c["id"], c["etiqueta"], c["texto_valor"], c["texto_limite"], c["unidad"], estado_md(c["estado"]), + c["req"], c["deps"]] for c in malas]), ""] + for s in ctx.secciones: + L += [seccion_md(s), ""] + L += ["## Anexo A. Todas las comprobaciones", ""] + L += [tabla_md(["ID", "Comprobación", "Apartado", "Valor", "Límite", "Unidad", "Resultado", "Requisito", "Depende de", "Nota"], + [[c["id"], c["etiqueta"], c["seccion"], c["texto_valor"], c["texto_limite"], c["unidad"], + estado_md(c["estado"]), c["req"], c["deps"], c["nota"]] for c in ctx.comprobaciones]), ""] + L += ["## Anexo B. Parámetros usados", "", + "Instantánea de todos los parámetros escalares en el momento del cálculo (ficheros de `calculos/entradas`). " + "Las tablas de MOSFET, pistas, interfaces, distancias y consumos se muestran en sus apartados.", ""] + grupos = {} + for p in params.values(): + grupos.setdefault((p.origen.split(":")[0], getattr(p, "seccion", "")), []).append(p) + for (fich, sec), ps in grupos.items(): + L += [f"**`{fich}` · {sec}**", ""] + L += [tabla_md(["Nombre", "Descripción", "Valor", "Unidad", "Fuente", "Estado", "Requisito / pendiente", "Nota"], + [[f"`{p.nombre}`", p.descripcion, p.texto, "" if p.unidad in UNIDADES_TEXTO else p.unidad, + p.fuente, estado_md(p.estado) if p.estado == "TBD" else p.estado, p.req, p.nota] for p in ps]), ""] + L += ["## Anexo C. Parámetros pendientes", "", + "Parámetros en estado TBD (valor provisional) y AC (a confirmar), con la cuestión que los resolverá.", ""] + L += [tabla_md(["Estado", "Nombre", "Descripción", "Valor provisional", "Requisito / pendiente"], + [[p.estado, f"`{p.nombre}`", p.descripcion, f"{p.texto} {'' if p.unidad in UNIDADES_TEXTO else p.unidad}".strip(), + p.req] for p in tbd + ac]), ""] + return "\n".join(L) + + +def resultados_json(ctx, meta): + def limpio(v): + return v if isinstance(v, (int, float, str)) or v is None else str(v) + return { + "generado": meta["fecha_iso"], + "commit": meta.get("commit"), + "version_motor": VERSION_MOTOR, + "entradas": ctx.ent.instantanea(), + "resultados": {k: {"valor": limpio(r["valor"]), "texto": r["texto"], "unidad": r["unidad"], + "etiqueta": r["etiqueta"], "seccion": r.get("seccion", "")} + for k, r in sorted(ctx.resultados.items())}, + "comprobaciones": {c["id"]: {"etiqueta": c["etiqueta"], "estado": c["estado"], "valor": c["texto_valor"], + "limite": c["texto_limite"], "unidad": c["unidad"], "seccion": c["seccion"]} + for c in ctx.comprobaciones}, + } diff --git a/H12/LPU/calculos/motor/lector.py b/H12/LPU/calculos/motor/lector.py new file mode 100644 index 0000000..55b8d21 --- /dev/null +++ b/H12/LPU/calculos/motor/lector.py @@ -0,0 +1,260 @@ +"""Lectura de las tablas Markdown y CSV de entrada.""" +import csv +import os +import re + +from .unidades import factor, normaliza_unidad, parse_numero, UNIDADES_TEXTO + + +class ErrorEntradas(Exception): + def __init__(self, errores): + self.errores = errores + super().__init__("\n".join(errores)) + + +class Param: + def __init__(self, nombre, descripcion, texto, unidad, fuente, estado, req, nota, origen): + self.nombre = nombre + self.descripcion = descripcion + self.texto = texto + self.unidad = unidad + self.fuente = fuente + self.estado = estado + self.req = req + self.nota = nota + self.origen = origen + self.valor = None + + +def tablas_markdown(ruta): + """Devuelve una lista de (seccion, linea, cabeceras, filas[(linea, celdas)]).""" + tablas = [] + seccion = "" + with open(ruta, encoding="utf-8") as f: + lineas = f.read().splitlines() + i = 0 + while i < len(lineas): + ln = lineas[i].strip() + if ln.startswith("#"): + seccion = ln.lstrip("#").strip() + if ln.startswith("|") and i + 1 < len(lineas) and re.match(r"^\|\s*:?-{2,}", lineas[i + 1].strip()): + cab = [c.strip() for c in ln.strip("|").split("|")] + filas = [] + j = i + 2 + while j < len(lineas) and lineas[j].strip().startswith("|"): + celdas = [c.strip() for c in lineas[j].strip().strip("|").split("|")] + if len(celdas) < len(cab): + celdas += [""] * (len(cab) - len(celdas)) + filas.append((j + 1, celdas[: len(cab)] if len(celdas) >= len(cab) else celdas)) + if len(celdas) > len(cab): + filas[-1] = (j + 1, celdas) + j += 1 + tablas.append((seccion, i + 1, cab, filas)) + i = j + continue + i += 1 + return tablas + + +def separa_unidad(cabecera): + m = re.match(r"^(.*?)\s*\(([^)]*)\)\s*$", cabecera) + if m: + return m.group(1).strip(), normaliza_unidad(m.group(2)) + return cabecera.strip(), None + + +class Entradas: + def __init__(self, dir_entradas, dir_normas): + self.dir_entradas = dir_entradas + self.dir_normas = dir_normas + self.params = {} + self.mosfet_filas = [] # (clave, etiqueta, unidad, nota) + self.mosfets = [] # dict: col, datos{clave: valor SI o texto}, textos{clave: texto con unidad} + self.tablas = {} # nombre -> lista de filas (dict) + self.errores = [] + self.ficheros = [] + for nombre in ("parametros.md", "componentes.md", "diseno.md"): + ruta = os.path.join(dir_entradas, nombre) + if not os.path.exists(ruta): + self.errores.append(f"No existe el fichero de entradas {ruta}") + continue + self.ficheros.append(ruta) + self._lee_md(ruta, nombre) + self._lee_normas() + if self.errores: + raise ErrorEntradas(self.errores) + + # ------------------------------------------------------------------ Markdown + def _err(self, origen, msg): + self.errores.append(f"{origen}: {msg}") + + def _lee_md(self, ruta, nombre): + for seccion, linea, cab, filas in tablas_markdown(ruta): + if cab[:4] == ["Nombre", "Descripción", "Valor", "Unidad"]: + self._tabla_escalar(nombre, seccion, cab, filas) + elif "Clave" in cab and "Parámetro" in cab and any(c.startswith("Candidato") for c in cab): + self._tabla_mosfet(nombre, cab, filas) + elif cab and cab[0] == "Clave": + tipo = None + for marca, t in (("Red", "pistas"), ("Interfaz", "interfaces"), ("Caso", "distancias"), + ("Consumidor", "consumos"), ("Componente", "barrera")): + if marca in cab: + tipo = t + if tipo is None: + self._err(f"{nombre}:{linea}", "tabla con columna «Clave» no reconocida") + continue + self._tabla_generica(nombre, tipo, cab, filas) + else: + self._err(f"{nombre}:{linea}", "tabla no reconocida (revise las cabeceras)") + + def _tabla_escalar(self, fichero, seccion, cab, filas): + idx = {c: i for i, c in enumerate(cab)} + for ln, c in filas: + origen = f"{fichero}:{ln}" + if len(c) != len(cab): + self._err(origen, f"la fila tiene {len(c)} columnas y la tabla {len(cab)} (¿hay un «|» dentro de una celda?)") + continue + g = lambda k: c[idx[k]] if k in idx else "" + nombre = g("Nombre") + if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", nombre): + self._err(origen, f"nombre de parámetro no válido: «{nombre}»") + continue + if nombre in self.params: + self._err(origen, f"el parámetro {nombre} está repetido (ya en {self.params[nombre].origen})") + continue + unidad = normaliza_unidad(g("Unidad")) + p = Param(nombre, g("Descripción"), g("Valor"), unidad, g("Fuente"), g("Estado"), + g("Requisito / pendiente"), g("Nota"), origen) + p.seccion = seccion + if p.estado not in ("OK", "AC", "TBD"): + self._err(origen, f"{nombre}: estado «{p.estado}» no válido (OK, AC o TBD)") + num = parse_numero(p.texto) + if num is None: + if unidad in UNIDADES_TEXTO and p.texto: + p.valor = p.texto + else: + self._err(origen, f"{nombre}: valor «{p.texto}» no es un número") + continue + else: + try: + p.valor = num * factor(unidad) + except KeyError: + self._err(origen, f"{nombre}: unidad «{unidad}» no reconocida") + continue + self.params[nombre] = p + + def _tabla_mosfet(self, fichero, cab, filas): + i_cl, i_par, i_un = cab.index("Clave"), cab.index("Parámetro"), cab.index("Unidad") + cols = [i for i, c in enumerate(cab) if c.startswith("Candidato")] + i_nota = len(cab) - 1 if not cab[-1].startswith("Candidato") else None + datos = {i: {} for i in cols} + textos = {i: {} for i in cols} + for ln, c in filas: + origen = f"{fichero}:{ln}" + if len(c) != len(cab): + self._err(origen, "número de columnas incorrecto en la tabla de MOSFET") + continue + clave, unidad = c[i_cl], normaliza_unidad(c[i_un]) + self.mosfet_filas.append((clave, c[i_par], unidad, c[i_nota] if i_nota is not None else "")) + for i in cols: + t = c[i] + if t == "": + continue + num = parse_numero(t) + if num is None: + if unidad in UNIDADES_TEXTO: + datos[i][clave] = t + else: + self._err(origen, f"{cab[i]}, {clave}: «{t}» no es un número") + continue + else: + try: + datos[i][clave] = num * factor(unidad) + except KeyError: + self._err(origen, f"{clave}: unidad «{unidad}» no reconocida") + continue + textos[i][clave] = f"{t} {unidad}".strip() if unidad not in UNIDADES_TEXTO else t + for i in cols: + if not datos[i]: + continue + ref = datos[i].get("ref") + if not ref: + self._err(fichero, f"{cab[i]} tiene datos pero no tiene «Referencia»") + continue + if any(m["ref"] == ref for m in self.mosfets): + self._err(fichero, f"la referencia {ref} está repetida en los candidatos") + self.mosfets.append({"col": cab[i], "ref": ref, "datos": datos[i], "textos": textos[i]}) + + def _tabla_generica(self, fichero, tipo, cab, filas): + lista = self.tablas.setdefault(tipo, []) + claves = {f["Clave"] for f in lista} + for ln, c in filas: + origen = f"{fichero}:{ln}" + if len(c) != len(cab): + self._err(origen, f"la fila tiene {len(c)} columnas y la tabla {len(cab)}") + continue + fila = {"_origen": origen, "_textos": {}} + for h, t in zip(cab, c): + nombre, unidad = separa_unidad(h) + fila["_textos"][h] = t + if unidad is None: + fila[nombre] = t + continue + num = parse_numero(t) + if num is None: + fila[nombre] = t if t else None # puede ser el nombre de una magnitud + else: + try: + fila[nombre] = num * factor(unidad) + except KeyError: + self._err(origen, f"unidad «{unidad}» no reconocida en la cabecera «{h}»") + if not re.match(r"^[A-Za-z0-9_]+$", fila["Clave"] or ""): + self._err(origen, f"clave no válida: «{fila['Clave']}» (solo letras, números y _)") + continue + if fila["Clave"] in claves: + self._err(origen, f"la clave {fila['Clave']} está repetida") + continue + claves.add(fila["Clave"]) + lista.append(fila) + + # ------------------------------------------------------------------ CSV + def _lee_normas(self): + def lee(nombre): + ruta = os.path.join(self.dir_normas, nombre) + if not os.path.exists(ruta): + self.errores.append(f"No existe {ruta}") + return [] + with open(ruta, encoding="utf-8", newline="") as f: + return list(csv.DictReader(f)) + ipc = lee("ipc2221b_tabla6-1.csv") + self.ipc_cols = ["B1", "B2", "B3", "B4", "A5", "A6", "A7"] + self.ipc_filas, self.ipc_por_voltio = [], None + for f in ipc: + if f["desde_V"] == "por_voltio": + self.ipc_por_voltio = {k: float(f[k]) for k in self.ipc_cols} + else: + self.ipc_filas.append({"desde": float(f["desde_V"]), "hasta": float(f["hasta_V"]), + **{k: float(f[k]) for k in self.ipc_cols}}) + self.iec_filas = [{"V": float(f["tension_V"]), "PD1": float(f["PD1_mm"]), "PD2": float(f["PD2_mm"]), + "verif": f["verificacion"]} for f in lee("iec60664-1_fuga_pcb.csv")] + self.poe = [(f["norma"], float(f["potencia_PD_W"])) for f in lee("poe.csv")] + + # ------------------------------------------------------------------ instantánea + def instantanea(self): + """Texto de cada entrada, para detectar cambios.""" + s = {} + for n, p in self.params.items(): + s[f"param.{n}"] = {"texto": f"{p.texto} {p.unidad}".strip() if p.unidad not in UNIDADES_TEXTO else p.texto, + "estado": p.estado, "fuente": p.fuente, "etiqueta": p.descripcion} + etiquetas = {k: e for k, e, _, _ in self.mosfet_filas} + for m in self.mosfets: + for k, t in m["textos"].items(): + s[f"mosfet.{m['ref']}.{k}"] = {"texto": t, "estado": "", "etiqueta": f"{m['ref']}: {etiquetas.get(k, k)}"} + for tipo, filas in self.tablas.items(): + for f in filas: + for h, t in f["_textos"].items(): + if h == "Clave": + continue + s[f"{tipo}.{f['Clave']}.{h}"] = {"texto": t, "estado": f.get("Estado", "") or "", + "etiqueta": f"{tipo} {f['Clave']}: {h}"} + return s diff --git a/H12/LPU/calculos/motor/mosfet.py b/H12/LPU/calculos/motor/mosfet.py new file mode 100644 index 0000000..0fcc8d4 --- /dev/null +++ b/H12/LPU/calculos/motor/mosfet.py @@ -0,0 +1,123 @@ +"""MOSFET: comparador de candidatos, pérdidas y temperatura de unión.""" +from .unidades import fmt + +NECESARIOS = ["ref", "VDS", "VGSmax", "VGSrec_on", "VGSrec_off", "Vth_min", "ID", "IDM", "Rthjc", "R25", "R25max", + "Rhot", "Thot", "VSD", "Qg", "Ciss", "Crss", "Rgint", "Eon", "Eoff", "Vtest", "Itest", "tdon", "tr", + "tdoff", "tf"] + + +def selecciona(ctx): + """Valida los candidatos y fija ctx.mos_sel con el elegido en MOSFET_sel.""" + cands = [] + for mo in ctx.ent.mosfets: + faltan = [k for k in NECESARIOS if k not in mo["datos"]] + if faltan: + mo["incompleto"] = faltan + else: + cands.append(mo) + ctx.candidatos = cands + sel = ctx.x.MOSFET_sel + for mo in cands: + if mo["ref"] == sel: + ctx.mos_sel = mo["datos"] + return + refs = ", ".join(m["ref"] for m in cands) or "ninguno" + raise ValueError(f"MOSFET_sel = «{sel}» no coincide con ningún candidato completo de componentes.md ({refs})") + + +def perdidas(ctx): + x = ctx.x + s = ctx.seccion(2, "MOSFET: comparador de candidatos", + "Pérdidas de primer orden: Eon/Eoff escaladas linealmente con V e I desde las condiciones de ensayo, y RDS(on) interpolada " + "linealmente con Tj entre 25 °C y la temperatura alta de la hoja de datos, escalada con la relación máx./típ. " + "El caso A supone Ipk en régimen permanente (conservador frente a t_pk). " + f"El MOSFET más cargado conduce una fracción D_worst = {fmt(x.D_worst)} del periodo.") + ctx.mos_calc = {} + for mo in ctx.candidatos: + d = mo["datos"] + c = {} + c["Rtyp_as"] = d["R25"] + (d["Rhot"] - d["R25"]) * (x.Tj_assumed - 25) / (d["Thot"] - 25) + c["Rmax_as"] = c["Rtyp_as"] * d["R25max"] / d["R25"] + c["FOM"] = d["R25"] * 1e3 * d["Qg"] * 1e9 + for caso, I in (("pk", x.Ipk), ("c", x.Irms_cont)): + esw = (d["Eon"] + d["Eoff"]) * (x.Vbus_max / d["Vtest"]) * (I / d["Itest"]) + c[f"Esw_{caso}"] = esw + c[f"Pc_{caso}"] = I ** 2 * c["Rmax_as"] * x.D_worst + c[f"Psw_{caso}"] = x.fsw * esw + c[f"Pdt_{caso}"] = 2 * x.t_dead * x.fsw * d["VSD"] * I + c[f"Pdev_{caso}"] = c[f"Pc_{caso}"] + c[f"Psw_{caso}"] + c[f"Pdt_{caso}"] + c[f"Pbr_{caso}"] = 2 * I ** 2 * c["Rmax_as"] + 2 * x.fsw * esw + 4 * x.t_dead * x.fsw * d["VSD"] * I + ctx.mos_calc[mo["ref"]] = c + sel = ctx.mos_calc[x.MOSFET_sel] + ctx.define("P_br_pk", sel["Pbr_pk"]) + ctx.define("P_br_c", sel["Pbr_c"]) + ctx.sec_mosfet = s + + +def temperatura(ctx): + """Tj y tabla comparativa (necesita Rth_c2w y T_cool_out del bloque térmico).""" + x, s = ctx.x, ctx.sec_mosfet + for mo in ctx.candidatos: + d, c = mo["datos"], ctx.mos_calc[mo["ref"]] + for caso in ("pk", "c"): + c[f"Tj_{caso}"] = x.T_cool_out + c[f"Pdev_{caso}"] * (d["Rthjc"] + x.Rth_c2w) + cands = ctx.candidatos + etq = {k: (e, u) for k, e, u, _ in ctx.ent.mosfet_filas} + cab = ["Parámetro", "Unidad"] + [f"{m['ref']}{' (sel.)' if m['ref'] == x.MOSFET_sel else ''}" for m in cands] + filas, reg = [], {} + + def fila(etiqueta, unidad, valores, clave=None, esc=1.0, dec=2, textos=None): + celdas = [] + for m, v in zip(cands, valores): + t = textos[cands.index(m)] if textos else fmt(v / esc if isinstance(v, (int, float)) else v, dec) + celdas.append(t) + if clave: + reg[f"mosfet.{m['ref']}.{clave}"] = (v, t, unidad, f"{m['ref']}: {etiqueta}") + filas.append([etiqueta, unidad] + celdas) + + def res_estado(v, lim, op, blando=False): + ok = v <= lim if op == "<=" else v >= lim + return "CUMPLE" if ok else ("**REVISAR**" if blando else "**NO CUMPLE**") + + filas.append(["**Datos principales**", ""] + [""] * len(cands)) + for k in ("pkg", "VDS", "R25", "Rthjc", "Qg", "Eon", "Eoff"): + e, u = etq.get(k, (k, "")) + fila(e, u, [None] * len(cands), textos=[m["textos"].get(k, "—").replace(f" {u}", "") for m in cands]) + D = lambda k: [ctx.mos_calc[m["ref"]][k] for m in cands] + fila("RDS(on) típica a Tj_assumed", "mΩ", D("Rtyp_as"), "Rtyp_as", 1e-3) + fila("RDS(on) máxima a Tj_assumed", "mΩ", D("Rmax_as"), "Rmax_as", 1e-3) + fila("Figura de mérito RDS(on)·Qg (25 °C)", "mΩ·nC", D("FOM"), "FOM", 1, 0) + for caso, titulo in (("pk", "**Caso A: Ipk**"), ("c", "**Caso B: Irms_cont**")): + filas.append([titulo, ""] + [""] * len(cands)) + fila("Energía de conmutación escalada", "µJ", D(f"Esw_{caso}"), f"Esw_{caso}", 1e-6, 0) + fila("Conducción del MOSFET más cargado", "W", D(f"Pc_{caso}"), f"Pc_{caso}", 1, 2) + fila("Conmutación (toda la de la rama en un MOSFET)", "W", D(f"Psw_{caso}"), f"Psw_{caso}", 1, 2) + fila("Diodo en los tiempos muertos", "W", D(f"Pdt_{caso}"), f"Pdt_{caso}", 1, 3) + fila("Pérdidas totales del MOSFET más cargado", "W", D(f"Pdev_{caso}"), f"Pdev_{caso}", 1, 1) + fila("Temperatura de unión", "°C", D(f"Tj_{caso}"), f"Tj_{caso}", 1, 1) + fila("Tj ≤ Tj_lim", "–", [None] * len(cands), + textos=[res_estado(v, x.Tj_lim, "<=") for v in D(f"Tj_{caso}")]) + if caso == "pk": + fila("Tj ≤ Tj_assumed (coherencia de la RDS(on))", "–", [None] * len(cands), + textos=[res_estado(v, x.Tj_assumed, "<=", True) for v in D("Tj_pk")]) + fila("Pérdidas del puente completo (4 MOSFET)", "W", D(f"Pbr_{caso}"), f"Pbr_{caso}", 1, 1) + filas.append(["**Márgenes**", ""] + [""] * len(cands)) + M = lambda k: [m["datos"][k] for m in cands] + fila("VDS / V_cont", "–", [v / x.V_cont for v in M("VDS")], "VDS_rel", 1, 2) + fila("VDS ≥ tensión del caso de fallo", "–", [None] * len(cands), + textos=[res_estado(v, x.V_fault_max, ">=") for v in M("VDS")]) + fila("Vgs_on ≤ VGS recomendada", "–", [None] * len(cands), + textos=[res_estado(x.Vgs_on, v, "<=", True) for v in M("VGSrec_on")]) + fila("Vgs_on ≤ VGS máxima absoluta", "–", [None] * len(cands), + textos=[res_estado(x.Vgs_on, v, "<=") for v in M("VGSmax")]) + fila("Vgs_off ≥ VGS recomendada en OFF", "–", [None] * len(cands), + textos=[res_estado(x.Vgs_off, v, ">=", True) for v in M("VGSrec_off")]) + fila("ID continua ≥ Ipk", "–", [None] * len(cands), textos=[res_estado(v, x.Ipk, ">=") for v in M("ID")]) + s.tabla(cab, filas, intro="Comparación de los candidatos completos de `componentes.md`. " + "Las temperaturas usan la cadena térmica y el agua del apartado 3.", registro=reg) + incompletos = [f"{m['ref']} (faltan: {', '.join(m['incompleto'])})" for m in ctx.ent.mosfets if m.get("incompleto")] + if incompletos: + s.nota("Candidatos ignorados por datos incompletos: " + "; ".join(incompletos) + ".") + sel = ctx.mos_calc[x.MOSFET_sel] + ctx.define("Tj_pk_sel", sel["Tj_pk"]) + ctx.define("Tj_c_sel", sel["Tj_c"]) diff --git a/H12/LPU/calculos/motor/nucleo.py b/H12/LPU/calculos/motor/nucleo.py new file mode 100644 index 0000000..cd7f5b3 --- /dev/null +++ b/H12/LPU/calculos/motor/nucleo.py @@ -0,0 +1,132 @@ +"""Contexto de cálculo, secciones del informe y comprobaciones.""" +from .unidades import fmt + +CUMPLE, NO_CUMPLE, REVISAR, SIN_DATO = "CUMPLE", "NO CUMPLE", "REVISAR", "—" + + +class ErrorCalculo(Exception): + pass + + +class Valores: + """Acceso por atributo a parámetros y resultados (en SI): x.Vbus_min""" + + def __init__(self, ctx): + object.__setattr__(self, "_ctx", ctx) + + def __getattr__(self, nombre): + ctx = self._ctx + if nombre in ctx.v: + return ctx.v[nombre] + raise ErrorCalculo(f"Falta el parámetro o magnitud «{nombre}» (¿se ha borrado o renombrado en las entradas?)") + + +class Ctx: + def __init__(self, entradas): + self.ent = entradas + self.v = {n: p.valor for n, p in entradas.params.items()} + self.x = Valores(self) + self.secciones = [] + self.resultados = {} # clave -> dict + self.comprobaciones = [] # lista de dict + + def define(self, clave, valor): + if clave in self.ent.params: + raise ErrorCalculo(f"La magnitud calculada «{clave}» coincide con un parámetro de entrada") + self.v[clave] = valor + + def resuelve(self, texto, unidad_si=1.0, origen=""): + """Número (en la unidad indicada) o nombre de una magnitud.""" + from .unidades import parse_numero + if texto is None or texto == "": + raise ErrorCalculo(f"{origen}: falta un valor") + if isinstance(texto, (int, float)): + return float(texto) + n = parse_numero(texto) + if n is not None: + return n * unidad_si + if texto in self.v and isinstance(self.v[texto], (int, float)): + return self.v[texto] + raise ErrorCalculo(f"{origen}: «{texto}» no es un número ni una magnitud calculada") + + def seccion(self, num, titulo, intro=""): + s = Seccion(self, num, titulo, intro) + self.secciones.append(s) + return s + + def recuento(self): + r = {CUMPLE: 0, NO_CUMPLE: 0, REVISAR: 0, SIN_DATO: 0} + for c in self.comprobaciones: + r[c["estado"]] = r.get(c["estado"], 0) + 1 + return r + + +def evalua(valor, limite, op, blando): + if valor is None or limite is None: + return SIN_DATO + ok = valor <= limite if op == "<=" else valor >= limite + return CUMPLE if ok else (REVISAR if blando else NO_CUMPLE) + + +class Seccion: + def __init__(self, ctx, num, titulo, intro): + self.ctx, self.num, self.titulo, self.intro = ctx, num, titulo, intro + self.items = [] + + # --- contenido + def sub(self, titulo, intro=""): + self.items.append(("sub", titulo, intro)) + + def nota(self, texto): + self.items.append(("nota", texto)) + + def res(self, clave, etiqueta, valor, unidad="", esc=1.0, dec=2, expr="", nota=""): + """Guarda la magnitud en SI (clave) y la muestra dividida por esc.""" + if clave: + self.ctx.define(clave, valor) + mostrado = valor / esc if isinstance(valor, (int, float)) else valor + fila = {"clave": clave, "etiqueta": etiqueta, "valor": valor, "texto": fmt(mostrado, dec), + "unidad": unidad, "expr": expr, "nota": nota, "seccion": f"{self.num}. {self.titulo}"} + if clave: + self.ctx.resultados[clave] = fila + self.items.append(("res", fila)) + return valor + + def dato(self, nombre, etiqueta, unidad="", esc=1.0, dec=2): + v = self.ctx.v[nombre] + p = self.ctx.ent.params.get(nombre) + estado = f" ({p.estado})" if p is not None and p.estado != "OK" else "" + mostrado = v / esc if isinstance(v, (int, float)) else v + self.items.append(("res", {"clave": None, "etiqueta": etiqueta, "valor": v, "texto": fmt(mostrado, dec), + "unidad": unidad, "expr": f"`{nombre}`", "nota": f"Entrada{estado}"})) + + def chk(self, cid, etiqueta, valor, limite, op, unidad="", esc=1.0, dec=2, req="", deps="", nota="", + blando=False, expr=""): + estado = evalua(valor, limite, op, blando) + c = {"id": cid, "etiqueta": etiqueta, "valor": valor, "limite": limite, "op": op, + "texto_valor": fmt(None if valor is None else valor / esc, dec), + "texto_limite": fmt(None if limite is None else limite / esc, dec), + "esc": esc, "unidad": unidad, "estado": estado, "req": req, "deps": deps, "nota": nota, + "expr": expr, "seccion": f"{self.num}. {self.titulo}"} + if any(k["id"] == cid for k in self.ctx.comprobaciones): + raise ErrorCalculo(f"Comprobación repetida: {cid}") + self.ctx.comprobaciones.append(c) + self.items.append(("chk", c)) + return estado + + def chk_texto(self, cid, etiqueta, ok, texto_valor, texto_limite, unidad="", req="", deps="", nota="", + blando=False, expr=""): + estado = CUMPLE if ok else (REVISAR if blando else NO_CUMPLE) + c = {"id": cid, "etiqueta": etiqueta, "valor": None, "limite": None, "op": "", + "texto_valor": texto_valor, "texto_limite": texto_limite, "unidad": unidad, "estado": estado, + "req": req, "deps": deps, "nota": nota, "expr": expr, "seccion": f"{self.num}. {self.titulo}"} + self.ctx.comprobaciones.append(c) + self.items.append(("chk", c)) + return estado + + def tabla(self, cabeceras, filas, intro="", registro=None): + """filas: listas de textos ya formateados. registro: {clave: (valor, texto, unidad, etiqueta)}.""" + self.items.append(("tabla", cabeceras, filas, intro)) + for clave, (valor, texto, unidad, etiqueta) in (registro or {}).items(): + self.ctx.resultados[clave] = {"clave": clave, "etiqueta": etiqueta, "valor": valor, "texto": texto, + "unidad": unidad, "seccion": f"{self.num}. {self.titulo}"} diff --git a/H12/LPU/calculos/motor/pistas.py b/H12/LPU/calculos/motor/pistas.py new file mode 100644 index 0000000..2b7309c --- /dev/null +++ b/H12/LPU/calculos/motor/pistas.py @@ -0,0 +1,60 @@ +"""Pistas y vías de potencia (fórmula de IPC-2221).""" +import math + +from .unidades import fmt + +MIL = 0.0254e-3 # m +OZ_MIL = 1.378 # espesor de 1 oz en mil +OZ_M = 35e-6 # espesor de 1 oz en m +RHO_CU = 1.724e-8 # Ω·m a 20 °C +ALFA_CU = 0.00393 # 1/K + + +def calcula(ctx): + x = ctx.x + s = ctx.seccion(4, "Pistas y vías de potencia", + "IPC-2221: I = k·ΔT^0,44·A^0,725 (A en mil²; k = 0,048 externa y 0,024 interna). La fórmula se obtuvo para corrientes y " + "anchos moderados (≈ hasta 35 A y 400 mil): con decenas de amperios es una extrapolación. Úsela como primera estimación y " + "confirme con IPC-2152 o simulación. Redes de `diseno.md`, tabla 1.") + filas, reg = [], {} + for f in ctx.ent.tablas.get("pistas", []): + o, cl = f["_origen"], f["Clave"] + I = ctx.resuelve(f["Corriente"], 1.0, o) + capa = f["Capa"] + if capa not in ("Ext", "Int"): + raise ValueError(f"{o}: Capa debe ser «Ext» o «Int»") + n = ctx.resuelve(f["Capas en paralelo"], 1.0, o) + w = ctx.resuelve(f["Ancho previsto"], 1.0, o) + L = ctx.resuelve(f["Longitud"], 1.0, o) + oz = x.oz_ext if capa == "Ext" else x.oz_int + tmil = oz * OZ_MIL + k = 0.048 if capa == "Ext" else 0.024 + dT = x.dT_trace + a_req = ((I / n) / (k * dT ** 0.44)) ** (1 / 0.725) + w_req = a_req / tmil * MIL + dT_est = ((I / n) / (k * (w / MIL * tmil) ** 0.725)) ** (1 / 0.44) if I > 0 else 0.0 + R = RHO_CU * (1 + ALFA_CU * (x.T_amb + dT_est - 20)) * L / (w * oz * OZ_M) / n + P = I ** 2 * R + filas.append([f["Red"], fmt(I, 2), f"{capa} × {fmt(n, 0)}", fmt(oz, 1), fmt(w_req * 1e3, 2), fmt(w * 1e3, 2), + fmt(dT_est, 1), fmt(R * 1e3, 3), fmt(P, 2), fmt(I * R * 1e3, 1), f.get("Estado") or ""]) + reg[f"pista.{cl}.w_req"] = (w_req, fmt(w_req * 1e3, 2), "mm", f"Ancho necesario: {f['Red']}") + reg[f"pista.{cl}.dT"] = (dT_est, fmt(dT_est, 1), "°C", f"ΔT estimado: {f['Red']}") + reg[f"pista.{cl}.P"] = (P, fmt(P, 2), "W", f"Pérdidas: {f['Red']}") + f["_calc"] = (w, w_req) + s.tabla(["Red", "I (A)", "Capa × nº", "Cu (oz)", "Ancho necesario por capa (mm)", "Ancho previsto (mm)", + "ΔT estimado (°C)", "R (mΩ)", "P (W)", "Caída (mV)", "Estado"], filas, + intro=f"Incremento de temperatura admisible: dT_trace = {fmt(x.dT_trace, 0)} °C. " + "La resistencia se evalúa a T_amb + ΔT estimado.", registro=reg) + for f in ctx.ent.tablas.get("pistas", []): + w, w_req = f["_calc"] + s.chk(f"PIS-{f['Clave']}", f"Ancho de pista: {f['Red']}", w, w_req, ">=", "mm", 1e-3, 1, + req="DCB-03 · LOAD-03", deps="MEC-03, PT-01, ME-I-03", expr="ancho previsto ≥ ancho necesario") + s.sub("4.1 Vías para corriente", + "Misma fórmula (k externa) con la sección de cobre del barril; diámetro y metalizado de las vías térmicas.") + a_mil2 = x.A_barrel / MIL ** 2 + s.res("I_via", "Corriente admisible por vía", 0.048 * x.dT_trace ** 0.44 * a_mil2 ** 0.725, "A", dec=2, + expr="0,048·dT_trace^0,44·A_barril^0,725") + s.res("N_via_Ipk", "Vías necesarias para Ipk (cambio de capa de la salida)", float(math.ceil(x.Ipk / x.I_via)), "–", + dec=0, expr="⌈Ipk/I_via⌉") + s.res("N_via_in", "Vías necesarias para la entrada de la primera placa", float(math.ceil(x.I_in_first / x.I_via)), + "–", dec=0, expr="⌈I_in_first/I_via⌉") diff --git a/H12/LPU/calculos/motor/potencia.py b/H12/LPU/calculos/motor/potencia.py new file mode 100644 index 0000000..c6defa4 --- /dev/null +++ b/H12/LPU/calculos/motor/potencia.py @@ -0,0 +1,162 @@ +"""Potencia y bus DC: rizado, di/dt, corrientes de bus, caso de fallo y fusible.""" +import math + +from .unidades import fmt + + +def calcula(ctx): + x = ctx.x + s = ctx.seccion(1, "Potencia y bus DC", + "Modelo de primer orden: L constante en cada punto de trabajo y conmutación ideal.") + bip = x.Modulacion == "Bipolar" + if x.Modulacion not in ("Unipolar", "Bipolar"): + raise ValueError("Modulacion debe ser «Unipolar» o «Bipolar»") + casos = [("EMS, L mínima", "L_EMS_min"), ("EMS, L máxima", "L_EMS_max"), + ("HEMS, L mínima", "L_HEMS_min"), ("HEMS, L máxima", "L_HEMS_max")] + + # 1.1 Rizado + s.sub("1.1 Rizado de corriente en la bobina", + "Unipolar: la salida conmuta entre 0 y ±Vbus a 2·fsw, ΔI = Vbus·m·(1−m)/(2·L·fsw), máximo en m = 0,5: Vbus/(8·L·fsw). " + "Bipolar: ΔI = Vbus·(1−m²)/(2·L·fsw), máximo en m = 0: Vbus/(2·L·fsw). Se desprecia R frente a L. " + f"Modulación seleccionada: **{x.Modulacion}**; fsw = {fmt(x.fsw / 1e3, 1)} kHz; rizado admisible = {fmt(x.dI_pp_max, 2)} A.") + filas, reg, sel = [], {}, [] + for et, n in casos: + L = ctx.v[n] + u_min = x.Vbus_min / (8 * L * x.fsw) + u_max = x.Vbus_max / (8 * L * x.fsw) + b_max = x.Vbus_max / (2 * L * x.fsw) + e = b_max if bip else u_max + sel.append(e) + estado = "CUMPLE" if e <= x.dI_pp_max else "**NO CUMPLE**" + filas.append([et, fmt(L * 1e3, 2), fmt(u_min), fmt(u_max), fmt(b_max), fmt(e), estado]) + for k, val in (("unip_Vmin", u_min), ("unip_Vmax", u_max), ("bip_Vmax", b_max)): + reg[f"rizado.{n}.{k}"] = (val, fmt(val), "A", f"Rizado {et} ({k})") + s.tabla(["Electroimán / punto", "L (mH)", "ΔI unip., Vbus_min (A)", "ΔI unip., Vbus_max (A)", + "ΔI bip., Vbus_max (A)", "ΔI con la modulación elegida (A)", "Resultado"], filas, registro=reg) + s.chk("POT-01", "Rizado en el peor caso (L mínima, Vbus_max)", max(sel), x.dI_pp_max, "<=", "A", dec=2, + req="LOAD-07", deps="EM-C-03, FW-08", nota="Doc. v0.2 §6.1.3: 1,30 A (EMS) y 0,40 A (HEMS) a 400 V, unipolar", + expr="max(ΔI) ≤ dI_pp_max") + k = 2 if bip else 8 + s.res("fsw_min_rizado", "fsw mínima para cumplir el rizado con L mínima", + x.Vbus_max / (k * min(x.L_EMS_min, x.L_HEMS_min) * x.dI_pp_max), "kHz", 1e3, 1, + expr="Vbus_max/(k·L_min·dI_pp_max)", nota="k = 8 (unipolar) o 2 (bipolar)") + + # 1.2 Dinámica + s.sub("1.2 Dinámica de la corriente", + "di/dt = (Vbus − R·I)/L. Tiempo de 0 a Ipk con L constante: τ·ln(Vbus/(Vbus − R·Ipk)), τ = L/R. " + "En rueda libre (SAF-05) la corriente cae al 1 % en 4,6·τ. Con L no lineal, los valores son orientativos.") + filas, reg = [], {} + for et, n in casos: + L = ctx.v[n] + d1 = x.Vbus_min / L / 1e3 + d2 = x.Vbus_max / L / 1e3 + d3 = (x.Vbus_min - x.Rcoil * x.Ipk) / L / 1e3 + t = (L / x.Rcoil * math.log(x.Vbus_min / (x.Vbus_min - x.Rcoil * x.Ipk)) * 1e3 + if x.Vbus_min > x.Rcoil * x.Ipk else None) + tau = L / x.Rcoil * 1e3 + filas.append([et, fmt(L * 1e3, 2), fmt(d1, 1), fmt(d2, 1), fmt(d3, 1), + fmt(t, 2) if t is not None else "no alcanzable", fmt(tau, 1)]) + reg[f"didt.{n}.Vmin"] = (d1, fmt(d1, 1), "A/ms", f"di/dt {et}, Vbus_min") + reg[f"didt.{n}.Vmax"] = (d2, fmt(d2, 1), "A/ms", f"di/dt {et}, Vbus_max") + reg[f"t_subida.{n}"] = (t, fmt(t, 2), "ms", f"Tiempo de 0 a Ipk, {et}") + reg[f"tau.{n}"] = (tau, fmt(tau, 1), "ms", f"L/R, {et}") + s.tabla(["Electroimán / punto", "L (mH)", "di/dt, Vbus_min, I = 0 (A/ms)", "di/dt, Vbus_max, I = 0 (A/ms)", + "di/dt, Vbus_min, I = Ipk (A/ms)", "t de 0 a Ipk, Vbus_min (ms)", "τ = L/R (ms)"], filas, registro=reg) + s.res("V_coil_pk", "Tensión en la bobina a Ipk", x.Rcoil * x.Ipk, "V", dec=1, expr="Rcoil·Ipk") + s.res("m_pk", "Índice de modulación a Ipk con Vbus_min", x.V_coil_pk / x.Vbus_min, "–", dec=3, expr="V_coil_pk/Vbus_min") + s.chk("POT-02", "Corriente máxima alcanzable con Vbus_min (sin pérdidas)", x.Vbus_min / x.Rcoil, x.Ipk, ">=", "A", + dec=1, req="LOAD-03", nota="Margen de tensión para el lazo de corriente", expr="Vbus_min/Rcoil ≥ Ipk") + + # 1.3 Corrientes + s.sub("1.3 Corrientes de bus y de DC-link", + "Corriente media de bus = P_bobina/(η·Vbus). La entrada del puente son pulsos de amplitud I y ciclo m: " + "I_rms,puente = I·√m; I_rms,C = I·√(m·(1−m)), con máximo I/2 en m = 0,5. Rizado del DC-link suponiendo que el bus solo aporta la media: " + "ΔV = I·m·(1−m)/(2·fsw·C) + I·ESR. Se ignoran la inductancia del bus y la interacción entre placas.") + s.res("P_coil_pk", "Potencia en la bobina a Ipk", x.Rcoil * x.Ipk ** 2, "W", dec=0, expr="Rcoil·Ipk²") + s.res("I_bus_pk", "Corriente media de bus a Ipk, Vbus_min", x.P_coil_pk / (x.eta_pwr * x.Vbus_min), "A", dec=2, + expr="P_coil_pk/(eta_pwr·Vbus_min)", nota="Doc. v0.2 §6.2: 13,5 A sin pérdidas") + s.res("I_bus_pk_Vmax", "Corriente media de bus a Ipk, Vbus_max", x.P_coil_pk / (x.eta_pwr * x.Vbus_max), "A", dec=2, + expr="P_coil_pk/(eta_pwr·Vbus_max)", nota="Doc. v0.2 §6.2: 10,8 A sin pérdidas") + s.res("I_bus_cont", "Corriente media de bus en continuo, Vbus_min", x.Rcoil * x.Irms_cont ** 2 / (x.eta_pwr * x.Vbus_min), + "A", dec=3, expr="Rcoil·Irms_cont²/(eta_pwr·Vbus_min)") + if x.Bus_topo not in ("Encadenado", "Estrella"): + raise ValueError("Bus_topo debe ser «Encadenado» o «Estrella»") + s.res("N_chain", "Placas cuya corriente atraviesa la entrada de la primera", + float(x.N_MD if x.Bus_topo == "Encadenado" else 1), "–", dec=0, expr="N_MD si Bus_topo = Encadenado; si no, 1") + s.res("I_in_first", "Corriente de entrada de la primera placa, todas a Ipk", x.N_chain * x.I_bus_pk, "A", dec=1, + expr="N_chain·I_bus_pk", nota="Peor caso simultáneo") + s.res("I_in_first_cont", "Corriente de entrada de la primera placa, en continuo", x.N_chain * x.I_bus_cont, "A", dec=2, + expr="N_chain·I_bus_cont") + s.res("I_bridge_rms", "Corriente eficaz de entrada al puente a Ipk", x.Ipk * math.sqrt(x.m_pk), "A", dec=1, + expr="Ipk·√m_pk", nota="Para las pistas DC-link ↔ puente") + s.res("I_cap_rms_pk", "Corriente eficaz en el DC-link en el punto Ipk, Vbus_min", + x.Ipk * math.sqrt(x.m_pk * (1 - x.m_pk)), "A", dec=1, expr="Ipk·√(m_pk·(1−m_pk))") + s.res("I_cap_rms_wc", "Corriente eficaz en el DC-link, peor caso (m = 0,5)", x.Ipk / 2, "A", dec=1, expr="Ipk/2", + nota="Por ejemplo, durante la subida de corriente; para elegir condensadores") + s.dato("Cdc", "Capacidad del DC-link", "µF", 1e-6, 1) + s.res("dV_dc_pk", "Rizado de tensión en el DC-link en el punto Ipk", + x.Ipk * x.m_pk * (1 - x.m_pk) / (2 * x.fsw * x.Cdc) + x.Ipk * x.ESR_dc, "V", dec=2, + expr="Ipk·m(1−m)/(2·fsw·Cdc) + Ipk·ESR_dc") + s.chk("POT-03", "Rizado de tensión en el DC-link, peor caso (m = 0,5)", + x.Ipk / (8 * x.fsw * x.Cdc) + x.Ipk * x.ESR_dc, x.dVdc_pct * x.Vbus_min, "<=", "V", dec=2, + req="DCB-07", deps="HW-02 (ESR)", nota="Límite: dVdc_pct·Vbus_min", expr="Ipk/(8·fsw·Cdc) + Ipk·ESR_dc") + s.res("C_min_rizado", "Capacidad mínima para el rizado (sin ESR)", x.Ipk / (8 * x.fsw * x.dVdc_pct * x.Vbus_min), + "µF", 1e-6, 1, expr="Ipk/(8·fsw·dVdc_pct·Vbus_min)") + s.res("f_dc_link", "Frecuencia fundamental de la corriente en el DC-link", (1 if bip else 2) * x.fsw, "kHz", 1e3, 1, + expr="2·fsw (unipolar) o fsw (bipolar)") + + # 1.4 Caso de fallo + s.sub("1.4 Caso de fallo: sin rueda libre, la energía vuelve al DC-link (DCB-08, SAF-07)", + "Balance de energía: ½·C·(Vf² − V0²) = E, luego Vf = √(V0² + 2E/C). Se desprecian las pérdidas durante la descarga (conservador). " + "Los mapas llegan a 55 A; la extrapolación a Ipk es cuadrática y solo orientativa, porque el HEMS no es lineal.") + s.res("E_map", "Energía peor caso según los mapas H11 (55 A)", max(x.E_EMS, x.E_HEMS), "J", dec=1, expr="max(E_EMS, E_HEMS)") + s.res("E_ext", "Energía extrapolada a Ipk", x.E_map * (x.Ipk / x.I_map) ** 2, "J", dec=1, expr="E_map·(Ipk/I_map)²", + nota="Orientativa (EM-D-03)") + s.res("E_const", "Cota pesimista con L constante", 0.5 * x.L_EMS_0 * x.Ipk ** 2, "J", dec=1, expr="½·L_EMS_0·Ipk²", + nota="Doc. v0.2 §6.3: 48 J") + s.res("C_abs", "Capacidad que absorbe la energía", x.Cdc * x.N_dc_abs, "µF", 1e-6, 1, expr="Cdc·N_dc_abs") + vf = lambda v0, e: math.sqrt(v0 ** 2 + 2 * e / x.C_abs) + s.res("V_fault_min_map", "Tensión final desde Vbus_min (energía de los mapas)", vf(x.Vbus_min, x.E_map), "V", dec=0, + expr="√(Vbus_min² + 2·E_map/C_abs)", nota="Doc. v0.2: ≈ 650 V") + s.res("V_fault_map", "Tensión final desde Vbus_max (energía de los mapas)", vf(x.Vbus_max, x.E_map), "V", dec=0, + expr="√(Vbus_max² + 2·E_map/C_abs)", nota="Doc. v0.2: ≈ 690 V") + s.res("V_fault_max", "Tensión final desde Vbus_max (energía extrapolada a Ipk)", vf(x.Vbus_max, x.E_ext), "V", dec=0, + expr="√(Vbus_max² + 2·E_ext/C_abs)") + s.res("V_fault_const", "Tensión final desde Vbus_max (cota pesimista)", vf(x.Vbus_max, x.E_const), "V", dec=0, + expr="√(Vbus_max² + 2·E_const/C_abs)", nota="Doc. v0.2: ≈ 1040 V") + s.chk("POT-04", "Tensión de fallo (extrapolada) frente a la tensión continua admisible", x.V_fault_max, x.V_cont, "<=", + "V", dec=0, req="DCB-08 · DCB-02", deps="HW-03, EM-D-03", nota="Sin clamp, chopper ni DC-link mayor", + expr="V_fault_max ≤ V_cont") + s.chk("POT-05", "Tensión de fallo frente a la tensión nominal del DC-link", x.V_fault_max, x.V_film, "<=", "V", dec=0, + req="DCB-08", deps="HW-02, HW-03", expr="V_fault_max ≤ V_film") + s.chk("POT-06", "Tensión de fallo frente a la VDS del MOSFET seleccionado", x.V_fault_max, ctx.mos_sel["VDS"], "<=", + "V", dec=0, req="PWR-02", deps="HW-02", expr="V_fault_max ≤ VDS") + s.chk("POT-07", "Capacidad instalada frente a la necesaria para no superar V_cont desde Vbus_max", + x.C_abs, 2 * x.E_ext / (x.V_cont ** 2 - x.Vbus_max ** 2), ">=", "µF", 1e-6, 0, + req="DCB-08", deps="HW-03, EM-D-03", nota="Doc. v0.2: ≥ 167 µF con 16,7 J", + expr="C_abs ≥ 2·E_ext/(V_cont² − Vbus_max²)") + s.res("C_need_map", "Capacidad necesaria con la energía de los mapas (sin extrapolar)", + 2 * x.E_map / (x.V_cont ** 2 - x.Vbus_max ** 2), "µF", 1e-6, 0, expr="2·E_map/(V_cont² − Vbus_max²)") + s.sub("1.5 Opción clamp o chopper (HW-03)") + s.res("R_chop_max", "Resistencia máxima del chopper", x.V_chop_on / x.Ipk, "Ω", dec=2, expr="V_chop_on/Ipk", + nota="Para que conduzca Ipk sin superar V_chop_on") + s.res("P_chop_pk", "Potencia de pico en la resistencia", x.V_chop_on * x.Ipk, "kW", 1e3, 1, expr="V_chop_on·Ipk") + s.res("E_chop", "Energía por evento en la resistencia (cota)", x.E_ext, "J", dec=1, expr="E_ext") + s.chk("POT-08", "Tensión de actuación del chopper por encima de Vbus_max con margen", x.V_chop_on, + x.k_chop_mrg * x.Vbus_max, ">=", "V", dec=0, req="HW-03", deps="HW-03", blando=True, + expr="V_chop_on ≥ k_chop_mrg·Vbus_max", nota="Para no actuar en servicio") + s.chk("POT-09", "Tensión de actuación del chopper por debajo de V_cont", x.V_chop_on, x.V_cont, "<=", "V", dec=0, + req="HW-03", deps="HW-03", expr="V_chop_on ≤ V_cont") + + # 1.6 Fusible + s.sub("1.6 Fusible en placa (DCB-06)") + s.chk("POT-10", "Corriente media de bus a Ipk frente al fusible con factor de utilización", x.I_bus_pk, + x.k_fuse * x.I_fuse, "<=", "A", dec=2, req="DCB-06", deps="EM-C-01, ME-I-03", blando=True, + expr="I_bus_pk ≤ k_fuse·I_fuse", nota="El pico dura t_pk: revisar con la curva tiempo-corriente del fusible") + s.chk("POT-11", "Corriente de entrada de la primera placa frente al fusible (si está en el camino encadenado)", + x.I_in_first, x.k_fuse * x.I_fuse, "<=", "A", dec=2, req="DCB-05 · DCB-06", deps="ME-I-03", blando=True, + expr="I_in_first ≤ k_fuse·I_fuse") + s.chk("POT-12", "Tensión nominal del fusible frente a V_cont", x.V_fuse, x.V_cont, ">=", "V", dec=0, + req="DCB-02 · DCB-06", deps="HW-02", expr="V_fuse ≥ V_cont", nota="El 0ADKC9150-BE es de 500 VDC") + s.chk("POT-13", "Tensión nominal del fusible frente a la tensión de fallo", x.V_fuse, x.V_fault_max, ">=", "V", dec=0, + req="DCB-08", deps="HW-02, HW-03", expr="V_fuse ≥ V_fault_max") diff --git a/H12/LPU/calculos/motor/puerta.py b/H12/LPU/calculos/motor/puerta.py new file mode 100644 index 0000000..8bac67a --- /dev/null +++ b/H12/LPU/calculos/motor/puerta.py @@ -0,0 +1,83 @@ +"""Gate driving: potencia de puerta, fuentes aisladas, dv/dt, Miller, tiempo muerto y DESAT.""" + + +def calcula(ctx): + x, m = ctx.x, ctx.mos_sel + s = ctx.seccion(8, "Gate driving", + f"Driver ADuM4146 y fuente aislada MGJ2 (datos en `componentes.md`). MOSFET seleccionado: **{m['ref']}**.") + s.sub("8.1 Tensiones de puerta frente al MOSFET") + s.chk("GDR-01", "Vgs_on dentro del rango recomendado del MOSFET", x.Vgs_on, m["VGSrec_on"], "<=", "V", dec=0, + req="GD-01", deps="HW-02", blando=True, expr="Vgs_on ≤ VGSrec_on", + nota="El TFG usa +20 V") + s.chk("GDR-02", "Vgs_on por debajo del máximo absoluto", x.Vgs_on, m["VGSmax"], "<=", "V", dec=0, + req="GD-01", deps="HW-02", expr="Vgs_on ≤ VGSmax") + s.chk("GDR-03", "Vgs_off dentro del rango recomendado", x.Vgs_off, m["VGSrec_off"], ">=", "V", dec=0, + req="GD-01", deps="HW-02", blando=True, expr="Vgs_off ≥ VGSrec_off") + + s.sub("8.2 Potencia de puerta y fuentes aisladas (por MOSFET)") + s.res("dVg", "Excursión de tensión de puerta", x.Vgs_on - x.Vgs_off, "V", dec=1, expr="Vgs_on − Vgs_off") + s.res("P_gate", "Potencia de puerta", m["Qg"] * x.dVg * x.fsw, "W", dec=3, expr="Qg·dVg·fsw", + nota="Qg medida con la excursión de la hoja de datos: aproximación") + s.res("P_iso_out", "Potencia en la salida de la fuente aislada", x.P_gate + x.D_IDD2 * x.dVg, "W", dec=3, + expr="P_gate + D_IDD2·dVg") + s.chk("GDR-04", "Carga de la fuente aislada frente a su potencia nominal", x.P_iso_out, x.G_P, "<=", "W", dec=3, + req="GD-01 · AUX-02", deps="HW-02", expr="P_iso_out ≤ G_P") + i_med = m["Qg"] * x.fsw + x.D_IDD2 + s.chk("GDR-05", "Corriente media de la salida +20 V", i_med, x.G_Ipos, "<=", "mA", 1e-3, 1, + req="GD-01", deps="HW-02", expr="Qg·fsw + D_IDD2 ≤ G_Ipos") + s.chk("GDR-06", "Corriente media de la salida −5 V", i_med, x.G_Ineg, "<=", "mA", 1e-3, 1, + req="GD-01", deps="HW-02", expr="Qg·fsw + D_IDD2 ≤ G_Ineg", + nota="Supone que toda la corriente de VDD2 vuelve por −5 V (conservador)") + s.res("P_iso_in", "Potencia de entrada de cada fuente aislada", x.P_iso_out / x.G_eta, "W", dec=3, + expr="P_iso_out/G_eta") + s.res("I_iso_in", "Corriente de entrada de cada fuente aislada", x.P_iso_in / x.G_Vin, "mA", 1e-3, 1, + expr="P_iso_in/G_Vin", nota="Se usa en el consumo de BT") + + s.sub("8.3 Corrientes de pico, dv/dt y Miller") + s.chk("GDR-07", "Corriente de pico de encendido", x.dVg / (x.Rg_on + m["Rgint"] + x.D_Rp), x.D_Isrc, "<=", "A", dec=2, + req="GD-01", deps="HW-02", expr="dVg/(Rg_on + Rgint + D_Rp) ≤ D_Isrc", + nota="Límite: corriente de cortocircuito del driver") + s.chk("GDR-08", "Corriente de pico de apagado", x.dVg / (x.Rg_off + m["Rgint"] + x.D_Rn), x.D_Isnk, "<=", "A", dec=2, + req="GD-01", deps="HW-02", expr="dVg/(Rg_off + Rgint + D_Rn) ≤ D_Isnk") + s.res("tau_on", "Constante de tiempo de encendido", (x.Rg_on + m["Rgint"]) * m["Ciss"], "ns", 1e-9, 1, + expr="(Rg_on + Rgint)·Ciss") + s.res("dvdt", "dv/dt estimada", x.Vbus_max / m["tr"], "kV/µs", 1e9, 1, expr="Vbus_max/tr", + nota="tr de la hoja de datos con su RG de ensayo: recalcular con la RG real") + for cid, lim, nombre, req, deps in (("GDR-09", x.D_CMTI, "del driver", "GD-01", "HW-02"), + ("GDR-10", x.S_CMTI, "de los sensores aislados", "SNS-01", "HW-02, FW-01"), + ("GDR-11", x.G_CMTI, "de la fuente aislada", "AUX-02", "HW-02")): + s.chk(cid, f"dv/dt frente a la CMTI {nombre}", x.dvdt, lim, "<=", "kV/µs", 1e9, 1, req=req, deps=deps, + expr="dvdt ≤ CMTI") + s.res("I_miller", "Corriente de Miller", m["Crss"] * x.dvdt, "A", dec=3, expr="Crss·dvdt", + nota="Crss a 600 V; a VDS baja es mayor") + s.chk("GDR-12", "Pico de VGS en OFF por efecto Miller", x.Vgs_off + x.I_miller * (x.D_Rclamp + m["Rgint"]), + m["Vth_min"], "<=", "V", dec=2, req="GD-01", deps="HW-02", + expr="Vgs_off + I_miller·(D_Rclamp + Rgint) ≤ Vth_min", nota="Con el Miller clamp activo") + + s.sub("8.4 Tiempo muerto") + s.res("t_dead_min", "Tiempo muerto mínimo", (m["tdoff"] + m["tf"] - m["tdon"]) + x.D_skew + x.t_dead_mrg, "ns", + 1e-9, 0, expr="(tdoff + tf − tdon) + D_skew + t_dead_mrg", nota="Tiempos del MOSFET con su RG de ensayo") + s.chk("GDR-13", "Tiempo muerto configurado frente al mínimo", x.t_dead, x.t_dead_min, ">=", "ns", 1e-9, 0, + req="PWR-04", deps="FW-08, HW-02", blando=True, expr="t_dead ≥ t_dead_min") + s.res("dV_dead", "Error de tensión media por el tiempo muerto", 2 * x.t_dead * x.fsw * x.Vbus_max, "V", dec=2, + expr="2·t_dead·fsw·Vbus_max") + s.res("dV_dead_rel", "Error relativo a la tensión de bobina a Ipk", x.dV_dead / x.V_coil_pk, "%", 0.01, 1, + expr="dV_dead/V_coil_pk", nota="Lo compensa el lazo de corriente") + + s.sub("8.5 Protección DESAT", + "Umbral en VDS ≈ V_DESAT − N·Vf − I_carga·R_serie. La corriente de disparo se estima con RDS(on) lineal: en la zona de saturación " + "la VDS crece más deprisa y el disparo real llega antes. Con pocos mΩ, un umbral de 9,2 V equivale a cientos de amperios: " + "valorar el grado A/C (3,5 V) o un divisor.") + s.res("V_ds_trip", "VDS de disparo", x.D_desat - x.N_d_desat * x.Vf_desat - x.D_Idesat * x.R_desat, "V", dec=2, + expr="D_desat − N_d_desat·Vf_desat − D_Idesat·R_desat") + s.res("I_trip_hot", "Corriente de disparo en caliente", x.V_ds_trip / m["Rhot"], "A", dec=0, expr="V_ds_trip/Rhot") + s.chk("GDR-14", "Corriente de disparo a 25 °C (peor caso) frente a la ID pulsada", x.V_ds_trip / m["R25"], m["IDM"], + "<=", "A", dec=0, req="GD-01 · SAF-04", deps="HW-01, HW-02", blando=True, expr="V_ds_trip/R25 ≤ IDM", + nota="En frío la RDS(on) es menor y el disparo llega más tarde") + s.res("t_blank", "Tiempo de blanking externo", x.C_blank * x.D_desat / x.D_Idesat, "µs", 1e-6, 3, + expr="C_blank·D_desat/D_Idesat") + s.chk("GDR-15", "Tiempo de respuesta DESAT (enmascaramiento + blanking)", x.D_mask + x.t_blank, x.t_desat_max, "<=", + "µs", 1e-6, 2, req="GD-01", deps="HW-01", blando=True, expr="D_mask + t_blank ≤ t_desat_max", + nota="No incluye la propagación interna ni el soft shutdown") + s.chk("GDR-16", "Tensión de VDD2 por encima del UVLO del driver", x.Vgs_on, x.D_UVLO, ">=", "V", dec=1, + req="GD-02", deps="HW-02", expr="Vgs_on ≥ D_UVLO") diff --git a/H12/LPU/calculos/motor/sensado.py b/H12/LPU/calculos/motor/sensado.py new file mode 100644 index 0000000..9909466 --- /dev/null +++ b/H12/LPU/calculos/motor/sensado.py @@ -0,0 +1,91 @@ +"""Sensado: shunt con amplificador aislado o modulador ΔΣ, divisor de Vbus y NTC.""" +import math + +from .unidades import fmt + + +def calcula(ctx): + x = ctx.x + s = ctx.seccion(7, "Sensado y adquisición", + "Opción A1: amplificador aislado (AMC1302) + ADC. Opción A2: modulador ΔΣ aislado (AMC1306) + filtro sinc en la FPGA. " + "La opción está pendiente (FW-01, HW-05). Datos de los sensores en `componentes.md`.") + s.sub("7.1 Corriente de bobina (shunt)") + s.dato("R_shunt", "Resistencia del shunt", "µΩ", 1e-6, 0) + s.res("I_FS", "Fondo de escala de corriente", x.Sd_Vlin / x.R_shunt, "A", dec=1, expr="Sd_Vlin/R_shunt", + nota="Mismo rango lineal en AMC1302 y AMC1306x05") + s.chk("SEN-01", "Fondo de escala frente a Ipk con margen", x.I_FS, x.I_margin * x.Ipk, ">=", "A", dec=1, + req="SNS-01", deps="HW-02", expr="I_FS ≥ I_margin·Ipk") + s.res("I_clip", "Corriente de recorte (AMC1302)", x.Sa_Vclip / x.R_shunt, "A", dec=1, expr="Sa_Vclip/R_shunt") + s.res("V_sh_pk", "Tensión en el shunt a Ipk", x.Ipk * x.R_shunt, "mV", 1e-3, 1, expr="Ipk·R_shunt") + s.chk("SEN-02", "Umbral de sobrecorriente por encima de Ipk", x.I_OC, x.Ipk, ">=", "A", dec=0, req="SAF-04", + deps="HW-01", expr="I_OC ≥ Ipk") + s.chk("SEN-03", "Umbral de sobrecorriente dentro del rango lineal", x.I_OC, x.I_FS, "<=", "A", dec=0, req="SAF-04", + deps="HW-01", expr="I_OC ≤ I_FS") + s.sub("7.2 Opción A1: amplificador aislado + ADC") + s.res("V_out_pk", "Tensión diferencial de salida a Ipk", x.Ipk * x.R_shunt * x.Sa_G, "V", dec=3, expr="Ipk·R_shunt·Sa_G") + s.res("LSB_A1", "Resolución (LSB) con el ADC", 2 * x.I_FS / 2 ** x.N_adc, "mA", 1e-3, 1, expr="2·I_FS/2^N_adc") + s.res("ret_A1", "Retardo de la cadena / semiperiodo de PWM", x.Sa_td * 2 * x.fsw, "%", 0.01, 1, expr="Sa_td·2·fsw") + s.sub("7.3 Opción A2: modulador ΔΣ + filtro sinc") + s.chk_texto("SEN-04", "Frecuencia de reloj dentro del rango del modulador", x.Sd_fmin <= x.f_clk_ds <= x.Sd_fmax, + fmt(x.f_clk_ds / 1e6, 1), f"{fmt(x.Sd_fmin / 1e6, 0)} a {fmt(x.Sd_fmax / 1e6, 0)}", "MHz", + req="SNS-05", deps="FW-01", expr="Sd_fmin ≤ f_clk_ds ≤ Sd_fmax") + s.res("f_data_ds", "Frecuencia de datos a la salida del filtro", x.f_clk_ds / x.OSR, "kHz", 1e3, 2, expr="f_clk_ds/OSR") + s.res("n_pwm_ds", "Muestras por periodo de PWM", x.f_clk_ds / x.OSR / x.fsw, "–", dec=2, expr="f_clk_ds/(OSR·fsw)") + s.res("bits_sinc", "Longitud de palabra a la salida del sinc", x.N_sinc * math.log2(x.OSR) + 1, "bit", dec=1, + expr="N_sinc·log2(OSR) + 1") + s.res("t_grupo_ds", "Retardo de grupo del filtro", x.N_sinc * (x.OSR - 1) / (2 * x.f_clk_ds), "µs", 1e-6, 2, + expr="N_sinc·(OSR − 1)/(2·f_clk_ds)") + s.chk("SEN-05", "Tiempo de establecimiento del filtro frente al semiperiodo de PWM", x.N_sinc * x.OSR / x.f_clk_ds, + 1 / (2 * x.fsw), "<=", "µs", 1e-6, 1, req="SNS-05 · SNS-06", deps="FW-01", blando=True, + expr="N_sinc·OSR/f_clk_ds ≤ 1/(2·fsw)", nota="Una muestra independiente por semiperiodo (muestreo sincronizado)") + s.res("LSB_A2", "Resolución efectiva (con el ENOB de la hoja de datos)", 2 * x.I_FS / 2 ** x.Sd_ENOB, "mA", 1e-3, 1, + expr="2·I_FS/2^Sd_ENOB", nota="ENOB dado con OSR = 256; recalcular con el OSR elegido") + + s.sub("7.4 Tensión de bus (divisor + sensor aislado)") + s.res("R_bot_eff", "Resistencia inferior efectiva (con la entrada del sensor)", + x.R_div_bot * x.R_sens_in / (x.R_div_bot + x.R_sens_in), "Ω", dec=1, expr="R_div_bot ∥ R_sens_in") + s.res("R_top", "Resistencia superior total", x.R_bot_eff * (x.V_meas_FS / x.V_sens_FS - 1), "kΩ", 1e3, 1, + expr="R_bot_eff·(V_meas_FS/V_sens_FS − 1)") + s.res("N_res", "Resistencias en serie necesarias en la rama superior", + float(math.ceil(max(x.V_cont, x.V_fault_max) / x.V_res_rat)), "–", dec=0, + expr="⌈max(V_cont, V_fault_max)/V_res_rat⌉") + s.res("R_each", "Valor de cada resistencia superior", x.R_top / x.N_res, "kΩ", 1e3, 1, expr="R_top/N_res") + i_div = x.V_cont / (x.R_top + x.R_bot_eff) + s.res("I_div", "Corriente del divisor a V_cont", i_div, "µA", 1e-6, 0, expr="V_cont/(R_top + R_bot_eff)") + s.chk("SEN-06", "Potencia en cada resistencia superior a V_cont", i_div ** 2 * x.R_top / x.N_res, + x.k_res * x.P_res_rat, "<=", "mW", 1e-3, 1, req="SNS-02", deps="HW-02", expr="I_div²·R_top/N_res ≤ k_res·P_res_rat") + s.chk("SEN-07", "Tensión en cada resistencia superior en el caso de fallo", + x.V_fault_max * x.R_top / (x.R_top + x.R_bot_eff) / x.N_res, x.V_res_rat, "<=", "V", dec=0, + req="DCB-02 · SNS-02", deps="HW-02, HW-03", expr="V_fault_max·R_top/(R_top + R_bot_eff)/N_res ≤ V_res_rat") + s.chk("SEN-08", "Fondo de escala de Vbus frente a la tensión de fallo", x.V_meas_FS, x.V_fault_max, ">=", "V", dec=0, + req="SNS-02 · DCB-08", deps="HW-03", expr="V_meas_FS ≥ V_fault_max") + s.res("V_sens_Vmax", "Tensión en el sensor a Vbus_max", x.Vbus_max * x.R_bot_eff / (x.R_top + x.R_bot_eff), "mV", + 1e-3, 1, expr="Vbus_max·R_bot_eff/(R_top + R_bot_eff)") + s.res("LSB_Vbus", "Resolución efectiva de Vbus", 2 * x.V_meas_FS / 2 ** x.Sd_ENOB, "V", dec=3, + expr="2·V_meas_FS/2^Sd_ENOB", nota="Rango bipolar del sensor con bus unipolar: se usa la mitad de los códigos") + s.chk("SEN-09", "Umbral de sobretensión por encima de Vbus_max", x.V_OV, x.Vbus_max, ">=", "V", dec=0, + req="SAF-01", deps="HW-01", expr="V_OV ≥ Vbus_max") + s.chk("SEN-10", "Umbral de sobretensión por debajo de V_cont", x.V_OV, x.V_cont, "<=", "V", dec=0, + req="SAF-01 · DCB-02", deps="HW-01", expr="V_OV ≤ V_cont") + s.res("V_sens_OV", "Tensión en el sensor en el umbral de sobretensión", x.V_OV * x.R_bot_eff / (x.R_top + x.R_bot_eff), + "mV", 1e-3, 1, expr="V_OV·R_bot_eff/(R_top + R_bot_eff)") + + s.sub("7.5 NTC de bobina (modelo β)", + "R(T) = R25·exp(β·(1/T − 1/298,15)); divisor con la NTC abajo: V = Vref·R/(R + R_pol). El modelo β pierde exactitud lejos de " + "25–85 °C: para el umbral definitivo, use la tabla R-T del fabricante o Steinhart-Hart.") + R = lambda T: x.NTC_R25 * math.exp(x.NTC_beta * (1 / (T + 273.15) - 1 / 298.15)) + V = lambda r: x.V_ntc_ref * r / (r + x.R_ntc_pu) + dV = lambda T, r: x.V_ntc_ref * x.R_ntc_pu * (-x.NTC_beta * r / (T + 273.15) ** 2) / (r + x.R_ntc_pu) ** 2 + rmax = R(x.T_coil_max) + s.res("R_ntc_max", "R de la NTC a T_coil_max", rmax, "Ω", dec=0, expr="R25·exp(β(1/T − 1/T25))") + s.res("V_ntc_max", "Tensión del divisor a T_coil_max (umbral del comparador)", V(rmax), "V", dec=3, + expr="V_ntc_ref·R/(R + R_ntc_pu)") + s.res("S_ntc_max", "Sensibilidad a T_coil_max", dV(x.T_coil_max, rmax), "mV/°C", 1e-3, 2, expr="dV/dT") + s.res("P_ntc_25", "Autocalentamiento: potencia en la NTC a 25 °C", + x.V_ntc_ref ** 2 * x.NTC_R25 / (x.NTC_R25 + x.R_ntc_pu) ** 2, "mW", 1e-3, 2, expr="Vref²·R25/(R25 + R_pol)²", + nota="Comparar con la constante de disipación de la NTC") + filas = [] + for T in range(-20, 151, 10): + r = R(T) + filas.append([str(T), fmt(r / 1e3, 3), fmt(V(r), 3), fmt(dV(T, r) * 1e3, 2)]) + s.tabla(["T (°C)", "R_NTC (kΩ)", "V_NTC (V)", "dV/dT (mV/°C)"], filas, intro="Curva de la NTC con la polarización elegida.") diff --git a/H12/LPU/calculos/motor/si.py b/H12/LPU/calculos/motor/si.py new file mode 100644 index 0000000..1e6bb37 --- /dev/null +++ b/H12/LPU/calculos/motor/si.py @@ -0,0 +1,96 @@ +"""Integridad de señal: impedancia, retardo, longitud crítica y presupuesto de tiempos.""" +import math + +from .unidades import fmt + +PS_MM = 3.336 # ps/mm en el vacío + + +def z0(capa, w, t, er, h, b): + """Impedancia de una línea (IPC-2141A). Longitudes en mm.""" + if capa == "MS": + return 87 / math.sqrt(er + 1.41) * math.log(5.98 * h / (0.8 * w + t)) + return 60 / math.sqrt(er) * math.log(4 * b / (0.67 * math.pi * (0.8 * w + t))) + + +def tpd(capa, er): + return PS_MM * math.sqrt(0.475 * er + 0.67) if capa == "MS" else PS_MM * math.sqrt(er) + + +def calcula(ctx): + x = ctx.x + er, h, b = x.er_pcb, x.h_ms * 1e3, x.b_sl * 1e3 + t_ext, t_int = x.oz_ext * 0.035, x.oz_int * 0.035 + s = ctx.seccion(6, "Integridad de señal", + "Impedancia con IPC-2141A (microstrip y stripline centrada) y National AN-905 (pares diferenciales); retardo de propagación y " + "longitud crítica con la regla tr/6. Aproximaciones de ±10 %: el valor final lo da el calculador del fabricante (PT-01).") + s.sub("6.1 Stackup", + f"εr = {fmt(er, 2)}; h (microstrip) = {fmt(h, 3)} mm; b (stripline) = {fmt(b, 3)} mm; " + f"cobre externo {fmt(t_ext, 3)} mm; interno {fmt(t_int, 3)} mm.") + s.res("w_50_ms", "Ancho para 50 Ω en microstrip", (5.98 * h / math.exp(50 * math.sqrt(er + 1.41) / 87) - t_ext) / 0.8 * 1e-3, + "mm", 1e-3, 3, expr="IPC-2141A despejada") + s.res("w_50_sl", "Ancho para 50 Ω en stripline", + (4 * b / (0.67 * math.pi * math.exp(50 * math.sqrt(er) / 60)) - t_int) / 0.8 * 1e-3, "mm", 1e-3, 3, + expr="IPC-2141A despejada", nota="Con 2 oz en capas internas el ancho queda muy fino: consultar al fabricante") + filas, reg, calc = [], {}, {} + for f in ctx.ent.tablas.get("interfaces", []): + o, cl = f["_origen"], f["Clave"] + tipo, capa = f["Tipo"], f["Capa"] + if tipo not in ("SE", "Dif") or capa not in ("MS", "SL"): + raise ValueError(f"{o}: Tipo debe ser SE o Dif y Capa MS o SL") + zt = ctx.resuelve(f["Z objetivo"], 1.0, o) + w = ctx.resuelve(f["Ancho"], 1.0, o) * 1e3 + tr = ctx.resuelve(f["tr"], 1.0, o) + L = ctx.resuelve(f["Longitud"], 1.0, o) * 1e3 + t = t_ext if capa == "MS" else t_int + z = z0(capa, w, t, er, h, b) + zd = None + if tipo == "Dif": + sp = ctx.resuelve(f["Separación"], 1.0, o) * 1e3 + zd = (2 * z * (1 - 0.48 * math.exp(-0.96 * sp / h)) if capa == "MS" + else 2 * z * (1 - 0.347 * math.exp(-2.9 * sp / b))) + zc = zd if tipo == "Dif" else z + err = (zc - zt) / zt + if capa == "MS": + valida = 0.1 <= w / h <= 2 + else: + valida = w / (b - t) < 0.35 and t / b < 0.25 + tp = tpd(capa, er) + lc = tr * 1e12 / (6 * tp) + linea = L > lc or tipo == "Dif" + filas.append([f["Interfaz"], f"{tipo}/{capa}", fmt(zt, 0), fmt(w, 3), fmt(z, 1), fmt(zd, 1) if zd else "—", + fmt(err * 100, 1), "Sí" if valida else "No", fmt(tp, 2), fmt(lc, 0), fmt(L, 0), + "Sí" if linea else "No", f.get("Estado") or ""]) + reg[f"si.{cl}.Z"] = (zc, fmt(zc, 1), "Ω", f"Impedancia: {f['Interfaz']}") + reg[f"si.{cl}.Lcrit"] = (lc, fmt(lc, 0), "mm", f"Longitud crítica: {f['Interfaz']}") + calc[cl] = {"zc": zc, "zt": zt, "tpd": tp, "td": L * tp, "nombre": f["Interfaz"], "valida": valida} + s.tabla(["Interfaz", "Tipo/capa", "Z obj. (Ω)", "w (mm)", "Z0 (Ω)", "Zdif (Ω)", "Desv. (%)", "Fórmula válida", + "tpd (ps/mm)", "L crítica (mm)", "L prevista (mm)", "¿Línea de transmisión?", "Estado"], filas, registro=reg, + intro="Interfaces de `diseno.md`, tabla 2. Validez: microstrip 0,1 ≤ w/h ≤ 2; stripline w/(b−t) < 0,35 y t/b < 0,25.") + for cl, c in calc.items(): + s.chk(f"SI-{cl}", f"Impedancia: {c['nombre']}", c["zc"], c["zt"], "<=", "Ω", dec=1, req="COM-03", + deps="PT-01, FW-06", expr="|Z − Z_obj| ≤ tol_Z·Z_obj", + nota=f"Tolerancia ±{fmt(x.tol_Z * 100, 0)} %") + # sustituye el resultado por la comparación con tolerancia + chk = ctx.comprobaciones[-1] + ok = abs(c["zc"] - c["zt"]) <= x.tol_Z * c["zt"] + chk["estado"] = "CUMPLE" if ok else "NO CUMPLE" + chk["texto_limite"] = f"{fmt(c['zt'], 0)} ± {fmt(x.tol_Z * 100, 0)} %" + s.sub("6.2 Presupuesto de tiempos de la interfaz FPGA–PHY", + "Interfaz síncrona con reloj común: holgura de setup = T − (tco_max + t_vuelo,datos + t_su + t_jit + t_desfase,reloj); " + "holgura de hold = tco_min + t_vuelo,datos − (t_h + t_desfase,reloj).") + for nom in ("SI_if_datos", "SI_if_reloj", "SI_if_par"): + if ctx.v[nom] not in calc: + raise ValueError(f"{nom} = «{ctx.v[nom]}» no es una clave de la tabla de interfaces") + s.res("T_bus_clk", "Periodo de reloj", 1 / x.f_bus_clk, "ns", 1e-9, 2, expr="1/f_bus_clk") + s.res("t_fl_data", "Tiempo de vuelo de los datos", calc[x.SI_if_datos]["td"] * 1e-12, "ns", 1e-9, 3, + expr="L·tpd de SI_if_datos") + s.res("t_skew_clk", "Desfase del reloj por diferencia de longitud", x.dL_clk * 1e3 * calc[x.SI_if_reloj]["tpd"] * 1e-12, + "ns", 1e-9, 3, expr="dL_clk·tpd de SI_if_reloj") + s.chk("SI-SETUP", "Holgura de setup", x.T_bus_clk - (x.tco_max + x.t_fl_data + x.t_su + x.t_jit + x.t_skew_clk), 0.0, + ">=", "ns", 1e-9, 2, req="COM-02", deps="FW-06", expr="T − (tco_max + t_fl_data + t_su + t_jit + t_skew_clk) ≥ 0") + s.chk("SI-HOLD", "Holgura de hold", x.tco_min + x.t_fl_data - (x.t_h + x.t_skew_clk), 0.0, ">=", "ns", 1e-9, 2, + req="COM-02", deps="FW-06", expr="tco_min + t_fl_data − (t_h + t_skew_clk) ≥ 0") + s.sub("6.3 Igualación de longitudes") + s.res("dL_pair_max", "Diferencia de longitud máxima dentro del par", x.skew_pair * 1e12 / calc[x.SI_if_par]["tpd"] * 1e-3, + "mm", 1e-3, 2, expr="skew_pair/tpd de SI_if_par") diff --git a/H12/LPU/calculos/motor/termico.py b/H12/LPU/calculos/motor/termico.py new file mode 100644 index 0000000..81879d3 --- /dev/null +++ b/H12/LPU/calculos/motor/termico.py @@ -0,0 +1,76 @@ +"""Térmico: cadena de resistencias térmicas, shunt, balance de la placa y refrigerante.""" +import math + + +def cadena(ctx): + x = ctx.x + s = ctx.seccion(3, "Térmico", + "Modelo unidimensional en régimen permanente; no sustituye a una simulación térmica.") + s.sub("3.1 Resistencia térmica cápsula → agua (por MOSFET)", + "Vías en paralelo con el FR-4: R_vía = t_pcb/(k_Cu·A_barril), A_barril = π/4·(d² − (d − 2·t_met)²). " + "Vías sin rellenar (conservador) y sin contar los planos internos. El pad de drenador está a potencial de HV: " + "el material de interfaz debe aislar (ISO-04).") + s.dato("A_pad", "Área del pad de drenador", "mm²", 1e-6, 1) + s.dato("N_via", "Número de vías térmicas", "–", 1, 0) + s.res("A_barrel", "Sección de cobre de una vía", math.pi / 4 * (x.d_via ** 2 - (x.d_via - 2 * x.t_plat) ** 2), + "mm²", 1e-6, 4, expr="π/4·(d_via² − (d_via − 2·t_plat)²)") + s.res("R_via1", "Resistencia de una vía", x.t_pcb / (x.k_Cu * x.A_barrel), "K/W", dec=1, expr="t_pcb/(k_Cu·A_barrel)") + s.res("R_vias", "Resistencia del conjunto de vías", x.R_via1 / x.N_via, "K/W", dec=3, expr="R_via1/N_via") + a_fr4 = x.A_pad - x.N_via * math.pi / 4 * x.d_via ** 2 + s.chk("TER-01", "Área de FR-4 restante bajo el pad", a_fr4, 0.0, ">=", "mm²", 1e-6, 1, req="PWR-06", + deps="HW-02, PT-01", expr="A_pad − N_via·π·d_via²/4 ≥ 0", nota="Si es negativa, las vías no caben en el pad") + s.res("R_fr4", "Resistencia del FR-4 bajo el pad", x.t_pcb / (x.k_FR4 * a_fr4) if a_fr4 > 0 else 1e9, "K/W", dec=1, + expr="t_pcb/(k_FR4·A_FR4)") + s.res("R_pcb", "Resistencia de la PCB (vías ∥ FR-4)", 1 / (1 / x.R_vias + 1 / x.R_fr4), "K/W", dec=3, + expr="1/(1/R_vias + 1/R_fr4)") + s.res("R_solder", "Resistencia de la soldadura", x.t_solder / (x.k_solder * x.A_pad), "K/W", dec=3, + expr="t_solder/(k_solder·A_pad)") + s.res("R_TIM", "Resistencia del material de interfaz", x.t_TIM / (x.k_TIM * x.A_pad * x.k_spread), "K/W", dec=3, + expr="t_TIM/(k_TIM·A_pad·k_spread)") + s.dato("Rth_wb", "Resistencia cara inferior → agua", "K/W", 1, 3) + s.res("Rth_c2w", "Rth cápsula → agua", x.R_solder + x.R_pcb + x.R_TIM + x.Rth_wb, "K/W", dec=3, + expr="R_solder + R_pcb + R_TIM + Rth_wb") + s.chk("TER-02", "Rigidez dieléctrica del material de interfaz", x.V_TIM_bd, x.V_cont, ">=", "V", dec=0, + req="ISO-04 · ISO-06", deps="ME-C-01, HW-04", blando=True, expr="V_TIM_bd ≥ V_cont", + nota="Margen exigido por definir con el tipo de aislamiento (HW-04)") + + s.sub("3.2 Shunt de corriente") + s.res("P_sh_pk", "Potencia en el shunt a Ipk", x.Ipk ** 2 * x.R_shunt, "W", dec=2, expr="Ipk²·R_shunt") + s.res("E_sh_pk", "Energía en el shunt durante el pico", x.P_sh_pk * x.t_pk, "J", dec=1, expr="P_sh_pk·t_pk", + nota="Comparar con la capacidad de pulso del shunt") + s.res("P_sh_c", "Potencia en el shunt en continuo", x.Irms_cont ** 2 * x.R_shunt, "W", dec=3, expr="Irms_cont²·R_shunt") + s.chk("TER-03", "Potencia de pico en el shunt frente a su potencia nominal", x.P_sh_pk, x.P_sh_rat, "<=", "W", dec=2, + req="SNS-01", deps="HW-02, EM-C-01", blando=True, expr="P_sh_pk ≤ P_sh_rat", + nota="Si el pico es corto, usar la curva de sobrecarga del shunt") + + s.sub("3.3 Balance de pérdidas de la placa y refrigerante") + s.res("P_br_pk_m", "Pérdidas del puente a Ipk (MOSFET seleccionado)", x.P_br_pk, "W", dec=0, expr="apartado 2") + s.res("P_br_c_m", "Pérdidas del puente en continuo (MOSFET seleccionado)", x.P_br_c, "W", dec=1, expr="apartado 2") + s.res("P_esr", "Pérdidas por ESR en el DC-link (peor caso)", x.I_cap_rms_wc ** 2 * x.ESR_dc, "W", dec=2, + expr="I_cap_rms_wc²·ESR_dc") + s.res("P_aux", "Consumo de la alimentación auxiliar (acaba en calor)", x.P_BT_total, "W", dec=1, expr="P_BT_total") + s.res("P_board_pk", "Pérdidas totales de la placa a Ipk", x.P_br_pk + x.P_sh_pk + x.P_esr + x.P_BT_total, "W", dec=0, + expr="P_br_pk + P_sh_pk + P_esr + P_BT_total") + s.res("P_board_c", "Pérdidas totales de la placa en continuo", + x.P_br_c + x.P_sh_c + x.P_esr * (x.Irms_cont / x.Ipk) ** 2 + x.P_BT_total, "W", dec=1, + expr="P_br_c + P_sh_c + P_esr·(Irms_cont/Ipk)² + P_BT_total") + s.res("m_dot", "Gasto másico de agua", x.Q_cool * 997, "kg/s", dec=4, expr="Q_cool·997 kg/m³") + s.res("dT_cool", "Incremento de temperatura del agua por placa a Ipk", x.P_board_pk / (x.m_dot * 4180), "K", dec=2, + expr="P_board_pk/(m_dot·4180)", nota="Toda la pérdida al agua (conservador)") + s.res("T_cool_out", "Temperatura del agua en la última placa en serie", x.T_cool + x.N_cool_ser * x.dT_cool, "°C", + dec=1, expr="T_cool + N_cool_ser·dT_cool") + s.res("E_acc", "Energía de todas las placas al acumulador durante el pico", x.N_MD * x.P_board_pk * x.t_pk, "kJ", + 1e3, 1, expr="N_MD·P_board_pk·t_pk", nota="Dato para ME-C (sin el calor de las bobinas)") + ctx.sec_termico = s + + +def union(ctx): + x, s = ctx.x, ctx.sec_termico + s.sub("3.4 Temperatura de unión del MOSFET seleccionado", f"MOSFET seleccionado: **{x.MOSFET_sel}**.") + s.chk("TER-04", "Tj a Ipk (régimen permanente)", x.Tj_pk_sel, x.Tj_lim, "<=", "°C", dec=1, req="PWR-05", + deps="EM-C-01, ME-C-01, ME-C-02, HW-02", expr="Tj_pk ≤ Tj_lim", + nota="Conservador si t_pk es corto frente a las constantes térmicas") + s.chk("TER-05", "Tj en continuo (Irms_cont)", x.Tj_c_sel, x.Tj_lim, "<=", "°C", dec=1, req="PWR-05", + deps="EM-C-01, ME-C-01, ME-C-02", expr="Tj_c ≤ Tj_lim") + s.chk("TER-06", "Coherencia: Tj a Ipk ≤ Tj_assumed usada para la RDS(on)", x.Tj_pk_sel, x.Tj_assumed, "<=", "°C", + dec=1, req="—", blando=True, expr="Tj_pk ≤ Tj_assumed", nota="Si falla, suba Tj_assumed y recalcule") diff --git a/H12/LPU/calculos/motor/unidades.py b/H12/LPU/calculos/motor/unidades.py new file mode 100644 index 0000000..fab16dd --- /dev/null +++ b/H12/LPU/calculos/motor/unidades.py @@ -0,0 +1,77 @@ +"""Conversión de valores y unidades a SI.""" +import math +import re + +MICRO = "µ" # U+00B5 + +# Factor que convierte la unidad indicada a la unidad SI (o a la unidad base indicada). +FACTORES = { + "": 1.0, "–": 1.0, "-": 1.0, "—": 1.0, "V/V": 1.0, "bit": 1.0, "oz": 1.0, + "%": 0.01, + "V": 1.0, "mV": 1e-3, "kV": 1e3, + "A": 1.0, "mA": 1e-3, "µA": 1e-6, + "Ω": 1.0, "mΩ": 1e-3, "µΩ": 1e-6, "kΩ": 1e3, "MΩ": 1e6, + "H": 1.0, "mH": 1e-3, "µH": 1e-6, "nH": 1e-9, + "F": 1.0, "mF": 1e-3, "µF": 1e-6, "nF": 1e-9, "pF": 1e-12, + "C": 1.0, "µC": 1e-6, "nC": 1e-9, + "J": 1.0, "mJ": 1e-3, "µJ": 1e-6, + "W": 1.0, "mW": 1e-3, "kW": 1e3, + "s": 1.0, "ms": 1e-3, "µs": 1e-6, "ns": 1e-9, "ps": 1e-12, + "Hz": 1.0, "kHz": 1e3, "MHz": 1e6, + "m": 1.0, "mm": 1e-3, "µm": 1e-6, "mm²": 1e-6, + "°C": 1.0, "K": 1.0, "K/W": 1.0, "W/(m·K)": 1.0, + "L/min": 1.0 / 60000.0, + "kV/µs": 1e9, "V/s": 1.0, +} + +UNIDADES_TEXTO = {"", "–", "-", "—"} + + +def normaliza_unidad(u): + u = (u or "").strip() + u = u.replace("μ", MICRO).replace("Ω", "Ω") # mu griega y ohmio compatible + if u.startswith("u") and len(u) > 1 and u[1:] in {"A", "F", "H", "J", "m", "s", "Ω", "C"}: + u = MICRO + u[1:] + return u + + +def factor(u): + u = normaliza_unidad(u) + if u not in FACTORES: + raise KeyError(u) + return FACTORES[u] + + +_NUM = re.compile(r"^[+-]?(\d+([.,]\d*)?|[.,]\d+)([eE][+-]?\d+)?$") + + +def parse_numero(texto): + """Devuelve float o None si el texto no es un número.""" + if texto is None: + return None + t = str(texto).strip().replace("−", "-").replace(" ", "").replace(" ", "") + if not t or not _NUM.match(t): + return None + return float(t.replace(",", ".")) + + +def fmt(v, dec=2): + """Formato español: coma decimal; texto tal cual; None como guion.""" + if v is None: + return "—" + if isinstance(v, str): + return v + if isinstance(v, bool): + return "Sí" if v else "No" + if math.isinf(v) or math.isnan(v): + return "—" + if dec is None: + s = f"{v:.4g}" + if "e" in s: + m, e = s.split("e") + s = f"{m}·10^{int(e)}" + else: + s = f"{v:.{dec}f}" + if s.startswith("-") and float(s.replace("·10^", "e").replace("-", "", 1) or 0) == 0: + s = s[1:] + return s.replace(".", ",").replace("-", "−") diff --git a/H12/LPU/calculos/normas/README.md b/H12/LPU/calculos/normas/README.md new file mode 100644 index 0000000..e45129b --- /dev/null +++ b/H12/LPU/calculos/normas/README.md @@ -0,0 +1,9 @@ +# Tablas de normas + +Datos de normas usados por los cálculos, en CSV con punto decimal. + +| Fichero | Contenido | Fuente y nivel de verificación | +|---|---|---| +| `ipc2221b_tabla6-1.csv` | IPC-2221B, tabla 6-1: separación mínima entre conductores (mm). B1 = capas internas; B2 = externas sin recubrir ≤ 3050 m; B3 = externas sin recubrir > 3050 m; B4 = externas con recubrimiento permanente de polímero; A5 = externas con conformal coating sobre el conjunto; A6 = terminales sin recubrir; A7 = terminales con conformal coating. La última fila es el incremento por voltio por encima de 500 V | Reproducción de ProtoExpress (Sierra Circuits); B1 por voltio = 0,0025 mm/V según el ejemplo de Siemens EDA (600 V → 0,50 mm). Fuente secundaria: contrastar con la norma (ya existe IPC-2221C, 2025) | +| `iec60664-1_fuga_pcb.csv` | IEC 60664-1: líneas de fuga en material de circuito impreso, aislamiento funcional/básico (mm) | Verificados en fuentes secundarias: 400 V/PD2 (TI SLUAAR5) y 1000 V (Vishay). El resto, a contrastar con la norma | +| `poe.csv` | Potencia disponible en el equipo alimentado por tipo de PoE | IEEE 802.3af/at/bt; valores no consultados en la norma durante la preparación | diff --git a/H12/LPU/calculos/normas/iec60664-1_fuga_pcb.csv b/H12/LPU/calculos/normas/iec60664-1_fuga_pcb.csv new file mode 100644 index 0000000..2d19367 --- /dev/null +++ b/H12/LPU/calculos/normas/iec60664-1_fuga_pcb.csv @@ -0,0 +1,8 @@ +tension_V,PD1_mm,PD2_mm,verificacion +250,0.56,1.25,sin verificar +320,0.75,1.6,sin verificar +400,1.0,2.0,parcial (PD2: TI SLUAAR5) +500,1.3,2.5,sin verificar +630,1.8,3.2,sin verificar +800,2.4,4.0,sin verificar +1000,3.2,5.0,verificado (Vishay) diff --git a/H12/LPU/calculos/normas/ipc2221b_tabla6-1.csv b/H12/LPU/calculos/normas/ipc2221b_tabla6-1.csv new file mode 100644 index 0000000..db19922 --- /dev/null +++ b/H12/LPU/calculos/normas/ipc2221b_tabla6-1.csv @@ -0,0 +1,11 @@ +desde_V,hasta_V,B1,B2,B3,B4,A5,A6,A7 +0,15,0.05,0.1,0.1,0.05,0.13,0.13,0.13 +16,30,0.05,0.1,0.1,0.05,0.13,0.25,0.13 +31,50,0.1,0.6,0.6,0.13,0.13,0.4,0.13 +51,100,0.1,0.6,1.5,0.13,0.13,0.5,0.13 +101,150,0.2,0.6,3.2,0.4,0.4,0.8,0.4 +151,170,0.2,1.25,6.4,0.4,0.4,0.8,0.4 +171,250,0.2,1.25,6.4,0.4,0.4,0.8,0.4 +251,300,0.2,1.25,12.5,0.4,0.4,0.8,0.8 +301,500,0.25,2.5,12.5,0.8,0.8,1.5,0.8 +por_voltio,>500,0.0025,0.005,0.025,0.00305,0.00305,0.00305,0.00305 diff --git a/H12/LPU/calculos/normas/poe.csv b/H12/LPU/calculos/normas/poe.csv new file mode 100644 index 0000000..7345840 --- /dev/null +++ b/H12/LPU/calculos/normas/poe.csv @@ -0,0 +1,5 @@ +norma,potencia_PD_W +IEEE 802.3af (Tipo 1),12.95 +IEEE 802.3at (Tipo 2),25.5 +IEEE 802.3bt (Tipo 3),51 +IEEE 802.3bt (Tipo 4),71.3 diff --git a/H12/LPU/docs/README.md b/H12/LPU/docs/README.md new file mode 100644 index 0000000..618e564 --- /dev/null +++ b/H12/LPU/docs/README.md @@ -0,0 +1,5 @@ +# Documentación + +- [Requisitos y arquitectura (borrador v0.2)](requisitos/01_requisitos_arquitectura_v0.2.md) +- [Informe de cálculos de diseño](calculos/informe.md) (generado automáticamente) +- [Historial de informes de cambios](calculos/cambios/README.md) (generado automáticamente) diff --git a/H12/LPU/docs/requisitos/01_requisitos_arquitectura_v0.2.md b/H12/LPU/docs/requisitos/01_requisitos_arquitectura_v0.2.md new file mode 100644 index 0000000..028b978 --- /dev/null +++ b/H12/LPU/docs/requisitos/01_requisitos_arquitectura_v0.2.md @@ -0,0 +1,506 @@ +# Magnet Driver Abejorro +**Convertidor DC/DC en puente en H con control local en FPGA** +## Documento de requisitos y arquitectura: borrador v0.2 + +| Campo | Valor | +|---|---| +| Módulo | **Magnet Driver Abejorro**: primera generación con control local en FPGA. Sustituye a la LPU (ver sección 8). | +| Versión | 0.2 (borrador de trabajo) | +| Fecha | 17/09/2026 | +| Estado | Incompleto: contiene requisitos por confirmar y huecos (TBD) | + +### Convenciones + +- **Fuente de cada requisito:** + - **[U]**: lo ha indicado el equipo (16 y 17/09/2026). + - **[TFG]**: se hereda del TFG de L. Navarro (2025/26) y no está revalidado. + - **[C]**: sale de un cálculo de la sección 6. + - **[P]**: es una propuesta de este documento y está pendiente de aprobación. +- **Estado de cada requisito:** + - **OK**: confirmado. + - **AC**: a confirmar. + - **TBD**: valor desconocido. Entre paréntesis se indica la cuestión pendiente de la sección 7 que lo resolverá. +- **Códigos de subsistema** (sección 7): EM-C (Electromagnetics-Control), EM-D (Electromagnetics-Design), ME-C (Mechanics-Cooling), ME-I (Mechanics-Electrical Integration), FW (Firmware), HW (Hardware) y PT (Partners). +- **Siglas de los electroimanes:** HEMS es el electroimán híbrido de levitación (4 unidades). EMS es el electroimán de guiado lateral (6 unidades). + +--- + +## 1. Objeto y alcance + +Este documento define los requisitos y la arquitectura de la PCB del Magnet Driver. El Magnet Driver es un convertidor DC/DC en puente en H que alimenta los electroimanes de levitación y guiado del prototipo Hyperloop y cierra localmente el lazo de corriente en una FPGA. + +**Entra en el alcance** la PCB completa: + +- etapa de potencia; +- gate driving; +- sensado; +- FPGA y circuitos auxiliares, incluidos los ADC externos; +- interfaz de comunicación con la VCU; +- alimentación auxiliar. + +**Queda fuera del alcance:** + +- el firmware de la FPGA, que es responsabilidad de Firmware; +- la lectura de los sensores de entrehierro y el lazo de entrehierro, que hace la VCU; +- la precarga del bus; +- el circuito de refrigeración aguas arriba del waterblock; +- el diseño de las bobinas. + +## 2. Contexto del sistema + +### 2.1 Arquitectura anterior + +``` +VCU ──► LCU (2 MCU "master/slave", Ethernet, CAN, entradas de sensores de entrehierro) + │ 10 × cable plano de 26 pines: PWM L/R, reset, ready/fault, + │ analógicas diferenciales de Vbus, Ishunt y NTC + ▼ + 10 × LPU (etapa de potencia sin inteligencia, TFG L. Navarro) +``` + +### 2.2 Arquitectura nueva + +``` +VCU (maestro de comunicaciones + lazo de entrehierro) + │ bus de campo (EtherCAT preferente): consigna de corriente ▼ / telemetría ▲ + │ lógica cableada de seguridad (redundante) + ├──► Magnet Driver #1 ──► HEMS 1 + ├──► ... + └──► Magnet Driver #10 ──► EMS 6 +``` + +- La LCU desaparece del vehículo [U]. +- La lectura de los sensores de entrehierro, que antes hacía la LCU, pasa a la VCU [U]. +- Hay que confirmar si la LCU tenía otras funciones que reasignar (HW-06). + +## 3. Documentos de referencia + +| Ref. | Documento | +|---|---| +| R1 | L. Navarro Torrejón, *Diseño, modelado e implementación de un convertidor DC/DC en puente en H de 400 V y 55 A pico con MOSFETs de SiC…*, TFG GIEIA, UPV, 2025/26 | +| R2 | Lattice, *MachXO5-NX Family Data Sheet*, FPGA-DS-02102 | +| R3 | Beckhoff, *EtherCAT IP Core for Lattice FPGAs – Hardware Description* (ET1825/26/27) | +| R4 | Reglamento de la competición (TBD: documento y versión) | +| R5 | Dpto. de electromagnetismo, `Inductancia_EMS_H11_datos.mat`: EMS del prototipo H11 (matriz L, 23/06/2026) | +| R6 | Dpto. de electromagnetismo, `Impedancia_EMS_H11.mat` (matriz Z, 23/06/2026) | +| R7 | Dpto. de electromagnetismo, `HEMS_H11_Inductancia.xlsx`: HEMS del prototipo H11 (entrehierro de 6 a 18 mm × corriente de ±55 A) | + +--- + +## 4. Requisitos + +### 4.1 Sistema (SYS) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| SYS-01 | Una única variante de placa para los 10 electroimanes (4 HEMS + 6 EMS) | U | OK | +| SYS-02 | El Magnet Driver ejecuta el lazo de corriente de forma autónoma; la VCU solo envía la consigna de corriente | U | OK | +| SYS-03 | Una sola PCB integra la potencia, el gate driving, el sensado, la FPGA, la comunicación y la alimentación auxiliar | U | OK | +| SYS-04 | Las funciones de la LCU eliminada quedan asignadas a la VCU o al Magnet Driver. La lectura de entrehierro la hace la VCU | U | Entrehierro: OK · resto: TBD (HW-06) | +| SYS-05 | Número de unidades a fabricar (10 + repuestos) | — | TBD (HW-07) | + +### 4.2 Carga: electroimanes (LOAD) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| LOAD-01 | Carga inductiva no lineal (prototipo H11, ver 6.1). L incremental: EMS de 1,92 a 117 mH; HEMS de 6,2 a 11,2 mH (con el flujo del imán de 0,84 a 1,05 Wb). **R = 1,2 Ω medida en la bobina construida**. Habrá un nuevo diseño de bobinas, así que el Magnet Driver debe admitir un rango de L y R | U, R5, R7 | R: OK · HEMS: OK (derivado) · EMS: AC (EM-D-01, EM-D-02) · nuevas bobinas: TBD (EM-D-04) | +| LOAD-02 | Corriente de salida bipolar | U | OK | +| LOAD-03 | Corriente de pico de 60 A durante los primeros segundos de la levitación (el TFG usaba 55 A) | U | OK · duración: TBD (EM-C-01) · mapas a 60 A: TBD (EM-D-03) | +| LOAD-04 | Corriente nominal idealmente 0 A en HEMS (el imán permanente sostiene el peso) | U, TFG | HEMS: OK · EMS: TBD (EM-C-01) | +| LOAD-05 | Corriente eficaz o perfil de misión para el dimensionado térmico (el TFG mencionaba 5 A en régimen permanente) | TFG | TBD (EM-C-01) | +| LOAD-06 | Ancho de banda o tiempo de respuesta del lazo de corriente | — | TBD (EM-C-03) | +| LOAD-07 | Rizado de corriente máximo admisible | — | TBD (EM-C-03) | +| LOAD-08 | Medida de temperatura de bobina mediante una NTC en el electroimán (TFG: GA10K4A1IA) | U, TFG | Función: OK · sensor: AC (HW-02) | + +### 4.3 Bus DC (DCB) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| DCB-01 | Tensión de bus de 320 V mín. a 400 V máx. La batería se va a rehacer | U | OK · revisar con la nueva batería (ME-I-07) | +| DCB-02 | Los componentes y el aislamiento deben soportar **600 V DC en continuo**. No hace falta prever un bus de 650 V (el TFG dimensionó el divisor de Vbus para ese valor) | U | OK | +| DCB-03 | Bus compartido por los 10 Magnet Driver | U | OK | +| DCB-04 | Precarga realizada por otro módulo, externo al Magnet Driver | U | OK | +| DCB-05 | Distribución del bus: encadenado placa a placa con dos pares de terminales (TFG) o en estrella | TFG | TBD (ME-I-03) | +| DCB-06 | Fusible en placa (TFG: 15 A / 500 VDC, 0ADKC9150-BE; no cubre los 600 V de DCB-02) | TFG | AC (ME-I-03, HW-02) | +| DCB-07 | Rizado de tensión en el DC-link ≤ 2 % (8 V) | TFG | AC (HW-02) | +| DCB-08 | Si la rueda libre no es posible (drivers sin alimentación o apagados por un fallo), la corriente de la bobina vuelve al DC-link por los diodos. El DC-link o un circuito de protección debe absorber esa energía sin superar la tensión nominal de ningún componente (ver 6.3) | C, P | TBD (HW-03) | + +### 4.4 Etapa de potencia (PWR) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| PWR-01 | Topología de puente en H completo | U | OK | +| PWR-02 | MOSFET de SiC de 900 V (TFG: SCT012H90G3AG, 12 mΩ). La placa del TFG no se llegó a ensayar y se estudiarán alternativas | TFG, U | Abierto (HW-02) | +| PWR-03 | Frecuencia de conmutación de 20 kHz y modulación unipolar (TFG). Con la FPGA ambos parámetros pasan a ser configurables | TFG, P | TBD (FW-08) | +| PWR-04 | Los tiempos muertos se generan en la FPGA y son configurables, en lugar de fijarse con la red RC de 100 ns del TFG | P | AC (FW-08) | +| PWR-05 | Refrigeración líquida mediante waterblock bajo la PCB; el agua se enfría en un acumulador térmico de PCM (fuera del alcance). En esta generación el waterblock puede tener otras dimensiones | U, TFG | OK | +| PWR-06 | Interfaz térmica y mecánica con el nuevo waterblock | U | TBD (ME-C-01) | + +### 4.5 Gate driving (GD) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| GD-01 | Drivers aislados con alimentación bipolar de +20 V / −5 V, protección DESAT (respuesta < 2 µs) y Miller clamp. En el TFG son ADuM4146 con fuentes MGJ2D052005BSC | TFG | AC (HW-02) | +| GD-02 | Los fallos internos del driver (DESAT, UVLO) se detectan por lógica programada (FPGA) y por lógica cableada | U | OK | +| GD-03 | Protección cruzada por hardware que impida el shoot-through aunque falle la FPGA | P | AC (HW-01) | +| GD-04 | Estado por defecto sin PWM o ante un fallo: **low-side en ON y high-side en OFF**, de modo que la corriente circula en rueda libre (como en el TFG) | U, TFG | OK | + +### 4.6 Sensado y adquisición (SNS) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| SNS-01 | Medida de corriente de bobina aislada. El rango debe cubrir ±60 A con margen; el TFG usaba un shunt de 500 µΩ con AMC1302 y fondo de escala de ±100 A | U, TFG | AC (HW-02) | +| SNS-02 | Medida de la tensión de bus aislada | U, TFG | OK | +| SNS-03 | Medida de la temperatura de bobina aislada: la NTC está en el dominio HV | U, TFG | OK | +| SNS-04 | No se requiere medir la temperatura de los MOSFET ni del waterblock | U | OK | +| SNS-05 | Conversión con ADC externos en la propia placa. Resolución, frecuencia de muestreo y latencia por definir | U | TBD (FW-01) | +| SNS-06 | Muestreo de corriente sincronizado con la PWM | P | AC (FW-01) | + +### 4.7 Control y FPGA (CTL) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| CTL-01 | FPGA Lattice MachXO5-NX (probable, sin elección definitiva). Densidad y encapsulado por definir | U | AC (FW-02) | +| CTL-02 | La FPGA implementa el lazo de corriente, la PWM, los tiempos muertos, las protecciones programadas y la pila de comunicación (si se usa IP core) | U, P | AC (FW-02, FW-06) | +| CTL-03 | Memoria de configuración. La MachXO5-NX lleva flash interna; ¿hace falta una flash externa? | — | TBD (FW-03) | +| CTL-04 | Conector JTAG accesible y actualización en campo por el bus (p. ej. FoE en EtherCAT) | P | TBD (FW-04) | +| CTL-05 | Comportamiento ante pérdida de comunicación: timeout y rampa a 0 A, o mantener la consigna | — | TBD (FW-05) | +| CTL-06 | Identificación de la posición del módulo (HEMS 1–4 / EMS 1–6) con una placa idéntica para todos | P | TBD (ME-I-06) | +| CTL-07 | El lazo de corriente debe ser estable y cumplir sus prestaciones con una inductancia de 1,92 a 117 mH (en el EMS varía unas 60 veces; ver 6.1). Para ello puede recibir el entrehierro por el bus o estimar L en línea. La arquitectura de control está pendiente | C, P, U | TBD (EM-C-02) | + +### 4.8 Comunicación con la VCU (COM) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| COM-01 | EtherCAT como bus preferente, aún sin decisión definitiva | U | AC (FW-06) | +| COM-02 | Esclavo EtherCAT implementado con un ESC dedicado o con el IP core de Beckhoff. El IP core (licencia ET1825/26/27) soporta MachXO5-NX y admite MII, RMII o RGMII | P, R3 | TBD (FW-06) | +| COM-03 | Topología en línea: 2 puertos por Magnet Driver (entrada y salida), cada uno con su PHY y sus magnéticos | P | AC (FW-06) | +| COM-04 | Maestro EtherCAT en la VCU (plataforma por definir) y tiempo de ciclo por definir | — | TBD (FW-06) | +| COM-05 | Datos intercambiados: consigna, habilitación, modo, límites y parámetros hacia el Magnet Driver; corriente, tensión, temperaturas y fallos hacia la VCU | P | TBD (FW-07) | +| COM-06 | EEPROM SII del esclavo, física o emulada | R3 | TBD (FW-06) | + +### 4.9 Alimentación auxiliar (AUX) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| AUX-01 | Alimentación de baja tensión desde la batería de BT (**24 V máximo**), o por PoE o una solución similar | U | 24 V máx.: OK · método: TBD (ME-I-02) | +| AUX-02 | Raíles necesarios: núcleo e I/O de la FPGA, PHY, cuatro fuentes aisladas de +20 V / −5 V para los drivers y una fuente aislada para el sensado en HV | P | AC (HW-05) | +| AUX-03 | Tensión mínima de entrada BT y consumo máximo permitido | — | TBD (ME-I-02) | + +### 4.10 Protecciones y seguridad (SAF) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| SAF-01 | Fallos detectados: fallos internos del gate driver, temperatura de bobina y tensión de bus | U | OK | +| SAF-02 | Cada fallo se detecta por lógica programada (FPGA) y por lógica cableada independiente | U | OK | +| SAF-03 | El Magnet Driver recibe órdenes de forma redundante: por EtherCAT (o el protocolo elegido) y por **señales cableadas de enable y fault** | U | OK · señales, niveles y conectores: TBD (ME-I-01) | +| SAF-04 | Varias protecciones analógicas o de lógica cableada en la propia placa, además de DESAT (por ejemplo, sobrecorriente de bobina) | U | OK · cuáles y umbrales: TBD (HW-01) | +| SAF-05 | Estado seguro: **rueda libre por los low-side**. La energía de la bobina se disipa en su propia resistencia (ver 6.3) | U | OK | +| SAF-06 | Tiempo de respuesta de cada protección | — | TBD (HW-01) | +| SAF-07 | La rueda libre debe mantenerse aunque se pierda la alimentación de BT o falle un driver; si no, hay que prever el camino alternativo de DCB-08 | C, P | TBD (HW-03) | +| SAF-08 | Las protecciones de hardware se pueden desactivar fácilmente (jumpers o similar) | U | OK · método: TBD (HW-01) | +| SAF-09 | La FPGA lee el estado de esos jumpers y lo reporta por el bus, para que no se opere con protecciones desactivadas sin saberlo | P | AC (HW-01) | + +### 4.11 Aislamiento (ISO) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| ISO-01 | Aislamiento galvánico entre HV y LV | U | OK | +| ISO-02 | Separación ≥ 4 mm con coating entre HV y LV (regla de la competición, vigente) | U, TFG | OK | +| ISO-03 | ≥ 2 mm entre distintas redes de HV | TFG | AC (HW-04) | +| ISO-04 | ≥ 4 mm entre los taladros de fijación del waterblock y cualquier conductor | TFG | AC (ME-C-01) | +| ISO-05 | Tensión de aislamiento y tipo de aislamiento (básico o reforzado) | — | TBD (HW-04) | +| ISO-06 | Distancias de aislamiento dimensionadas para 600 V DC en continuo (DCB-02) | U, C | AC (HW-04) | + +### 4.12 Mecánica, fabricación y entorno (MEC) + +| ID | Requisito | Fuente | Estado | +|---|---|---|---| +| MEC-01 | Tamaño de la placa por acordar: el waterblock puede cambiar de dimensiones en esta generación (el TFG estaba limitado a 190 × 100 mm) | U, TFG | TBD (ME-C-01) | +| MEC-02 | Altura máxima de componentes de 40 mm | TFG | AC (ME-C-01) | +| MEC-03 | PCB de 6 capas simétrica con cobre de 2 oz (el TFG usaba el stackup de NCAB); fabricante por definir | TFG, U | AC (PT-01) | +| MEC-04 | Herramienta EDA: **KiCad** (el TFG usaba Altium Designer) | U | OK | +| MEC-05 | Operación a **temperatura y presión atmosféricas**, no en vacío. A diferencia de lo que suponía el TFG, hay convección | U | OK | +| MEC-06 | Valores numéricos del rango de temperatura ambiente (el TFG asumía ≥ 25 °C por competir en verano) | TFG | TBD (ME-C-02) | +| MEC-07 | Conectores de potencia, de bus de campo, de lógica cableada, de NTC y de alimentación BT | — | TBD (ME-I-04) | +| MEC-08 | Nivel de vibraciones (depende en parte del propio convertidor) | U | TBD (ME-I-05) | +| MEC-09 | Fabricación y montaje: fabricante de PCB y capacidad de montaje de BGA | U | TBD (PT-01, PT-02) | + +--- + +## 5. Arquitectura de la PCB + +### 5.1 Diagrama de bloques (opción A, propuesta) + +``` + HV+ / HV- (bus compartido, 320-400 V) entrada y salida hacia otros Magnet Driver (TBD) + │ + ┌─┴──────────────────────── DOMINIO HV ───────────────────────────────────────┐ + │ [Fusible]─►[DC-link film+cerámicos]─►[Puente H SiC x4]─►[Shunt]─► BOBINA │ + │ │ ▲ │ │ NTC │ + │ [Divisor Vbus] [Gate drivers aislados x4] │ │ │ + │ │ [Fuentes aisladas +20/-5 x4] │ │ │ + │ [Medida aislada Vbus] [Medida aislada Ibob] [Medida aislada Tbob] │ + └══════╪══════════════════════ BARRERA DE AISLAMIENTO ══════╪═══════════╪═════════┘ + ┌──────┴────────────────────── DOMINIO LV ──────────────────┴───────────┴────────┐ + │ [ADC externos / filtros de decimación] │ + │ │ │ + │ [FPGA MachXO5-NX] lazo de corriente · PWM + tiempos muertos · protecciones │ + │ │ │ programadas · ESC EtherCAT (IP core) o bus hacia un ESC │ + │ │ └──►[Lógica cableada de protección]──► inhibición de los drivers │ + │ │ ▲ fallos de driver, Tbob, Vbus, señales de la VCU │ + │ [PHY 1]─[Magnéticos]─► EtherCAT IN [PHY 2]─[Magnéticos]─► EtherCAT OUT │ + │ [EEPROM SII] [Oscilador] [JTAG] [LEDs] [ID de posición] │ + │ [Alimentación auxiliar: entrada BT/PoE ─► raíles LV + fuentes aisladas hacia HV]│ + └────────────────────────────────────────────────────────────────────────────────┘ +``` + +### 5.2 Posición de la barrera de aislamiento: decisión pendiente (HW-05, FW-01) + +| | **Opción A: FPGA en el dominio LV** (propuesta) | **Opción B: FPGA en el dominio HV** | +|---|---|---| +| Qué cruza la barrera | Las 3 medidas analógicas, las 4 señales de puerta y la alimentación | El bus de campo, las señales cableadas de la VCU y la alimentación | +| Sensado | Moduladores ΔΣ aislados (p. ej. familia AMC13xx de TI; la FPGA hace el filtro sinc) o amplificador aislado + ADC | ADC sin aislamiento, cerca del shunt | +| Depuración | JTAG seguro con HV presente | JTAG referido a HV: hace falta un depurador aislado | +| Continuidad con el TFG | Alta: reutiliza su filosofía de aislamiento | Baja | +| Riesgos | Más fuentes aisladas y latencia de los ΔΣ | Aislamiento de EtherCAT y de la lógica de seguridad; riesgo en pruebas | + +### 5.3 Bloques: qué se hereda del TFG y qué es nuevo + +| Bloque | Base en el TFG | Cambio | +|---|---|---| +| Puente en H y DC-link | Sí, sin ensayar | Revisar para 60 A, 600 V y el caso de fallo de 6.3; estudiar alternativas (HW-02) | +| Gate drivers | Sí, sin ensayar | Los tiempos muertos y el reset pasan a la FPGA; el estado por defecto sigue siendo rueda libre por los low-side (SAF-05) | +| Sensado | Sí (salida analógica hacia la LCU) | Ahora se digitaliza en placa | +| Lógica ready/fault | Puertas AND con salida hacia la LCU | Se integra en la lógica cableada de protección, desactivable por jumpers (SAF-08) | +| FPGA y control | No | Nuevo | +| EtherCAT | No | Nuevo | +| Alimentación auxiliar | Parcial: recibía +3V3, +5V y VBAT por el conector | Nueva etapa de entrada BT/PoE | + +--- + +## 6. Comprobaciones preliminares [C] + +Salvo que se indique otra cosa, los cálculos usan R = 1,2 Ω (medida en la bobina construida) e I = 60 A. + +### 6.1 Inductancia de los electroimanes del prototipo H11 (R5, R6, R7) + +En los dos electroimanes, la inductancia es del orden de **mH** y R = 1,2 Ω (medida en la bobina construida [U]). + +#### 6.1.1 EMS de guiado (ficheros MATLAB, sin vectores de ejes) + +- **L:** matriz de 111 × 71, con valores entre **1,92 y 117 mH** (en henrios dentro del fichero). +- **Z:** matriz de 105 × 67. Es exactamente |R + j·2π·2000·L| sobre la submatriz interior de L, con R = 0,503 Ω. Es decir, es el módulo de la impedancia a 2 kHz calculado con la R del modelo, no con la de la bobina construida. +- **Ejes (hipótesis reforzada):** + - **Filas = corriente de −55 a +55 A en pasos de 1 A.** Es el mismo eje de 111 puntos que tiene el Excel de los HEMS, y las filas son exactamente simétricas, como corresponde a un EMS sin imán. + - **Columnas = entrehierro creciente.** A corriente cero, L cae como 1/g, y la corriente a la que empieza la saturación crece con el entrehierro. + - Los ficheros MATLAB guardan los ejes al revés que el Excel. +- **Definición de L:** solo la lectura como inductancia **incremental** (dψ/di) da un flujo monótono. Esto no coincide con la definición del Excel de los HEMS (6.1.2), así que hay que confirmarlo. +- **Valor citado:** 26,7 mH corresponde a I = 0 entre las columnas 5 y 6. + +#### 6.1.2 HEMS de levitación (Excel, con ejes) + +- **Ejes:** filas = entrehierro de 6 a 18 mm en pasos de 1 mm; columnas = corriente de −55 a +55 A en pasos de 1 A [U]. +- **El valor tabulado es ψ/|i| e incluye el flujo del imán permanente.** + - Por eso vale unos 1000 mH cerca de 0 A y se comporta como 1/|i|. + - El valor en 0 A es la media de sus dos vecinos. + - **No debe usarse directamente como inductancia de circuito.** +- **Flujo reconstruido:** ψ = L·|i| es continuo y crece de forma monótona con la corriente. El flujo del imán a 0 A vale entre 0,84 Wb (18 mm) y 1,05 Wb (6 mm). +- **Inductancia incremental derivada:** dψ/di va de **6,2 a 11,2 mH** en todo el mapa. Es menor con corriente positiva, que refuerza al imán y satura antes. + +#### 6.1.3 Consecuencias para el diseño + +| Magnitud | EMS | HEMS | Diseño (peor caso) | +|---|---|---|---| +| L incremental | 1,92 a 117 mH (×61) | 6,2 a 11,2 mH (×1,8) | 1,92 a 117 mH | +| Rizado pico a pico a 20 kHz (unipolar, D = 0,5), a 320 V / 400 V | 1,04 A / 1,30 A | 0,32 A / 0,40 A | 1,30 A | +| di/dt a 320 V / 400 V | de 2,7 a 208 A/ms | de 29 a 65 A/ms | de 2,7 a 208 A/ms | +| Energía devuelta al desmagnetizar desde 55 A | ≈ 15,6 J | ≈ 16,7 J (desde −55 A, 6 mm); ≈ 11 J desde +55 A | ≈ 16,7 J | + +- **El lazo de corriente es mucho más exigente en el EMS**, donde la planta varía unas 60 veces con la corriente y el entrehierro (ver CTL-07 y EM-C-02). En el HEMS apenas varía. +- **Como la placa es la misma para los dos electroimanes** (SYS-01), el control debe cubrir ambos casos: por parámetros según la posición (CTL-06) o con un control que se adapte. +- **Los datos cubren hasta ±55 A.** Para 60 A hay que extrapolar o pedir mapas ampliados. +- **La corriente nula en los HEMS** (LOAD-04) es coherente con el flujo del imán, que sostiene el peso. + +### 6.2 Potencia a 60 A en régimen permanente + +- La tensión en bornes de la bobina es 72 V y disipa 4,3 kW. +- La corriente media del bus es de 13,5 A a 320 V y de 10,8 A a 400 V por Magnet Driver, sin contar pérdidas. Ese valor está muy cerca del fusible de 15 A del TFG, por lo que hay que revisar el fusible con el perfil real (EM-C-01, ME-I-03). +- Con el bus encadenado, la primera placa conduciría la suma de las 10. + +### 6.3 Desmagnetización y estado seguro + +**Funcionamiento normal (estado seguro elegido, SAF-05).** + +- Con los low-side en ON, la bobina queda cortocircuitada y su corriente decae sobre su propia resistencia (1,2 Ω más la de los MOSFET y el shunt, que es despreciable). No se devuelve energía al bus. +- **Constante de tiempo orientativa (L/R):** + - HEMS: de 5 a 9 ms. + - EMS: de 1,6 a 98 ms, según el punto de trabajo. + - La corriente cae prácticamente a cero en unas pocas constantes de tiempo. +- **Observación:** con la bobina cortocircuitada, un cambio de entrehierro induce corriente, sobre todo en los HEMS por el flujo del imán. El efecto es un amortiguamiento (EM-C-04). + +**Caso de fallo: la rueda libre no es posible (DCB-08, SAF-07).** + +Si los drivers se quedan sin alimentación o se apagan por un fallo, los MOSFET de SiC quedan en OFF y la corriente vuelve al DC-link por los diodos. + +- **Con el peor caso de H11 (≈ 16,7 J, HEMS a −55 A) y los 105 µF del TFG:** el DC-link llegaría a ≈ **650 V desde 320 V** y a ≈ **690 V desde 400 V**. + - Queda por debajo de los 900 V de los MOSFET. + - **Supera los 600 V continuos que deben soportar los componentes (DCB-02) y los 500 V de los condensadores de film del TFG.** +- **Para no pasar de 600 V partiendo de 400 V** harían falta ≥ 167 µF. +- **Estimación inicial** (26,7 mH constantes a 60 A, 48 J): ≈ 1040 V. Queda como cota pesimista. +- **Opciones (HW-03):** + - mantener la alimentación de los drivers low-side el tiempo suficiente; + - añadir un clamp o un chopper; + - aumentar el DC-link. + +--- + +## 7. Cuestiones pendientes + +Cada cuestión se asigna al subsistema que debe resolverla. ★ marca las que bloquean la arquitectura. + +| Código | Subsistema | +|---|---| +| EM-C | Electromagnetics – Control | +| EM-D | Electromagnetics – Design | +| ME-C | Mechanics – Cooling | +| ME-I | Mechanics – Electrical Integration | +| FW | Firmware | +| HW | Hardware (este equipo) | +| PT | Partners (fabricación) | + +### 7.1 Electromagnetics – Control (EM-C) + +| ID | Cuestión | Afecta a | +|---|---|---| +| EM-C-01 ★ | Perfil de corriente: duración y forma del pico de 60 A, corriente eficaz en crucero en HEMS y EMS, y corriente nominal del EMS | LOAD-03, LOAD-04, LOAD-05, DCB-06, térmica | +| EM-C-02 ★ | Arquitectura de control (pendiente). ¿El Magnet Driver recibe el entrehierro de la VCU para ajustar las ganancias, estima L en línea o usa un control robusto? | CTL-07, COM-05 | +| EM-C-03 | Ancho de banda del lazo de corriente y rizado máximo admisible | LOAD-06, LOAD-07, PWR-03, SNS-05 | +| EM-C-04 | En rueda libre, un cambio de entrehierro induce corriente en la bobina, sobre todo en los HEMS. ¿Es aceptable o incluso deseable como amortiguamiento? | SAF-05, 6.3 | + +### 7.2 Electromagnetics – Design (EM-D) + +| ID | Cuestión | Afecta a | +|---|---|---| +| EM-D-01 | Ficheros MATLAB del EMS: vectores de ejes (hipótesis: filas = corriente de ±55 A; columnas = entrehierro) y valor del entrehierro en cada columna | LOAD-01, 6.1.1 | +| EM-D-02 | Definición de L. En el EMS parece incremental; en el Excel de los HEMS es ψ/\|i\| e incluye el flujo del imán. ¿Se calcularon de forma distinta? | LOAD-01, CTL-07 | +| EM-D-03 | Mapas de inductancia ampliados a 60 A | LOAD-03, 6.1.3 | +| EM-D-04 | Rango esperado de L y R de las nuevas bobinas | LOAD-01 | + +### 7.3 Mechanics – Cooling (ME-C) + +| ID | Cuestión | Afecta a | +|---|---|---| +| ME-C-01 ★ | Dimensiones e interfaz del nuevo waterblock: superficie de contacto, fijaciones, material térmico y altura máxima de componentes. Estos datos fijan el tamaño de la placa | PWR-06, MEC-01, MEC-02, ISO-04 | +| ME-C-02 | Temperatura y caudal del refrigerante; valores del rango de temperatura ambiente | MEC-06, térmica | + +### 7.4 Mechanics – Electrical Integration (ME-I) + +| ID | Cuestión | Afecta a | +|---|---|---| +| ME-I-01 ★ | Señales cableadas con la VCU: cuáles (enable, fault, lazo de seguridad), con qué niveles y conectores, y si el Magnet Driver solo las recibe o también debe abrir el lazo | SAF-03 | +| ME-I-02 ★ | Alimentación de BT: tensión mínima de la batería de 24 V, consumo permitido por placa y método (mazo de 24 V, PoE o similar). PoE es un enlace punto a punto y no encaja directamente con EtherCAT en línea; EtherCAT P (Beckhoff) es una alternativa | AUX-01, AUX-03 | +| ME-I-03 | Distribución del bus de HV: encadenado placa a placa (TFG) o en estrella; ¿fusible en cada placa? | DCB-05, DCB-06, 6.2 | +| ME-I-04 | Conectores: potencia, bobina, NTC, bus de campo, señales cableadas y BT | MEC-07 | +| ME-I-05 | Especificación de vibraciones | MEC-08 | +| ME-I-06 | Identificación de la posición de cada placa (alias en EEPROM, selector físico o codificación en el mazo), junto con Firmware | CTL-06, sección 8 | +| ME-I-07 | Rango de tensión de la nueva batería de HV | DCB-01 | + +### 7.5 Firmware (FW) + +| ID | Cuestión | Afecta a | +|---|---|---| +| FW-01 ★ | Adquisición. ¿Se acepta la opción A (FPGA en BT) con moduladores ΔΣ aislados como ADC externos, o se prefieren ADC en paralelo o SPI? ¿Qué resolución, frecuencia de muestreo y latencia hacen falta? | 5.2, SNS-05, SNS-06 | +| FW-02 | Elección definitiva de la FPGA: densidad y encapsulado. Según Lattice, la MachXO5-NX se ofrece en encapsulados BGA | CTL-01, PT-02 | +| FW-03 | ¿Hace falta una flash de configuración externa? | CTL-03 | +| FW-04 | Programación y depuración: conector JTAG y actualización por el bus | CTL-04 | +| FW-05 | Comportamiento ante pérdida de comunicación, junto con EM-C | CTL-05 | +| FW-06 ★ | EtherCAT: confirmación del protocolo; ESC dedicado o IP core de Beckhoff (de pago); plataforma del maestro en la VCU; tiempo de ciclo; EEPROM SII | COM-01, COM-02, COM-03, COM-04, COM-06 | +| FW-07 | Datos intercambiados con la VCU (mapa de objetos) | COM-05 | +| FW-08 | Rango de frecuencia de conmutación y tipo de modulación que debe admitir el hardware | PWR-03 | + +### 7.6 Hardware (HW) + +| ID | Cuestión | Afecta a | +|---|---|---| +| HW-01 | Qué protecciones analógicas o de lógica cableada se incluyen, con qué umbrales y tiempos de respuesta, y cómo se desactivan (jumpers) y se reportan | SAF-04, SAF-06, SAF-08, SAF-09 | +| HW-02 | Alternativas a los componentes del TFG, que no se llegó a ensayar: MOSFET, drivers, sensado, fusible y condensadores del DC-link para 600 V | PWR-02, GD-01, SNS-01, DCB-02, DCB-06, DCB-07 | +| HW-03 ★ | Caso de fallo sin rueda libre: mantener alimentados los drivers low-side, añadir un clamp o un chopper, o aumentar el DC-link (≥ 167 µF para no pasar de 600 V desde 400 V) | DCB-08, SAF-07, 6.3 | +| HW-04 | Tensión y tipo de aislamiento, distancias para 600 V, y documento y versión del reglamento (R4) | ISO-05, ISO-06 | +| HW-05 ★ | Posición de la barrera de aislamiento (opción A o B), junto con Firmware | 5.2, AUX-02 | +| HW-06 | ¿Tenía la LCU otras funciones que haya que reasignar, además de la lectura de entrehierro? | SYS-04 | +| HW-07 | Número de unidades a fabricar | SYS-05 | + +### 7.7 Partners (PT) + +| ID | Cuestión | Afecta a | +|---|---|---| +| PT-01 | Fabricante de PCB: capacidades (6 capas, 2 oz, fresados), stackup y aplicación del coating | MEC-03, MEC-09, ISO-02 | +| PT-02 | Montaje en casa o externo, con capacidad para BGA | MEC-09, FW-02 | +| PT-03 | Patrocinio o disponibilidad de componentes (MOSFET de SiC, etc.) y, si se elige el IP core, la licencia de Beckhoff | HW-02, FW-06 | + +### 7.8 Preguntas resueltas (numeración de la versión 0.1) + +| Pregunta | Respuesta | Dónde se recoge | +|---|---|---| +| N1 (en parte) | H11 es la generación del prototipo; R = 1,2 Ω medida en la bobina construida; los ejes del Excel de los HEMS son conocidos | LOAD-01, 6.1 | +| N2 | La VCU lee los sensores de entrehierro | 2.2, SYS-04 | +| N3 | Estado seguro: rueda libre por los low-side | GD-04, SAF-05, 6.3 | +| N6 | El waterblock puede tener otras dimensiones en esta generación | PWR-05, MEC-01 | +| N7 | La batería de BT es de 24 V máximo | AUX-01 | +| N8 (en parte) | Redundancia: señales cableadas de enable y fault más órdenes por el bus | SAF-03 | +| N9 | Varias protecciones analógicas o cableadas en placa, fáciles de desactivar con jumpers | SAF-04, SAF-08 | +| N10 | La separación de 4 mm con coating sigue vigente | ISO-02 | +| N11 | No hace falta prever 650 V, pero sí soportar 600 V en continuo | DCB-02, ISO-06 | +| N15 | No se mide la temperatura de los MOSFET ni del waterblock | SNS-04 | +| N16 (en parte) | La placa del TFG no se ensayó; se discutirán alternativas | PWR-02, HW-02 | +| N17 | Nombre: Magnet Driver Abejorro | Sección 8 | +| N18 | El documento sigue en Markdown | — | +| N19 (en parte) | Herramienta EDA: KiCad; fabricante por definir | MEC-04, PT-01 | +| N20 (en parte) | Temperatura y presión atmosféricas; vibraciones por definir | MEC-05, ME-I-05 | +| N21 | La arquitectura de control sigue pendiente | EM-C-02 | + +Las demás preguntas de la versión 0.1 siguen abiertas con su nuevo código: + +- N4 → HW-05, FW-01 +- N5 → EM-C-01 +- N12 → ME-I-03 +- N13 → FW-06 +- N14 → ME-I-06 + +--- + +## 8. Nomenclatura y control de versiones + +- **Esquema de nombres** [U]: + - Cada generación de hardware se llama "Magnet Driver" seguido de un nombre de insecto. + - Se descartan las siglas. + - Los nombres no tienen que seguir ningún orden. +- **Primera generación con FPGA** [U]: **Magnet Driver Abejorro**. +- **Para qué sirve el nombre** [U]: identifica de un vistazo la compatibilidad de conectores y de firmware. +- **Regla propuesta** [P]: + - Se asigna un **nuevo nombre** cuando se rompe la compatibilidad. Eso ocurre si cambia el tipo o el pinout de algún conector externo, o si cambia la interfaz con el firmware (pinout de la FPGA, periféricos o mapa de entradas y salidas). + - Si se mantiene la compatibilidad, se conserva el nombre y se sube la revisión: "Abejorro, revisión 2". +- **Registro de compatibilidad** [P]: + +| Nombre | Revisión | Conectores | Firmware compatible | Notas | +|---|---|---|---|---| +| Abejorro | 1 | TBD | TBD | Primera generación con FPGA | + +- **Identificación de cada unidad** [P]: nombre más posición, por ejemplo "Abejorro HEMS-1" o "Abejorro EMS-6" (ver ME-I-06). +- **Nombres disponibles** para generaciones futuras: Esfinge, Grillo, Hormiga, Libélula, Mantis, Sírfido, Tijereta. +- **Nombres descartados:** + - Escarabajo: es el nombre del Volkswagen Beetle en español. + - Mariposa: se confunde con la válvula de mariposa. + - Abeja: demasiado parecido a Abejorro. + - Mariquita, Cigarra, Zángano, Chinche, Polilla, Cucaracha, Mosquito y Zapatero: por sus connotaciones. + +--- + +## 9. Historial de versiones + +| Versión | Fecha | Cambios | +|---|---|---| +| 0.1 | 16/09/2026 | Primer borrador: requisitos, arquitectura, análisis de inductancias y nombre del módulo | +| 0.2 | 17/09/2026 | Respuestas a N2, N3, N6 a N11 y N15 a N21; estado seguro en rueda libre; 600 V en continuo; KiCad; operación a presión atmosférica; cuestiones pendientes por subsistema | + diff --git a/H12/LPU/extras/tareas-para-la-raiz.json b/H12/LPU/extras/tareas-para-la-raiz.json new file mode 100644 index 0000000..43123e0 --- /dev/null +++ b/H12/LPU/extras/tareas-para-la-raiz.json @@ -0,0 +1,23 @@ +{ + "_comentario": "Tareas equivalentes a las de H12/LPU/.vscode/tasks.json, pero con las rutas desde la raíz del repositorio. Si abres el repositorio entero en VS Code, copia estas dos tareas dentro del array \"tasks\" de .vscode/tasks.json de la raíz (si ese fichero ya existe, añade solo las tareas, sin duplicar \"version\").", + "version": "2.0.0", + "tasks": [ + { + "label": "LPU · Cálculos: vista previa", + "detail": "Recalcula y deja el informe general y el de cambios en H12/LPU/salida/ (no modifica docs/)", + "type": "shell", + "command": "python H12/LPU/calculos/calcular.py", + "windows": { "command": "py H12\\LPU\\calculos\\calcular.py" }, + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "LPU · Cálculos: pruebas del motor", + "detail": "Comprueba que las fórmulas siguen dando los resultados de referencia", + "type": "shell", + "command": "python -m pytest -q -p no:cacheprovider H12/LPU/tests", + "windows": { "command": "py -m pytest -q -p no:cacheprovider H12\\LPU\\tests" }, + "problemMatcher": [] + } + ] +} diff --git a/H12/LPU/requirements.txt b/H12/LPU/requirements.txt new file mode 100644 index 0000000..64c5c77 --- /dev/null +++ b/H12/LPU/requirements.txt @@ -0,0 +1,2 @@ +pytest +tzdata; sys_platform == "win32" diff --git a/H12/LPU/tests/datos_v0.2/entradas/componentes.md b/H12/LPU/tests/datos_v0.2/entradas/componentes.md new file mode 100644 index 0000000..c380b27 --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/entradas/componentes.md @@ -0,0 +1,106 @@ +# Datos de componentes + +Datos de hoja de datos de los componentes candidatos. Se editan al añadir o cambiar un candidato; las referencias están en «Fuentes» de `calculos/README.md`. + +Las mismas reglas de formato que en `parametros.md`: decimales con coma o punto, sin `|` dentro de las celdas. + +## 1. MOSFET: candidatos + +Una columna por candidato (se pueden añadir columnas «Candidato N»; una columna vacía se ignora). La columna «Clave» la usan las fórmulas y no debe cambiarse. El candidato activo se elige con `MOSFET_sel` en `parametros.md`, por su referencia. + +| Parámetro | Clave | Unidad | Candidato 1 | Candidato 2 | Candidato 3 | Fuente / nota | +|---|---|---|---|---|---|---| +| Fabricante | fab | – | STMicroelectronics | | | | +| Referencia | ref | – | SCT012H90G3AG | | | Candidato del TFG, sin ensayar (PWR-02) | +| Encapsulado | pkg | – | H²PAK-7 (SMD) | | | ST, hoja de datos SCT012H90G3AG | +| VDS máxima | VDS | V | 900 | | | ST, hoja de datos | +| VGS máxima absoluta (positiva) | VGSmax | V | 22 | | | ST, hoja de datos | +| VGS máxima absoluta (negativa) | VGSmin | V | -10 | | | ST, hoja de datos | +| VGS recomendada en ON (máxima) | VGSrec_on | V | 18 | | | ST: rango recomendado −5 a 18 V | +| VGS recomendada en OFF (mínima) | VGSrec_off | V | -5 | | | ST, hoja de datos | +| VGS(th) mínima | Vth_min | V | 1,8 | | | ST (típ. 3,1 V; máx. 4,2 V) | +| ID continua (Tc = 25 °C) | ID | A | 110 | | | ST (110 A también a Tc = 100 °C) | +| ID pulsada | IDM | A | 454 | | | ST, hoja de datos | +| Tj máxima | Tjmax | °C | 175 | | | ST, hoja de datos | +| Rth unión-cápsula | Rthjc | K/W | 0,24 | | | ST, hoja de datos | +| RDS(on) típica a 25 °C | R25 | mΩ | 12 | | | ST: VGS = 18 V, ID = 60 A | +| RDS(on) máxima a 25 °C | R25max | mΩ | 15,8 | | | ST: VGS = 18 V, ID = 60 A | +| RDS(on) típica a temperatura alta | Rhot | mΩ | 18,5 | | | ST: VGS = 18 V, ID = 60 A | +| Temperatura de la RDS(on) anterior | Thot | °C | 175 | | | ST, hoja de datos | +| Tensión del diodo interno | VSD | V | 2,8 | | | ST: ISD = 60 A, VGS = 0 V | +| Carga de recuperación inversa | Qrr | nC | 335 | | | ST: ISD = 60 A | +| Carga total de puerta | Qg | nC | 138 | | | ST: VDD = 600 V, ID = 60 A | +| Ciss | Ciss | pF | 3880 | | | ST: VDS = 600 V, f = 1 MHz | +| Coss | Coss | pF | 244 | | | ST: VDS = 600 V | +| Crss | Crss | pF | 20 | | | ST: VDS = 600 V (a VDS bajas es mayor) | +| Resistencia interna de puerta | Rgint | Ω | 1,8 | | | ST, hoja de datos | +| Energía de encendido | Eon | µJ | 708 | | | ST: condiciones de ensayo abajo | +| Energía de apagado | Eoff | µJ | 889 | | | ST, hoja de datos | +| Tensión de ensayo de Eon/Eoff | Vtest | V | 600 | | | ST, hoja de datos | +| Corriente de ensayo de Eon/Eoff | Itest | A | 60 | | | ST, hoja de datos | +| RG de ensayo de Eon/Eoff y de los tiempos | Rgtest | Ω | 15 | | | ST: VGS = −5 a 18 V | +| td(on) | tdon | ns | 37 | | | ST, hoja de datos | +| tr | tr | ns | 47 | | | ST, hoja de datos | +| td(off) | tdoff | ns | 82 | | | ST, hoja de datos | +| tf | tf | ns | 30 | | | ST, hoja de datos | + +## 2. Gate driver: ADuM4146 + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| D_Isrc | Corriente de cortocircuito de fuente | 11 | A | ADI | OK | GD-01 | | +| D_Isnk | Corriente de cortocircuito de sumidero | 9 | A | ADI | OK | GD-01 | | +| D_Rp | RDS(on) del PMOS de salida (máx.) | 0,975 | Ω | ADI | OK | GD-01 | | +| D_Rn | RDS(on) del NMOS de salida (máx.) | 0,807 | Ω | ADI | OK | GD-01 | | +| D_skew | Desfase de propagación entre dispositivos (máx.) | 25 | ns | ADI | OK | PWR-04 | | +| D_desat | Umbral DESAT (grado B) | 9,2 | V | ADI | OK | GD-01 | Grados A y C: 3,5 V sin fuente de corriente interna | +| D_Idesat | Corriente de carga DESAT (grado B, típ.) | 527 | µA | ADI | OK | GD-01 | | +| D_mask | Tiempo de enmascaramiento DESAT (máx.) | 340 | ns | ADI | OK | GD-01 | 260 a 340 ns | +| D_Rclamp | Resistencia del Miller clamp (máx.) | 2,75 | Ω | ADI | OK | GD-01 | | +| D_CMTI | CMTI | 100 | kV/µs | ADI | OK | GD-01 | | +| D_IDD2 | Corriente de VDD2 (máx.) | 6,5 | mA | ADI | OK | AUX-02 | | +| D_UVLO | UVLO de VDD2, umbral de subida (grados B/C) | 11,5 | V | ADI | AC | GD-02 | Grado A: 14,5 V. Grado del TFG por confirmar | + +## 3. Fuente aislada de puerta: MGJ2D052005BSC + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| G_Vin | Tensión de entrada nominal | 5 | V | Murata | OK | AUX-02 | 4,5 a 5,5 V | +| G_P | Potencia nominal | 2 | W | Murata | OK | GD-01 | | +| G_eta | Rendimiento mínimo a plena carga | 74 | % | Murata | OK | AUX-02 | Típ. 78,5 % | +| G_Ipos | Corriente máxima de la salida +20 V | 80 | mA | Murata | OK | GD-01 | | +| G_Ineg | Corriente máxima de la salida −5 V | 40 | mA | Murata | OK | GD-01 | | +| G_CMTI | CMTI | 200 | kV/µs | Murata | OK | GD-01 | > 200 kV/µs | + +## 4. Sensores aislados: AMC1302 y AMC1306 + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Sa_Vlin | AMC1302: entrada diferencial lineal | 50 | mV | TI | OK | SNS-01 | ±50 mV | +| Sa_Vclip | AMC1302: entrada de recorte | 64 | mV | TI | OK | SNS-01 | ±64 mV | +| Sa_G | AMC1302: ganancia fija | 41 | – | TI | OK | SNS-01 | | +| Sa_BW | AMC1302: ancho de banda (mínimo) | 220 | kHz | TI | OK | SNS-05 | 220 a 280 kHz | +| Sa_td | AMC1302: retardo de señal 50–90 % (máximo) | 3 | µs | TI | OK | SNS-05 | | +| Sd_Vlin | AMC1306x05: entrada diferencial lineal | 50 | mV | TI | OK | SNS-01 | x25: ±250 mV | +| Sd_fmin | AMC1306: frecuencia de reloj mínima | 5 | MHz | TI | OK | FW-01 | | +| Sd_fmax | AMC1306: frecuencia de reloj máxima | 21 | MHz | TI | OK | FW-01 | A 5 V | +| Sd_ENOB | AMC1306: ENOB con sinc3 y OSR = 256 | 14 | bit | TI | AC | FW-01 | ≈ 14 bits; con otro OSR es distinto | +| S_CMTI | CMTI mínimo de los sensores aislados | 100 | kV/µs | TI | OK | SNS-01 | AMC1302: 100 kV/µs mín.; AMC1306: 100 kV/µs típ. | + +## 5. Componentes que cruzan la barrera HV–BT + +Distancias de fuga y tensiones de trabajo del encapsulado. La «Clave» identifica la comprobación en el informe. + +| Clave | Componente | Fuga (mm) | Tensión de trabajo (V) | Fuente / nota | +|---|---|--:|--:|---| +| ADUM4146 | ADuM4146 (gate driver) | 8,3 | 2150 | ADI: 8,3 mm mín.; VIORM 2150 V pico | +| AMC1302 | AMC1302 (amplificador aislado) | 8,5 | 2121 | TI: ≥ 8,5 mm; VIORM 2121 V pico | +| AMC1306 | AMC1306 (modulador ΔΣ aislado) | 8,5 | 2120 | TI: ≥ 8,5 mm; VIORM 2120 V pico | +| MGJ2 | MGJ2D052005BSC (fuente aislada de puerta) | 2 | 2400 | Murata: 2 mm en el apartado de la aprobación UL60950; 2400 V de tensión continua de barrera. Medir la distancia real entre pines en el footprint | + +## 6. EtherCAT P + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| ECP_V | Tensión de alimentación de EtherCAT P | 24 | V | Beckhoff | OK | AUX-01 | | +| ECP_Imax | Corriente máxima por alimentación (US o UP) | 3 | A | Beckhoff | OK | AUX-01 | Solo aplica si se elige EtherCAT P (ME-I-02) | diff --git a/H12/LPU/tests/datos_v0.2/entradas/diseno.md b/H12/LPU/tests/datos_v0.2/entradas/diseno.md new file mode 100644 index 0000000..117951c --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/entradas/diseno.md @@ -0,0 +1,60 @@ +# Tablas de diseño + +Datos del layout y de las interfaces. Casi todos son **provisionales** hasta tener el diseño de la PCB. Se pueden añadir filas; la columna «Clave» identifica cada fila en el informe y debe ser única. + +## 1. Pistas de potencia + +«Corriente» admite un valor en amperios (`12,5`) o el nombre de una magnitud calculada: `I_in_first` (entrada de la primera placa a Ipk), `I_in_first_cont` (ídem en continuo), `I_bridge_rms` (DC-link ↔ puente), `Ipk`, `Irms_cont`. «Capa»: `Ext` o `Int`. + +| Clave | Red | Corriente | Capa | Capas en paralelo | Ancho previsto (mm) | Longitud (mm) | Estado | Nota | +|---|---|---|---|--:|--:|--:|---|---| +| entrada_pk | Entrada HV (1.ª placa), Ipk | I_in_first | Ext | 2 | 35 | 60 | TBD | PROVISIONAL | +| entrada_cont | Entrada HV (1.ª placa), continuo | I_in_first_cont | Ext | 2 | 10 | 60 | TBD | PROVISIONAL | +| puente_rms | DC-link ↔ puente, eficaz a Ipk | I_bridge_rms | Ext | 2 | 15 | 20 | TBD | PROVISIONAL | +| bobina_pk | Puente → bobina, Ipk | Ipk | Ext | 2 | 15 | 80 | TBD | PROVISIONAL | +| bobina_cont | Puente → bobina, continuo | Irms_cont | Ext | 2 | 5 | 80 | TBD | PROVISIONAL | + +## 2. Interfaces de señal + +«Tipo»: `SE` (simple) o `Dif` (diferencial). «Capa»: `MS` (microstrip, capa externa) o `SL` (stripline, capa interna). La separación solo se usa en los pares diferenciales. + +| Clave | Interfaz | Tipo | Capa | Z objetivo (Ω) | Ancho (mm) | Separación (mm) | tr (ns) | Longitud (mm) | Estado | Nota | +|---|---|---|---|--:|--:|--:|--:|--:|---|---| +| mdi_in | EtherCAT IN (MDI) | Dif | MS | 100 | 0,2 | 0,2 | 3 | 30 | TBD | 100 Ω diferencial en el MDI de Ethernet | +| mdi_out | EtherCAT OUT (MDI) | Dif | MS | 100 | 0,2 | 0,2 | 3 | 30 | TBD | | +| reloj_phy | Reloj FPGA–PHY | SE | SL | 50 | 0,12 | | 1 | 60 | TBD | | +| datos_phy | Datos FPGA–PHY | SE | SL | 50 | 0,12 | | 1 | 60 | TBD | | +| reloj_ds | Reloj de los moduladores ΔΣ | SE | SL | 50 | 0,12 | | 1 | 80 | TBD | | +| datos_ds | Datos de los moduladores ΔΣ | SE | SL | 50 | 0,12 | | 2 | 80 | TBD | | +| pwm | PWM a los drivers | SE | MS | 50 | 0,29 | | 2 | 100 | TBD | | +| jtag_tck | JTAG TCK | SE | MS | 50 | 0,29 | | 2 | 100 | TBD | | + +## 3. Distancias de aislamiento previstas + +Distancia mínima medida en el layout para cada caso. Las claves son fijas: `HVLV_coat`, `HVHV_coat`, `HVHV_int`, `HVHV_bare`, `WB`. + +| Clave | Caso | Previsto (mm) | Estado | Nota | +|---|---|--:|---|---| +| HVLV_coat | HV–BT, superficie con coating | 4,5 | TBD | PROVISIONAL | +| HVHV_coat | HV–HV, superficie con coating | 2,5 | TBD | PROVISIONAL | +| HVHV_int | HV–HV, capas internas | 2,5 | TBD | PROVISIONAL | +| HVHV_bare | HV–HV, terminales sin recubrir (conectores, etc.) | 3,5 | TBD | PROVISIONAL | +| WB | Taladros del waterblock – cualquier conductor | 4,5 | TBD | PROVISIONAL | + +## 4. Consumos de la alimentación auxiliar (BT) + +«Corriente» admite un valor en mA o el nombre de una magnitud calculada (`I_iso_in`: entrada de cada fuente aislada de puerta). «Rendimiento» es el del convertidor que genera ese raíl. + +| Clave | Consumidor | Raíl (V) | Corriente (mA) | Nº | Rendimiento (%) | Fuente | Estado | Nota | +|---|---|--:|---|--:|--:|---|---|---| +| fpga_core | FPGA MachXO5-NX: núcleo | 1 | 150 | 1 | 85 | — | TBD | PROVISIONAL (FW-02) | +| fpga_io | FPGA MachXO5-NX: bancos de E/S | 3,3 | 30 | 1 | 85 | — | TBD | PROVISIONAL (FW-02) | +| phy | PHY Ethernet | 3,3 | 60 | 2 | 85 | — | TBD | PROVISIONAL (FW-06); 2 puertos (COM-03) | +| osc | Oscilador | 3,3 | 10 | 1 | 85 | — | TBD | PROVISIONAL | +| eeprom | EEPROM SII | 3,3 | 1 | 1 | 85 | — | TBD | PROVISIONAL (COM-06) | +| ds_lv | Moduladores ΔΣ, lado BT (DVDD) | 3,3 | 6 | 3 | 85 | TI AMC1306 | AC | Máx. de la hoja de datos; rendimiento provisional | +| ds_hv | Moduladores ΔΣ, lado HV (AVDD) vía fuente aislada | 5 | 9,8 | 3 | 70 | TI AMC1306 | AC | Máx. de la hoja de datos; rendimiento de la fuente aislada PROVISIONAL | +| drv_lv | Gate drivers ADuM4146, lado BT (VDD1) | 5 | 2,17 | 4 | 85 | ADI ADuM4146 | AC | Máx. de la hoja de datos; rendimiento provisional | +| iso_gd | Fuentes aisladas de puerta (entrada a 5 V) | 5 | I_iso_in | 4 | 85 | Cálculo | AC | Corriente calculada en el apartado de gate driving | +| logica | Lógica cableada y comparadores | 3,3 | 10 | 1 | 85 | — | TBD | PROVISIONAL (HW-01) | +| leds | LEDs | 3,3 | 2 | 4 | 85 | — | TBD | PROVISIONAL | diff --git a/H12/LPU/tests/datos_v0.2/entradas/parametros.md b/H12/LPU/tests/datos_v0.2/entradas/parametros.md new file mode 100644 index 0000000..4082b3b --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/entradas/parametros.md @@ -0,0 +1,175 @@ +# Parámetros de diseño + +Parámetros escalares de los cálculos del Magnet Driver Abejorro. **Este es el fichero que se edita normalmente.** + +- Cambie solo las columnas «Valor», «Estado», «Fuente» y «Nota». La columna «Nombre» es la que usan las fórmulas: no la cambie. +- Decimales con coma o con punto (`1,92` o `1.92`), sin separador de miles. Signo menos con `-` o `−`. +- Unidades admitidas: ver `calculos/README.md`. Un valor en `%` se interpreta como fracción (2 % → 0,02). +- «Estado»: `OK` (confirmado), `AC` (a confirmar) o `TBD` (desconocido: el valor es **provisional** y solo sirve para calcular). +- «Fuente»: `U` (equipo), `TFG` (TFG de L. Navarro), `C` (cálculo del documento de requisitos), `P` (propuesta) o el documento o fabricante citado. +- No use el carácter `|` dentro de las celdas. + + +## 1. Bus DC + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Vbus_min | Tensión mínima del bus de HV | 320 | V | U | OK | DCB-01 | Revisar con la nueva batería (ME-I-07) | +| Vbus_max | Tensión máxima del bus de HV | 400 | V | U | OK | DCB-01 | Revisar con la nueva batería (ME-I-07) | +| V_cont | Tensión continua que deben soportar componentes y aislamiento | 600 | V | U | OK | DCB-02 · ISO-06 | | +| N_MD | Número de Magnet Driver conectados al bus | 10 | – | U | OK | DCB-03 · SYS-01 | 4 HEMS + 6 EMS | +| Bus_topo | Distribución del bus: «Encadenado» o «Estrella» | Encadenado | – | TFG | TBD | DCB-05 · ME-I-03 | Encadenado: la primera placa conduce la corriente de todas | +| dVdc_pct | Rizado máximo de tensión en el DC-link (fracción de Vbus_min) | 2 | % | TFG | AC | DCB-07 · HW-02 | 2 % de 400 V = 8 V según el TFG | +| eta_pwr | Rendimiento de la etapa de potencia (para la corriente media de bus) | 97 | % | P | TBD | — | PROVISIONAL. Ajustar con las pérdidas del puente del informe cuando se elija componente | + +## 2. Carga: electroimanes (prototipo H11) + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Rcoil | Resistencia de la bobina | 1,2 | Ω | U | OK | LOAD-01 | Medida en la bobina construida. Nuevas bobinas: EM-D-04 | +| Ipk | Corriente de pico (primeros segundos de la levitación) | 60 | A | U | OK | LOAD-03 | | +| t_pk | Duración del pico de corriente | 2 | s | — | TBD | LOAD-03 · EM-C-01 | PROVISIONAL. «Primeros segundos» | +| Irms_cont | Corriente eficaz en régimen permanente (dimensionado térmico) | 5 | A | TFG | TBD | LOAD-05 · EM-C-01 | Valor mencionado en el TFG, sin revalidar | +| L_EMS_min | Inductancia incremental mínima del EMS | 1,92 | mH | R5 | AC | LOAD-01 · EM-D-01/02 | Sección 6.1.1 | +| L_EMS_max | Inductancia incremental máxima del EMS | 117 | mH | R5 | AC | LOAD-01 · EM-D-01/02 | Sección 6.1.1 | +| L_HEMS_min | Inductancia incremental mínima del HEMS (dψ/di derivada) | 6,2 | mH | C | OK | LOAD-01 | Sección 6.1.2 (derivado del Excel R7) | +| L_HEMS_max | Inductancia incremental máxima del HEMS (dψ/di derivada) | 11,2 | mH | C | OK | LOAD-01 | Sección 6.1.2 | +| L_EMS_0 | Inductancia del EMS a I = 0 (valor citado, columnas 5–6) | 26,7 | mH | R5 | AC | 6.1.1 | Solo para la cota pesimista de energía (L constante) | +| E_EMS | Energía devuelta al desmagnetizar el EMS desde 55 A (mapa H11) | 15,6 | J | C | AC | 6.1.3 · EM-D-03 | Mapa hasta ±55 A | +| E_HEMS | Energía devuelta al desmagnetizar el HEMS desde −55 A, 6 mm (mapa H11) | 16,7 | J | C | AC | 6.1.3 · EM-D-03 | Peor caso del H11 | +| I_map | Corriente máxima cubierta por los mapas de inductancia | 55 | A | R7 | OK | EM-D-03 | | +| dI_pp_max | Rizado de corriente pico a pico máximo admisible | 2 | A | — | TBD | LOAD-07 · EM-C-03 | PROVISIONAL. | +| T_coil_max | Temperatura máxima de la bobina (umbral de protección) | 120 | °C | — | TBD | SAF-01 · sin pendiente | PROVISIONAL. No hay cuestión abierta que lo resuelva: proponer a EM-D | + +## 3. Conmutación y MOSFET + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| fsw | Frecuencia de conmutación | 20 | kHz | TFG | TBD | PWR-03 · FW-08 | | +| Modulacion | Modulación: «Unipolar» o «Bipolar» | Unipolar | – | TFG | TBD | PWR-03 · FW-08 | | +| t_dead | Tiempo muerto | 100 | ns | TFG | AC | PWR-04 · FW-08 | Red RC del TFG; pasa a ser configurable en la FPGA | +| MOSFET_sel | Referencia del MOSFET usado en el resto de cálculos (una de las columnas de componentes.md) | SCT012H90G3AG | – | P | AC | PWR-02 · HW-02 | | +| D_worst | Fracción del periodo que conduce el MOSFET más cargado | 1 | – | P | OK | SAF-05 | 1 = rueda libre permanente por los low-side (conservador). Con PWM en régimen, el más cargado conduce (1 + m)/2 | +| Tj_assumed | Tj supuesta para evaluar RDS(on) | 125 | °C | P | AC | — | Debe ser ≥ Tj calculada (comprobación en el apartado MOSFET) | +| Tj_lim | Tj máxima de diseño (criterio) | 150 | °C | P | AC | — | Margen sobre los 175 °C del SCT012H90G3AG | +| t_dead_mrg | Margen adicional en el cálculo del tiempo muerto mínimo | 20 | ns | P | TBD | PWR-04 | PROVISIONAL. | + +## 4. DC-link y protección del bus + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Cdc | Capacidad del DC-link en placa | 105 | µF | TFG | AC | DCB-07 · HW-02 · HW-03 | | +| V_film | Tensión nominal de los condensadores del DC-link | 500 | V | TFG | AC | DCB-02 · HW-02 | No cubre los 600 V de DCB-02 | +| ESR_dc | ESR equivalente del DC-link a 2·fsw | 2 | mΩ | — | TBD | HW-02 | PROVISIONAL. | +| N_dc_abs | Número de DC-link que absorben la energía en el caso de fallo | 1 | – | P | AC | DCB-08 · HW-03 | 1 = solo la propia placa (conservador) | +| V_chop_on | Tensión de actuación del clamp o chopper (si se usa) | 550 | V | P | TBD | HW-03 | PROVISIONAL. | +| k_chop_mrg | Margen mínimo de V_chop_on sobre Vbus_max (criterio) | 1,1 | – | P | TBD | HW-03 | PROVISIONAL | +| I_fuse | Corriente nominal del fusible en placa | 15 | A | TFG | AC | DCB-06 · HW-02 | 0ADKC9150-BE | +| V_fuse | Tensión nominal DC del fusible | 500 | V | TFG | AC | DCB-06 · HW-02 | 0ADKC9150-BE | +| k_fuse | Factor de utilización máximo del fusible (criterio) | 0,75 | – | P | TBD | DCB-06 | PROVISIONAL. Confirmar con la curva de derating del fabricante | + +## 5. Gate driving + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| Vgs_on | Tensión de puerta en ON | 20 | V | TFG | AC | GD-01 · HW-02 | | +| Vgs_off | Tensión de puerta en OFF | -5 | V | TFG | AC | GD-01 · HW-02 | | +| Rg_on | Resistencia de puerta externa de encendido | 5 | Ω | — | TBD | HW-02 | PROVISIONAL. | +| Rg_off | Resistencia de puerta externa de apagado | 5 | Ω | — | TBD | HW-02 | PROVISIONAL. | +| N_d_desat | Número de diodos en serie en el circuito DESAT | 1 | – | — | TBD | HW-01 | PROVISIONAL. | +| Vf_desat | Caída de tensión de cada diodo DESAT | 0,7 | V | — | TBD | HW-01 | PROVISIONAL. | +| R_desat | Resistencia serie del circuito DESAT | 1 | kΩ | — | TBD | HW-01 | PROVISIONAL. | +| C_blank | Condensador de blanking del DESAT | 47 | pF | — | TBD | HW-01 | PROVISIONAL. | +| t_desat_max | Tiempo de respuesta máximo de la protección DESAT | 2 | µs | TFG | AC | GD-01 | | + +## 6. Sensado + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| R_shunt | Resistencia del shunt de corriente | 500 | µΩ | TFG | AC | SNS-01 · HW-02 | | +| P_sh_rat | Potencia nominal del shunt | 3 | W | — | TBD | HW-02 | PROVISIONAL. Referencia del shunt del TFG no documentada aquí | +| I_margin | Margen mínimo fondo de escala / Ipk (criterio) | 1,2 | – | P | AC | SNS-01 | | +| I_OC | Umbral de sobrecorriente por hardware | 80 | A | — | TBD | SAF-04 · HW-01 | PROVISIONAL. | +| f_clk_ds | Frecuencia de reloj de los moduladores ΔΣ | 20 | MHz | P | TBD | SNS-05 · FW-01 | PROVISIONAL. | +| OSR | Relación de sobremuestreo del filtro sinc | 128 | – | P | TBD | SNS-05 · FW-01 | PROVISIONAL. | +| N_sinc | Orden del filtro sinc | 3 | – | P | TBD | FW-01 | sinc3 es el habitual con moduladores de 2.º orden | +| N_adc | Resolución del ADC (opción amplificador aislado + ADC) | 12 | bit | — | TBD | SNS-05 · FW-01 | PROVISIONAL. | +| V_meas_FS | Fondo de escala deseado de la medida de Vbus | 700 | V | P | TBD | SNS-02 | PROVISIONAL. Debe cubrir la tensión del caso de fallo | +| V_sens_FS | Entrada lineal del sensor aislado de Vbus | 0,25 | V | — | TBD | SNS-02 · HW-02 | PROVISIONAL. Ejemplo: AMC1306x25 (±250 mV) | +| R_sens_in | Impedancia de entrada del sensor de Vbus | 22 | kΩ | — | TBD | HW-02 | AMC1306x25: 22 kΩ diferencial (hoja de datos) | +| R_div_bot | Resistencia inferior del divisor de Vbus | 1 | kΩ | — | TBD | SNS-02 | PROVISIONAL. | +| V_res_rat | Tensión máxima de trabajo de cada resistencia del divisor | 200 | V | — | TBD | HW-02 | PROVISIONAL. Depende del encapsulado elegido | +| P_res_rat | Potencia nominal de cada resistencia del divisor | 0,25 | W | — | TBD | HW-02 | PROVISIONAL. | +| k_res | Factor de utilización de potencia de las resistencias (criterio) | 0,5 | – | P | TBD | — | PROVISIONAL. | +| V_OV | Umbral de sobretensión de bus por hardware | 450 | V | — | TBD | SAF-01 · HW-01 | PROVISIONAL. | +| NTC_R25 | Resistencia de la NTC a 25 °C | 10 | kΩ | TFG | AC | LOAD-08 · HW-02 | GA10K4A1IA (TE) | +| NTC_beta | Constante β(25/85) de la NTC | 3694 | K | TE | AC | LOAD-08 | Página de producto TE; contrastar con la tabla R-T | +| R_ntc_pu | Resistencia de polarización de la NTC | 10 | kΩ | — | TBD | SNS-03 | PROVISIONAL. | +| V_ntc_ref | Tensión de referencia del divisor de la NTC | 5 | V | — | TBD | SNS-03 | PROVISIONAL. | + +## 7. Refrigeración y entorno + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| T_amb | Temperatura ambiente máxima | 40 | °C | — | TBD | MEC-06 · ME-C-02 | PROVISIONAL. El TFG asumía ≥ 25 °C | +| T_cool | Temperatura del refrigerante a la entrada | 40 | °C | — | TBD | ME-C-02 | PROVISIONAL. | +| Q_cool | Caudal de refrigerante | 2 | L/min | — | TBD | ME-C-02 | PROVISIONAL. | +| N_cool_ser | Placas en serie en el circuito de agua (la última es la peor) | 1 | – | — | TBD | ME-C-01 · ME-C-02 | PROVISIONAL. | +| Rth_wb | Resistencia térmica cara inferior de la PCB → agua, por MOSFET | 0,3 | K/W | — | TBD | PWR-06 · ME-C-01 | PROVISIONAL. | +| t_TIM | Espesor del material de interfaz térmica | 0,5 | mm | — | TBD | ME-C-01 | PROVISIONAL. | +| k_TIM | Conductividad del material de interfaz térmica | 3 | W/(m·K) | — | TBD | ME-C-01 | PROVISIONAL. | +| V_TIM_bd | Rigidez dieléctrica del material de interfaz (tensión de ruptura) | 4000 | V | — | TBD | ISO-04 · ME-C-01 | PROVISIONAL. El pad de drenador está en HV | +| k_spread | Área efectiva bajo el TIM / área del pad (ensanchamiento) | 2 | – | P | TBD | — | PROVISIONAL. | +| A_pad | Área del pad de drenador del MOSFET | 80 | mm² | — | TBD | HW-02 | PROVISIONAL. Comprobar con el footprint H2PAK-7 | +| d_via | Diámetro de taladro de las vías térmicas | 0,3 | mm | — | TBD | PT-01 | PROVISIONAL. | +| t_plat | Espesor del metalizado de las vías | 25 | µm | — | TBD | PT-01 | PROVISIONAL. | +| N_via | Número de vías térmicas bajo cada MOSFET | 40 | – | — | TBD | — | PROVISIONAL. | +| t_solder | Espesor de la soldadura bajo el pad | 0,1 | mm | — | TBD | PT-02 | PROVISIONAL. | +| k_solder | Conductividad de la soldadura | 50 | W/(m·K) | — | AC | — | Orden de magnitud de aleaciones SAC | +| k_Cu | Conductividad del cobre | 385 | W/(m·K) | — | OK | — | Valor físico | +| k_FR4 | Conductividad del FR-4 en el eje z | 0,3 | W/(m·K) | — | AC | PT-01 | Orden de magnitud; confirmar con el laminado | + +## 8. PCB: stackup, pistas y aislamiento + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| t_pcb | Espesor total de la PCB | 1,6 | mm | — | TBD | MEC-03 · PT-01 | PROVISIONAL. | +| oz_ext | Cobre en capas externas | 2 | oz | TFG | AC | MEC-03 · PT-01 | 1 oz ≈ 35 µm | +| oz_int | Cobre en capas internas | 2 | oz | TFG | AC | MEC-03 · PT-01 | El documento indica 2 oz sin distinguir capas | +| dT_trace | Incremento de temperatura admisible en pistas (criterio) | 20 | °C | P | AC | — | | +| er_pcb | Permitividad relativa del dieléctrico | 4,3 | – | — | TBD | PT-01 | PROVISIONAL. | +| h_ms | Dieléctrico entre capa externa y su plano de referencia (microstrip) | 0,2 | mm | — | TBD | PT-01 | PROVISIONAL | +| b_sl | Distancia entre planos de referencia de la stripline | 0,5 | mm | — | TBD | PT-01 | PROVISIONAL | +| tol_Z | Tolerancia admisible de impedancia (criterio) | 10 | % | P | AC | PT-01 | Tolerancia habitual de fabricación; confirmar con el fabricante | +| Ins_type | Tipo de aislamiento HV–BT: «Básico» o «Reforzado» | Básico | – | — | TBD | ISO-05 · HW-04 | PROVISIONAL. | +| PD_coat | Grado de contaminación bajo el coating (IEC 60664-3) | 1 | – | P | AC | ISO-02 · HW-04 | | +| PD_bare | Grado de contaminación en superficies sin coating | 2 | – | P | TBD | HW-04 | PROVISIONAL. | +| d_HV_LV | Separación mínima HV–BT con coating (regla de la competición) | 4 | mm | U | OK | ISO-02 | | +| d_HV_HV | Separación mínima entre redes de HV | 2 | mm | TFG | AC | ISO-03 · HW-04 | | +| d_WB | Separación mínima taladros del waterblock – conductores | 4 | mm | TFG | AC | ISO-04 · ME-C-01 | | + +## 9. Alimentación auxiliar (BT) + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| V_BT_max | Tensión máxima de la batería de BT | 24 | V | U | OK | AUX-01 | | +| V_BT_min | Tensión mínima de la batería de BT | 18 | V | — | TBD | AUX-03 · ME-I-02 | PROVISIONAL. | +| P_BT_lim | Consumo máximo permitido por placa | 15 | W | — | TBD | AUX-03 · ME-I-02 | PROVISIONAL. | +| eta_BT | Rendimiento de la etapa de entrada BT | 85 | % | — | TBD | AUX-02 · HW-05 | PROVISIONAL. | +| P_margin | Margen sobre el consumo total estimado (criterio) | 20 | % | P | AC | — | | + +## 10. Integridad de señal: presupuesto de tiempos (interfaz MII/RMII u otra síncrona) + +| Nombre | Descripción | Valor | Unidad | Fuente | Estado | Requisito / pendiente | Nota | +|---|---|--:|---|---|---|---|---| +| f_bus_clk | Frecuencia de reloj de la interfaz FPGA–PHY | 50 | MHz | P | TBD | COM-02 · FW-06 | PROVISIONAL. RMII: 50 MHz; MII: 25 MHz | +| tco_max | Retardo de salida máximo del emisor (clock-to-out) | 10 | ns | — | TBD | FW-06 | PROVISIONAL. Hoja de datos del PHY / FPGA | +| tco_min | Retardo de salida mínimo del emisor | 2 | ns | — | TBD | FW-06 | PROVISIONAL. | +| t_su | Tiempo de setup del receptor | 4 | ns | — | TBD | FW-06 | PROVISIONAL. | +| t_h | Tiempo de hold del receptor | 2 | ns | — | TBD | FW-06 | PROVISIONAL. | +| t_jit | Jitter y otras incertidumbres del reloj | 0,5 | ns | — | TBD | FW-06 | PROVISIONAL. | +| dL_clk | Diferencia de longitud del reloj entre emisor y receptor | 10 | mm | — | TBD | — | PROVISIONAL | +| skew_pair | Desfase admisible dentro de un par diferencial | 5 | ps | — | TBD | COM-03 | PROVISIONAL. Criterio del fabricante del PHY o de los magnéticos | +| SI_if_datos | Interfaz de datos usada en el presupuesto de tiempos (clave de diseno.md) | datos_phy | – | P | TBD | COM-02 · FW-06 | | +| SI_if_reloj | Interfaz de reloj usada en el presupuesto de tiempos (clave de diseno.md) | reloj_phy | – | P | TBD | COM-02 · FW-06 | | +| SI_if_par | Par diferencial usado para la igualación de longitudes (clave de diseno.md) | mdi_in | – | P | TBD | COM-03 | | diff --git a/H12/LPU/tests/datos_v0.2/esperado.json b/H12/LPU/tests/datos_v0.2/esperado.json new file mode 100644 index 0000000..af57a3e --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/esperado.json @@ -0,0 +1,380 @@ +{ + "_nota": "Resultados de referencia con las entradas de tests/datos_v0.2. Verificados frente a la hoja Excel v0.1 (75 comprobaciones, mismos valores y resultados).", + "comprobaciones": { + "POT-01": { + "estado": "CUMPLE", + "valor": 1.3020833333333335, + "limite": 2.0 + }, + "POT-02": { + "estado": "CUMPLE", + "valor": 266.6666666666667, + "limite": 60.0 + }, + "POT-03": { + "estado": "CUMPLE", + "valor": 3.691428571428572, + "limite": 6.4 + }, + "POT-04": { + "estado": "NO CUMPLE", + "valor": 733.866215461637, + "limite": 600.0 + }, + "POT-05": { + "estado": "NO CUMPLE", + "valor": 733.866215461637, + "limite": 500.0 + }, + "POT-06": { + "estado": "CUMPLE", + "valor": 733.866215461637, + "limite": 900.0 + }, + "POT-07": { + "estado": "NO CUMPLE", + "valor": 0.00010499999999999999, + "limite": 0.00019874380165289254 + }, + "POT-08": { + "estado": "CUMPLE", + "valor": 550.0, + "limite": 440.00000000000006 + }, + "POT-09": { + "estado": "CUMPLE", + "valor": 550.0, + "limite": 600.0 + }, + "POT-10": { + "estado": "REVISAR", + "valor": 13.917525773195877, + "limite": 11.25 + }, + "POT-11": { + "estado": "REVISAR", + "valor": 139.17525773195877, + "limite": 11.25 + }, + "POT-12": { + "estado": "NO CUMPLE", + "valor": 500.0, + "limite": 600.0 + }, + "POT-13": { + "estado": "NO CUMPLE", + "valor": 500.0, + "limite": 733.866215461637 + }, + "GDR-01": { + "estado": "REVISAR", + "valor": 20.0, + "limite": 18.0 + }, + "GDR-02": { + "estado": "CUMPLE", + "valor": 20.0, + "limite": 22.0 + }, + "GDR-03": { + "estado": "CUMPLE", + "valor": -5.0, + "limite": -5.0 + }, + "GDR-04": { + "estado": "CUMPLE", + "valor": 0.2315, + "limite": 2.0 + }, + "GDR-05": { + "estado": "CUMPLE", + "valor": 0.009260000000000001, + "limite": 0.08 + }, + "GDR-06": { + "estado": "CUMPLE", + "valor": 0.009260000000000001, + "limite": 0.04 + }, + "GDR-07": { + "estado": "CUMPLE", + "valor": 3.2154340836012865, + "limite": 11.0 + }, + "GDR-08": { + "estado": "CUMPLE", + "valor": 3.286446693834626, + "limite": 9.0 + }, + "GDR-09": { + "estado": "CUMPLE", + "valor": 8510638297.872339, + "limite": 100000000000.0 + }, + "GDR-10": { + "estado": "CUMPLE", + "valor": 8510638297.872339, + "limite": 100000000000.0 + }, + "GDR-11": { + "estado": "CUMPLE", + "valor": 8510638297.872339, + "limite": 200000000000.0 + }, + "GDR-12": { + "estado": "CUMPLE", + "valor": -4.225531914893617, + "limite": 1.8 + }, + "GDR-13": { + "estado": "REVISAR", + "valor": 1.0000000000000001e-07, + "limite": 1.2000000000000002e-07 + }, + "GDR-14": { + "estado": "REVISAR", + "valor": 664.4166666666666, + "limite": 454.0 + }, + "GDR-15": { + "estado": "CUMPLE", + "valor": 1.160493358633776e-06, + "limite": 2e-06 + }, + "GDR-16": { + "estado": "CUMPLE", + "valor": 20.0, + "limite": 11.5 + }, + "BT-01": { + "estado": "CUMPLE", + "valor": 3.775808472832694, + "limite": 15.0 + }, + "BT-02": { + "estado": "CUMPLE", + "valor": 1.573253530346956, + "limite": 3.0 + }, + "TER-01": { + "estado": "CUMPLE", + "valor": 7.717256661176918e-05, + "limite": 0.0 + }, + "TER-02": { + "estado": "CUMPLE", + "valor": 4000.0, + "limite": 600.0 + }, + "TER-03": { + "estado": "CUMPLE", + "valor": 1.8, + "limite": 3.0 + }, + "TER-04": { + "estado": "NO CUMPLE", + "valor": 648.1301470060163, + "limite": 150.0 + }, + "TER-05": { + "estado": "CUMPLE", + "valor": 55.938707473887106, + "limite": 150.0 + }, + "TER-06": { + "estado": "REVISAR", + "valor": 648.1301470060163, + "limite": 125.0 + }, + "PIS-entrada_pk": { + "estado": "CUMPLE", + "valor": 0.035, + "limite": 0.03430559770434707 + }, + "PIS-entrada_cont": { + "estado": "CUMPLE", + "valor": 0.01, + "limite": 3.616706587425037e-05 + }, + "PIS-puente_rms": { + "estado": "CUMPLE", + "valor": 0.015, + "limite": 0.003842195322725168 + }, + "PIS-bobina_pk": { + "estado": "CUMPLE", + "valor": 0.015, + "limite": 0.01074855581861601 + }, + "PIS-bobina_cont": { + "estado": "CUMPLE", + "valor": 0.005, + "limite": 0.0003489990618511684 + }, + "AIS-HVLV_coat": { + "estado": "CUMPLE", + "valor": 0.0045000000000000005, + "limite": 0.004 + }, + "AIS-HVHV_coat": { + "estado": "CUMPLE", + "valor": 0.0025, + "limite": 0.002 + }, + "AIS-HVHV_int": { + "estado": "CUMPLE", + "valor": 0.0025, + "limite": 0.002 + }, + "AIS-HVHV_bare": { + "estado": "CUMPLE", + "valor": 0.0035, + "limite": 0.0030384615384615385 + }, + "AIS-WB": { + "estado": "CUMPLE", + "valor": 0.0045000000000000005, + "limite": 0.004 + }, + "AIS-F-ADUM4146": { + "estado": "CUMPLE", + "valor": 0.0083, + "limite": 0.004 + }, + "AIS-V-ADUM4146": { + "estado": "CUMPLE", + "valor": 2150.0, + "limite": 600.0 + }, + "AIS-F-AMC1302": { + "estado": "CUMPLE", + "valor": 0.0085, + "limite": 0.004 + }, + "AIS-V-AMC1302": { + "estado": "CUMPLE", + "valor": 2121.0, + "limite": 600.0 + }, + "AIS-F-AMC1306": { + "estado": "CUMPLE", + "valor": 0.0085, + "limite": 0.004 + }, + "AIS-V-AMC1306": { + "estado": "CUMPLE", + "valor": 2120.0, + "limite": 600.0 + }, + "AIS-F-MGJ2": { + "estado": "REVISAR", + "valor": 0.002, + "limite": 0.004 + }, + "AIS-V-MGJ2": { + "estado": "CUMPLE", + "valor": 2400.0, + "limite": 600.0 + }, + "SI-mdi_in": { + "estado": "CUMPLE", + "valor": 97.98612819130618, + "limite": 100.0 + }, + "SI-mdi_out": { + "estado": "CUMPLE", + "valor": 97.98612819130618, + "limite": 100.0 + }, + "SI-reloj_phy": { + "estado": "CUMPLE", + "valor": 50.481053779845965, + "limite": 50.0 + }, + "SI-datos_phy": { + "estado": "CUMPLE", + "valor": 50.481053779845965, + "limite": 50.0 + }, + "SI-reloj_ds": { + "estado": "CUMPLE", + "valor": 50.481053779845965, + "limite": 50.0 + }, + "SI-datos_ds": { + "estado": "CUMPLE", + "valor": 50.481053779845965, + "limite": 50.0 + }, + "SI-pwm": { + "estado": "CUMPLE", + "valor": 50.10923277448832, + "limite": 50.0 + }, + "SI-jtag_tck": { + "estado": "CUMPLE", + "valor": 50.10923277448832, + "limite": 50.0 + }, + "SI-SETUP": { + "estado": "CUMPLE", + "valor": 5.015762621517089e-09, + "limite": 0.0 + }, + "SI-HOLD": { + "estado": "CUMPLE", + "valor": 3.4588384177350645e-10, + "limite": 0.0 + }, + "SEN-01": { + "estado": "CUMPLE", + "valor": 100.0, + "limite": 72.0 + }, + "SEN-02": { + "estado": "CUMPLE", + "valor": 80.0, + "limite": 60.0 + }, + "SEN-03": { + "estado": "CUMPLE", + "valor": 80.0, + "limite": 100.0 + }, + "SEN-04": { + "estado": "CUMPLE", + "valor": null, + "limite": null + }, + "SEN-05": { + "estado": "CUMPLE", + "valor": 1.92e-05, + "limite": 2.5e-05 + }, + "SEN-06": { + "estado": "CUMPLE", + "valor": 0.03359189471243043, + "limite": 0.125 + }, + "SEN-07": { + "estado": "CUMPLE", + "valor": 183.4010300961716, + "limite": 200.0 + }, + "SEN-08": { + "estado": "NO CUMPLE", + "valor": 700.0, + "limite": 733.866215461637 + }, + "SEN-09": { + "estado": "CUMPLE", + "valor": 450.0, + "limite": 400.0 + }, + "SEN-10": { + "estado": "CUMPLE", + "valor": 450.0, + "limite": 600.0 + } + } +} \ No newline at end of file diff --git a/H12/LPU/tests/datos_v0.2/normas/README.md b/H12/LPU/tests/datos_v0.2/normas/README.md new file mode 100644 index 0000000..e45129b --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/normas/README.md @@ -0,0 +1,9 @@ +# Tablas de normas + +Datos de normas usados por los cálculos, en CSV con punto decimal. + +| Fichero | Contenido | Fuente y nivel de verificación | +|---|---|---| +| `ipc2221b_tabla6-1.csv` | IPC-2221B, tabla 6-1: separación mínima entre conductores (mm). B1 = capas internas; B2 = externas sin recubrir ≤ 3050 m; B3 = externas sin recubrir > 3050 m; B4 = externas con recubrimiento permanente de polímero; A5 = externas con conformal coating sobre el conjunto; A6 = terminales sin recubrir; A7 = terminales con conformal coating. La última fila es el incremento por voltio por encima de 500 V | Reproducción de ProtoExpress (Sierra Circuits); B1 por voltio = 0,0025 mm/V según el ejemplo de Siemens EDA (600 V → 0,50 mm). Fuente secundaria: contrastar con la norma (ya existe IPC-2221C, 2025) | +| `iec60664-1_fuga_pcb.csv` | IEC 60664-1: líneas de fuga en material de circuito impreso, aislamiento funcional/básico (mm) | Verificados en fuentes secundarias: 400 V/PD2 (TI SLUAAR5) y 1000 V (Vishay). El resto, a contrastar con la norma | +| `poe.csv` | Potencia disponible en el equipo alimentado por tipo de PoE | IEEE 802.3af/at/bt; valores no consultados en la norma durante la preparación | diff --git a/H12/LPU/tests/datos_v0.2/normas/iec60664-1_fuga_pcb.csv b/H12/LPU/tests/datos_v0.2/normas/iec60664-1_fuga_pcb.csv new file mode 100644 index 0000000..2d19367 --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/normas/iec60664-1_fuga_pcb.csv @@ -0,0 +1,8 @@ +tension_V,PD1_mm,PD2_mm,verificacion +250,0.56,1.25,sin verificar +320,0.75,1.6,sin verificar +400,1.0,2.0,parcial (PD2: TI SLUAAR5) +500,1.3,2.5,sin verificar +630,1.8,3.2,sin verificar +800,2.4,4.0,sin verificar +1000,3.2,5.0,verificado (Vishay) diff --git a/H12/LPU/tests/datos_v0.2/normas/ipc2221b_tabla6-1.csv b/H12/LPU/tests/datos_v0.2/normas/ipc2221b_tabla6-1.csv new file mode 100644 index 0000000..db19922 --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/normas/ipc2221b_tabla6-1.csv @@ -0,0 +1,11 @@ +desde_V,hasta_V,B1,B2,B3,B4,A5,A6,A7 +0,15,0.05,0.1,0.1,0.05,0.13,0.13,0.13 +16,30,0.05,0.1,0.1,0.05,0.13,0.25,0.13 +31,50,0.1,0.6,0.6,0.13,0.13,0.4,0.13 +51,100,0.1,0.6,1.5,0.13,0.13,0.5,0.13 +101,150,0.2,0.6,3.2,0.4,0.4,0.8,0.4 +151,170,0.2,1.25,6.4,0.4,0.4,0.8,0.4 +171,250,0.2,1.25,6.4,0.4,0.4,0.8,0.4 +251,300,0.2,1.25,12.5,0.4,0.4,0.8,0.8 +301,500,0.25,2.5,12.5,0.8,0.8,1.5,0.8 +por_voltio,>500,0.0025,0.005,0.025,0.00305,0.00305,0.00305,0.00305 diff --git a/H12/LPU/tests/datos_v0.2/normas/poe.csv b/H12/LPU/tests/datos_v0.2/normas/poe.csv new file mode 100644 index 0000000..7345840 --- /dev/null +++ b/H12/LPU/tests/datos_v0.2/normas/poe.csv @@ -0,0 +1,5 @@ +norma,potencia_PD_W +IEEE 802.3af (Tipo 1),12.95 +IEEE 802.3at (Tipo 2),25.5 +IEEE 802.3bt (Tipo 3),51 +IEEE 802.3bt (Tipo 4),71.3 diff --git a/H12/LPU/tests/regenera_esperado.py b/H12/LPU/tests/regenera_esperado.py new file mode 100644 index 0000000..f97afb8 --- /dev/null +++ b/H12/LPU/tests/regenera_esperado.py @@ -0,0 +1,23 @@ +"""Regenera tests/datos_v0.2/esperado.json. + +Úselo SOLO después de cambiar una fórmula a propósito y de comprobar que los nuevos +resultados son correctos: python tests/regenera_esperado.py +""" +import json +import os +import sys + +RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(RAIZ, "calculos")) +from motor.calculo import ejecuta # noqa: E402 + +DATOS = os.path.join(RAIZ, "tests", "datos_v0.2") +ctx = ejecuta(os.path.join(DATOS, "entradas"), os.path.join(DATOS, "normas")) +ruta = os.path.join(DATOS, "esperado.json") +with open(ruta, encoding="utf-8") as f: + nota = json.load(f).get("_nota", "") +datos = {"_nota": nota, "comprobaciones": {c["id"]: {"estado": c["estado"], "valor": c["valor"], "limite": c["limite"]} + for c in ctx.comprobaciones}} +with open(ruta, "w", encoding="utf-8") as f: + json.dump(datos, f, ensure_ascii=False, indent=1) +print(f"Actualizado {ruta} con {len(ctx.comprobaciones)} comprobaciones.") diff --git a/H12/LPU/tests/test_motor.py b/H12/LPU/tests/test_motor.py new file mode 100644 index 0000000..754d833 --- /dev/null +++ b/H12/LPU/tests/test_motor.py @@ -0,0 +1,130 @@ +"""Pruebas del motor de cálculo. + +Usan una copia fija de las entradas (tests/datos_v0.2), así que no fallan al cambiar +los parámetros del proyecto: solo detectan cambios no intencionados en las fórmulas. +Si se modifica una fórmula a propósito, hay que regenerar esperado.json (ver calculos/README.md). +""" +import json +import os +import shutil +import sys + +import pytest + +RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(RAIZ, "calculos")) + +from motor.calculo import ejecuta # noqa: E402 +from motor.cambios import compara # noqa: E402 +from motor.informe import informe, resultados_json # noqa: E402 +from motor.lector import ErrorEntradas # noqa: E402 +from motor.unidades import parse_numero, fmt # noqa: E402 + +DATOS = os.path.join(RAIZ, "tests", "datos_v0.2") +META = {"fecha_iso": "2026-09-17T00:00:00", "fecha_texto": "17/09/2026 00:00", "fecha_fichero": "x", + "commit": "0000000", "version_texto": "prueba"} + + +def calcula(dir_datos=DATOS): + return ejecuta(os.path.join(dir_datos, "entradas"), os.path.join(dir_datos, "normas")) + + +@pytest.fixture +def copia(tmp_path): + destino = tmp_path / "datos" + shutil.copytree(DATOS, destino) + return destino + + +def cambia_parametro(dir_datos, nombre, nuevo_valor): + ruta = os.path.join(dir_datos, "entradas", "parametros.md") + with open(ruta, encoding="utf-8") as f: + lineas = f.read().splitlines() + for i, ln in enumerate(lineas): + if ln.startswith(f"| {nombre} |"): + celdas = ln.split("|") + celdas[3] = f" {nuevo_valor} " + lineas[i] = "|".join(celdas) + break + else: + raise AssertionError(nombre) + with open(ruta, "w", encoding="utf-8") as f: + f.write("\n".join(lineas) + "\n") + + +def test_regresion_frente_a_la_hoja_excel(): + with open(os.path.join(DATOS, "esperado.json"), encoding="utf-8") as f: + esperado = json.load(f)["comprobaciones"] + obtenido = {c["id"]: c for c in calcula().comprobaciones} + assert set(obtenido) == set(esperado) + for cid, e in esperado.items(): + o = obtenido[cid] + assert o["estado"] == e["estado"], cid + for k in ("valor", "limite"): + if e[k] is None: + assert o[k] is None, cid + else: + assert o[k] == pytest.approx(e[k], rel=1e-9), f"{cid} {k}" + + +def test_valores_del_documento_de_requisitos(): + ctx = calcula() + assert ctx.v["V_coil_pk"] == pytest.approx(72) + r = ctx.resultados + assert r["rizado.L_EMS_min.unip_Vmax"]["valor"] == pytest.approx(1.302, abs=1e-3) # §6.1.3: 1,30 A + assert r["rizado.L_HEMS_min.unip_Vmax"]["valor"] == pytest.approx(0.403, abs=1e-3) # §6.1.3: 0,40 A + assert ctx.v["V_fault_min_map"] == pytest.approx(648.5, abs=0.5) # §6.3: ≈ 650 V + assert ctx.v["V_fault_map"] == pytest.approx(691.4, abs=0.5) # §6.3: ≈ 690 V + assert ctx.v["C_need_map"] == pytest.approx(167e-6, rel=1e-3) # §6.3: ≥ 167 µF + assert ctx.v["E_const"] == pytest.approx(48.06, abs=0.01) # §6.3: 48 J + + +def test_numeros_y_formato(): + assert parse_numero("1,92") == 1.92 + assert parse_numero("1.92") == 1.92 + assert parse_numero("−5") == -5 + assert parse_numero("1e-3") == 0.001 + assert parse_numero("Encadenado") is None + assert parse_numero("1.000,5") is None + assert fmt(1.302, 2) == "1,30" + assert fmt(-4.2255, 2) == "−4,23" + + +def test_decimal_con_punto_y_unidades(copia): + cambia_parametro(copia, "L_EMS_min", "1.92") + ctx = calcula(copia) + assert ctx.v["L_EMS_min"] == pytest.approx(1.92e-3) + assert ctx.v["dVdc_pct"] == pytest.approx(0.02) + assert ctx.v["Cdc"] == pytest.approx(105e-6) + + +def test_errores_legibles(copia): + cambia_parametro(copia, "Ipk", "sesenta") + with pytest.raises(ErrorEntradas) as e: + calcula(copia) + assert "Ipk" in str(e.value) and "no es un número" in str(e.value) + + +def test_informe_de_cambios(copia): + base = calcula(copia) + ant = resultados_json(base, META) + cambia_parametro(copia, "Cdc", "220") + nuevo = calcula(copia) + act = resultados_json(nuevo, META) + texto, hay = compara(ant, act, META) + assert hay + assert "| Capacidad del DC-link en placa (`Cdc`) | 105 µF | 220 µF |" in texto + estados = {c["id"]: c["estado"] for c in nuevo.comprobaciones} + for cid in ("POT-04", "POT-07", "SEN-08"): + assert estados[cid] == "CUMPLE" + assert f"| {cid} |" in texto + texto_igual, hay_igual = compara(act, act, META) + assert not hay_igual and "Sin cambios" in texto_igual + + +def test_informe_general_se_genera(): + ctx = calcula() + texto = informe(ctx, META) + assert "## 1. Potencia y bus DC" in texto + assert "## Anexo C. Parámetros pendientes" in texto + assert texto.count("| POT-04 |") >= 2