DESARROLLO DE DRIVERS CON DRIVERWORKS

 
SEGUIR LEYENDO
DESARROLLO DE DRIVERS CON
      DRIVERWORKS
El asistente de DriverWorks

       Para iniciar Visual C++ con soporte para DDK utilizaremos: Programas->Compuware
DriverStudio->Tools->DDK Build Settings(SetDDKGo) ->launch program.

       A continuación se explican los pasos seguidos para generar el driver de la FPGA; algunos
de los pasos son específicos para PCI, y otros son comunes a todos los drivers que se desarrollen
con DriverWorks.

PASO 1
       Escogemos ruta y nombre para el driver.

PASO 2
       En nuestro caso el tipo de driver a utilizar es WDM Driver; puesto que el driver que se
desea conseguir es el más genérico posible en Windows.

PASO 3
       WDM Function Driver: nuestro driver va a controlar un dispositivo hardware directamente,
no a través de otro driver.

PASO 4
       Bus PCI. Rellenamos los datos del fabricante y dispositivo. En nuestro caso:

   •   PCI Vendor ID: E159

   •   PCI Device ID: 0002

   •   PCI Subsystem ID: 00010080

   •   PCI Revision ID: 00

PASO 5
       Establecemos los nombres para la clase que se va a generar, y el nombre de fichero.

PASO 6
       Dependiendo de lo que se haya implementado en la placa. Las opciones marcadas por
defecto crean funciones de lectura y escritura genéricas. Quitaremos read y write, puesto que el
dispositivo que tenemos para realizar ejemplo se puede leer y escribir en diferentes direcciones,
por lo que las funciones de lectura y escritura genéricas no tienen mucho sentido.

PASO 7
       Si utilizamos la primera opción, las peticiones que se hagan al driver se atenderán
directamente, en lugar de encolarlas. En caso de elegir encolar las peticiones en el código
generado se encontrarán las sentencias necesarias para ir pasando de una petición a la siguiente
en la cola, lo cual corresponde a las siguientes opciones.

PASO 8
       Parámetros que se guardarán en el registro de windows. El que viene por defecto se puede
dar por bueno.

PASO 9
       En este paso es donde definiremos los recursos que utilizará nuestro dispositivo.

       Pestaña Resources
              Añadiremos tantos IO Ports como direcciones base tengamos contempladas para
       nuestro dispositivo; en nuestro caso añadiremos un IO Port con la dirección base 0 (los
       dispositivos WDM son plug and play, y la dirección real se asignará; aquí se pone el índice
       de la dirección base a utilizar.).

       Pestaña Interface
              Lo normal es utilizar el interfaz WDM, definiendo el class GUID, usándose el que
       define el propio asistente. El interfaz de enlace se utiliza cuando se hace un driver más
       genérico, para sistemas operativo que no utilizan el WDM para crear drivers

       Pestaña Buffers
              Siempre he utilizado Direct, puesto que nuestra placa cuando se desea leer o
       escribir en un registro se desea hacerlo en un instante determinado; es decir, la lectura y la
       escritura son directas. Sólo se utilizaría BUFFER si deseamos crear una pequeña reserva
       de memoria para realizar a través e ella toda la transferencia

       Pestaña Power
              Aquí escogeremos si el driver manejará el estado de energía de la placa, es decir;
       qué hacer al iniciarla, qué hacer si se va a suspender, qué hacer al recuperarse, etc.

PASO 10
       Aquí añadiremos todas las funciones que se utilizarán para acceder a la placa; en este
paso conviene pensar bien todas las operaciones que se desean añadir, aunque se incluyan
algunas que después no se utilicen, puesto que después en el código es muy fácil cometer errores
al añadir una nueva función (ver apartado ¿Cómo añadir una nueva función de control desde el
código fuente?).

       Al añadir desde el asistente una nueva IOCTL, se nos pedirán los siguientes parámetros:

   •   El nombre de la función.

   •   El ordinal de la función (normalmente usaremos el mismo que se nos da).

   •   El método. En los tres modos que se muestran a continuación, viene implementado parte
       del acceso a los parámetros pasados y al lugar donde dejar el resultado; lo único que
       habrá que hacer es leer lo que nos interese, y proporcionar los resultados que nos
       interese. Los modos son:

          o   Buffered (Se utilizarán unos buffers destinados a estas operaciones).

          o   In_Direct ó Out_Direct (No utilizan buffers, el mapeo de memoria se hace de forma
              diferente; de todos modos el acceso a los parámetros y la forma de devolver
              resultados ya viene implementada o al menos explicada en el código generado por
              el asistente).

          o   Neither (En este modo, y bajo el punto de vista de DriverWorks se utilizará un tercer
              Buffer; de todos modos también debe venir implementado parte del acceso).

   •   El tipo de acceso:

          o   Any: cualquier tipo de acceso.

          o   Read: para lectura.

          o   Write: para escritura.

          o   Read_Write: para lectura/escritura (en realidad no conozco muy bien la diferencia
              de este tipo con el Any).

