← Volver al sitio
SKILL · Cadence Virtuoso

extractor_gmid_saturacion.il
;; =========================================================================
;; PLUGIN: DETECTOR DE TRANSISTORES, GM/ID Y SATURACION
;; Recorre jerarquia (hasta gmidMaxDepth niveles) desde el esquematico activo
;;
;; Deteccion automatica de transistores MOS: una celda hoja (sin vista
;; schematic*) cuyo simbolo tiene pines D, G y S. La polaridad se deduce del
;; nombre de la celda y del parametro "model". "Tipos de transistores" solo
;; hace falta para forzar celdas concretas.
;; =========================================================================

unless( boundp('gmidTests) gmidTests = nil )
unless( boundp('gmidSatMargin) gmidSatMargin = "0" )
unless( boundp('gmidMaxDepth) gmidMaxDepth = 3 )

;; t = detectar transistores por sus pines; nil = usar solo las listas
unless( boundp('gmidAutoDetect) gmidAutoDetect = t )

;; Celdas forzadas como transistor (nombre de celda en minusculas). Tienen
;; prioridad sobre la deteccion automatica y fijan la polaridad.
unless( boundp('gmidNmosCells) gmidNmosCells = '("ne" "nmos" "nch" "nfet" "nelna") )
unless( boundp('gmidPmosCells) gmidPmosCells = '("pe" "pmos" "pch" "pfet" "pe3lna") )

;; Archivo de configuracion persistente
gmidConfigFile = strcat(getShellEnvVar("HOME") "/.gmid_tipos_transistores.il")
when( isFile(gmidConfigFile)
    load(gmidConfigFile)
)

;; Lista de (cellName vista). Se rellena desde el popup de destino.
unless( boundp('gmidViewOverrides) gmidViewOverrides = nil )

;; Ultimo cellview de esquematico valido visto (se actualiza cada vez que
;; gmidCvTrabajo() consigue uno de verdad). Sirve de respaldo cuando una
;; funcion se ejecuta desde el callback de un popup (OK de "Destino de las
;; expresiones", de "Mismatch", etc.): en ese momento la ventana activa es
;; el propio formulario, no el esquematico, y geGetEditCellView() directo
;; falla con *WARNING* (GE-2067).
unless( boundp('gmidCvActual) gmidCvActual = nil )

;; Otros archivos (p.ej. mismatch_pelgrom.il) pueden anadir aqui sus propios
;; items para que aparezcan en el mismo menu "Crear expresiones".
unless( boundp('gmidExtraMenuItems) gmidExtraMenuItems = nil )

gmidAccionPendiente = nil
gmidDestTestsOrdered = nil
gmidDestCeldas = nil
gmidCeldasAcc = nil
gmidHojasAcc = nil
gmidDestForm = nil
gmidTiposForm = nil
gmidMosCache = nil

;; -------------------------------------------------------------------------
;; DETECCION DE TRANSISTORES
;; -------------------------------------------------------------------------

;; Nombres de los pines del simbolo de una instancia
procedure( gmidTerminalesMaster(inst)
    let( (res)
        res = errset(inst~>master~>terminals~>name)
        if( res then car(res) else nil)
    )
)

;; Nombres de las vistas que empiezan por "schematic" de una celda
procedure( gmidVistasSchematic(libName cellName)
    let( (objRes obj nombres)
        nombres = nil
        objRes = errset(ddGetObj(libName cellName))
        obj = if( objRes then car(objRes) else nil)
        when( obj
            nombres = setof(n obj~>views~>name rexMatchp("^schematic" n))
        )
        nombres
    )
)

;; Deteccion por pines: D, G y S, y sin vista schematic* (primitiva)
procedure( gmidDetectarMos(inst)
    let( (nombres l hasD hasG hasS sinSchem)
        if( gmidAutoDetect then
            nombres = gmidTerminalesMaster(inst)
            l = mapcar(lambda((x) if(stringp(x) lowerCase(x) "")) nombres)
            hasD = member("d" l) || member("drain" l)
            hasG = member("g" l) || member("gate" l)
            hasS = member("s" l) || member("source" l)
            sinSchem = !gmidVistasSchematic(inst~>libName inst~>cellName)
            if( hasD && hasG && hasS && sinSchem then t else nil)
        else
            nil
        )
    )
)

;; Es transistor si esta en las listas forzadas o lo detecta por pines.
;; El resultado se guarda en cache por lib/celda (gmidMosCache).
procedure( gmidEsMos(inst)
    let( (key ent res cn)
        key = sprintf(nil "%s/%s" inst~>libName inst~>cellName)
        ent = assoc(key gmidMosCache)
        if( ent then
            res = eq(cadr(ent) 'yes)
        else
            cn = lowerCase(or(inst~>cellName ""))
            if( member(cn gmidNmosCells) || member(cn gmidPmosCells) then
                res = t
            else
                res = gmidDetectarMos(inst)
            )
            gmidMosCache = cons(list(key if(res 'yes 'no)) gmidMosCache)
        )
        res
    )
)

;; Polaridad segun un texto (nombre de celda o modelo): 'n, 'p o nil
procedure( gmidPolaridadStr(s)
    if( stringp(s) then
        cond(
            ( rexMatchp("pmos" s) 'p )
            ( rexMatchp("pfet" s) 'p )
            ( rexMatchp("pch" s) 'p )
            ( rexMatchp("nmos" s) 'n )
            ( rexMatchp("nfet" s) 'n )
            ( rexMatchp("nch" s) 'n )
            ( rexMatchp("^p" s) 'p )
            ( rexMatchp("^n" s) 'n )
            ( t nil )
        )
    else nil)
)

;; Polaridad de una instancia: 'n, 'p o 'u (desconocida)
procedure( gmidPolaridad(inst)
    let( (cn val mdl)
        cn = lowerCase(or(inst~>cellName ""))
        cond(
            ( member(cn gmidNmosCells) 'n )
            ( member(cn gmidPmosCells) 'p )
            ( t
                val = gmidGetInstParam(inst "model")
                mdl = if( stringp(val) then lowerCase(val) else "")
                or(gmidPolaridadStr(cn) gmidPolaridadStr(mdl) 'u)
            )
        )
    )
)

;; Vista a usar para una celda: la elegida en el popup; si no hay eleccion,
;; "schematic" si existe, o la primera vista schematic* que tenga.
;; Devuelve nil si la celda no tiene ninguna vista schematic* (primitiva):
;; en ese caso no se intenta abrir.
procedure( gmidResolverVista(libName cellName)
    let( (ov vistas)
        ov = assoc(cellName gmidViewOverrides)
        vistas = if( ov then nil else gmidVistasSchematic(libName cellName))
        cond(
            ( ov cadr(ov) )
            ( member("schematic" vistas) "schematic" )
            ( vistas car(vistas) )
            ( t nil )
        )
    )
)

;; Evalua de forma segura un string como expresion SKILL (para numeros o
;; variables de diseno). Si la variable no existe o la evaluacion falla
;; por cualquier motivo, devuelve nil en vez de lanzar un error que
;; interrumpiria todo el recorrido de la jerarquia.
procedure( gmidSafeEvalNum(s)
    let( (r)
        r = errset(evalstring(s))
        if( r && numberp(car(r)) then car(r) else nil)
    )
)

;; Dice si una ventana es un entorno grafico editable (esquematico/layout)
;; sin llamar a geGetEditCellView(w) directamente: esa llamada, sobre una
;; ventana que no lo es (CIW, log, un popup), imprime el *WARNING* (GE-2067)
;; aunque este dentro de un errset. Comprobar la propiedad editCellView del
;; objeto ventana evita el aviso.
procedure( gmidEsVentanaGrafica(w)
    let( (r)
        r = errset(w~>editCellView)
        r != nil && car(r) != nil
    )
)

;; cv de trabajo robusto frente al popup "Destino de las expresiones" (y al
;; de Mismatch): cuando una funcion se ejecuta desde el callback OK de un
;; formulario, la ventana activa es el formulario, no el esquematico, y
;; geGetEditCellView() directo falla con *WARNING* (GE-2067) porque esa
;; ventana no es un entorno grafico editable. Si la llamada directa
;; funciona (p.ej. invocado desde el menu del esquematico o desde el CIW
;; con el esquematico como ultima ventana activa de verdad), se usa y
;; ademas se guarda en gmidCvActual; si falla, se devuelve el ultimo cv
;; bueno conocido.
procedure( gmidCvTrabajo()
    let( (wRes w r cv)
        wRes = errset(hiGetCurrentWindow())
        w = if( wRes then car(wRes) else nil)
        cv = if( w && windowp(w) && gmidEsVentanaGrafica(w) then
                r = errset(geGetEditCellView(w))
                if( r then car(r) else nil)
            else nil)
        if( cv then
            gmidCvActual = cv
            cv
        else
            gmidCvActual
        )
    )
)

;; Parseo de valores tipo "40u" / "180.0n" / "2u" a micras (float).
;; Si el valor es una variable de diseno parametrizada (p.ej. "MULT_DIFF_PAIR",
;; sin parte numerica reconocible, o que no se puede evaluar), devuelve nil
;; en vez de fallar: el transistor queda sin W/L conocido para este calculo.
procedure( gmidParseLenToUm(str)
    let( (n i numStr suf mult numVal)
        n = strlen(str)
        i = n
        while( i > 0 &&
               member(substring(str i 1) '("0" "1" "2" "3" "4" "5" "6" "7" "8" "9" "." "e" "E" "+" "-"))
            i = i - 1
        )
        numStr = substring(str 1 i)
        suf = lowerCase(if( i < n then substring(str i+1 n-i) else ""))
        mult = case( suf
            ("" 1.0)
            ("f" 1.0e-15) ("p" 1.0e-12) ("n" 1.0e-9)
            ("u" 1.0e-6) ("m" 1.0e-3)
            ("k" 1.0e3) ("meg" 1.0e6) ("g" 1.0e9)
            (t nil)
        )
        numVal = gmidSafeEvalNum(numStr)
        if( mult == nil || numVal == nil then nil else (numVal * mult * 1.0e6))
    )
)

;; Lee un parametro (w/l/m/model) del CDF resuelto de una instancia
procedure( gmidGetInstParam(inst paramName)
    let( (cdfRes cdfId p val)
        val = nil
        cdfRes = errset(cdfGetInstCDF(inst))
        cdfId = if( cdfRes then car(cdfRes) else nil)
        when( cdfId
            foreach( p cdfId~>parameters
                when( p~>name == paramName val = p~>value )
            )
        )
        val
    )
)

;; Devuelve list(w_um l_um m) para una instancia de transistor. Si W, L o m
;; estan parametrizados con una variable que no se puede evaluar a numero,
;; ese campo queda en nil (no se interrumpe el calculo de los demas).
procedure( gmidGetWLM(inst)
    let( (wStr lStr mStr w l m mVal)
        wStr = gmidGetInstParam(inst "w")
        lStr = gmidGetInstParam(inst "l")
        mStr = gmidGetInstParam(inst "m")
        w = if( wStr then gmidParseLenToUm(wStr) else nil)
        l = if( lStr then gmidParseLenToUm(lStr) else nil)
        mVal = if( mStr then gmidSafeEvalNum(mStr) else nil)
        m = if( mVal then (mVal * 1.0) else if( mStr then nil else 1.0))
        list(w l m)
    )
)

;; Devuelve lista de (fullPath polaridad w_um l_um m), con polaridad 'n 'p 'u
procedure( RecorrerJerarquiaGmId(cv path depth maxDepth)
    let( (resultado fullPath subCvRes subCv vName wlm)
        resultado = nil
        when( cv && cv~>instances && depth <= maxDepth
            foreach( inst cv~>instances
                fullPath = if( path == "" then inst~>name else sprintf(nil "%s/%s" path inst~>name))
                if( gmidEsMos(inst) then
                    wlm = gmidGetWLM(inst)
                    resultado = cons(list(fullPath gmidPolaridad(inst) car(wlm) cadr(wlm) caddr(wlm)) resultado)
                else
                    vName = gmidResolverVista(inst~>libName inst~>cellName)
                    when( vName
                        subCvRes = errset(dbOpenCellViewByType(inst~>libName inst~>cellName vName nil "r"))
                        subCv = if( subCvRes then car(subCvRes) else nil)
                        when( subCv
                            resultado = append(RecorrerJerarquiaGmId(subCv fullPath depth+1 maxDepth) resultado)
                            dbClose(subCv)
                        )
                    )
                )
            )
        )
        resultado
    )
)

;; Recorre la jerarquia y acumula en gmidCeldasAcc las celdas que tienen
;; alguna vista schematic*: lista de (cellName listaDeVistas)
procedure( gmidDescubrirCeldas(cv depth maxDepth)
    let( (vistas vName subCvRes subCv)
        when( cv && cv~>instances && depth <= maxDepth
            foreach( inst cv~>instances
                unless( gmidEsMos(inst)
                    vistas = gmidVistasSchematic(inst~>libName inst~>cellName)
                    when( vistas
                        unless( assoc(inst~>cellName gmidCeldasAcc)
                            gmidCeldasAcc = cons(list(inst~>cellName vistas) gmidCeldasAcc)
                            vName = gmidResolverVista(inst~>libName inst~>cellName)
                            when( vName
                                subCvRes = errset(dbOpenCellViewByType(inst~>libName inst~>cellName vName nil "r"))
                                subCv = if( subCvRes then car(subCvRes) else nil)
                                when( subCv
                                    gmidDescubrirCeldas(subCv depth+1 maxDepth)
                                    dbClose(subCv)
                                )
                            )
                        )
                    )
                )
            )
        )
        t
    )
)

;; Diagnostico: acumula las celdas hoja (sin vista schematic*) que NO se han
;; detectado como transistor, con sus pines: lista de (lib/celda pines)
procedure( gmidRecogerHojas(cv depth maxDepth)
    let( (vName subCvRes subCv clave nombres pines)
        when( cv && cv~>instances && depth <= maxDepth
            foreach( inst cv~>instances
                unless( gmidEsMos(inst)
                    vName = gmidResolverVista(inst~>libName inst~>cellName)
                    if( vName then
                        subCvRes = errset(dbOpenCellViewByType(inst~>libName inst~>cellName vName nil "r"))
                        subCv = if( subCvRes then car(subCvRes) else nil)
                        when( subCv
                            gmidRecogerHojas(subCv depth+1 maxDepth)
                            dbClose(subCv)
                        )
                    else
                        clave = sprintf(nil "%s/%s" inst~>libName inst~>cellName)
                        unless( assoc(clave gmidHojasAcc)
                            nombres = gmidTerminalesMaster(inst)
                            pines = if( nombres then buildString(nombres ",") else "?")
                            gmidHojasAcc = cons(list(clave pines) gmidHojasAcc)
                        )
                    )
                )
            )
        )
        t
    )
)

;; Diagnostico: celdas hoja que NO se cuentan como transistor, con sus pines.
;; Si ves algun transistor en la lista, anadelo desde "Tipos de transistores".
procedure( ListarHojasGmId()
    let( (cv)
        cv = gmidCvTrabajo()
        gmidMosCache = nil
        gmidHojasAcc = nil
        gmidRecogerHojas(cv 1 gmidMaxDepth)
        printf("Celdas hoja no detectadas como transistor:\n")
        foreach( h gmidHojasAcc
            printf("  %s   pines: %s\n" car(h) cadr(h))
        )
        t
    )
)

;; Diagnostico: transistores detectados, con polaridad y W/L/m
procedure( ListarTransistoresGmId()
    let( (cv xt)
        cv = gmidCvTrabajo()
        gmidMosCache = nil
        xt = RecorrerJerarquiaGmId(cv "" 1 gmidMaxDepth)
        printf("Transistores detectados: %d\n" length(xt))
        foreach( par xt
            printf("  %s  polaridad=%L  W=%L L=%L m=%L\n"
                car(par) cadr(par) caddr(par) cadddr(par) car(cddddr(par)))
        )
        t
    )
)

;; -------------------------------------------------------------------------
;; TIPOS DE TRANSISTORES (opcional): fuerza celdas y polaridad. Se guarda en
;; disco y se carga solo en cada sesion.
;; -------------------------------------------------------------------------

;; "a b, c" -> ("a" "b" "c") en minusculas
procedure( gmidParseCeldas(str)
    let( (res)
        res = nil
        when( str && str != ""
            foreach( p parseString(str " ,;")
                when( p != ""
                    res = cons(lowerCase(p) res)
                )
            )
        )
        reverse(res)
    )
)

;; Escribe la configuracion actual en el archivo persistente
procedure( gmidGuardarTipos()
    let( (fid)
        fid = outfile(gmidConfigFile)
        when( fid
            fprintf(fid ";; Configuracion de tipos de transistores (generado automaticamente)\n")
            fprintf(fid "gmidNmosCells = '%L\n" gmidNmosCells)
            fprintf(fid "gmidPmosCells = '%L\n" gmidPmosCells)
            close(fid)
            printf("Tipos de transistores guardados en %s\n" gmidConfigFile)
        )
        unless( fid
            printf("No se pudo escribir %s\n" gmidConfigFile)
        )
        t
    )
)

procedure( TiposTransistoresGmId()
    let( (nStr pStr)
        nStr = if( gmidNmosCells then buildString(gmidNmosCells " ") else "")
        pStr = if( gmidPmosCells then buildString(gmidPmosCells " ") else "")
        gmidTiposForm = hiCreateAppForm(
            ?name 'gmidTiposForm
            ?formTitle "Tipos de transistores (opcional)"
            ?fields list(
                hiCreateStringField(
                    ?name 'gmidTiposNmos
                    ?prompt "Celdas NMOS forzadas (separadas por espacios)"
                    ?value nStr
                )
                hiCreateStringField(
                    ?name 'gmidTiposPmos
                    ?prompt "Celdas PMOS forzadas (separadas por espacios)"
                    ?value pStr
                )
            )
            ?buttonLayout 'OKCancel
            ?callback "GuardarTiposGmId()"
        )
        hiDisplayForm(gmidTiposForm)
    )
)

procedure( GuardarTiposGmId()
    let( (f nStr pStr)
        f = hiGetCurrentForm()
        nStr = (f->gmidTiposNmos)~>value
        pStr = (f->gmidTiposPmos)~>value
        gmidNmosCells = gmidParseCeldas(nStr)
        gmidPmosCells = gmidParseCeldas(pStr)
        gmidMosCache = nil
        printf("NMOS: %L | PMOS: %L\n" gmidNmosCells gmidPmosCells)
        gmidGuardarTipos()
    )
)

;; Devuelve los tests elegidos (que existan en la sesion). nil si no hay
;; ninguno elegido: en ese caso no se crea nada.
procedure( gmidTestsElegidos(sess)
    if( gmidTests
        then setof(tn maeGetSetup(?session sess) member(tn gmidTests))
        else nil)
)

;; Resuelve sesion, tests y transistores para el esquematico activo
procedure( gmidPrepare(cv)
    let( (sess tests xtors)
        gmidMosCache = nil
        sess = car(maeGetSessions())
        unless( sess
            sess = maeOpenSetup(cv~>libName cv~>cellName "maestro" ?mode "a"))
        tests = gmidTestsElegidos(sess)
        xtors = RecorrerJerarquiaGmId(cv "" 1 gmidMaxDepth)
        list(sess tests xtors)
    )
)

;; Lee el valor (lista posicional t/nil) del selector de vista numero idx
;; (campos gmidDestView0 ... gmidDestView7). Devuelve nil si no existe.
procedure( gmidLeerVistaField(f idx)
    let( (r)
        r = cond(
            ( idx == 0 errset((f->gmidDestView0)~>value) )
            ( idx == 1 errset((f->gmidDestView1)~>value) )
            ( idx == 2 errset((f->gmidDestView2)~>value) )
            ( idx == 3 errset((f->gmidDestView3)~>value) )
            ( idx == 4 errset((f->gmidDestView4)~>value) )
            ( idx == 5 errset((f->gmidDestView5)~>value) )
            ( idx == 6 errset((f->gmidDestView6)~>value) )
            ( idx == 7 errset((f->gmidDestView7)~>value) )
            ( t nil )
        )
        if( r then car(r) else nil)
    )
)

;; -------------------------------------------------------------------------
;; POPUP DE DESTINO: test(s) + vista de esquematico. Se abre cada vez y, al
;; pulsar OK, ejecuta la accion pedida (string con la llamada, p.ej.
;; "GenerarGmId()").
;; -------------------------------------------------------------------------
procedure( gmidPedirDestino(accion)
    let( (cv sess tests choices i sym fields c vchoices vnames j vsym nVw)
        cv = gmidCvTrabajo()
        sess = car(maeGetSessions())
        unless( sess
            printf("Abre primero el Maestro (ADE Assembler) de este esquematico.\n")
        )
        when( sess && cv && cv~>cellViewType == "schematic"
            gmidAccionPendiente = accion
            gmidMosCache = nil

            ;; Campo de tests
            tests = maeGetSetup(?session sess)
            gmidDestTestsOrdered = nil
            choices = nil
            i = 0
            foreach( tn tests
                sym = concat('gmidTst i)
                choices = cons(list(sym tn) choices)
                gmidDestTestsOrdered = cons(tn gmidDestTestsOrdered)
                i = i + 1
            )
            fields = list(
                hiCreateToggleField(
                    ?name 'gmidDestTests
                    ?choices choices
                    ?numSelect length(choices)
                    ?prompt "Test(s) donde crear las expresiones"
                    ?value nil
                )
            )

            ;; Un selector de vista por cada celda con varias vistas schematic*
            ;; (maximo 8)
            gmidCeldasAcc = nil
            gmidDescubrirCeldas(cv 1 gmidMaxDepth)
            gmidDestCeldas = nil
            nVw = 0
            foreach( c reverse(gmidCeldasAcc)
                when( length(cadr(c)) > 1 && nVw < 8
                    vchoices = nil
                    vnames = nil
                    j = 0
                    foreach( vn cadr(c)
                        vsym = concat('gmidVw nVw 'x j)
                        vchoices = cons(list(vsym vn) vchoices)
                        vnames = cons(vn vnames)
                        j = j + 1
                    )
                    fields = append(fields list(
                        hiCreateToggleField(
                            ?name concat('gmidDestView nVw)
                            ?choices vchoices
                            ?numSelect 1
                            ?prompt sprintf(nil "Vista de %s" car(c))
                            ?value nil
                        )
                    ))
                    gmidDestCeldas = cons(list(car(c) vnames nVw) gmidDestCeldas)
                    nVw = nVw + 1
                )
            )

            gmidDestForm = hiCreateAppForm(
                ?name 'gmidDestForm
                ?formTitle "Destino de las expresiones"
                ?fields fields
                ?buttonLayout 'OKCancel
                ?callback "GuardarDestinoGmId()"
            )
            hiDisplayForm(gmidDestForm)
        )
    )
)

procedure( GuardarDestinoGmId()
    let( (f selT selList nameList elegidos valores entry idx vistaElegida nuevo kept)
        f = hiGetCurrentForm()

        ;; Tests elegidos (lista posicional t/nil)
        selT = (f->gmidDestTests)~>value
        elegidos = nil
        selList = selT
        nameList = gmidDestTestsOrdered
        while( selList
            when( car(selList)
                elegidos = cons(car(nameList) elegidos)
            )
            selList = cdr(selList)
            nameList = cdr(nameList)
        )
        gmidTests = elegidos

        ;; Vistas elegidas por celda (si no eliges ninguna, se conserva la anterior)
        nuevo = nil
        foreach( entry gmidDestCeldas
            idx = caddr(entry)
            valores = gmidLeerVistaField(f idx)
            vistaElegida = nil
            selList = valores
            nameList = cadr(entry)
            while( selList
                when( car(selList)
                    vistaElegida = car(nameList)
                )
                selList = cdr(selList)
                nameList = cdr(nameList)
            )
            when( vistaElegida
                nuevo = cons(list(car(entry) vistaElegida) nuevo)
            )
        )
        kept = setof(o gmidViewOverrides !assoc(car(o) nuevo))
        gmidViewOverrides = append(nuevo kept)

        printf("Destino: tests %L | vistas %L\n" gmidTests gmidViewOverrides)
        if( gmidTests
            then evalstring(gmidAccionPendiente)
            else printf("No has elegido ningun test: no se crea nada.\n"))
    )
)

;; Entradas de menu: piden el destino y luego ejecutan la accion
procedure( GmIdConDestino()
    gmidPedirDestino("GenerarGmId()")
)

procedure( SaturacionConDestino()
    gmidPedirDestino("GenerarSaturacion()")
)

;; -------------------------------------------------------------------------
;; GM/ID
;; -------------------------------------------------------------------------
procedure( GenerarGmId()
    let( (cv prep sess tests xtors nN nP nU unk n1 r1 nErr path)
        cv = gmidCvTrabajo()
        when( cv && cv~>cellViewType == "schematic"
            prep = gmidPrepare(cv)
            sess = car(prep)
            tests = cadr(prep)
            xtors = caddr(prep)
            nN = 0
            nP = 0
            nU = 0
            unk = nil
            foreach( par xtors
                cond(
                    ( eq(cadr(par) 'n) nN = nN + 1 )
                    ( eq(cadr(par) 'p) nP = nP + 1 )
                    ( t nU = nU + 1 unk = cons(car(par) unk) )
                )
            )
            nErr = 0
            foreach( tst tests
                foreach( par xtors
                    path = car(par)
                    n1 = sprintf(nil "gmid_%s" gmidSafeName(path))
                    errset(maeDeleteOutput(n1 tst ?session sess))
                    r1 = maeAddOutput(n1 tst ?outputType "point"
                        ?expr sprintf(nil "OP(\"/%s\" \"gm\") / abs(OP(\"/%s\" \"id\"))" path path)
                        ?session sess)
                    unless( r1
                        nErr = nErr + 1
                        printf("Fallo gm/Id %s en %s\n" path tst))
                )
            )
            if( tests then
                maeSaveSetup(?session sess)
                printf("gm/Id: %d NMOS, %d PMOS, %d sin polaridad | tests %L | %d fallos\n"
                    nN nP nU tests nErr)
                when( nU > 0
                    printf("Aviso: polaridad desconocida en %L. gm/Id no la necesita, pero Mismatch si: anade su celda en Tipos de transistores.\n" unk)
                )
            else
                printf("Ningun test elegido.\n")
            )
        )
    )
)

;; -------------------------------------------------------------------------
;; SATURACION
;;
;; vdsmargin = abs(vds) - abs(vdsat). Guia de interpretacion:
;;   > ~50-100 mV  -> bien saturado
;;   0-50 mV       -> al limite
;;   negativo      -> en triodo
;; -------------------------------------------------------------------------
procedure( GenerarSaturacion()
    let( (cv prep sess tests xtors n2 r2 r3 nErr path)
        cv = gmidCvTrabajo()
        when( cv && cv~>cellViewType == "schematic"
            prep = gmidPrepare(cv)
            sess = car(prep)
            tests = cadr(prep)
            xtors = caddr(prep)
            nErr = 0
            foreach( tst tests
                foreach( par xtors
                    path = car(par)
                    n2 = sprintf(nil "vdsmargin_%s" gmidSafeName(path))
                    errset(maeDeleteOutput(n2 tst ?session sess))
                    r2 = maeAddOutput(n2 tst ?outputType "point"
                        ?expr sprintf(nil "abs(OP(\"/%s\" \"vds\")) - abs(OP(\"/%s\" \"vdsat\"))" path path)
                        ?session sess)
                    r3 = maeSetSpec(n2 tst ?gt gmidSatMargin ?session sess)
                    unless( r2 && r3
                        nErr = nErr + 1
                        printf("Fallo saturacion %s en %s\n" path tst))
                )
            )
            if( tests then
                maeSaveSetup(?session sess)
                printf("Saturacion: %d transistores | tests %L | spec > %s | %d fallos\n"
                    length(xtors) tests gmidSatMargin nErr)
            else
                printf("Ningun test elegido.\n")
            )
        )
    )
)

;; -------------------------------------------------------------------------
;; MENU (admite items extra anadidos por otros archivos via gmidExtraMenuItems)
;; -------------------------------------------------------------------------
procedure( RegistrarMiMenuGmId(args)
    let( (itemGmid itemSat itemTipos menuCustom win allItems)
        win = if( windowp(args) then args else args->window )
        when( win && windowp(win)
            itemGmid = hiCreateMenuItem(
                ?name 'ItemGmId
                ?itemText "gm/Id"
                ?callback "GmIdConDestino()"
            )
            itemSat = hiCreateMenuItem(
                ?name 'ItemSat
                ?itemText "Saturación"
                ?callback "SaturacionConDestino()"
            )
            itemTipos = hiCreateMenuItem(
                ?name 'ItemTipos
                ?itemText "Tipos de transistores"
                ?callback "TiposTransistoresGmId()"
            )
            allItems = append(list(itemGmid itemSat itemTipos) gmidExtraMenuItems)
            menuCustom = hiCreatePulldownMenu(
                'MenuGmId
                "Crear expresiones"
                allItems
            )
            hiInsertBannerMenu(win menuCustom length(hiGetBannerMenus(win)))
        )
    )
)

;; Quita el menu "Crear expresiones" de todas las ventanas donde ya este
;; puesto. ChequearMenusGmId() lo vuelve a anadir en su siguiente ciclo
;; (hasta 1s despues) con la lista de items actualizada: hace falta cuando
;; se carga otro archivo (p.ej. mismatch_pelgrom.il) que anade su propio
;; item a gmidExtraMenuItems y quiere verlo sin esperar o sin reabrir el
;; esquematico.
procedure( gmidQuitarMenus()
    foreach( w hiGetWindowList()
        when( windowp(w) && member('MenuGmId hiGetBannerMenus(w))
            errset(hiRemoveBannerMenu(w 'MenuGmId))
        )
    )
    t
)

procedure( ChequearMenusGmId()
    errset(
        foreach( w hiGetWindowList()
            errset(
                when( windowp(w) && gmidEsVentanaGrafica(w) &&
                      !member('MenuGmId hiGetBannerMenus(w))
                    RegistrarMiMenuGmId(list(nil 'window w))
                )
            )
        )
    )
    hiRegTimer("ChequearMenusGmId()" 1000)
)

ChequearMenusGmId()