PDA

View Full Version : Interrupt handler in 8086 assembler



SwedaGuy
September 29th, 2010, 05:53 PM
Hello all! Long time, no type...I'm pretty sure it's been a few years since I hit this forum. Someone has really spiffed up the place.

I'm posed with an interesting task, though I must admit the practical applications are limited.

I'm writing an interrupt handler to intercept data from a keyboard (not THE keyboard, but rather a serial-attached special purpose keyboard), interpret the keystrokes and update certain totalizers and counters. Basically, when a byte is received on the COM port (2), my program needs to spring into action and handle the data. This may involve printing on an attached printer or alpha numeric pole display (COM 1), feeding back to the keyboard (COM 2) or sending data to a MICR encoder (COM 3).

I'm thinking this has to be done in Assembler, due to the fact that I have to fit a ton of processing into the 64K limit of a .COM file. Actually, the processing has to be in one 32K block and the data has to be in the other 32K block. This leads me to my problem: The data portion of the code has to always be loaded into the same segment:offset. There are a few segments for me to choose from, but no matter which I choose, I can't just let DOS decide when the program is launched where to stick it...the data always has to be at the pre-specified segment and offset. If you are wondering why, one specific 32K block of RAM (preselected by dip-switch) is shadowed by Non-Volatile RAM, and that is where the data needs to go so it can be retained when the power is shut off.

That said, I've done quite a bit of work in assembler, mostly 8085, so I understand the fundamentals, and have read over a book called "Memory Resident Programming on the IBM PC" by Thomas Wadlow. The book gives me the information on how to create a resident application, and has some interesting examples, but nothing on actually determining the location in memory in which the application should run--which is weird, as it goes into replacing the interrupt vector, but doesn't tell you how to derive the number that goes into the table--or how to make your program load in a pre-defined space.

Am I even on the right track, or am I missing something obvious? If it matters, I've got MS Macro Assembler 5.x. The machine is a 386, but I want to keep to the basic 8086 instruction set if possible.

I'm confident that if I can get the skeleton in place, handling the keystrokes and other I/O will be fairly straightforward, but I'm lost on this one foundation element of the program...

Thanks for any suggestions or guidance!

mbbrutman
September 29th, 2010, 06:08 PM
Just a few things to consider:

The card you are interfacing to probably uses memory ranges above 640K. (That is what you are selecting with the DIP switches.) Even within a .COM structured executable you can reference data in another segment far far away .. using 'far' pointers naturally.

COM is a simpler file format, making it quick and easy for DOS to load. It only restricts your code/data size in the file. Using far pointers you can get anywhere you need to, and in the case of data at a known location in higher memory that wouldn't even count against your 64K limit.


Given the above, you probably don't care where your code loads. As long as you can build far pointers to the memory that the hardware provides, where the code loads is irrelevant.


Also given the above, you can write in C, ASM, or whatever. I think you'll have plenty of space even with a high level language.

geneb
September 29th, 2010, 07:00 PM
Turbo Pascal 3.01! :)

It generates COM files and will do overlays too. :)

g.

Chuck(G)
September 29th, 2010, 09:09 PM
I'm not following the "predefined segment:offset" idea. Any location in memory can be expressed by thousands of segment:offset combinations. If you mean "location", please say so. (I'm not trying to be a smart-alec, just trying to get an idea of what you're trying to do).

If it's a predetermined location, is said location part of MS-DOS allocatable memory space? If so, has it been allocated?

Regarding interrupts, believe it or not, you can get pretty efficient ones in C or other languages if the necessary library "hooks" are there. Sure, assembly will always be fastest (you can optimize your use of registers), but the library routines for interrupts may be fast enough.

You never want to do more processing in an interrupt routine than is absolutely necessary; otherwise you can lose events or data. Get your data, stuff it into a circular buffer, update the "in" pointer and leave. (If you don't know what a circular queue or buffer is, it's easy. You have an area in memory with two pointers; an "in" and an "out" pointer. The routine that puts data into the buffer updates the "in" pointer, wrapping back to the beginning; the routine that takes data out of the buffer updates the "out" pointer. Pointers are never allowed to cross and when "in" = "out", the buffer is empty.)

It's all pretty simple and very straightforward, if we can determine what you need in the way of memory.

SwedaGuy
September 30th, 2010, 01:20 PM
Just a few things to consider:

The card you are interfacing to probably uses memory ranges above 640K. (That is what you are selecting with the DIP switches.) Even within a .COM structured executable you can reference data in another segment far far away .. using 'far' pointers naturally.