PASO 11
       Normalmente en esta pestaña se pide que genere una aplicación de prueba, que haga una
señal en el punto de entrada del driver, que genere código para depuración (Trace Code), se
cambia el símbolo que se utilizará para etiquetar las áreas de memoria que el driver reserve, de
manera que haya algo que se entienda mejor (tener en cuenta en este punto que la etiqueta que
se ponga aquí debe ir escrita al revés). Se debe quitar la opción de la configuración de 64 bits,
puesto que no tenemos de momento necesidad de drivers para un ordenador de 64bits de ancho
de palabra de datos. Tras este último paso hacemos click en Finish y se nos generará el código
fuente según todas las opciones antes proporcionadas.
Conviene salvar/guardar el contenido de la pantalla final de información de los pasos que
hemos realizado para diseñar el driver.
¿Cómo añadir una nueva función de control desde el

código fuente?

        Lo explicaré con un ejemplo. Supongamos que nuestro driver se llama DRIVER1. Abrimos
el fichero DRIVER1ioctl.h y le añadimos una línea del tipo:

         #define DRIVER1_IOCTL_nombre_del_ioctl_nuevo CTL_CODE
       (FILE_DEVICE_UNKNOWN, ordinal, método de acceso, acceso permitido)

        En ordinal pondremos el siguiente a los ya definidos en el fichero.

        En método de acceso pondremos:

   •    METHOD_BUFFERED (se utilizarán unos buffers destinados a estas operaciones).

   •    METHOD_IN_DIRECT ó METHOD_OUT_DIRECT (no utilizan buffers, el mapeo de
        memoria se hace de forma diferente; de todos modos el acceso a los parámetros y la
        forma de devolver resultados ya viene implementada o al menos explicada en el código
        generado por el asistente).

   •    METHOD_NEITHER (en este modo, y bajo el punto de vista de DriverWorks se utilizará un
        tercer Buffer; de todos modos también debe venir implementado parte del acceso).

        En acceso permitido pondremos:

   •    FILE_ANY_ACCESS: cualquier tipo de acceso.

   •    FILE_READ_ACCESS: para lectura.

   •    FILE_WRITE_ACCESS: para escritura.

   •    FILE_READ_WRITE_ACCESS: para lectura/escritura.

        Abrimos el fichero DRIVER1Device.h y DRIVER1Device.cpp: en el primero añadimos una
función en la sección public cuyo prototipo debe ser del tipo:

         NTSTATUS DRIVER1_IOCTL_nombre_del_ioctl_nuevo_Handler (KIrp I);
        En el segundo fichero implementamos dicha función:

         NTSTATUS DRIVER1_IOCTL_nombre_del_ioctl_nuevo_Handler (KIrp I)
         {
         NTSTATUS status=STATUS_SUCCESS;
         //Codigo de la función.
         I.Information() = 0; //En el campo Information() debemos poner el
         número de bytes leídos o escritos, según la operación.
         return status;
         }
        Yo normalmente utilizo el METHOD_BUFFERED, de forma que los parámetros de
entrada/salida de la función los tenemos en un buffer que está en I.IoctlBuffer(); para obtener los
de entrada simplemente definiremos un puntero y haremos un cast a I.IoctlBuffer para asignarlo a
este puntero, por ejemplo:

         ULONG *inbuffer=(ULONG *)I.IoctlBuffer();
       Para el buffer de salida deberemos utilizar también el I.IoctlBuffer, por lo que lo es
