A Hello World Device Driver - Part 3/6
Written by Alger Pike
Passing Your Own Data To and From Your Device Driver
[NOTE: You can download the source code here.]
Up to this point we have built a driver that when issued a DosDevIOCtl will produce a beep at 2000 Hz for 500 ms. The "hard-wired" nature of the code as it is now does not make it useful for real world situations. You could imagine, if you were controlling a device, for example a data acquisition card, that the device driver for it might contain IOCTL calls where the user would want to collect data for a variable amount of time. Clearly we must make some changes to our code so that the we can pass our own data back and forth from the code.
Near and Far Pointers
Passing data to and from the device driver is not too difficult. The most important thing to remember is that you must typecast all of the variables correctly. This way the compiler can make the conversion between the near and far pointers and set the data correctly.
As a quick refresher let's define what near and far variables are; Simply put a near or far variable refers to the memory scheme that it uses for its address. A near variable uses a 16-bit pointer. All code pointers and all data pointers contain only 16 bits because they are in the same 64K range. All of the code is contained in the 64K code segment allocated to the driver, and all of the data is within the driver's 64K data segment. Note that if it is required, a device driver can be written in the large model. A far variable uses two sixteen bit pointers. These variables are not in the same 64K segment so the require an additional 16-bits to represent the segment pointer in addition to the regular offset pointer.
The program which accesses the device driver is a 32-bit program. All of the pointers for code and data use 32-bit addressing and can point to anywhere in memory. The tricky part is converting between the two, inside the device driver code. When using the DosDevIOCtl interface however the conversions are made easy. The conversion is done for us by the DosDevIOCtl call. The pointers that are passed for Param Packet and Data Packet are converted for you, to addresses that the 16-bit code can use. In the device driver it will be necessary to copy our far pointer data to a near pointer so that the 16-bit code can access it correctly. Note that with Watcom, this can most easily be done in C by typecasting both the source and destination operands.
Passing Data To and From the Driver
We can put this knowledge to good use now and modify the IOCTL section of our strategy routine. The changes will allow us to send the driver a value which it will use to set the frequency parameter of the DosBeep call. In this way we can send the driver different values and produce tones with different frequencies. There are two pointers contained in the request packet that we will use to help us do this. Both of these are far pointers that point to the parameter packet and the data packet that we send to our driver using DosDevIOCtl. The trick will be to copy the values of these address to a near variable which can then be used in the DosBeep call. Then we will make the conversion back the other way and return the frequency the driver beeped at in the data packet. In this way we will be passing data both to and from our code. This will be the final stepping stone which will give us all the basic tools we need to start writing our own real world device drivers.
The IOCTL code is changed as follows:
case 0x01: *((WORD16 *) &freq) = *((BYTE FAR*)rp->ParmPacket); DosBeep(freq, 500); *((BYTE FAR*)rp->DataPacket) = *((WORD16 *) &freq); break;
Figure 16.) Typecasting required to use the parameter packet's Parm and Data packets correctly
Notice for this code to work correctly both the source and destination operands must be cast as dereferenced pointers. (The compiler should do the correct conversions for us without the type casting but when I try the driver crashes. Also upon examination of the resulting assembler code, the code without the typecasts is incorrect. Timur Tabi has worked with me on this problem and we have isolated it to compiler switches but haven't found the offending switch.) This will allow the compiler to correctly copy the data from the far address to the near address. The same is true for setting the value of the return data. You can convince yourself the above is true by changing the way you make the typecasts. Be forewarned though that this is dangerous. Your results can be simply from the driver not working to the driver causing an exception giving you one of those annoying "the system is stopped" black screens. (If anyone figures out the problem with the switches I would appreciate it if they would let me know.)
Now that we have set our driver up correctly to use our data packet and parameter packet, we need to pass these variables to the driver. This is done with the remaining six parameters of DosDevIoCtl that until this point always were zero. The first three parameters deal with the data packet. The first is a pointer to the variable which contains the data, the second is the length of this variable, and the third is a pointer to the length of the variable. The remaining three parameters are for describing the data packet. The same format applies to the data packet as to the parameter packet. To construct the proper call to the driver issue a statement as follows:
LONG Params, Return; ULONG p,c; p = sizeof(Params); c = sizeof(Return); Params = 250; rc = DosDevIOCtl(driver_handle, 0x92, 0x01, (void *) &Params, p, &p, (void *) &Return, c, &c);
Figure 17.) Setting up the test application to pass Parm and Data Packet values
This code sets up the proper memory spaces and converts the 32-bit pointers to the proper far pointers. At this point it should be possible to set Params to any value you want and the device driver should then beep at the frequency specified by Params.
Hopefully, this article has given you a sense of what is involved in writing a device driver. At this point you should be familiar with all of the basic skills required to write a device driver for your own device. These skills are merely the tip of the iceberg when it comes to what a driver can do. Here I have outlined the basic strategy behind writing a simple driver and provided the means necessary to communicate with it. Good sources of information on what else a driver can do are Writing OS/2 2.1 Device Drivers in C, 2nd Edition, Mastrianni, and the OS/2 DDK which you can subscribe to through IBM.