Thanks for the input, Mike...I guess I've misunderstood the .COM application type. It was my understanding that you couldn't use Far pointers, that it would have to be a Tiny memory model application, with both Code and Data residing within one segment. When I get home tonight, I'm going to look over that book one more time, as I was certain it outlined that limitation right off the bat...but I certainly have missed or misunderstood things like that in the past.

If I could map that area, and reserve it in DOS, to reference by far pointers, I would be very happy. As it is, I've been worrying about fitting all the functionality into 32K. As for the location in memory, I'm not sure, but I believe you are right--I think the options are C0000h, C8000h, D0000h or D8000h. It's dip-switch selectable on the motherboard. I'll investigate that one tonight.

Looks like I've got some reading to do tonight...

Chuck(G)
September 30th, 2010, 01:27 PM
Absolute segment addresses are permitted in any non-protected code. Relocatable segment addresses are not allowed in .COM files because they're essentially a memory image--they contain no relocation information for the system. However, it's entirely possible to write a .COM file that relocates segments during execution.

commodorejohn
September 30th, 2010, 01:43 PM
Assuming you're right on those memory addresses, those are most definitely in I/O space - real mode's 640KB RAM only goes up to 9FFFFh. Which should mean that you can just write to it as you please, unless there's some other TSR already making use of it.

SwedaGuy
September 30th, 2010, 02:37 PM
I'm not following the "predefined segment:offset" idea. Any location in memory can be expressed by thousands of segment:offset combinations. If you mean "location", please say so. (I'm not trying to be a smart-alec, just trying to get an idea of what you're trying to do).

If it's a predetermined location, is said location part of MS-DOS allocatable memory space? If so, has it been allocated?

Regarding interrupts, believe it or not, you can get pretty efficient ones in C or other languages if the necessary library "hooks" are there. Sure, assembly will always be fastest (you can optimize your use of registers), but the library routines for interrupts may be fast enough.

You never want to do more processing in an interrupt routine than is absolutely necessary; otherwise you can lose events or data. Get your data, stuff it into a circular buffer, update the "in" pointer and leave. (If you don't know what a circular queue or buffer is, it's easy. You have an area in memory with two pointers; an "in" and an "out" pointer. The routine that puts data into the buffer updates the "in" pointer, wrapping back to the beginning; the routine that takes data out of the buffer updates the "out" pointer. Pointers are never allowed to cross and when "in" = "out", the buffer is empty.)



I'm sorry that I left you with unclear information. It's very straight forward in my mind, but getting it across to others can be a challenge :)

In a nutshell, I need to be able to define a variable like "GRNDTTL" at Location C8665hm(for example), which is in the part of memory shadowed by NVRAM. Actually, I have a whole memory map of totalizers, counters, and records to accomodate in that 32K chunk. I was previously working under the assumption that the code and the data it references had to be in the same segment. Mike indicated in his post that this would not be the case, which means I can start my program with a bunch of defines for the data elements that place them where I need them, then access said data by Far pointer.

I haven't specifically allocated memory yet, but I will when I decide what portion of memory to use.

You are also not the first person to suggest I use a higher level language. Oddly enough I am quite a bit more comfortable with Assembler than C, but if I have a whole segment to work with for code, C may be a much nicer approach. Assembler requires a lot of "reinventing the wheel".

