Home Page. Click Here.

Hardware Access With Delphi & Assembler


This code is extracted from a project where an off-the-shelf data acquisition board was used to create a unique application (a dynomometer.) This needed to read a high speed counter with some accuracy as well as read Analog sensor data translated via an A/D section. The hardware board was configured to use I/O address 300H - 30FH, so all registers used are addresses of BASE + OFFSET.

The method of talking to the board is pretty standard. First, you write to the base register to tell it what you intend to do next. This is done because the registers are read/write for many possible data items; e.g. register BASE + 4 may be used to pass a byte off to a counter or to a A/D unit, so by writing to the BASE you tell it what the BASE + 4 destination is.

In creating this application we ran across a couple of interesting things. In the days of Turbo Pascal through Delphi 1.0, there was a built-in set of routines used strictly to talk to hardware: inportb, inport, outportb, and outport. The "b" suffix denoted 1 byte. In Delphi beyond 1.0, these arrays were missing. It seems as though the powers that be didn't want us to talk to hardware easily. No problem, a simple bit of assembler does the trick. Just use the OUT instruction and specify the port, as follows:

   OUT $300, $FF   { output FFh to I/O port 300h }

But this didn't work either! That's funny, it worked in all of the previous compiler versions, and it's a standard instruction on an Intel chip... and then it hit us -- we are not allowed to use a literal mode of addressing to talk to hardware! (And the government is talking to Microsoft about BROWSERS??) What to do? Microsoft would like us to use a driver, obviously, but if one can't talk to the address, how are we supposed to write a driver? Not a problem. The handy assembler references say that you can also use indirect addressing:

   MOV AX, $FF    { value in AX }
   MOV DX, $300   { I/O port number }
   OUT DX, AX     ( same OUT as before but with register references }

Since we weren't writing this as a consumer app, this worked well enough that we decided to bypass the driver. The following set of routines show how we managed to:

  • reset a counter
  • read a backward running counter
  • initiate an A/D conversion
  • read an A/D conversion
  • minimize the number of clocks manipulating result data

Note that the assembler routines do not have an explicit return; the return is implicit and whatever you place into the AX register before terminating. Delphi's built-in assembler handles all of the stack manipulation and RET handling for you, which makes assembler coding pretty simple.

Also, note that we're manipulating the data quite a bit before returning. This is because we're already accessing the registers and the data needs to be manipulated anyway, so what we do is force the compiler to do the work we need done using the least number of clock cycles possible. For instance, we could have just as easily returned the raw contents of the counter and used some basic multiplication etc. in Delphi to format it to be useful. On the other hand the basic XOR or SHR take a minimum number of clocks and do the same job even faster. When in doubt, you should always assume that the compiler is a general purpose device and is not interested in optimizing clocks (or able to.)

Here then is the code for your enjoyment.

Delphi Assembler Code

procedure zerocounterzero;
begin
   asm
      MOV AL, 00110000b  { write 2 bytes @ counter 0 }
      MOV DX, 0307h      {counter 0 is at BASE + 7 }
      OUT DX, AL
      MOV AL, 00h      { our bytes will be 0 }
      MOV DX, 0304h    { counter 0 := $0304}
      OUT DX, AL       { write LSB = 0 }
      OUT DX, AL       { write MSB = 0 }
   end;
end;

function readcounterzero: smallint; assembler;
asm
   MOV AL, 00000000b  { we want to latch }
   MOV DX, 0307h      
   OUT DX, AL         { do it }
   MOV DX, 0304h      { counter 0 }
   IN AL, DX          { read LSB }
   MOV AH, AL         { put LSB in AH }
   IN AL, DX          { read MSB }
                      { AH = LSB, AL = MSB, so swap }
   MOV DL, AH         { put LSB in DL }
   MOV AH, AL         { put MSB in AH }
   MOV AL, DL         { put LSB back in AL }
   { the counter counts backwards, so now we have to normalize }
   { the registers... essentially this means subtracting 255 from }
   { each byte... the fast way to do this is to XOR each byte with
   { 255. Example: 38 counts would be 255 - 38 = 217. 217 = $D9, }
   { or 11011001b. XOR 217 with $FF = $26, or 38. }
   XOR AH, 255
   XOR AL, 255
end;

function readcounter: smallint;
var
   returnval:       smallint;
begin
   if(testmode = TRUE) then begin
      // if we are testing, and the loopback is chosen, then
      // we want to read counter 1.
      if(dc.testloopcheck.checked = TRUE) then returnval := readcounterone
      else                                     returnval := readcounterzero;
   end else begin
      // not test mode, so we're wanting counter 0 by default
      returnval := readcounterzero;
   end;
   result := returnval;
end;

procedure ADZeroForceConvert; assembler;
asm
   MOV AL, 00h       { specify a/d 0 }
   MOV DX, 0302h     { status register = base + 2}
   OUT DX, AL
   MOV DX, 0301h
   OUT DX, AL        { write to 301h forces 12 bit a/d conversion}
end;

function ADZeroRead: smallint; assembler;
asm
   MOV AL, 00h       { specify a/d 0 }
   MOV DX, 0302h     { status register = base + 2}
   OUT DX, AL
   MOV DX, 0301h
   IN AL, DX         { read the MSB bits }
   MOV AH, AL        { put the MSB in AH so as to free AL }
   MOV DX, 0300h
   IN AL, DX         { read the LSB bits }
   { This is a 12 bit conversion. the MSB is in AH, and the lower 4 }
   { bits are in the upper nibble of AL: ddddddddddddxxxx }
   SHR AX, 4         { rotate 4 bits right: xxxxdddddddddddd }
   AND AX, 0FFFh     { mask off the upper nibble: 0000dddddddddddd }
end;



Home

Copyright ©1998 - 2002 by AlstonLabs.com - ALL RIGHTS RESERVED