0% encontró este documento útil (0 votos)
105 vistas

Vivado

Este documento describe la síntesis de alto nivel con Vivado HLS. Explica que la síntesis de alto nivel convierte una descripción algorítmica en C en una descripción de hardware a nivel de registro de transferencia (RTL) para acelerar el diseño de hardware. Detalla que la extracción del control y el flujo de datos del código C se traduce en un diseño de máquina de estado finito y un comportamiento de flujo de control y datos. Además, explica que la programación y el enlace son fundamentales en HLS, donde la programación

Cargado por

Alex Santana
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PPTX, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
105 vistas

Vivado

Este documento describe la síntesis de alto nivel con Vivado HLS. Explica que la síntesis de alto nivel convierte una descripción algorítmica en C en una descripción de hardware a nivel de registro de transferencia (RTL) para acelerar el diseño de hardware. Detalla que la extracción del control y el flujo de datos del código C se traduce en un diseño de máquina de estado finito y un comportamiento de flujo de control y datos. Además, explica que la programación y el enlace son fundamentales en HLS, donde la programación

Cargado por

Alex Santana
Derechos de autor
© © All Rights Reserved
Formatos disponibles
Descarga como PPTX, PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 54

Objetivos

Después de completar este módulo, podrá:

Describir el flujo de síntesis de alto nivel


Comprender el control y la extracción de rutas de datos
Describir las fases de programación y enlace del flujo HLS
Enumerar las prioridades de las directivas establecidas por Vivado HLS
Identificar los pasos involucrados en los flujos de validación y verificación
Introducción a la síntesis de alto nivel
Síntesis de alto nivel con Vivado HLS
Flujo de validación
Necesidades de síntesis de alto nivel
Los enfoques basados en algoritmos se están volviendo populares debido al tiempo de
diseño acelerado y el tiempo de comercialización (TTM)
Los diseños más grandes plantean desafíos en el diseño y la verificación de hardware a nivel HDL

La tendencia de la industria se está moviendo hacia la aceleración de hardware para


mejorar el rendimiento y la productividad
Las tareas intensivas de CPU se pueden descargar al acelerador de hardware en FPGA
Los aceleradores de hardware requieren mucho tiempo para comprender y diseñar

La herramientaVivado HLS convierte la descripción algorítmica escrita en el flujo de


diseño basado en C en descripción de hardware (RTL)
Eleva el nivel de abstracción de RTL a algoritmos

La síntesis
de alto nivel es esencial para mantener la productividad del diseño para
diseños grandes.
Síntesis de alto nivel: HLS

Síntesis de alto nivel


Crea una implementación RTL a partir de código
kernel C, C++, System C, OpenCL API C

Extrae el control y el flujo de datos del código fuente

Implementa el diseño en función de los valores


predeterminados y las directivas aplicadas por el
usuario

Muchas implementaciones son


posibles a partir de la misma
descripción de origen
Diseños más pequeños, diseños más rápidos,
diseños óptimos
Permite la exploración del diseño
Exploración de diseño con directivas