recomendable recoger los parámetros de entrada en variables locales, para luego rellenar el buffer
con los parámetros de salida. En I.Information() deberemos colocar la cantidad de datos que se
devuelven (número de bytes o numero de palabras). Siempre que se devuelva un STATUS que
signifique error, deberemos poner I.Information() a cero. Hasta el momento siempre hemos
utilizado registros para acceder al dispositivo; por lo que tendremos una variable en el driver (si le
hemos asignado el recurso en el paso 9 del asistente); dicha variable tiene que ser del tipo
KIoRange, y podremos encontrarla en el fichero .h de nuestro driver. El proceso normal para
realizar una entrada/salida con este tipo de variables, dentro del driver, es el siguiente:

   •   Comprobamos si la variable está en un estado válido, comprobando el resultado de
       variable_iorange->IsValid()

   •   En caso afirmativo realizamos la operación de entrada/salida. Para llevar a cabo dicha
       entrada salida utilizaremos las siguientes funciones:

           o   Operaciones con bytes:

                   ƒ   variable_iorange->inb(ULONG ByteOffset): devuelve un byte leido en la
                       dirección ByteOffset a partir de la dirección base.

                   ƒ   variable_iorange->inb(ULONG ByteOffset, PUCHAR Buffer, ULONG Count):
                       lee y vuelca en el buffer apuntado por Buffer tantos bytes como indique
                       Count; leidos a partir del ByteOffset+Dirección Base.

                   ƒ   variable_iorange->outb(ULONG ByteOffset, UCHAR Data): escribe Data en
                       la dirección indicada por ByteOffset+Dirección Base.

                   ƒ   variable_iorange->outb(ULONG        ByteOffset,   PUCHAR      Buffer,   ULONG
                       Count): escribe en la dirección ByteOffset+Dirección Base los bytes
                       almacenados en el buffer apuntado por Buffer; escribe Count bytes de los
                       almacenados en Buffer.

           o   Operaciones con palabras (mismo funcionamiento que las anteriores pero lee datos
               de 16bits):

                   ƒ   USHORT inw(ULONG ByteOffset);

                   ƒ   VOID variable_iorange->inw(ULONG ByteOffset, PUSHORT Buffer, ULONG
                       Count);

                   ƒ   VOID outw(ULONG ByteOffset, USHORT Data);
ƒ   VOID outw(ULONG ByteOffset, PUSHORT Buffer, ULONG Count);

           o   Operaciones con long (mismo funcionamiento que la anterior pero lee datos de
               32bits):

                  ƒ   ULONG ind(ULONG ByteOffset);

                  ƒ   VOID ind(ULONG ByteOffset, PULONG Buffer, ULONG Count);

                  ƒ   VOID outd(ULONG ByteOffset, ULONG Data);

                  ƒ   VOID outd(ULONG ByteOffset, PULONG Buffer, ULONG Count);

       Una vez hecho esto buscamos la función DeviceControl(KIrp I); cuyo cuerpo tendrá una
sentencia switch, en la que añadimos un nuevo caso llamado como nuestro IOCTL, y dentro del
cual llamamos a la función definida anteriormente.

       NOTA: para depurar el funcionamiento interno de cualquier parte del driver podemos
mostrar mensajes del sistema utilizando una instrucción del tipo t
Cómo llamar a las funciones del driver desde una

aplicación.

        Tenemos dos formas de llamar al driver desde una aplicación:

    1. Creando un enlace simbólico al dispositivo.

    2. Exportando un interface. Ésta es la opción más común puesto que para drivers WDM se
        recomienda, y debido a que preserva las credenciales de seguridad del identificador del
        dispositivo, garantizándose la creación de una forma de acceder al dispositivo única e
        independiente del lenguaje utilizado.

Abriendo un manejador de un dispositivo que exporta un interface.
        DriverWorks nos proporciona dos clases que nos ayudan en esta tarea; CdeviceInterface y
CdeviceInterfaceClass.

        CdeviceInterfaceClass encapsula información acerca de todas las interfaces de dispositivo para una
clase de dispositivo en particular.

        Una aplicación puede usar una instancia de CdeviceInterfaceClass para obtener una o más
instancias de CdeviceInterface; esta última abstrae una sola interfaz de dispositivo. Su función
DevicePath() devuelve un puntero a un path que puede pasarse a la función CreateFile para abrir el
dispositivo.

        Abajo se muestra un ejemplo que aparece en el driver desarrollado (es una de las funciones que el
propio DriverWorks crea para la aplicación de testeo, y que he aprovechado en la DLL):

          HANDLE OpenByInterface(
          GUID* pClassGuid, // points to the GUID that identifies the
          interface class
          DWORD instance, // specifies which instance of the
          enumerated devices to open
          PDWORD pError // address of variable to receive error
          status
          )
          {
               HANDLE hDev;
               CDeviceInterfaceClass DevClass(pClassGuid, pError);
               if(*pError != ERROR_SUCCESS)
                     return INVALID_HANDLE_VALUE;
               CDeviceInterface DevInterface(&DevClass, instance, pError);
               if(*pError != ERROR_SUCCESS)
                     return INVALID_HANDLE_VALUE;
               hDev = CreateFile(DevInterface.DevicePath(),
                                GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
                                   NULL,
                                   OPEN_EXISTING,
                                   FILE_ATTRIBUTE_NORMAL,
                                   NULL
                                   );
                  if(hDev == INVALID_HANDLE_VALUE)
                        *pError = GetLastError();
                  return hDev;
        }

