Cómo implementar una función de retardo precisa en Keil c51 (para c8051) que espera la cantidad exacta de relojes de CPU, la cantidad de relojes varía de 1 a 255

Entonces, como dice el título, necesito retrasos exactos, no demasiado largos, idealmente en el rango de 0 a 350 relojes de CPU, pero si algo funciona en un rango más estrecho, el rango mínimo absoluto es de 20 a 127 relojes de CPU. Por lo tanto, estos están por debajo o justo por encima de los retrasos de un solo microsegundo (reloj de CPU de 50 MHz), relativamente cortos de varios relojes a varias decenas de relojes. El problema de sondear un temporizador es que la precisión da como resultado un paso de 7 relojes como máximo, según la implementación, por ejemplo:

  1. while(!TF0) {} Mientras que, no, y el operador de bits, todos juntos toman 7 relojes. Entonces, si llamo algo entre los relojes 15-21, resultará en una demora plana de 21 relojes...
  2. Uso de la interrupción en el temporizador y el modo de parada de la CPU : brinda buenos resultados para más de 50 relojes, probablemente depende de la condición actual de la CPU, por lo que a veces va mucho más allá de los 50 relojes, en un rango de 100 relojes debido a la interrupción y la latencia de activación, pero cualquier cosa por debajo de nuevo 50 (o 100) relojes de CPU planos.
  3. El uso de switch-case , con, por ejemplo, 30 entradas para 30 retrasos con un incremento de 1 reloj, con un número diferente de NOP como retraso, da como resultado una optimización del compilador que lo hace impredecible en términos de tiempo y, en gran medida, demasiado largo, más de 100 relojes. Esto hace que el enfoque sea inutilizable.
  4. Estoy planeando probar la tabla de puntero a funciones con diferentes números de NOP. Pero antes de intentarlo ya le veo dos problemas a ese enfoque: a. requerirá mucha memoria y me quedan 1k; b. la latencia de entrada y salida de una función nula (void) es de alrededor de 18 relojes, por lo que es muy, muy ajustado para cumplir con el mínimo absoluto de 20 relojes que necesito...

¿Cómo abordar este tipo de problema? ¿Alguna idea será más que bienvenida?

Por cierto, lo ejecuto en el microcontrolador C8051F38x de Silicon Labs, usando C51 y Keil para codificar y compilar si eso importa.

El código que surgió como una solución parcial, parece que sigue el mismo tiempo mientras que el bucle en C lo hace, y la instrucción "djnz" toma 5-6 ciclos de CPU, en lugar de la hoja de datos indicada 2/4.

ACC_save=   ACC;
ACC     =   counter102;
P0b3    =   1;  //             Start the Pulse
#pragma ASM     //             Precice DELAY using assembler
clr C           //  ; 1       Clear Carry
rrc A           //  ; 1       C = 1 if odd
jnc even        //  ; 2 or 4  extra 2 cycles if branch taken (spoils cache)
nop             //  ; 1
nop             //  ; 1
clr   C         //  ; 1
even:
subb  A,#4      //  ; 1
mov   R7,A      //  ; 1
loop:
djnz  R7, loop  //  ; supposed to be 2, but practically takes 5 to 6 cycles!
#pragma ENDASM`
P0b3    =   0;  //             Stop the Pulse

EDITAR

Muchas gracias a todos por sus excelentes aportes, no podía imaginar que el flujo de ideas pudiera ser tan positivo y, lo que es más importante, productivo. Así que mi profundo agradecimiento a todos los que contribuyeron y contribuirán en el futuro. Entonces, después de sus valiosos aportes y sus excelentes ideas, se me ocurrió algo que funciona para mí, hasta cierto punto. El código está abajo:

void    delay(unsigned char delay_time) {
switch (delay_time)
{case     8:    goto     Q08;
case      9:    goto     Q09;
case     10:    goto     Q10;
case     11:    goto     Q11;
case     12:    goto     Q12;
case     13:    goto     Q13;
case     14:    goto     Q14;
case     15:    goto     Q15;
case     16:    goto     Q16;
case     17:    goto     Q17;
case     18:    goto     Q18;
case     19:    goto     Q19;
case     20:    goto     Q20;
default      :  goto     Q00;   }

Q19:    PORT_ACTIVE(1); //  2clk
Q17:    PORT_ACTIVE(1); //  2clk
Q15:    PORT_ACTIVE(1); //  2clk
Q13:    PORT_ACTIVE(1); //  2clk
Q11:    PORT_ACTIVE(1); //  2clk
Q09:    PORT_ACTIVE(1); //  2clk
    _nop_();            //  1clk
goto EXIT1;             //  Skip the Even delay part

Q20:    PORT_ACTIVE(1); //  2clk
Q18:    PORT_ACTIVE(1); //  2clk
Q16:    PORT_ACTIVE(1); //  2clk
Q14:    PORT_ACTIVE(1); //  2clk
Q12:    PORT_ACTIVE(1); //  2clk
Q10:    PORT_ACTIVE(1); //  2clk
Q08:    PORT_ACTIVE(1); //  2clk
Q00:                    //  0clk
EXIT1:
return;                 //  Exit from the function takes 7 clocks
}   //  END of function delay

// Continued execution after the delay function
PORT_ACTIVE(0);     //  2clk

Entonces PORT_ACTIVE(x) es una función #define que activa el puerto pulsante. Dado que tengo todo el tiempo que necesito antes de comenzar el pulso, pude exprimir la mayor parte de los gastos generales relacionados con las decisiones antes de la activación real del puerto. Luego, la instrucción de retorno toma prácticamente siempre la misma cantidad de tiempo, por lo que ahora puedo generar un pulso con un mínimo de 8 ciclos de clk de ancho y hasta 20 ciclos. Ahora lo estoy ampliando hasta 100 relojes, a expensas de la memoria de almacenamiento disponible, por supuesto. Y entonces, esta solución es, de hecho, gracias a la idea de JimmyB de colocar la activación del pulso en la función y no antes, y por supuesto, gracias a las excelentes ideas de TCROSLEY, de cómo administrar los retrasos pares e impares, es solo que cambiar a ensamblaje no es realmente amigable para la experiencia de depuración,

Una nota más, es que tan pronto como terminé de celebrar una solución funcional, llegué al siguiente problema.

SEGUNDO PROBLEMA

Necesito ejecutar un segundo pulso espalda con espalda al primero con ancho independiente. Por lo tanto, no hay sobrecarga para el segundo pulso, de lo contrario, terminará con un ancho variable. Prácticamente me devuelve al punto en el que estaba antes, ya que el segundo pulso está nuevamente limitado al cuello de botella de 6 ciclos del ciclo while, a menos que haya una manera de colocar la sobrecarga de bifurcación para el segundo pulso antes del primer pulso. ¿Alguna idea al respecto?

¿Estos tiempos de retardo cambiarán dinámicamente durante el tiempo de ejecución (es decir, parámetro variable para la función de retardo), o pueden calcularse en tiempo de compilación (parámetro constante para la función de retardo)? ¿Cuántas llamadas diferentes a la función de retardo imaginas?
Da un paso atrás y describe el problema que realmente estás resolviendo. Si necesita demoras que sean tan precisas, entonces una subrutina probablemente NO sea la forma correcta de hacerlo. Si necesita controlar con precisión los tiempos entre dos eventos externos, debe tener en cuenta TODO el código entre las instrucciones reales asociadas con esos eventos, incluida la llamada y el retorno de su rutina de retraso junto con cualquier otra lógica en la función de llamada.
--- Entonces, para seguir un poco el problema.
--El código viene a ejecutar un pulso preciso de cierto ancho. Hay un procedimiento de ajuste antes de la ejecución real, donde la MCU ajusta el ancho del pulso en función de alguna respuesta del sistema. Entonces, en general, es así: MCU recibe parámetros de pulso INICIO: Cálculo de retrasos + inicios PUERTO = 1 PUERTO DE RETARDO PRECISO = 0 Análisis de resultados Volver a INICIO si los resultados no son satisfactorios, o continuar a otro punto en la ejecución del programa... Así que el el retraso es dinámico. El número de llamadas a retrasar podría ser solo 1 o terminar siendo miles antes de que el algoritmo satisfaga todos los parámetros.
Para hacer un retraso tan dinámico, debe hacer una subrutina de retraso con el mínimo retraso posible y luego llamarla tantas veces como lo requiera el programa. Aquí debe considerar el retraso de la sobrecarga de llamadas, el retraso del bucle, etc. Diría que busque programas para generar retrasos en el ensamblaje y luego escriba el código según sus requisitos.
Entonces, ¿necesita algo que, digamos, genere 1, espere 1 clk y genere 0? ¿Entonces algo con 2 clk de espera y así sucesivamente?
Si es así, podría probar e implementar un par de funciones diferentes que produzcan diferentes anchos de pulso de 1, 2, 3, ... clk's, hasta una cantidad de ciclos (20? 50?) que pueden ser manejados dinámicamente por un bucle, tal vez precedido por una especie de diapositiva NOP para permitir un control preciso.
Nota: he agregado un código adicional a mi respuesta para manejar los saltos de 6 ciclos.
JimmyB gracias por su aporte, un enfoque muy interesante, que he probado y esa es la solución que se me ocurrió. Pero hay un segundo problema, golpeé inmediatamente después de eso. Cualquier entrada sobre eso. Gracias.