loop: for (i=3;i>=0;i--) {
Un cuerpo de código: if
if (i==0)
(i==0) {{
Antes de entrar en detalles, echemos
Muchos resultados de hardware acc+=x*c[0];
shift_reg[0]=x;
un vistazo bajo el principal....
} else {
shift_reg[i]=shift_reg[i-1];
shift_reg[i]=shift_reg[i-1];
acc+=shift_reg[i]*c[i];
acc+=shift_reg[i]*c[i];
}
}
….

Se utiliza el mismo hardware para cada iteración Se utiliza hardware diferente para cada iteración Se ejecutan diferentes iteraciones
del bucle: del bucle: simultáneamente:
Área pequeña Zona más alta Zona más alta
Latencia larga Latencia corta Latencia corta
Bajo rendimiento Mejor rendimiento Mejor rendimiento
Introducción a la síntesis de alto nivel

¿Cómo se extrae el hardware del código C?


El control y la ruta de datos se pueden extraer del código C en el nivel superior

Los mismos principios utilizados en el ejemplo se pueden aplicar a las subredes


En algún momento del flujo de control de nivel superior, el control se pasa a una subfunción

La subfunción se puede implementar para ejecutarse simultáneamente con el nivel superior y otras subfunciones

¿Cómo se convierte este control y flujo de datos en un diseño de hardware?


Vivado HLS asigna esto al hardware a través de procesos de programación y enlace

¿Cómo se crea mi diseño?


¿Cómo se asignan las funciones, los bucles, las matrices y los puertos de E/S?
HLS: Extracción de control

Code Control Behavior

void
voidfirfir(( Finite State Machine (FSM)
data_t
data_t*y, *y, states
coef_t
coef_tc[4],c[4],
data_t
data_txx
)){{ Function Start
static
staticdata_t
data_tshift_reg[4];
shift_reg[4];
acc_t acc;
acc_t acc;
int
0
inti;i;

acc=0;
acc=0;
loop:
loop:for for(i=3;i>=0;i--)
(i=3;i>=0;i--){{ For-Loop Start
ifif(i==0)
(i==0){{
acc+=x*c[0];
acc+=x*c[0]; 1
shift_reg[0]=x;
shift_reg[0]=x;
}}else
else{{
shift_reg[i]=shift_reg[i-1];
shift_reg[i]=shift_reg[i-1];
acc+=shift_reg[i]*c[i];
acc+=shift_reg[i]*c[i];
}}
}} For-Loop End
*y=acc;
}}
*y=acc; 2
Function End

De cualquier ejemplo de Los bucles en el código C correlacionados Este comportamiento se extrae en un equipo
código C .. con estados de comportamiento de estado de hardware
HLS: Control y extracción de rutas de datos

Code Operations Control Behavior Control & Datapath Behavior

void
voidfirfir(( Finite State Machine (FSM) Control Dataflow
data_t
data_t*y, *y, states
coef_t
coef_tc[4],c[4],
data_t RDx
data_txx
)){{
RDc
static
staticdata_t
data_tshift_reg[4];
shift_reg[4];
acc_t acc;
acc_t acc; >= 0
int
inti;i; RDx RDc
-
acc=0; >= -
acc=0;
loop:
loop:for for(i=3;i>=0;i--)
(i=3;i>=0;i--){{ == == -
ifif(i==0)
(i==0){{
acc+=x*c[0];
acc+=x*c[0]; + 1 + *
shift_reg[0]=x;
shift_reg[0]=x;
}}else
else{{ * + *
shift_reg[i]=shift_reg[i-1];
shift_reg[i]=shift_reg[i-1];
acc+=shift_reg[i]*c[i];
acc+=shift_reg[i]*c[i]; +
}}
}} *
*y=acc;
}}
*y=acc;
WRy
2 WRy

De cualquier ejemplo de Las operaciones se El control es Se crea un comportamiento de flujo


código C .. extraen... conocido de datos de control unificado.
Síntesis de alto nivel: programación y encuadernación
Programación y encuadernación
La programación y la vinculación están en el corazón de HLS

La programación determina en qué ciclo de reloj se producirá una operación


Tiene en cuenta las directivas de control, flujo de datos y usuario

La asignación de recursos puede verse limitada

El enlace determina qué celda de biblioteca se utiliza para cada operación


Tiene en cuenta los retrasos de los componentes, las directivas de usuario

Design Source Technology


(C, C++, SystemC) Library

Scheduling Binding

User RTL
(Verilog, VHDL, SystemC)
Directives
Planificación
Las operaciones en el gráfico de flujo de control se asignan en ciclos de reloj

void foo ( a
… *
t1 = a * b;
b
+
t2 = c + t1; c
t3 = d * t2; d *
out = t3 – e;
} e - out

Schedule 1
* + * -

La tecnología y las restricciones del usuario afectan el cronograma


Una tecnología más rápida (o un reloj más lento) puede permitir que se realicen más operaciones en el mismo ciclo de reloj.

Schedule 2 * + * -

El código también afecta a la programación


Las implicaciones del código y las dependencias de datos deben obedecerse
Binding
El enlace es donde las operaciones se asignan a los núcleos de la biblioteca de hardware
Los operadores se asignan a los núcleos

* + * -
Decisión vinculante: compartir
Teniendo en cuenta este cronograma:
El enlace debe usar 2 multiplicadores, ya que ambos están en el mismo ciclo
Puede decidir usar un sumador y un subtractor o compartir un addub

* + * -
Decisión vinculante: o no compartir
Teniendo en cuenta este cronograma:
Binding puede decidir compartir los multiplicadores (cada uno se utiliza en un ciclo diferente)
O puede decidir que el costo de compartir (muxing) afectaría el tiempo y puede decidir no compartirlos.
También puede tomar esta misma decisión en el primer ejemplo anterior.
Introducción a la síntesis de alto nivel
Síntesis de alto nivel con Vivado HLS
Flujo de validación
RTL vs Lenguaje de Alto Nivel
Beneficios de Vivado HLS

Productividad
– Verificación
• Funcional Video Design Example

• Arquitectónico Input C Simulation Time RTL Simulation Time Improvement

– Abstracción 10 frames 10s ~2 days ~12000x


1280x720 (ModelSim)
• Datatypes
• Interface
• Clases
– Automatización RTL (Spec) RTL (Sim)

C (Spec/Sim) RTL (Sim)

Especificación a nivel de bloque y verificación significativamente reducida


Beneficios de Vivado HLS

Portabilidad
– Procesadores y FPGA
– Migración de tecnología
– Reducción de costes
– Reducción de potencia

Diseño y reutilización ip RTL

RTL

RTL
RTL
RTL

RTL
Beneficios de Vivado HLS

Permutabilidad Untimed
Untimed C C
Specification
Specification
– Exploración de arquitectura

Area
• Timing
– Parallelization
RTL
– Pipelining
• Resources
– Sharing

eess
ssoouurrcc
– Mejor QoR

reRRee
SShhaare
RTL
RTL
rt Pipe
Insert
Inse liness
Pipeline
s
opps
RTL ll LLooo
RTL oll
nrro
Un
U
rtitio
Parti
Pa n Ar
tion rays
Arra ys

Timed RTL
Architectures

Performance
La rápida exploración de diseño ofrece QoR que rivaliza con RTL
codificado a mano
Entendiendo la síntesis de Vivado HLS

Vivado HLS
Determina en qué operaciones de ciclo deben realizarse (programación)
Determina qué unidades de hardware utilizar para cada operación (enlace)
– Realiza síntesis de alto nivel mediante:
– Obedecer los valores predeterminados integrados
– Obedecer las directivas y restricciones del usuario para anular los valores predeterminados
• Cálculo de retrasos y área utilizando la tecnología/dispositivo especificado
• Prioridad de las directivas en Vivado HLS
Conozca el rendimiento (clock & throughput)
Vivado HLS permitirá que una ruta de reloj local falle si esto es necesario para cumplir con el rendimiento
A menudo es posible que el tiempo se pueda cumplir después de la síntesis lógica
A continuación, minimice la latencia
A continuación, minimice el área
Los atributos clave del código C

Functions: Todo el código se compone de funciones que representan la


void fir ( jerarquía de diseño: lo mismo en hardware
data_t *y,
coef_t c[4],
data_t x Top Level IO : Los argumentos de la función de nivel superior determinan
){ los puertos de interfaz RTL de hardware
static data_t shift_reg[4];
acc_t acc; Types: Todas las variables son de un tipo definido. El tipo puede influir en
int i; el área y el rendimiento
acc=0;
loop: for (i=3;i>=0;i--) { Loops: Las funciones suelen contener bucles. La forma en que se manejan
if (i==0) {
acc+=x*c[0]; estos puede tener un gran impacto en el área y el rendimiento.
shift_reg[0]=x;
} else {
shift_reg[i]=shift_reg[i-1]; Arrays: Las matrices se utilizan a menudo en código C. Pueden influir en la
acc+=shift_reg[i] * c[i];
}
E/S del dispositivo y convertirse en cuellos de botella de rendimiento.
}
*y=acc;
}
Operators: Los operadores en el código C pueden requerir el uso
compartido para controlar el área o implementaciones de hardware
específicas para cumplir con el rendimiento.

Examinemos el comportamiento de síntesis predeterminado de estos ...


Funciones y jerarquía RTL
Cada función se traduce en un bloque RTL
Verilog module, VHDL entity

Source Code RTL hierarchy


void A() { ..body A..}
void B() { ..body B..} foo_top
void C() { C
B(); B
} A
void D() {
B();
} D
B
void foo_top() {
A(…);
C(…);
D(…) Cada función/bloque se puede compartir como cualquier otro componente (add, sub,
} my_code.c
etc.) siempre que no esté en uso al mismo tiempo.

De forma predeterminada, cada función se implementa mediante una instancia común


Las funciones pueden estar delineadas para disolver su jerarquía
– Las funciones pequeñas se pueden delinear automáticamente
Tipos = Tamaños de bits del operador

Code Operations Types

void fir ( Standard C types


data_t *y,
coef_t c[4], long long (64-bit) short (16-bit) unsigned types
data_t x RDx
){ int (32-bit) char (8-bit)
RDc
static data_t shift_reg[4]; float (32-bit) double (64-bit)
acc_t acc; >=
int i;
-
acc=0;
loop: for (i=3;i>=0;i--) { == Arbitary Precision types
if (i==0) {
acc+=x*c[0]; + C: ap(u)int types (1-1024)
shift_reg[0]=x;
} else { * C++: ap_(u)int types (1-1024)
shift_reg[i]=shift_reg[i-1]; ap_fixed types
acc+=shift_reg[i]*c[i]; + C++/SystemC: sc_(u)int types (1-1024)
}
} * sc_fixed types
*y=acc;
} WRy Can be used to define any variable to be a specific bit-width (e.g. 17-bit, 47-
bit etc).

De cualquier ejemplo de Las operaciones se Los tipos C definen el tamaño del hardware utilizado:
código C ... extraen... manejado automáticamente
Loops

De forma predeterminada, los bucles se enrollan

Cada iteración de bucle C  Implementado en el mismo estado N


Cada iteración de bucle C  Implementado con los mismos recursos
void foo_top (…) {
... foo_top
Add: for (i=3;i>=0;i--) {
b = a[i] + b;
...
} Synthesis b

+
a[N]

Los bucles requieren etiquetas si se quiere hacer


referencia a ellas mediante directivas Tcl
(LA GUI agregará etiquetas automáticamente)

– Los bucles se pueden desenrollar si sus índices son estáticamente determinables en el momento de la elaboración
• No cuando el número de iteraciones es variable
• Los bucles no enrollados dan como resultado más elementos para programar, pero una mayor movilidad del operador
– Veamos un ejemplo....
Data Dependencies: Good

void fir ( Default Schedule



acc=0;
loop: for (i=3;i>=0;i--) { == * >= == * >= == * >= == * >= WRy
WRy

if (i==0) { - - + - - + - - + - +
acc+=x*c[0]; RDc RDc RDc RDx RDc
RDc RDc RDc RDx RDc
shift_reg[0]=x;
} else {
shift_reg[i]=shift_reg[i-1]; Iteration 1 Iteration 2 Iteration 3 Iteration 4
acc+=shift_reg[i]*c[i];
}
}
La operación read X tiene
*y=acc;
} buena movilidad

Ejemplo de buena movilidad


La lectura en el puerto de datos X puede ocurrir en cualquier lugar desde el inicio hasta la iteración 4
La única restricción en RDx es que ocurra antes de la multiplicación final
Vivado HLS tiene mucha libertad con esta operación
Espera hasta que se requiera la lectura, guardando un registro
No hay ventajas de leer antes (a menos que desee que se registre)
Las lecturas de entrada se pueden registrar opcionalmente
La multiplicación final está muy restringida...
Dependencias de datos: Malas
void fir ( Default Schedule

acc=0;
loop: for (i=3;i>=0;i--) { == * >= == * >= == * >= == * >= WRy
WRy

if (i==0) { - - + - - + - - + - RDx
RDx
+
acc+=x*c[0]; RDc RDc RDc RDc
RDc RDc RDc RDc
shift_reg[0]=x;
} else {
shift_reg[i]=shift_reg[i-1]; Iteration 1 Iteration 2 Iteration 3 Iteration 4
acc+=shift_reg[i]*c[i];
}
}
Agregar está muy
*y=acc;
restringido
} Ejemplo de mala movilidad

La suma final debe ocurrir antes de la escritura final y después de la última multiplicación y la suma anterior
Podría ocurrir en el mismo ciclo si el tiempo lo permite
Los bucles se enrollan de forma predeterminada
Cada iteración no puede comenzar hasta que se complete la iteración anterior
La adición final (en la iteración 4) debe esperar a que se completen las iteraciones anteriores
La estructura del código está forzando una programación particular
Hay poca movilidad para la mayoría de las operaciones
Las optimizaciones permiten desenrollar los bucles dando mayor libertad
Programación después de la optimización del bucle

Con el bucle desenrollado (completamente)


La dependencia de las iteraciones de bucle ha desaparecido RDc
RDc RDc
RDc

RDc
RDc RDc
RDc
Las operaciones ahora pueden ocurrir en paralelo
RDx
RDx
Si las dependencias de datos lo permiten * *
Si el tiempo del operador lo permite * *
+ +
El diseño terminó más rápido pero utiliza más operadores
+
2 multiplicadores y 2 sumadores WRy
WRy

Resumen del cronograma void fir (



Toda la lógica asociada con los contadores de bucle y la comprobación de índices ha desaparecido acc=0;
loop: for (i=3;i>=0;i--) {
Dos multiplicaciones pueden ocurrir al mismo tiempo if (i==0) {
acc+=x*c[0];
Los 4 podrían, pero está limitado por el número de lecturas de entrada (2) en el puerto de coeficiente C shift_reg[0]=x;
} else {
shift_reg[i]=shift_reg[i-1];
¿Por qué 2 lecturas en el puerto C? acc+=shift_reg[i]*c[i];
El comportamiento predeterminado para las matrices ahora limita la programación... }
}
*y=acc;
}
Arreglos de discos en HLS
Una matriz en código C es implementada por una memoria en el RTL

De forma predeterminada, las matrices se implementan como RAM, opcionalmente


foo_top un FIFO
N-1 SPRAMB
void foo_top(int x, …) A[N]
{ N-2 A_in DIN DOUT A_out
int A[N];
L1: for (i = 0; i < N; i++) … Synthesis ADDR
A[i+x] = A[i] + i;
1 CE
}
0 WE

La matriz se puede dirigir a cualquier recurso de memoria de la biblioteca

Los puertos (Dirección, CE activo alto, etc.) y el funcionamiento secuencial (relojes desde la dirección hasta la salida de
datos) están definidos por el modelo de biblioteca

Todas las RAM se enumeran en la Guía de la Biblioteca Vivado HLS

Los arreglos de discos se pueden combinar con otros arreglos de discos y reconfigurarlos

Para implementarlos en la misma memoria o en una de diferentes anchos y tamaños

Las matrices se pueden particionar en elementos individuales


Implementado como RAM o registros más pequeños
Puertos de E/S de nivel superior
Argumentos de función de nivel superior
Todos los argumentos de función de nivel superior tienen un tipo de puerto de hardware predeterminado
Cuando la matriz es un argumento de la función de nivel superior
La matriz/RAM está "fuera de chip"
El tipo de recurso de memoria determina los puertos de E/S de nivel superior
Las matrices en la interfaz se pueden mapear y particionar

{
Por ejemplo, particionado en puertos separados para cada elemento de la matriz
void foo_top( int A[3*N] , int x) DPRAMB
foo_top DIN0 DOUT0
L1: for (i = 0; i < N; i++)
A[i+x] = A[i] + i;
Synthesis ADDR0

++
}
CE0
WE0

Number of ports defined by the DIN1 DOUT1


RAM resource ADDR1

CE1
WE1

Recurso de RAM predeterminado


RAM de doble puerto si el rendimiento se puede mejorar, de lo contrario, RAM de un solo puerto
Programe después de una optimización del arreglo de
discos
Con el código existente y los valores predeterminados
loop: for (i=3;i>=0;i--) {
El puerto C es una RAM de doble puerto RDc
RDc RDc
RDc
if (i==0) {
RDc
RDc RDc
RDc acc+=x*c[0];
Permite 2 lecturas por ciclos de reloj RDx
RDx
shift_reg[0]=x;
} else {
El comportamiento de E/S afecta al rendimiento * * shift_reg[i]=shift_reg[i-1];
acc+=shift_reg[i]*c[i];
Nota: Podría haber realizado 2 lecturas en el diseño original, pero no había * * }
ninguna ventaja ya que el bucle enrollado forzaba una sola lectura por ciclo. + + }
*y=acc;
+
WRy
WRy

RDc
Con el puerto C particionado en (4) puertos separados RDc
RDc
RDc
Todas las lecturas y mults pueden ocurrir en un ciclo RDc
RDc
RDc
Si el tiempo lo permite RDc

RDx
RDx
Las adiciones también pueden ocurrir en el mismo ciclo.
*
La escritura se puede realizar en los mismos ciclos *
Opcionalmente, las lecturas y escrituras del puerto podrían registrarse *
*
+
+
+
WRy
WRy
Operadores
Los tamaños de los operadores se definen por el tipo
El tipo de variable define el tamaño del operador

Vivado HLS intentará minimizar el número de operadores

De forma predeterminada, Vivado HLS buscará minimizar el área después de que se satisfagan las restricciones.

El usuario puede establecer límites y objetivos específicos para los recursos utilizados
La asignación se puede controlar

Se puede establecer un límite superior en el número de operadores o núcleos asignados para el diseño: esto se puede usar para forzar el intercambio
Por ejemplo, limitar el número de multiplicadores a 1 obligará a Vivado HLS a compartir

Use 1 mult, pero tome 4 ciclos, incluso si se pudiera


hacer en 1 ciclo usando 4 mults
33 22 11 0

Se pueden especificar recursos

Se pueden especificar los núcleos utilizados para implementar cada operador


– p. ej.. Implemente cada multiplicador utilizando un núcleo canalizado de 2 etapas (hardware)

Las mismas operaciones de 4 mult se podrían hacer


33 11 con 2 mults pipelined (con asignación limitando los
22 0
mults a 2)
Introducción a la síntesis de alto nivel
Síntesis de alto nivel con Vivado HLS
Flujo de validación
Validación C y verificación RTL

Hay dos pasos para verificar el diseño


Pre-síntesis: Validación C
Validar que el algoritmo es correcto
Post-síntesis: Verificación RTL
• Verifique que el RTL sea correcto

Validación C Validate C

Una GRAN razón por la que los usuarios quieren usar HLS
Verificación rápida y gratuita
Validar que el algoritmo es correcto antes de la síntesis
Siga los consejos del banco de pruebas dados sobre
Verify RTL

Verificación RTL
– Vivado HLS puede co-simular el RTL con el banco de pruebas original
Vivado Design Suite

Vivado es la nueva herramienta que solo es compatible con la serie 7 FPGA, UltraScale y
todas las familias más recientes.

Completamente re-desarrollado desde cero

Los algoritmos para Vivado se implementan teniendo en cuenta el tamaño cada vez
mayor de las FPGA.

Nuevo algoritmo determinista de lugar y ruta

Todos los pasos tienen la misma visión sobre una estructura de datos global

Vivado HLS: Herramienta de síntesis de alto nivel

Restricciones de diseño de Xilinx


Todas las herramientas de Vivado excepto SDK y Vivado HLS están integradas como
parte de la GUI
Nueva GUI integrada

ISE

Vivado Todas las herramientas en


Vivado excepto
SDK y Vivado HLS son
parte integrada de la GUI
Vivado GUI
Creación de un nuevo proyecto
Project manager
IP Integrator
Simulación: crear banco de pruebas
Simulación: editar banco de pruebas
Simulación en ejecución
Especificación de restricciones: Especificación de restricciones

XDC Consttraints (sustitución de UCF)


Las restricciones XDC son una combinación de:
Restricciones de diseño de Synopsys (SDC) estándar de la industria, y
– Restricciones físicas patentadas de Xilinx
Las restricciones XDC tienen las siguientes propiedades:
No son simples cadenas, sino que son comandos que siguen la semántica Tcl.
Pueden ser interpretados como cualquier otro comando Tcl por el intérprete Tcl de Vivado.
– Se leen y analizan secuencialmente de la misma manera que otros comandos Tcl.
UCF to XDC

Source: Vivado Design Suite Migration Methodology Guide (UG911) p 23


Creación o adición de archivos XDC
Creación de archivos XDC
Maestro XDC

http://zedboard.org/support/documentation/1521
Elaboración del diseño: Planificación de E/S
Elaboración de diseño
Síntesis y diseño sintetizado
Implementación y diseño implementado
Informes
Generación de flujo de bits
Administrador de hardware: destino abierto
Administrador de hardware: destino abierto
Administrador de hardware: dispositivo de programa

También podría gustarte