Realizando operaciones de E/S en el dispositivo.
       Una vez que la aplicación tiene un descriptor de dispositivo válido se puede utilizar
llamadas al API Win32 para generar peticiones al dispositivo. La tabla muestra la correspondencia
entre la llamada de WinAPI y el driver.

Win32 API           DRIVER_FUNCTION_xxx KDevice subclass member function
                    IRP_MJ_xxx

CreateFile         CREATE                   Create

ReadFile           READ                     Read

WriteFile           WRITE                   Write

DeviceIoControl DEVICE_CONTROL              DeviceControl

CloseHandle        CLOSE                    Close
                    CLEANUP                 CleanUp

       Si el dispositivo no soporta alguna de las funciones, la llamada al API causa un error de
función no válida.

       Ejemplo de llamada completa a un dispositivo, para leer un byte; se inicializa el dispositivo
y luego se lee:

        ULONG bufInput[IOCTL_INBUF_SIZE]; // Input to device
        CHAR bufOutput[IOCTL_OUTBUF_SIZE]; // Output from device
        ULONG nOutput; // Count written to bufOutput
        DWORD Error;
        hDevice = OpenByInterface( &ClassGuid, 0, &Error);
        if (hDevice == INVALID_HANDLE_VALUE)
        {
             Exit(1);
        }
        bufInput[0]=wPortAddr;
if (!DeviceIoControl(hDevice,
                     IOCTL_READ_BYTE,
                     bufInput,
                     IOCTL_INBUF_SIZE,
                     bufOutput,
                     IOCTL_OUTBUF_SIZE,
                     &nOutput,
                     NULL)
     )
{
          Exit(1);
}
CloseIfOpen();
return bufOutput[0];
Cómo crear una DLL en Visual C++.

    (Obtenido del tutorial de National Instruments)

 1. Crear un proyecto DLL.

       •   Abrimos un nuevo proyecto en Visual C++, del tipo “Win32 Dinamic-Link Library”;
           escogeremos del tipo de proyecto “A simple DLL project”. Esto crea un proyecto de
           DLL con un fichero de código fuente que tiene el mismo nombre que el proyecto.
           También genera un fichero stdafx.cpp; que es necesario, pero normalmente no
           necesitarás editarlo.

 2. Editar el código fuente.

       •   Cada DLL debe tener una función llamada DllMain, que será el punto de entrada a
           la DLL; a no ser que se necesite una inicialización compleja, o la DLL sea para un
           único propósito, bastará con la función creada al crear el proyecto. En caso
           contrario, complete la función.

       •   A continuación inserte las funciones que considere necesarias en la DLL.

       •   En este punto puedes compilar y enlazar la DLL, pero no se exportará ninguna
           función, así que no será de mucha utilidad.

 3. Exportar símbolos.

       •   Para podder acceder a las funciones de la DLL desde una aplicación es necesario
           indicar al compilador que exporte los símbolos deseados.

       •   Para evitar que Visual C++ compile las funciones a modo C++, declararemos todas
           las funciones a exportar como extern “C” en la declaración de la función.

                  extern “C” int suma(int x, int y);
       •   Para exportar la función hay dos maneras. La primera y más simple es utilizar la
           directiva __declspec(dllexport) en el prototipo de la función que se desee exportar.
           Se deberá de incluir la directiva tanto en la declaración de la función como en la
           definición.

                  extern “C” __declspec(dllexport) int suma(int x, int y);
                  ...
                  extern “C” __declspec(dllexport) int suma(int x, int y)
                        {
                             ...
                        }
       •   La segunda manera es utilizar un fichero .def para declarar qué funciones deben
           ser exportadas. Dicho fichero es un fichero de texto que contiene información que
           utiliza el enlazador para decidir qué exportar. Tiene el siguiente formato:
LIBRARY 
                        DESCRIPTION ""
                        EXPORTS
                         @1
                        @2
                         @3
                        ...
