6

After successfully using port[0] to read a port value in Turbo Pascal, I thought I'd try to understand how to do this using inline assembly/machine code.

I'm able to write to the port (my OutPort procedure works fine), but unfortunately my read procedure (InPort) doesn't work... it just does nothing. Due to my limited assembly/machine code knowledge, I'm unable to see what I'm doing wrong.

Using Turbo Pascal 3.01A (CP/M-80, Z80).

Edit: I'm using an RC2014 with the Digital IO module.

program IOBtnASM;
{$U+}
  var i: Byte;

procedure OutPort(Port: Byte; Value: Byte);
begin
  inline ($3A/Port/       {      ld a,Port     }
          $4F/            {      ld c,a        }
          $3A/Value/      {      ld a,(Value)  }
          $ED/$79         {      out (c),a     });
end;

procedure InPort(Port: Byte; var Value: Byte);
begin
  (* TODO: This inline doesn't work; read port value using asm/mc *)
  inline ($3A/Port/       {      ld a,Port     }
          $4F/            {      ld c,a        }
          $ED/            {      out a,(c)     }
          $3A/Value       {      ld (Value),a  });
end;

begin
  writeln('IO Button ASM');
  writeln('Using Assembly, light LED when button pressed');
  writeln('Press Ctrl+C to break');

  while true do
  begin
    InPort(0, i);
    OutPort(0, i);
    delay(50);
  end;
end.

Edit: Adapted from: Simple Turbo Pascal program to output byte to an I/O port

Edit 2

For reference, the following simpler program works, using port[0] to read and write. I'm writing it in assembly/machine code for fun.

program IOBtnLED;
{$U+}
  var i: integer;

begin
  writeln('IO Button LED');
  writeln('Light LED when button pressed');
  writeln('Press Ctrl+C to break');

  while true do
  begin
    i := port[0];
    port[0] := i;
    delay(50);
  end;
end.

Edit 3

Based on Raffzahn's answer, I tried the following, but unfortunately this still doesn't read the value from the port.

program IOBtnASM;
{$U+}
  var i: Byte;

procedure OutPort(Port: Byte; Value: Byte);
begin
  inline (
    $3A/Port/       {      ld a,Port     }
    $4F/            {      ld c,a        }
    $3A/Value/      {      ld a,(Value)  }
    $ED/$79         {      out (c),a     }
    );
end;

(* Raffzahn's 1st attempt (missing something) *)
procedure InPort(Port: Byte; var Value: Byte);
begin
  inline (
    $3A/Port/       {      ld a,Port     }
    $4F/            {      ld c,a        }
    $ED/$78/        {      in a,(c)      }
    $32/Value       {      ld (Value),a  }
    );
end;

begin
  writeln('IO Button ASM');
  writeln('Using Assembly, light LED when button pressed');
  writeln('Press Ctrl+C to break');

  while true do
  begin
    InPort(0, i);
    OutPort(0, i);
    delay(50);
  end;
end.

Edit 4

Working code, thanks to Raffzahn's very detailed answer: IOBtnASM.pas (don't forget to upvote the answer).

20
  • 1
    Yes, perhaps edits would have been better. Fixed. Commented Jun 30, 2022 at 21:44
  • 2
    Concerning InPort: Since Value is a parameter by reference, I would expect its address for example in HL, not at a static address. An instruction like ld (hl),a should work then. Unfortunately I don't have the time to check, and no CP/M system at hand at all. -- Why don't you use a function that returns the value? This seems more natural to me. Commented Jul 1, 2022 at 8:14
  • 1
    @NickBolton Ok, Cool, I'll update the answer later on with a detailed description how it works.
    – Raffzahn
    Commented Jul 1, 2022 at 11:18
  • 1
    @Raffzahn My vague, waking instinct was right. Hehe.
    – jonk
    Commented Jul 1, 2022 at 16:55
  • 1
    @NickBolton No problem. I've written C compilers in my past. And could 'guess' about this. Thanks for the kind words!
    – jonk
    Commented Jul 1, 2022 at 17:06

1 Answer 1

6

TL;DR:

There are a few misconceptions, see below. In addition VAR parameters seem to be pointers so they needed to be loaded into HL prior to use to be loaded first (Thanks to The_Bussybee for reminding me), so this may have to look more like:

procedure InPort(Port: Byte; var Value: Byte);
begin
  inline ($3A/Port/       {      ld a,Port     }
          $4F/            {      ld c,a        }
          $ED/$78/        {      in a,(c)      }
          $2A/Value/      {      ld hl,(Value) }
          $77             {      ld (hl),a     });
end;

The section about inline in the Turbo Pascal manual is really a bit ... let's say compact.


Further Reading:

It seems as if you copied that Outport function from some site (where?), duplicated that and only changed sequence and comments, but not function. Also loosing the second opcode byte of out in turn.

     $ED/            {      out a,(c)     }

First OUT with an indexed port in C is a two byte opcode, $ED/$79

Second OUT does exactly that, it outputs (i.e. stores) a byte to a port. To input (read) from a port an IN instruction is more appropriate. In this case $ED/$78.

Last, but not least, the text between curly brackets is only a comment. It's custom to write the mnemonics code as comment within inline statements for readability, the compiler doesn't care for this, it just takes whats written outside.

     $3A/Value       {      ld (Value),a  });

