Ejemplo DMI

Vamos con el Direct Memory Interface (DMI) que tenemos definido en TLM-2.0. DMI viene a ser “pasar” de transacciones, protocolos y todo eso. Es decir, el Target pasa un puntero de su región de memoria (o parte de ella) al Initiator para que acceda directamente sin usar transacciones. Así ganamos en velocidad de simulación, ya que todo es mucho más sencillo. Esto del DMI está enfocado a dispositivos tipo memoria (de ahi lo de Direct Memory Interface 😉 ) conectados a módulos que accedan muchísimas veces a esos dispositivos de memoria, como por ejemplo CPUs o DMAs. Pero no eso no quita que lo podamos usar en cualquiera de nuestros módulos :-).

Y qué hay que hacer? Pues vamos por partes…

Target

En la parte target hay que hacer dos cosas: registrar la función que publica el puntero y rellenar los datos de la estructura para publicar el puntero.

Para registrar la función, es tan fácil como hacer:

target_socket.register_get_direct_mem_ptr(this, &Target::my_get_direct_mem_ptr);

y luego escribir la función en cuestión:

bool Target::my_get_direct_mem_ptr(tlm::tlm_generic_payload&, tlm::tlm_dmi& dmi_data)
{
  dmi_data.allow_read_write();
  dmi_data.set_start_address( 0 );
  dmi_data.set_end_address( 10 );
  dmi_data.set_read_latency( sc_time(10, SC_NS) );
  dmi_data.set_write_latency( sc_time(10, SC_NS) );

  dmi_data.set_dmi_ptr( reinterpret_cast ( &data[0]) );
  return true;
}

Fíjate que en esta función sólo rellenamos la estructura dmi.data, en este caso diciendo que en la región de memoria en cuestión se podrá leer y escribir, cual es su dirección de inicio y de final y qué latencia tenemos para escritura y lectura; estos dos últimos datos los podrá usar el Initiator para tener en cuenta los tiempos de acceso a nuestro dispositivo (si se desea, claro). Por último, ponemos el puntero a nuestra zona de memoria (nuestro array data), haciéndole un cast a unsigned char, que es como siempre se usan las zonas de memoria en TLM.

Luego, nos falta decirle al Initiator que nuestro dispositivo permite DMI. Para ello, marcamos un campo especial de la transacción:

void Target::b_transport( tlm::tlm_generic_payload& transaction, sc_time& delay )
...
transaction.set_dmi_allowed( true );
...

De esta forma, cuando un Initiator se comunique con nuestro Target a través de una transacción normal i corriente (bloqueante o no, da igual), podrá comprobar que este Target tiene capacidad para DMI.

Initiator

Lo primero que debemos implementar en el Initiator para poder usar DMI es la función que le permite al Target decirle al Initiator que el puntero DMI que le pasó ya no es válido (por la razón que sea):
Así pues, añadimos un método nuevo a nuestra clase Initiator:

void Initiator::invalidate_direct_mem_ptr(sc_dt::uint64 start, sc_dt::uint64 end)
{
  dmi_ptr_valid = false;
}

Y la registramos en el socket así:

Initiator::Initiator(sc_module_name name_): 
...
  initiator_socket.register_invalidate_direct_mem_ptr(this, &Initiator::invalidate_direct_mem_ptr); 
  dmi_ptr_valid = false;
...
}

Luego, el Initiator deberá primero averiguar si el dispositivo al que quiere acceder permite DMI o no (recordemos que es opcional). Para ello, lo que tiene que hacer es consultar un campo concreto de una transacción ya contestada por el Target. Si el Target permite DMI, habrá marcado el flag en cuestión.

...
  if (transaction.is_dmi_allowed() ) {
   dmi_ptr_valid = initiator_socket->get_direct_mem_ptr( transaction, dmi_data);
  }
...

Fíjate que la classe transaction tiene el método is_dmi_allowed() que nos indica si el Target acepta o no DMI.
Luego si el Target acepta DMI, debemos preguntarle si el puntero DMI es valido o no. Usando el método get_direct_mem_ptr del socket obtenemos la respuesta y la guardamos en la variable dmi_ptr_valid de tipo bool que nos indica a partir de ahora si el puntero DMI es válido o no.

En el ejemplo en cuestión, si el Target soporta DMI, lo que hacemos es lo siguiente:

...
      // El target acepta DMI, usamos el puntero directamente
      unsigned char* dmi_ptr = dmi_data.get_dmi_ptr(); 
      memcpy(dmi_ptr + (i*sizeof(data)), &data, sizeof(data) );
      std::cout << "Initiator: " << sc_time_stamp() << "Escribiendo " << data << ". DMI OK" << std::endl;
...

Lo que hacemos en primer lugar es obtener el puntero en cuestión con el método dmi_data.get_dmi_ptr(); para luego usarlo directamente. En este caso copiamos a esa dirección de memoria más un offset el dato que queremos escribir al Target.

Aunque yo lo he dejado tal cual, no hace falta obtener el puntero DMI cada vez que queramos usarlo, podemos pedirlo una sola vez, y solamente comprobar que sigue siendo válido (en nuestro caso con la variable global dmi_ptr_valid) cada vez que queramos usarlo.

Y hasta aquí el ejemplo, para tenerlo completo del todo faltaría añadirle la opción al Target de invalidar el puntero de vez en cuando. Pero os lo dejo como ejercicio 😉 (si el Target quiere invalidar el puntero DMI, sólo tiene que llamar al método invalidate_direct_mem_ptr de su socket)

Puedes bajarte los ficheros del ejemplo en este enlace.

This entry was posted in Ejemplos and tagged , , . Bookmark the permalink. Follow any comments here with the RSS feed for this post. Post a comment or leave a trackback.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Your email address will never be published.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.