4. Especificando la convención de llamada a funciones.

      •   Tipo C: utilizamos la directiva __cdecl en la declaración y en la definición de la
          función:

          extern “C” __declspec(dllexport) int __cdecl suma(int x, int y);
          ...
          extern “C” __declspec(dllexport) int __cdecl suma(int x, int y)
          {
                ...
          }
      •   Estándar: utilizamos la directiva __stdcall en la declaración y en la definición de la
          función.

                 extern “C” __declspec(dllexport) int __stdcall suma(int x,
 int y);
                 ...
                 extern “C” __declspec(dllexport) int __stdcall suma(int x,
 int y)
                 {
                        ...
                 }
5. Compilando y enlazando la DLL

      •   Una vez que tenemos escrito el código, las funciones a exportar declaradas como
          tales, y establecida la convención de llamada a funciones, estás listo para construir
          la DLL. Ve al menú “Build”->”Build ”. Debería compilar y enlazar la
          DLL.
ANDRES_PIO_DIO.DLL: Funciones disponibles.

1. DriverInit
  Inicializa el driver poniendo las variables dentro de la dll al valor necesario.
          unsigned int __stdcall PIODIO_DriverInit(void);

2. PIODIO_InputByte
  Recoge un byte de entrada del dispositivo. Parámetros:

      •    wPortAddr: offset del registro del que se leerá.

      •    Devuelve: el byte leído.
          unsigned char __stdcall PIODIO_InputByte(unsigned long wPortAddr);

3. PIODIO_OutputByte
  Escribe un byte en el dispositivo. Parámetros:

      •    wPortAddr: offset del registro en el que se escribirá.

      •    bOutputValue: byte a escribir.
       void __stdcall PIODIO_OutputByte(unsigned long wPortAddr, unsigned
     short int bOutputValue);

4. PIODIO_OutputMatrixDLL
  Escribe una matriz en el dispositivo. Parámetros:

      •    wPortAddr: offset del registro en el que se escribirá la matriz.

      •    wIndexAddr: offset del registro donde se escribe el índice que recorre la matriz.

      •    width: valor del ancho de la matriz.

      •    height: valor del alto de la matriz.

      •    bOutputValue: puntero a los datos.
  NOTA: por compatibilidad con LabView la matriz debe ser bidimensional, pero sólo
utilizaremos la primera fila, se considerarán datos útiles a partir de la 4ª posición.
       void __stdcall PIODIO_OutputMatrixDLL(unsigned long
     wPortAddr,unsigned long wIndexAddr, unsigned long width, unsigned long
height, short int **bOutputValue);

5. PIODIO_InputMatrixDLL
  Lee una matriz del dispositivo. Parámetros:

      •   wPortAddr: offset del registro del que se leerá la matriz.

      •   wIndexAddr: offset del registro donde se escribe el índice que recorre la matriz.

      •   width: valor del ancho de la matriz.

      •   height: valor del alto de la matriz.

      •   bInputValue: aquí es donde se recogerán los datos.
  NOTA: por compatibilidad con LabView la matriz debe ser bidimensional, pero sólo
utilizaremos la primera fila, se considerarán datos útiles a partir de la 4ª posición.
       void __stdcall PIODIO_InputMatrixDLL( unsigned long wPortAddr,
     unsigned long wIndexAddr, unsigned long width, unsigned long height,
     short int **bInputValue);

6. PIODIO_Output1DArray
  Pasa un array de 1dimension al dispositivo. Parámetros:

      •   wPortAddr: offset del registro en el que se escribirá el array.

      •   wIndexAddr: offset del registro donde se escribe el índice que recorre el array.

      •   length: longitud del array.

      •   bOutputValue: array de los bytes a escribir.
       void __stdcall PIODIO_Output1DArray(unsigned long wPortAddr, unsigned
     long wIndexAddr, unsigned long lenght, unsigned char *bOutputValue);

7. PIODIO_Input1DArray
  Lee un array de 1dimension del dispositivo. Parámetros:

      •   wPortAddr: offset del registro del que se leerá el array.

      •   wIndexAddr: offset del registro donde se escribe el índice que recorre el array.

      •   length: longitud del array.

      •   bInputValue: array donde se depositan los bytes leídos.
void __stdcall PIODIO_Input1DArray(unsigned long wPortAddr, unsigned
long wIndexAddr, unsigned long length, unsigned char *bInputValue);
También puede leer