Similar here just changing the comment doesn't change anything. Turbo Pascal does not include an Assembler. The Inline function just takes a sequence of bytes and words and puts it verbatim into the generated program. It's pure machine code, the only help is the ability to insert variable references using their symbolic names.

All conversion from mnemonics to binary, except for addresses has to be done by hand by the programmer. Thus it's always helpful to have at least an 8080 (or Z80) instruction table at hand. This Z80 Table also include links to a basic description for each instruction - just in case one wants to go ahead without reading a Z80 book first :))

Last but not least, as the Value parameter is now declared as VAR, it's not handed over by value, but as by reference (aka as a pointer). So that pointer must be loaded into HL to be then used to store the read value to wherever the real Value is.

  • Load the pointer to Value into HL: $2A/Value/ { hl,(Value) }

  • Use HL to store the read value (in A) to where the pointer points to: $77 { ld (hl),a }

The - rather compact information about inline is to be found in the Turbo Pascal Manual in chapter 22.16 on page 157ff.


P.S.: What About Using a Function?

A routine returning a value, like InPort, can of course be done as procedure with VAR parameters. But since there is a return parameter wich very much is the result of that routine, a function may seem way more natural and allow seamless use in expressions. After all, returning a value is the whole point of having a function, isn't it? So why not try (*1):

function InPort(Port: Byte):Byte;
begin
  inline ($3A/Port/       {      ld a,Port      }
          $4F/            {      ld c,a         }
          $ED/$78/        {      in a,(c)       }
          $2A/InPort/     {      ld hl,(InPort) }
          $77             {      ld (hl),a      });
end;

This would allow to go

    i = InPort(0);
    OutPort(0, i);

Doesn't that ease reading the flow of information?

Or even in a single line:

    OutPort(0, InPort(0));

Which read much like an English sentence now:

Set Port 0 to the value of read port 0`

Maybe name those procedures/functions more like what they do (SetPort/ReadPort), not what assembly instruction (OUT, IN) they are based on. Good documentation is about meaning, not usage (*2). And meaningful names are by far the most useful form of documentation.


*1 - this is written under the assumption that the return value is as well handed by reference. The manual doesn't tell, so if this doesn't work, try

function InPort(Port: Byte):Byte;
begin
  inline ($3A/Port/       {      ld a,Port     }
          $4F/            {      ld c,a        }
          $ED/$78/        {      in a,(c)      }
          $32/InPort      {      ld (InPort),a });
end;

Yes, I know, remote testing sucks :))

*2 - It's the age old story of

INC HL ; Increment HL as being redundant, useless and booooring

vs.

INC HL ; Advance pointer to next byte adding meaning ad readability

8
  • Aha, of course. They're comments. Being unfamiliar with inline asm in Pascal, I didn't notice. Commented Jun 30, 2022 at 21:02
  • Yes, I'm using an RC2014. The digital IO board has both read and write on the same port: rc2014.co.uk/modules/digital-io Commented Jun 30, 2022 at 21:04
  • It's specific to Turbo. Pascal does not know Inline per se. So, that loop will work. Though, I'm not sure if CTRL-C will break that loop, as IIRC it's only checked during Read and ReadLn - btu I might be wrong.
    – Raffzahn
    Commented Jun 30, 2022 at 21:28
  • 1
    Hmm, so I tried converting the procedure to a function, and the function seemed to return garbage. I'm sure I made a mistake. I used the function name in the inline code to store the return value, so that's probably not it. Tried saving the function result to a variable and also returning directly into the 2nd param of OutPort... same result either way. I'll take a break and re-visit another day. Commented Jul 1, 2022 at 12:37
  • 1
    @NickBolton I did two versions for the function example, as I do not know (without testing) which one is right. One right after the PS header, the other in footnote #1. They differ in the way the value is returned. You may want to try either. I do not have a TP setup handy to test - everything so far is guesswork from memory (hinted by the manual).
    – Raffzahn
    Commented Jul 1, 2022 at 19:53

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .