This article is full of concepts. If you wanted to get yourselves a headache,
you've found the place ;) This will not show what Assembler is nor the basics
of Assembler programming. It intends to put some light on the topic "How do i
interface 'C' with a86 ?".
The sample code shows two buttons, they let you control two timers. You can
start and stop them individually. The timers indicate their activities by
incrementing two counters.
Show Asm Code
There's a piece of code floating around the net that shows how to 'hook' a system
timer. We start from that. This timer is simply an assembler proc called a number
of times a second. It's been tested and this number seems to be 40;
The system keeps an interrupt table starting from physical address 0 to 1023.
Interrupts are called by an asm instruction called (guess what) 'int'. Its
parameter is the interrupt number, it ranges from 0 to 255. So why 1024 bytes?
The table actually contains 32 bits addresses, four bytes long. Now what does
it happen if i divide 1024 by 4? I get 256 entries, each entry stores 4 bytes.
Every far pointer or address is made of 2 words. The high one is called the
Segment selector, the low one is the Offset. It turns out that an address takes
this form ssss:oooo. By the way, near pointers share the same segment.
How can my application get notified by the timer?. Somewhere the system calls
a procedure whose address is stored in the interrupt table, index 83h. Every
entry is called 'vector'. By changing this vector we can lead the system
wherever we want (a crash if we don't know what we're doing)
Let's define some constant. They show we make two timers out of one!
;; -----------------------------------------------
;; Timers Manager
;; -----------------------------------------------
INTNR EQU 83h
TMR1_STATE EQU 0
TMR1_VALUE EQU 1
TMR1_CNT EQU 3
TMR2_STATE EQU 5
TMR2_VALUE EQU 6
TMR2_CNT EQU 8
This is our global data we put is in the data segment. We allocate 2 structs for
the timers. Each timer has a flag that says whether it is currently used of free.
We store the interval after which we want to be notified and the a counter that
counts up to 'Interval', when it gets to it we fire an event. "oldintvect"
stores the address we replace in the interrupt table.
DATA DSEG
oldintvec: dw 0,0 ; old 83h interrupt vector
TIMERS:
db 0 ; Free/Allocated - timer1
dw 0 ; Interval
dw 0 ; Counter
db 0 ; Free/Allocated - timer2
dw 0 ; Interval
dw 0 ; Counter
Here it starts the code segment. What is OnTimer_? Being 'EXTRN', it is an
identifier defined elsewhere, not in this module. It's actually the callback
function we fire when the counter reaches its Interval. We must implement this
function in our C code. Just notice we append an underscore at the end of the
name. This is a C compiler convention which, at asm level, decorates identifiers
like that. So if we want to refer to a C function in asm, we must call it that way.
; ------------------------------------------------------------
intproc_TEXT CSEG
.186
;------------------------------------------------------------
;void OnTimer(int TmrNr)
;------------------------------------------------------------
EXTRN OnTimer_
We export 4 functions. SetTimer is the first. It allocates a timer if it
finds a free one. It returns the timer identifier, 1 or 2, or -1 if they are
both used. We pass it the time it has to elapse before we get notified.
Where do i see all that stuff?? Now keep cool...
We define a label by writing its identifier and append ':' to mark it as
a label. A double ':' makes the label available to other modules. Don't forget
the underscore because C translates SetTimer into SetTimer_.
How do C and ASM exchange parameters? Chapter 6. "Programming for i8086" enumerates
4 rules. See 'function call protocol'. Let's just say that the compiler tries
to put parameters into the registers (AX, BX, CX, DX) before calling the funcion.
If they are used it uses the stack. The same happens with the return value.
So we get Interval into AX and return the timer nr into AX. Hard to believe?
;------------------------------------------------------------
; int SetTimer(int Interval)
;------------------------------------------------------------
SetTimer_::
test [TIMERS + TMR1_STATE].b, 1
jnz st_check2
inc [TIMERS + TMR1_STATE].b
mov [TIMERS + TMR1_VALUE], ax
mov [TIMERS + TMR1_CNT].w, 0
mov ax, 1
retf
st_check2:
test [TIMERS + TMR2_STATE].b, 1
jnz st_full
inc [TIMERS + TMR2_STATE].b
mov [TIMERS + TMR2_VALUE], ax
mov [TIMERS + TMR2_CNT].w, 0
mov ax, 2
retf
st_full:
mov ax, 0FFFFh
retf
This stops the timer TmrNr and frees its resources.
;------------------------------------------------------------
; void KillTimer(int TmrNr)
;------------------------------------------------------------
KillTimer_::
cmp ax, 1
jne kt_check2
dec [TIMERS + TMR1_STATE].b
retf
kt_check2:
cmp ax, 2
jne kt_skip
dec [TIMERS + TMR2_STATE].b
kt_skip:
retf
The next two functions hook and unhook the system timer interrupt.
Init replaces entry 83h with our Interrupt Service Address 'newintproc'.
Terminate restores the original address 'oldintproc'
;------------------------------------------------------------
; void InitTimers();
;------------------------------------------------------------
Timers_Init_::
mov [TIMERS + TMR1_STATE].b, 0 ; mark as free
mov [TIMERS + TMR2_STATE].b, 0
push ax
push es
pushf
cli
xor ax,ax
mov es,ax
mov ax, es:[INTNR*4]
mov [oldintvec], ax
mov ax, es:[INTNR*4+2]
mov [oldintvec+2], ax
mov ax, newintproc
mov es:[INTNR*4].w, ax
mov ax, cs
mov es:[INTNR*4+2].w, ax
in ax, 0ah
and ax, 0fff7h
out 0ah, ax
in ax, 08h
or ax, 08h
out 08h, ax
in ax, 0ah
or ax, 08h
out 0ah, ax
popf
pop es
pop ax
retf
;------------------------------------------------------------
; void Timers_Terminate();
;------------------------------------------------------------
Timers_Terminate_::
push ax
push es
pushf
in ax, 0ah
and ax, 0fff7h
out 0ah, ax
in ax, 08h
and ax, 0fff7h
out 08h, ax
cli
xor ax, ax
mov es, ax
mov ax, [oldintvec]
mov es:[INTNR*4], ax
mov ax, [oldintvec+2]
mov es:[INTNR*4+2], ax
popf
pop es
pop ax
retf
This is the heart of the module. This function gets called 40 times a second.
It starts up by making room for the return address in order to call the
old interrupt service when returning.
;------------------------------------------------------------
; Interrupt Proc
;------------------------------------------------------------
newintproc:
push ax ; dummy for retf address
push ax ; dummy for retf address
push bp
mov bp, sp
push ax
push ds
mov ax, DATA
mov ds, ax
NOW! Before thinking you can do what you want with this, ask an old dos
programmer what reentrant code is. The problem is you don't know
when the interrupt starts. When it happens, every code being executed is
stopped and the CPU services the interrupt. The code that is stopped has
a context that cannot be changed (memory and regs). If you change it, when
the the code resumes, the system may become unstable and crash.
Now they used to put a simple counter increment here. Up to the C code to
find out when the counter is done by polling. What harm can it do? But what
if we setup a more complex scheme? We check the timer and call back a C function.
We could potentially call a C function everytime the interrupt is generated.
Actually we only call it after the intervals requested.
We almost push everything onto the stack, it's a safety measure trying to
keep the contest as stable as we can. We know what goes on in the code here,
we write it. But what after callf OnTimer_? Despite all, it works,
we keep on "feeling lucky" ;)
So we check for the counters and fire OnTimer passing it the timer number
push ax
push bx
push cx
push dx
push si
push di
;
test [TIMERS + TMR1_STATE].b, 1
jz int_check2
mov ax, [TIMERS + TMR1_CNT]
cmp ax, [TIMERS + TMR1_VALUE]
jne int_skip1
mov ax, 1
callf OnTimer_
mov ax, 0FFFFh
int_skip1:
inc ax
mov [TIMERS + TMR1_CNT], ax
int_check2:
test [TIMERS + TMR2_STATE].b, 1
jz int_done
mov ax, [TIMERS + TMR2_CNT]
cmp ax, [TIMERS + TMR2_VALUE]
jne int_skip2
mov ax, 2
callf OnTimer_
mov ax, 0FFFFh
int_skip2:
inc ax
mov [TIMERS + TMR2_CNT], ax
int_done:
pop di
pop si
pop dx
pop cx
pop bx
pop ax
We're done and we're not selfish so we call the old interrupt service too.
mov ax, [oldintvec]
mov [bp+2], ax
mov ax, [oldintvec+2]
mov [bp+4], ax
pop ds
pop ax
pop bp
retf ; this will pop the two dummy pushes
You compile this code as usual. Put it in your app C directory just like
other c modules but name it somehing.a86. The compiler will take care.
How are you?
Show C Code
Let's see how we can use the timers.
#include <stdrom.h>
#include "libc.h"
#define BTN_TMRA_X 60
#define BTN_TMRA_Y 20
#define BTN_TMRA_W 46
#define BTN_TMRA_H 12
#define BTN_TMRB_X 60
#define BTN_TMRB_Y 40
#define BTN_TMRB_W 46
#define BTN_TMRB_H 12
#define EVENT_TCH 1
If 'int 83h' is called 40 a second how long do i have to wait before 'x'
millisecods?
#define MSEC(x) (x / 25)
These functions are defined somewhere else, aren't they?
extern far Timers_Init();
extern far Timers_Terminate();
extern int far SetTimer(int Interval);
extern void far KillTimer(int TmrNr);
trm_x store the timer identifier SetTimer returns, cnt_x are our counters,
then we have the button captions they're not constants we need to change
them.
int tmr_a, tmr_b, cnt_a, cnt_b;
char btna_cap[16] = "Start A";
char btnb_cap[16] = "Start B";
OooH!
void PollEvents(TCHSTS far *tsts, byte event_mask)
{
union REGS reg;
reg.x.ax = 0x0200 | event_mask;
reg.x.di = FP_OFF(tsts);
reg.x.es = FP_SEG(tsts);
int86(0x50, ®, ®);
}
No DoAnimate is not such a naive function as it wants you to believe. It flashes a
button and i replaced the LibWait functions with for loops. Well it happens
that the LibXXXWait functions are rather selfish in using the timer. They
eventually stop it. Lesson number one: if you want ot use a timer other
than OBJ_500MSEC don't use LibXXXWait functions. Why does GrayLib use it instead?
Well i suppose that since it continuosly calls LibWait and the timer is repeatedly
switched on/off it gives us the impression that it's never off.
That's the reason why i don't like patches. If you can't work together with
the system, chances are that one day you'll have to start all over again.
That said, PollEvents is really useful, it doesn't beep? We don't waste
battery energy. The first time i opened my PV it kept on playing beeps on
every touch. I still remember people faces. Now it doesn't beep anymore!
No i haven't shot at it :)
void DoAnimate(int x1, int y1, int x2, int y2)
{
int i;
LibGdsReverse(x1, y1, x2, y2);
LibPutDispBox(x1, y1, x2 - x1, y2 - y1);
for (i = 0; i < 2000; i++) i = i;
LibGdsReverse(x1, y1, x2, y2);
LibPutDispBox(x1, y1, x2 - x1, y2 - y1);
for (i = 0; i < 2000; i++) i = i;
}
void AddButton(int BX, int BY, int BW, int BH, char *BCAP)
{
int sz;
LibClrBox(BX, BY, BW, BH);
LibGdsBox(BX, BY, BX + BW, BY + BH);
sz = LibGetProStrSize(IB_PFONT2, BCAP);
LibPutProStr(IB_PFONT2, BX + (BW - sz) / 2, BY + (BH - 7) / 2, BCAP, sz);
LibPutDispBox(BX, BY, BW + 1, BH + 1);
}
bool InRect(int px, int py, int x, int y, int w, int h)
{
return (x < px && px < x + w) && (y < py && py < y + h);
}
Here it comes, the magic function. It comes out of nothing. Can you see it
called in any part of this C code? But it's alive.
void OnTimer(int TmrNr)
{
char tmp[8];
if (TmrNr == tmr_a)
{
sprintf(tmp, "%d", cnt_a);
LibClrBox(40, 100, 20, 10);
LibPutProStr(IB_PFONT2, 40, 100, tmp, 20);
LibPutDispBox(40, 100, 20, 10);
if (cnt_a == 99)
cnt_a = 0;
else
cnt_a++;
}
if (TmrNr == tmr_b)
{
sprintf(tmp, "%d", cnt_b);
LibClrBox(120, 100, 20, 10);
LibPutProStr(IB_PFONT2, 120, 100, tmp, 20);
LibPutDispBox(120, 100, 20, 10);
if (cnt_b == 99)
cnt_b = 0;
else
cnt_b++;
}
}
This time we can't ignore the main code. See how we init the timers and
terminate them. We start one timer with 1 sec interval and one with 2 secs.
After a button is pressed it starts a timer and changes its label to indicate
that if it's pressed again it will stop the same timer. We need a flag this
time, if PollEvents is freedom, this means it has its own drawbacks. If you
hold the pen on a button, the button code is continuosly executed
setting and killing the timer and you haven't a chance to realize what's
going on. We implement a flag to execute the button code on every first
time the pen touches the button area.
void main()
{
TCHSTS tsts;
bool bExit = FALSE;
bool flag;
cnt_a = cnt_b = tmr_a = tmr_b = 0;
Timers_Init();
LibClrDisp();
LibPutDisp();
AddButton(BTN_TMRA_X, BTN_TMRA_Y,
BTN_TMRA_W, BTN_TMRA_H, btna_cap);
AddButton(BTN_TMRB_X, BTN_TMRB_Y,
BTN_TMRB_W, BTN_TMRB_H, btnb_cap);
flag = FALSE;
for (;bExit == FALSE;)
{
PollEvents(&tsts, EVENT_TCH);
if (tsts.y == -1 || tsts.x == -1) flag = FALSE;
if (flag) continue;
if (tsts.y != -1 || tsts.x != -1)
{
if (InRect(tsts.x, tsts.y,
BTN_TMRA_X, BTN_TMRA_Y,
BTN_TMRA_W, BTN_TMRA_H))
{
flag = TRUE;
DoAnimate(BTN_TMRA_X, BTN_TMRA_Y,
BTN_TMRA_X + BTN_TMRA_W,
BTN_TMRA_Y + BTN_TMRA_H);
if (tmr_a == 0)
{
strcpy(btna_cap, "Stop A");
AddButton(BTN_TMRA_X,
BTN_TMRA_Y,
BTN_TMRA_W,
BTN_TMRA_H,
btna_cap);
tmr_a = SetTimer(MSEC(1000));
}
else
{
KillTimer(tmr_a);
strcpy(btna_cap, "Start A");
AddButton(BTN_TMRA_X,
BTN_TMRA_Y,
BTN_TMRA_W,
BTN_TMRA_H,
btna_cap);
tmr_a = 0;
}
}
if (InRect(tsts.x, tsts.y,
BTN_TMRB_X, BTN_TMRB_Y,
BTN_TMRB_W, BTN_TMRB_H))
{
flag = TRUE;
DoAnimate(BTN_TMRB_X, BTN_TMRB_Y,
BTN_TMRB_X + BTN_TMRB_W,
BTN_TMRB_Y + BTN_TMRB_H);
if (tmr_b == 0)
{
strcpy(btnb_cap, "Stop B");
AddButton(BTN_TMRB_X,
BTN_TMRB_Y,
BTN_TMRB_W,
BTN_TMRB_H,
btnb_cap);
tmr_b = SetTimer(MSEC(2000));
}
else
{
KillTimer(tmr_b);
strcpy(btnb_cap, "Start B");
AddButton(BTN_TMRB_X,
BTN_TMRB_Y,
BTN_TMRB_W,
BTN_TMRB_H,
btnb_cap);
tmr_b = 0;
}
}
if (tsts.y > 159)
bExit = TRUE;
}
}
Timers_Terminate();
LibJumpMenu();
}
Still there?
|