Respuestas (3)

Como han mencionado otros, esto se hace mejor en el ensamblaje. Aquí está mi intento original de codificar esto, cuando pensé que las instrucciones de salto tomaban 2 o 4 ciclos (ver Editar a continuación para ver la versión revisada).

void delay_sub(unsigned char i)
{
// convert 20, 21, 22 etc to count in R7 of 1, 2, 3 (extra cycle added if i is odd)
                    ; cycles
    rrc A           ; 1            c = 1 if odd
    jnc even        ; 2 or 4       extra 2 cycles if branch taken (spoils cache)
    nop             ; 1            delete if using lcall's instead of acall's
    nop             ; 1            same
    clc             ; 1            in either case carry is clear prior to subb
even:
    subb A,#9       ; 1

    mov R7,A        ; 1            R7 now = (i / 2) - 9
    //while (i--);
loop:
    djnz R7, loop   ; 2     loop address should be in cache, so no extra cycles needed
    ret             ; 6
}

timing calculation (assuming acall's)
if i even:
    5+7+R7*2+6 = minimum of 20 22 24 ... => R7 = 1, 2, 3 ...
if i odd:
    5+8+R7*2+6 = minimum of 21 23 25 ... => R7 = 1, 2, 3 ...

Supone que se realiza una llamada como ACALL(nn), donde nn es una constante o una variable en una variable de byte, de modo que el parámetro se puede pasar mediante una instrucción MOV A,#n de un ciclo, por ejemplo. El cronometraje mínimo que puedes hacer es de 20 relojes, como lo pediste.

mov  A,#n        ; 1   
acall delay_sub  ; 4

No hay verificación de que el parámetro sea mayor o igual a 20, cualquier valor menor a 20 dará una temporización incorrecta.

La instrucción mov y acall tomarán 5 ciclos. En primer lugar, la cuenta (i) se divide por dos para dar cuenta de que la instrucción DJNZ toma dos ciclos. Luego, el conteo se ajusta para agregar un ciclo si i es impar. Finalmente, se resta un valor fijo para que el valor en el registro a decrementar (R7) esté en el rango 1, 2, 3 ... R7 luego se decrementa en un ciclo cerrado (dos ciclos por conteo). Hay un número de ciclos fijo de 6 para la devolución.

Si tiene que usar un LCALL en lugar de un ACALL, el tiempo mínimo que puede hacer será de 21 relojes en lugar de 20, y deberá eliminar los dos nop después de la instrucción jnc. Tienes que usar todos los ACALL o LCALL, no puedes mezclarlos.

Evitaría usar C para llamar a la función a menos que pueda garantizar que el compilador no agregue una sobrecarga adicional. Además, estoy usando R7 como registro de scratch; su manual del compilador le dirá qué registros se pueden usar dentro de una función de ensamblador sin tener que guardarlos (si corresponde).

Esto tampoco tiene en cuenta la desactivación y reactivación de las interrupciones, si es necesario, para garantizar que la rutina de temporización no se interrumpa.

El comportamiento de las instrucciones de salto se basa en la hoja de datos del C8051F38x tal como lo entiendo (en términos de cuándo se daña o no la memoria caché de instrucciones). Esto puede ser diferente para otras versiones del 8051.

Finalmente, no he mostrado la sintaxis para saltar al ensamblado en línea y volver a salir. La subrutina también podría colocarse en un archivo separado y ensamblarse.

Editar

Desde que escribí el código original, el OP me informó que la cantidad de ciclos de reloj para un salto en su 8051 es 5 o 6, no los 2 o 4 indicados en la hoja de datos que leí. Así que he reescrito la rutina para tener esto en cuenta. Desafortunadamente, esto aumenta el recuento mínimo de ciclos que se puede programar en 32 en lugar de 20. Por lo tanto, si es absolutamente necesario manejar los recuentos entre 20 y 31, será necesario escribir un código de propósito especial específico para ese caso (ver más abajo).

void delay_sub(unsigned char i)
{
// minimum value of i is 32 
                    ; cycles
    clr C           ; 1
    subb A,#32      ; 1   adjust for overhead of call and this routine
                    ; a branch could be added here in case the result is negative
    mov B,#6        ; 1
    div AB          ; 4            quotient in A, remainder in B
    mov DPTR,#adjustcycles   ; 1
    mov R7,B        ; 1
    mov B,A         ; 1   save quotient in B as temp
    mov A,#6        ; 1
    clr C           ; 1
    subb A,R7       ; 1   A now has 5 - B (remainder)
    mov R7,#0       ; 1
    jmp @A+DPTR     ; 6   jump into table to add clocks based on remainder

adjustcycles:       ; execute additional cycles based on remainder
    inc R7          ; 1   for remainder of 5
    inc R7          ; 1   for remainder of 4 
    inc R7          ; 1   for remainder of 3 
    inc R7          ; 1   for remainder of 2
    inc R7          ; 1   for remainder of 1 
    nop             ; 1   for remainder of 0

    mov A,B         ; 1   now has i / 6, have already adjusted for remainder
loop:
    djnz loop       ; 6
    ret             ; 6

timing in clock cycles is: 5 (call) + 21 (fixed overhead) + 6*(i/6) + (i%6) + 6 (ret)

if i = 0, 5 + 21 + 6 = 32 therefore that is the minimum count

En lugar de dividir el parámetro i por 2 como en el ejemplo anterior, ahora tengo que dividirlo por 6 porque asumo que la instrucción DJNZ toma 6 ciclos. Entonces, necesitamos hacer un ciclo i / 6 veces, y también agregar de 0 a 5 ciclos para el resto (i % 6).

El resto de mis comentarios anteriores se aplican bastante bien a este ejemplo. Dejo el código original, en caso de que alguien tenga un 8051 con una instrucción DJNZ de dos ciclos.

Para cuentas de 20 a 31, podría crear una subrutina con solo un nop, que toma 12 ciclos, incluida la llamada y la devolución:

void delay12(void)
{
    nop
}

Para 20-23 conteos, lo llamaría una vez más agregaría de 8 a 11 nops después de la llamada (o un salto ficticio a la siguiente instrucción que consumiría 6 ciclos más 2 a 5 nops, por lo que retrasar 20 ciclos costaría solo cuatro instrucciones más la subrutina que se supone que se utiliza más de una vez). Para conteos de 24 a 31, llamaría a delay12 dos veces y agregaría de 0 a 5 nops y/o una instrucción de salto según sea necesario.

Así que para retrasar 20 ciclos:

    acall delayl12
    jump next
next:
    nop
    nop
Gracias por la respuesta detallada. Tomé su código y, con ligeras modificaciones, pude ejecutarlo en línea usando pragmas ASM entre los comandos del puerto. No pude ejecutarlo como una función, el vinculador se volvió loco por la incapacidad de vincular todas las cosas. Dos problemas principales con la ejecución en línea. 1. Una vez que se cambió a ensamblaje, la depuración está en ensamblaje, simplemente imposible que se depure mi código de 15k. 2. Cualquier instrucción de salto toma 6 !!!!! sí, 6 ciclos en lugar de los 2 prometidos. Verifiqué dos veces la hoja de datos, dice 2/4 ciclos, ¡pero no, se necesitan 6! ??? ¿¿¿Como es eso???
¿Incluso el DJNZ en el circuito cerrado toma seis ciclos? Eso claramente no es lo que dice la hoja de datos. ¿Realmente lo está ejecutando en hardware real o en un simulador? La única forma de probar esto realmente es en hardware real.
Sí, el DJNZ en el siguiente ciclo toma unos 5 unos 6 ciclos, me vuelve loco. Aquí está el código que uso: 'ACC_save = ACC; ACC = contador102; P0b3 = 1; #pragma ASM // ; ciclos clr C // ; 1 Limpiar Llevar rrc A // ; 1 C = 1 si impar jnc par // ; 2 o 4 2 ciclos adicionales si se toma la bifurcación (estropea el caché) nop // ; 1 nop // ; 1 clr C // ; 1 sub A,#4 // ; 1 movimiento R7,A // ; 1 bucle: djnz R7, bucle //; 2 #pragma ENDASMO'
@cezar No necesitas el clc antes del rrc. El rrc escribirá sobre él. Tienes subb A, #4, debería ser #9. No dijiste si estás probando con un simulador. Sospecho que lo eres. No confío en ellos. Realmente necesitas tener esto como una función. Intente poner el código asm en un archivo separado y use un ensamblador como sugerí en mi respuesta. Luego ponga 100 llamadas a la función seguidas y póngalas en un bucle que se ejecute 100000 veces. (El motivo de las 100 llamadas es reducir la sobrecarga a ~ 1%) Ejecute esto en hardware real. Debería tomar muy cerca de 4 segundos a 50 MHz.
Hola Tcrosley, gracias por tu rápida respuesta. Hago pruebas en hardware real con un osciloscopio decente (tektronix MSO 5104 - 10GS/s) a mi lado, así que lo que veo en el depurador es una confirmación de tiempo en el osciloscopio con una precisión de hasta 1 ns. Sin CLR C, el RRC no sobrescribe el acarreo por alguna razón, y obtengo grandes números en A. Volví a calcular el tiempo debido a que el código estaba en línea y no como una función, y cambié "subb A, # 4", entonces no hay RET 6 ciclos, y pasando la variable a función...
@cezar Es bueno escuchar que estás en hardware real. Hago el cambio de bit de puerto y mido mucho con un alcance. El rrc debe establecer el acarreo para los números impares y borrarlo para los pares. Mmm. Entienda ahora acerca de su cambio de subb. Pero en su programa final, esto realmente debería ser una función y no en línea. Buena suerte, me voy a trabajar.
Entonces, Tcrosley, ¿tiene alguna idea de por qué podría ser de 5 a 6 ciclos y no de los tan deseados 2 ciclos? Estaba tratando de sustituir el djnz R7, loopcon dos comandos dec A:; y luego JNZ loop, pero el mismo resultado ... Estoy haciendo algo mal y no soy lo suficientemente bueno en ensamblaje para averiguar qué es ...
@Cezar Para probar realmente el esquema de conteo, haga esto: active el bit de puerto, mov R7, 100; bucle: nop; nop; bucle djnz; pon eso en un bucle infinito y mide el tiempo desde que el puerto se enciende hasta que se apaga. Si realmente son 6 ciclos por salto, deberían ser 8*100*.02 µ = 16 µS.
En primer lugar, gracias por las excelentes ideas, son realmente geniales. Renuncié a la implementación del ensamblaje, no gano nada (los mismos 6 relojes que puedo obtener del ciclo while) pero necesito renunciar a la capacidad de depurar convenientemente, y tengo 15k de código compilado para depurar. Después de la solución que publiqué anteriormente, me encuentro con un problema con un segundo pulso ... y renunciaré a la precisión del segundo pulso, o puede que con su ayuda aquí los muchachos puedan encontrar alguna forma de obtener la decisión sobre la cabeza antes del primer pulso. Gracias de nuevo.

Podrías (creo, no recuerdo bien la arquitectura 8051) hacer un salto calculado en un 'mar' de nops. Quizás combínelo con un ciclo para reducir el número requerido de nops (o puede haber una forma más sofisticada de hacerlo...)

Lo hice una vez en la arquitectura MCS-48 anterior, pero relacionada, para lidiar con la latencia variable o algo similar.

Debe trabajar en ensamblaje para precisión de ciclo único. Keil admite un par de métodos para usar ensamblado en combinación con C, y probablemente el ensamblado en línea más simple funcione para usted.

Independientemente de lo que haga, habrá algunos gastos generales, por lo que lo mejor que puede hacer será algo como n+1 a n+255 ciclos de retraso. n <= 20 ciclos deberían ser factibles.

Gracias por su aporte, buscaré poner un código ensamblador para la parte crítica del tiempo. Estaba tratando de evitar eso hasta ahora, pero parece que no voy con C aquí.

Sí, como señaló Spehro, debe trabajar en ensamblaje y puede obtener una precisión de hasta un ciclo de máquina usando ensamblaje pero no un ciclo de reloj.

Dos formas de usar lenguaje ensamblador

  1. Use los comandos y bucles nop de tal manera que el tiempo de ejecución de los comandos y nops sea el tiempo de demora requerido. De esta manera, necesita saber cuántos ciclos de máquina toma cada comando en su código.

  2. Usa temporizadores.

Los compiladores modificarán su código C para optimizar el código, lo que seguramente cambiaría su retraso, por lo que debemos avanzar hacia el ensamblaje para mayor precisión.

Como he estudiado lenguaje ensamblador de 8051 hace un año. No tiene mucho conjunto de instrucciones y si estudias seguramente aprenderás mucho sobre microcontroladores y su arquitectura básica. Aunque no tiene que aprender a ensamblar ningún microcontrolador, seguramente sería útil saber cómo funciona el lenguaje ensamblador para al menos un microcontrolador.

Gracias por tu comentario. De hecho, probé ambos en C, por lo que probablemente lo intentaré en ensamblador. Casi veo la forma de implementarlo usando NOPes, pero no estoy tan seguro de cómo hacerlo con los temporizadores en el ensamblaje, todavía requiere una decisión condicional, que requiere muchos ciclos por sí solo.
circuitstoday.com/delay-using-8051-timer Tiene un código atractivo para generar un retraso de 1 ms (puede cambiar los valores dentro de los registros del temporizador en su código para generar retrasos según sus requisitos). Aunque el autor no ha considerado el retraso de los comandos en la subrutina de retraso en el ensamblaje, también puede considerar que es 100% preciso.
Hola Jasser, gracias por el seguimiento. Probé el caso del temporizador, que es mucho más simple en términos de adición de código ensamblador, pero desafortunadamente, por alguna razón, obtengo algunos resultados extraños, la instrucción "JNB TF2H, $" que uso para el sondeo de desbordamiento del temporizador, toma 6-7 ciclos para ejecutar, mientras que se supone que es 3/5 según la hoja de datos. No puedo encontrar la razón para que sea tan larga. ¿Puede estar conectado a la ubicación del código en la memoria externa? ¿Algunas ideas?
Lo único que importa son las instrucciones y el tiempo que tarda en ejecutarse y no dónde reside el código (los retrasos en las instrucciones tienen en cuenta la residencia externa del código, si corresponde). Le recomendaría que pruebe el hardware para verificar la cantidad de retrasos que está teniendo. No he encontrado tal cosa hasta ahora, Keil, pero tal vez... tenga algo que ver con el software... ¡Intente verificar los retrasos en un osciloscopio!
Hola Jasser, Gracias por una pronta respuesta. Así que me siento junto al microcontrolador y un osciloscopio, y lo que veo en los relojes del depurador es lo que obtengo en el osciloscopio. No puedo entender cómo es eso posible. Es casi el doble de la cantidad de tiempo que los laboratorios de silicio indican en su hoja de datos. Ciertamente estoy haciendo algo mal aquí, y no puedo entender qué.
1. Si tiene un kit de hardware, depure el código paso a paso y vea los cambios dentro de los registros. 2. Escriba un código simple en ensamblador que use la misma instrucción djnz y descubra si se está comportando de la misma manera.
Hola Jasser, muchas gracias por tu respuesta. Lo hice con un kit de depuración y pude echar un vistazo a los registros, por lo que usando un código simple, la MCU hace exactamente el mismo truco cada vez con la instrucción djnz. Gracias de nuevo por tu aportación.
¿Has probado también a cambiar el MCU? Por cierto, ¿cuál es tu proyecto? ¿O lo está haciendo simplemente para producir esos retrasos?
Probaré con otra unidad MCU, me refiero al mismo modelo pero solo otra unidad física, gracias por señalarlo. El proyecto es generar dos pulsos con un ancho preciso pero independiente, que se cambia en función de los parámetros pasados ​​a través de un canal de comunicación a la MCU. Luego, la MCU recopila los datos de la respuesta del sistema que invocaron estos pulsos y los envía a través del canal de comunicación o almacena localmente alguna descripción del sistema que se usará más adelante.