DESARROLLO DE DRIVERS CON DRIVERWORKS
←
→
Transcripción del contenido de la página
Si su navegador no muestra la página correctamente, lea el contenido de la página a continuación
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