In any case, I'm fuzzy about the queue style input handling you indicated. I envision the resident part of the application being triggered by a byte input on a com port. The program needs to intercept the byte, act on it and then terminate and stay resident until the next byte on the com port activates it again. I hadn't thought about needing any kind of buffer (since it's a keyboard attached to the com port sending bytes and the computer can process the keystrokes much faster than I can press the keys) but then it occurred to me that the keyboard can also send longer strings from the intregrated mag stripe reader, in which case 50 characters or so would be fed to the program at 56kbps. But still, the computer should be able to process each byte like an individual keystroke (think keyboard wedge) much faster than 56kbps communication would feed it, or am I wrong? We're talking about a 386 here...

For example, if 34h appears on the port, that represents column 3, row 4 on the keyboard. My program has a table that tells it that key position is the numeric "7", therefore it puts it in the numeric input buffer. Some of these buttons fill one or more buffers, others cause these buffers to be acted upon. After each key is handled, the program suspends. Do you think I'm going to need any kind of buffer at all, or is the processor fast enough to handle the data as it comes in? I'm figuring the numeric keystrokes are going to require 100 instructions (max) to process, where as some of the function keys will need to execute between 1000 and 1500 instructions before returning the program to idle. I would think that the computer would be able to execute 1500 instructions between each of my keystrokes, but depending on what is going on at the PC (probably something like WordPerfect or dBase) I can't estimate the processor load.

SwedaGuy
September 30th, 2010, 02:48 PM
I'm going to have to do some reading in that book...I think I really missed something in the memory allocation area, because that wasn't at all how I read it...

But, I sure appreciate you input...just the other night I was starting to wonder about the feasibility of this project, but I've really got a lot more optimism now...

SwedaGuy
September 30th, 2010, 02:49 PM
Assuming you're right on those memory addresses, those are most definitely in I/O space - real mode's 640KB RAM only goes up to 9FFFFh. Which should mean that you can just write to it as you please, unless there's some other TSR already making use of it.

At this point, that's a big assumption...I'm working from memory here, and I've been reading about the low level interfaces for each of the peripherals, as well as my going back to my book on TSRs and refreshing my assembly skills, so if I've confused some numbers I wouldn't be surprised!

Chuck(G)
September 30th, 2010, 03:32 PM
Here are my thoughts on this.

First of all, do consider C (or Pascal or Modula-2) as your primary implementation language. Far pointers (inter-segment) can be handled in most versions and in all memory models. Spend a lot of time working on your definition files, abstracting datatypes where appropriate, so that if you do change operating systems or memory models, you make it easy on yourself. ("typedef" is your friend).

Almost all varieties of C allow for inline assembly sequences, so you need never leave the C environment.

While .COM files are convenient, is there any pressing need for them? .EXE can be somewhat more compact and are checked during program load against simple corruption. .COM files are loaded as-is without checking.

Whether or not you'll need a queue depends on exactly how much processing you need to do--and if your processing can tolerate interrupts from higher-priority events, such as the time-of-day interrupt that occurs every 55 msec. The safe way is to queue the data in the interrupt routine.

Is your program a "background" task, such that there are other things running in the foreground? If so, that's another complication that can affect how you do things.

I'd recommend some sort of initialization code that locates the segment where the board is configured automatically. That way, you've relieved users from the responsibility to tell you and also isolated yourself from the situation where some idiot has played with the board DIP switches.

SwedaGuy
October 2nd, 2010, 04:32 PM
Here are my thoughts on this.

While .COM files are convenient, is there any pressing need for them? .EXE can be somewhat more compact and are checked during program load against simple corruption. .COM files are loaded as-is without checking.

Is your program a "background" task, such that there are other things running in the foreground? If so, that's another complication that can affect how you do things.


Yes, in fact, this is going to run in the background. The primary applications that might be run in the foreground are WordPerfect, dBase III+ or a label printing program I wrote a number of years ago. None of these are full time applications, and the machine is idle quite a bit...but when I want to make entries in this new program, it won't be convenient to quit what I'm doing and start another application. That said, I thought TSRs had to be .COM files? Am I mistaken on that?

You guys have basically got me sold on using C. That way some information can be retained in NVRAM, but I can also write files out to disk much more easily, allowing greater file sizes and significant flexibility in sorting and indexing info. In addition, I can use standard dBase files so I can run other queries if need be.

Chuck(G)
October 2nd, 2010, 04:39 PM
That said, I thought TSRs had to be .COM files? Am I mistaken on that?

Yes, .EXE files can use DOS function 31H - "Terminate and Stay Resident".

SwedaGuy
October 3rd, 2010, 10:57 AM
I don't suppose you can recommend any good books on the subject? The one that I have (which I assumed would be authoratative) seems to have glossed over some things. For example, it led me to believe that the TSR had to be a .COM, and that all program data had to be in the same segment as the code. I'll grant that this is an old book (1984, I think), but it sounds like it isn't painting a complete picture for me. I have several C references (no to mention a C guru in the family), but they only talk about TSRs in the most general terms, and seem to view them as some kind of "black art", to be used by only the most highly experienced coders. It doesn't seem like it should be rocked science...

JohnElliott
October 3rd, 2010, 11:28 AM
You can have the data anywhere, but when the interrupt handler gets called you can't rely on the value of DS. So if your data segment is somewhere else, you need to point DS at it (and restore the original DS on return from your handler).

mbbrutman
October 3rd, 2010, 11:30 AM
You probably should be looking for a few books, as they all have strengths and weaknesses. Here is what is on my bookshelf:


Microsoft MS-DOS Programmer's Reference Version 5, Microsoft Press
Write TSRs Now with Borland's Turbo Assembler, Turbo C++ and Turbo Pascal, Jim Ras
Advanced MS DOS Programming, Ray Duncan
Undocumented DOS, 2nd Edition, Schulman et al.
Writing DOS Device Drivers in C, Adams and Tondo
Writing MS-DOS Device Drivers, Robert S. Lai


And that doesn't count the assembly language and other language references. :-)

per
October 3rd, 2010, 11:42 AM
As of my understanding, you can do anything you'd like using .COM programs, as long as the code doesn't exceed 64KB in size.

Of course, when a .COM file is loaded, DOS assign both the data and code segment to where the program is loaded, but you can still take full control over the machine regardless of anything else in memory. However, if you wish to be able to return control back to DOS, you has to make sure you allocate any extra conventional RAM you take use of, so DOS or other programs don't overwrite it.

The 8086 and 8088 is fairly simple; the CPU can only do one thing at a time, and while one program runs; that's the only thing that happens in the computer. Because of this, any runing program can in fact do anything possible, without any limitations. However, since most programs runs of an operating system, it should not break the 'rules' of it, as that would make it unstable if control ever was returned to it.

mbbrutman
October 3rd, 2010, 11:50 AM
This is probably repeating a lot of what has already been written, but there are a few things to keep in mind:


COM files are limited to just under 64KB for code and data. That is the code and data within the COM file; data that you are referencing elsewhere, like in the ROM or in your hardware adapter memory space is not part of the COM file and does not count toward the COM file size limit.
COM files are very simple; they are basically just a memory image of what your program's code and data should look like. This is simple and they load fast, but they have restrictions because of this. EXE files have all sorts of meta data in them, requiring a more sophisticated loader but also giving them more flexibility.
Terminate and Stay Resident is a DOS function call. It just tells DOS to leave your program and its memory resident in memory. Any language capable of making that DOS function call is capable of being a TSR. Assembly language is a good choice for any small program, but C and Pascal are viable too. And the more complex your code is, the more likely it is that you will want to use a higher level language.
Separate from being a TSR is the ability to 'hook an interrupt'. Hooking an interrupt allows your code to run whenever the timer ticks or a key is pressed; it depends on which interrupts you hook. The work 'hook' is supposed to be descriptive of the process by which you insert your code into the interrupt vector before the existing interrupt code, thus allowing it to run first. (And usually you pass control to the original interrupt code so that it can do whatever function it needed, assuming that you are not replacing it entirely.)


There are rules for hooking interrupts and writing TSRs that must be obeyed - hence the wide variety of books.

One of the bigger issues is 'what is safe' to do and what is not in your interrupt handler. You have to remember that you code can wake up and run while DOS is doing something fairly deep. DOS is not reentrant, so if you interrupt DOS and call another DOS routine you are probably going to hose the system.

Chuck(G)
October 3rd, 2010, 12:39 PM
One frequent question on writing background tasks that need to read or write data to disk is "Once I relinquish control, how do I get it back?" There are two answers, depending on your requirements.

The first is to hook interrupt 28h, the so-called "DOS Idle Interrupt", which is invoked by DOS everytime console I/O is requested. The problem is that not every program uses DOS for console I/O, preferring to go through the BIOS instead. (This is one of the reasons that OS/2 1.x was so terrible at multitasking DOS applications). So it's unreliable at best.

The most reliable way to get your piece of the pie is to hook interrupt 08 (timer tick) to get the CPU every 55 msec. In your hook routine, check the 8259 ISR (you only need check the primary one) to see if the timer has interrupted another lower-priority interrupt (only the bit for IRQ 0 should be set). If the ISR is clear, but for IRQ 0, check the InDOS flag to see if it's okay to talk to DOS. If that's clear, you can push a status word and long pointer to your background program servicer on the stack, then pass control to the regular interrupt 08 handler. When it exits, it will exit to your background program, where it's safe to issue your DOS request (make sure your save and set up any registers that it needs). When your task has done its dirty work, restore the registers and exit with a RETI.

A third way if you're working on a PC AT or later, is to use the Int 15 timer services to post a "wakeup" call. These have much finer granularity (usually about 1 msec.) than the timer "tiick", but are not available on earlier systems.

While in your service task, be careful not to write console output--you don't know what mode the display is in, nor do you know how it's being handled.