Discussion:
serial port
(too old to reply)
Paul Edwards
2023-04-11 11:18:14 UTC
Permalink
Hi.

As far as I can tell from testing, the BIOS INT 14H doesn't
work on most real hardware I have tried (computers made
maybe 10 years ago that still have a serial port).

So I am gearing up to replace that with 80386 PM32 code.

I have some comms routines for MSDOS (pdcomm) that I
wrote decades ago, but I want to do it a different way this time.

Less sophisticated, but more straightforward.

And designed for a single-tasking system with only one
CPU enabled (PDOS/386).

Most of the code can be done in C, but the last bit I want
to do in assembler.

Prior to hitting the assembler, I will have installed the
new interrupt address (which is assembler code, gotint,
below).

I'm just trying to confirm the sequence in the final assembler.

outb port1, transmit_byte
outb port2, tbemask ; enable transmit buffer empty (only)
xxx:
hlt ; this could get interrupted by timer interrupts and
; then processing continues so we need a jmp
jmp xxx
gotint: ; this is the interrupt address installed by C caller
outb port2, oldmask ; restore previous interrupt mask (everything disabled)
add esp, 12 ; we don't return to the previous instruction, which was hlt
; instead we skip over the return address, segment and flags

ret ; return to C caller which will do the EOI or whatever
; via separate calls to individual simple assembler functions
; like outportb()

Note that this was inspired by something similar I wrote
for S/370.

It's basically quite minimal assembler and straightforward.

There is a loop in the assembler, which I didn't have in my
old routines, but it's not really processing logic.

After transmit is working I'll try a variation of the above for receive.

I especially don't know if these two need to be swapped:

outb port1, transmit_byte
outb port2, tbemask ; enable transmit buffer empty (only)

I don't want to miss an interrupt. I want the order to
guarantee that I will get the interrupt. ie if I have already
attempted to write the transmit byte, will the interrupt
pend until I enable it, or will it be skipped?

Or is the other way around? If I haven't enabled interrupts
will it just transmit the byte and not bother interrupting?

Assume that the transmit is very fast, or the CPU is very
slow, so that there is a gap between the two outb
instructions where a decision is made on whether to
interrupt or not.

Thanks. Paul.
Terje Mathisen
2023-04-11 19:09:45 UTC
Permalink
I last looked at my RS232 drivers at least 30+ years ago, but from
memory what I had to do was to get the data from the serial port chip,
at this point I re-enable all other interrupts except the one I am
servicing.

After saving the received byte I optionally loop back until the input
buffer is empty (more modern PCs had a 16-byte buffer you could enable).

Before returning from the interrupt with IRET, I would reenable the
serial port IRQ channel (stil with interrupts disabled), then IRET back
to the interrupted process.

If you are interested I can see if I can locate my source code...

Terje
Post by Paul Edwards
Hi.
As far as I can tell from testing, the BIOS INT 14H doesn't
work on most real hardware I have tried (computers made
maybe 10 years ago that still have a serial port).
So I am gearing up to replace that with 80386 PM32 code.
I have some comms routines for MSDOS (pdcomm) that I
wrote decades ago, but I want to do it a different way this time.
Less sophisticated, but more straightforward.
And designed for a single-tasking system with only one
CPU enabled (PDOS/386).
Most of the code can be done in C, but the last bit I want
to do in assembler.
Prior to hitting the assembler, I will have installed the
new interrupt address (which is assembler code, gotint,
below).
I'm just trying to confirm the sequence in the final assembler.
outb port1, transmit_byte
outb port2, tbemask ; enable transmit buffer empty (only)
hlt ; this could get interrupted by timer interrupts and
; then processing continues so we need a jmp
jmp xxx
gotint: ; this is the interrupt address installed by C caller
outb port2, oldmask ; restore previous interrupt mask (everything disabled)
add esp, 12 ; we don't return to the previous instruction, which was hlt
; instead we skip over the return address, segment and flags
ret ; return to C caller which will do the EOI or whatever
; via separate calls to individual simple assembler functions
; like outportb()
for S/370.
It's basically quite minimal assembler and straightforward.
There is a loop in the assembler, which I didn't have in my
old routines, but it's not really processing logic.
After transmit is working I'll try a variation of the above for receive.
outb port1, transmit_byte
outb port2, tbemask ; enable transmit buffer empty (only)
I don't want to miss an interrupt. I want the order to
guarantee that I will get the interrupt. ie if I have already
attempted to write the transmit byte, will the interrupt
pend until I enable it, or will it be skipped?
Or is the other way around? If I haven't enabled interrupts
will it just transmit the byte and not bother interrupting?
Assume that the transmit is very fast, or the CPU is very
slow, so that there is a gap between the two outb
instructions where a decision is made on whether to
interrupt or not.
Thanks. Paul.
--
- <Terje.Mathisen at tmsw.no>
"almost all programming can be viewed as an exercise in caching"
Paul Edwards
2023-04-11 20:57:26 UTC
Permalink
Post by Terje Mathisen
After saving the received byte I optionally loop back until the input
buffer is empty (more modern PCs had a 16-byte buffer you could enable).
This sounds like the normal/proper way to do things.

I did that with a proper buffer too:

https://sourceforge.net/projects/pdos/files/pdcomm/PDCOMM%202.50/

But now I would like to do things differently (simpler).
Post by Terje Mathisen
If you are interested I can see if I can locate my source code...
Thanks, but I already have source code for a traditional
implementation. Note that I've never used "hlt" ever - I
just heard about it, and the code I wrote above is what
I picked up by osmosis.

So I'm unsure whether the logic is correct or not. Bypassing
the iret too is new. On return to the C caller I might need to
reenable interrupts because I bypassed the iret.

BFN. Paul.
Paul Edwards
2023-04-12 01:29:04 UTC
Permalink
Post by Terje Mathisen
at this point I re-enable all other interrupts
Actually, that made me rethink - maybe it is more straightforward to:

Do this from C code, to signal that I am interested in TBE:

outb port2, tbemask ; enable transmit buffer empty (only)

Disable all interrupts (cli) from C code as well

Do this from C code:

outb port1, transmit_byte

And at this point, if the byte is transmitted very quickly,
the UART knows that it needs to do an interrupt, but
interrupts are temporarily disabled, so it knows that it
needs to queue the interrupt, not discard it.

And then I call this assembler:

xxx:
hlt ; this could get interrupted by timer interrupts and
; then processing continues so we need a jmp
jmp xxx

And actually, even that can go into a function called halt_loop(),
called by C.

And then I can have this generic assembler routine:

gotint: ; this is the interrupt address installed by C caller
add esp, 12 ; we don't return to the previous instruction, which was hlt
; instead we skip over the return address, segment and flags

Actually, no I can't. They need to be combined.

sti
xxx:
hlt ; this could get interrupted by timer interrupts and
; then processing continues so we need a jmp
jmp xxx

gotint: ; this is the interrupt address installed by C caller
add esp, 12 ; we don't return to the previous instruction, which was hlt
; instead we skip over the return address, segment and flags
sti ; interrupts will have been disabled automatically. need to reenable
ret

Possibly take that sti out and do it from C, as C will have
done a call to disable interrupts, so this would be a good match.

And this is put back into C code:

outb port2, oldmask ; restore previous interrupt mask (everything disabled)

So down to just 5 assembler instructions.

That's probably neat. And yes, I know it is inefficient.

BFN. Paul.
Paul Edwards
2023-04-13 11:05:42 UTC
Permalink
I committed code that I expected to work:

https://sourceforge.net/p/pdos/gitcode/ci/3260d2aabe9f133f89bae3f9148070c188eb42b1/

But I am only getting the first character transmitted,
and then it never returns, thus never reboots.

So I need to begin debugging.

BFN. Paul.



#ifdef __32BIT__
static void writecomm(int port, int ch)
{
UART uart;
unsigned long old1;
unsigned long old2;
unsigned long intdesc1;
unsigned long intdesc2;
unsigned long intaddr;
int xch;
int intno = 4;
int imr = 0x21;

uartInit(&uart);
uartAddress(&uart, 0x3f8);
uartDisableInts(&uart);
/* IRQs 0-7 are at 0xb0 instead of 8 now */
/* we are using IRQ 4 for COM1 */
old1 = G_intloc[(intno + 0xb0) * 2];
old2 = G_intloc[(intno + 0xb0) * 2 + 1];
intaddr = (unsigned long)hltinthit;

/* we are interested in this interrupt */
xch = PREADB(imr);
xch &= ~(1 << (intno % 8));
PWRITEB(imr, xch);

uartEnableGPO2(&uart);

uartEnableTBE(&uart);
intdesc1 = (0x8 << 16) | (intaddr & 0xffff);
intdesc2 = (intaddr & 0xffff0000)
| (1 << 15)
| (0 << 13)
| (0x0e << 8);
disable();
G_intloc[(intno + 0xb0) * 2] = intdesc1;
G_intloc[(intno + 0xb0) * 2 + 1] = intdesc2;
uartTxCh(&uart, ch);
hltintgo();
G_intloc[(intno + 0xb0) * 2] = old1;
G_intloc[(intno + 0xb0) * 2 + 1] = old2;
enable();
PosReboot();
uartReset(&uart);
}
#endif


/ enable interrupts and then halt until interrupt hit
_hltintgo:
sti
hloop:
/ I believe hlt will be interrupted by other interrupts, like
/ the timer interrupt, so we need to do it in a loop
hlt
jmp hloop
_hltinthit:
/ remove return address, segment and flags from the stack as we
/ do not intend to return to the jmp following the hlt instruction
/ that was likely interrupted
add %esp, 12
/ note that interrupts will be disabled again (I think) by virtue
/ of the fact that an interrupt occurred. The caller would have
/ disabled interrupts already, so we are returning to the same
/ disabled state.
ret
Andrew Cooper
2023-04-13 12:17:34 UTC
Permalink
Post by Paul Edwards
/ enable interrupts and then halt until interrupt hit
sti
/ I believe hlt will be interrupted by other interrupts, like
/ the timer interrupt, so we need to do it in a loop
hlt
jmp hloop
/ remove return address, segment and flags from the stack as we
/ do not intend to return to the jmp following the hlt instruction
/ that was likely interrupted
add %esp, 12
/ note that interrupts will be disabled again (I think) by virtue
/ of the fact that an interrupt occurred. The caller would have
/ disabled interrupts already, so we are returning to the same
/ disabled state.
ret
This is why things get stuck.

The first time through, you hlt with interrupts enabled, so will wake up
on the fist interrupt.

But when you loop, you'll hlt again, this time with interrupts disabled,
and will never ever wake up again (other than for an NMI).

Your loop needs to read:

_hltintgo:
sti
hlt
cli
jmp _hltintgo
_hltinthit:
...

to make things work as you intend.

sti;hlt as a pair is important for getting the blocked-by-STI shadow to
cover you into the hlt state, but it only works on the rising edge of
IF, so you need to explicitly clear interrupts in order to make the
sti;hlt on the second loop iteration work.

~Andrew
Paul Edwards
2023-04-13 16:26:54 UTC
Permalink
On Thursday, April 13, 2023 at 8:27:03 PM UTC+8, Andrew Cooper wrote:

Hi Andrew. Thanks for your reply.
Post by Andrew Cooper
The first time through, you hlt with interrupts enabled, so will wake up
on the fist interrupt.
But when you loop, you'll hlt again, this time with interrupts disabled,
and will never ever wake up again (other than for an NMI).
I'm not expecting that behavior. I'm expecting the first
interrupt to move me to the hltinthit location and I never
return to the jmp.

Only a timer interrupt is expected to interfere with that,
and I would have expected that to be unlikely. And also
I expect the timer interrupt to do a rti and restore the
interrupt status to enabled.

I made the change anyway:

https://sourceforge.net/p/pdos/gitcode/ci/cdd5921434e5a827ad29d0024d456b059d9188b4/

But it still hangs.

I assume I haven't set up the interrupt properly.

BFN. Paul.


/ enable interrupts and then halt until interrupt hit
_hltintgo:
hloop:
/ I believe hlt will be interrupted by other interrupts, like
/ the timer interrupt, so we need to do it in a loop
sti
hlt
cli
jmp hloop
_hltinthit:
/ remove return address, segment and flags from the stack as we
/ do not intend to return to the jmp following the hlt instruction
/ that was likely interrupted
add %esp, 12
/ note that interrupts will be disabled again (I think) by virtue
/ of the fact that an interrupt occurred. The caller would have
/ disabled interrupts already, so we are returning to the same
/ disabled state.
ret
s***@nospicedham.conman.org
2023-04-14 00:12:01 UTC
Permalink
Post by Paul Edwards
https://sourceforge.net/p/pdos/gitcode/ci/3260d2aabe9f133f89bae3f9148070c188eb42b1/
But I am only getting the first character transmitted,
and then it never returns, thus never reboots.
So I need to begin debugging.
So if you are writing code that only runs one program at a time, why do
you even need to mess with interrupts in the first place? Assuming a C
funcion inb() to read a byte from an IO port, and outb() to write a byte to
an IO port, I would think this would work:

/* Assuming UART has been initialized, but its IRQ has been disabled */
static void writecomm(int c)
{
/* read line status register to detect if the xmit buffer */
/* is ready to send. */

while (inb(0x3f8+5) & 0x20 == 0)
{
/* do nothing but wait */
}

/* we can now write the data */
outb(0x3f8,c);
}

It seems simpler to me than trying to muck with interrupts and adjusting
the return stack and all that.

-spc
Paul Edwards
2023-04-14 02:59:08 UTC
Permalink
Post by s***@nospicedham.conman.org
Post by Paul Edwards
https://sourceforge.net/p/pdos/gitcode/ci/3260d2aabe9f133f89bae3f9148070c188eb42b1/
But I am only getting the first character transmitted,
and then it never returns, thus never reboots.
So I need to begin debugging.
So if you are writing code that only runs one program at a time, why do
you even need to mess with interrupts in the first place? Assuming a C
funcion inb() to read a byte from an IO port, and outb() to write a byte to
/* Assuming UART has been initialized, but its IRQ has been disabled */
static void writecomm(int c)
{
/* read line status register to detect if the xmit buffer */
/* is ready to send. */
while (inb(0x3f8+5) & 0x20 == 0)
{
/* do nothing but wait */
}
/* we can now write the data */
outb(0x3f8,c);
}
It seems simpler to me than trying to muck with interrupts and adjusting
the return stack and all that.
-spc
My solution is in-between those two extremes.

I only support a single task but I don't run hot polling.
s***@nospicedham.conman.org
2023-04-14 07:11:13 UTC
Permalink
Post by Paul Edwards
Post by s***@nospicedham.conman.org
So if you are writing code that only runs one program at a time, why do
you even need to mess with interrupts in the first place? Assuming a C
funcion inb() to read a byte from an IO port, and outb() to write a byte to
/* Assuming UART has been initialized, but its IRQ has been disabled */
static void writecomm(int c)
{
/* read line status register to detect if the xmit buffer */
/* is ready to send. */
while (inb(0x3f8+5) & 0x20 == 0)
{
/* do nothing but wait */
}
/* we can now write the data */
outb(0x3f8,c);
}
It seems simpler to me than trying to muck with interrupts and adjusting
the return stack and all that.
My solution is in-between those two extremes.
I only support a single task but I don't run hot polling.
Okay. If this is on a x86 system (an assumption on my part), and you
don't want to poll, then this method might work. The interrupt handler does
little more than sending the EOI (end of interrupt) to the 8259 PIC:

uart_irq_handler:
push ax ; eax if 32 bit
mov al,20h
out 020h,al ; signal end of interrupt
pop ax
iret

and then your transmit routine (in assembly):

; assume character to transmit in AL
; also assume that the transmitter
; register is empty

mov dx,[port] ; get port address
out dx,al ; write character
inc dx ; point to interrupt enable reg
in al,dx
or al,2 ; enable transmit
out dx,al
inc dx ; now point to interrupt ID reg

pause:
hlt ; halt CPU

; read interrupt ID reg; this also clears the IRQ on the serial chip
; and checks to see if we're the cause of the IRQ, and not something
; else like the keyboard.

in al,dx
and al,0eh ; isolate interrupt ID bits
cmp al,2 ; transmitter empty bit IRQ?
bne pause ; we're not the cause, keep waiting
dec dx ; point back to interrupt enable reg
in al,dx
and al,0FDh ; disable transmit empty IRQ
out dx,al

; and we're done with transmitting the character

I think you may not have been telling the 8259 that the IRQ has been
handled. But you also need to clear the IRQ from the UART itself as well,
which you might not have been doing.

-spc
Paul Edwards
2023-04-14 16:55:12 UTC
Permalink
Post by Paul Edwards
So I need to begin debugging.
add %esp, 12
This was the main problem.

Should have been add $12, %esp

Current status is that it is outputting the first character,
then it is printing "id is 2" (TBE) then "id is 0" (no pending),
and then no further screen output, but there is another
character that goes to the com port, but not the expected
character, so I would expect that means it is transmitting
instead of waiting for the interrupt, but I don't see how it
can get ahead of itself.

I've tried lots of things, still don't understand what is happening.

I'll try debugging again when I wake up.

BFN. Paul.


/ enable interrupts and then halt until interrupt hit
_hltintgo:
hloop:
/ I believe hlt will be interrupted by other interrupts, like
/ the timer interrupt, so we need to do it in a loop
sti
hlt
cli
jmp hloop
_hltinthit:
/ remove return address, segment and flags from the stack as we
/ do not intend to return to the jmp following the hlt instruction
/ that was likely interrupted
add $12, %esp
/ note that interrupts will be disabled again (I think) by virtue
/ of the fact that an interrupt occurred. The caller would have
/ disabled interrupts already, so we are returning to the same
/ disabled state.
ret



#ifdef __32BIT__
static void writecomm(int port, int ch)
{
UART uart;
unsigned long old1;
unsigned long old2;
unsigned long intdesc1;
unsigned long intdesc2;
unsigned long intaddr;
int xch;
int intno = 4;
int a8259 = 0x20;
int imr = 0x21;
int id;

uartInit(&uart);
uartAddress(&uart, 0x3f8);
PREADB(a8259); /* we don't use the result of this */
uartDisableInts(&uart);
/* IRQs 0-7 are at 0xb0 instead of 8 now */
/* we are using IRQ 4 for COM1 */
old1 = G_intloc[(intno + 0xb0) * 2];
old2 = G_intloc[(intno + 0xb0) * 2 + 1];
intaddr = (unsigned long)hltinthit;

/* we are interested in this interrupt */
xch = PREADB(imr);
xch &= ~(1 << (intno % 8));
PWRITEB(imr, xch);

uartEnableGPO2(&uart);

uartEnableTBE(&uart);
/* uartEnableModem(&uart); */
/* uartRaiseDTR(&uart); */
/* uartRaiseRTS(&uart); */
/* uartCTS(&uart); */
intdesc1 = (0x8 << 16) | (intaddr & 0xffff);
intdesc2 = (intaddr & 0xffff0000)
| (1 << 15)
| (0 << 13)
| (0x0e << 8);
disable();
G_intloc[(intno + 0xb0) * 2] = intdesc1;
G_intloc[(intno + 0xb0) * 2 + 1] = intdesc2;
uartTxCh(&uart, ch);
hltintgo();
enable();
do
{
id = uartGetIntType(&uart);
printf("id is %d\n", id);
} while (id != UART_NO_PENDING);
PWRITEB(0x20, 0x20);
uartDisableInts(&uart);
uartDisableGPO2(&uart);

xch = PREADB(imr);
xch |= (1 << (intno % 8));
PWRITEB(imr, xch);

uartReset(&uart);
uartTerm(&uart);

disable();
G_intloc[(intno + 0xb0) * 2] = old1;
G_intloc[(intno + 0xb0) * 2 + 1] = old2;
enable();
}
#endif
Paul Edwards
2023-04-15 09:29:14 UTC
Permalink
Post by Paul Edwards
uartEnableTBE(&uart);
I have found that I need this in order to get an
interrupt, as expected.
Post by Paul Edwards
uartTxCh(&uart, ch);
But I was surprised to find that I didn't need this.

Basically it appears that TBE wakes up once to let
me know that it is currently empty, but once I start
sending it data, and then halting, it doesn't feel the
need to let me know it is empty again.

But my whole design is centered around being
informed of that.

Maybe what is happening is that interrupts are disabled
so often/sometimes the character is transmitted
immediately and it isn't allowed to interrupt, so it doesn't.

I then enable interrupts and hlt but the show is already over.

So maybe what I need to do instead is send the character
after enabling interrupts.

I'll try that next. Not as neat as the previous generic solution.

BFN. Paul.
Paul Edwards
2023-04-15 12:44:53 UTC
Permalink
Post by Paul Edwards
uartEnableTBE(&uart);
So far I have confirmed that after I have issued this
(but without installing my own interrupt handler),
I can read the uart to find out what interrupts are
available and I see that TBE is the only one.

I think transmit a byte, and I do get an interrupt,
so I do get an additional interrupt even though
I've read all the previous ones.

Then when I go through for the second time, enabling
TBE does the same thing, and I read the same thing
from the uart.

But this time when I try to transmit a byte I either don't
get an interrupt or I get a hang for some other reason.

So I don't know what is going on.

If I can get consistent behavior I can have a simple design.

BFN. Paul.
Paul Edwards
2023-04-15 14:24:27 UTC
Permalink
I finally realized that since I'm getting an interrupt from
the TBE enable (for unknown reasons), then if I moved
the disable (cli) before that, then by the time I had
outputted a byte, the interrupt would still be pending
and even if I didn't get one for the outputted byte, it
was enough to get one for the TBE call.

And now it is working, with the simple design.

I will look into refinements now that the basics are working.

static void writecomm(int port, int ch)
{
UART uart;
unsigned long old1;
unsigned long old2;
unsigned long intdesc1;
unsigned long intdesc2;
unsigned long intaddr;
int xch;
int intno = 4;
int a8259 = 0x20;
int imr = 0x21;
int id;

uartInit(&uart);
uartAddress(&uart, 0x3f8);
PREADB(a8259); /* we don't use the result of this */
uartDisableInts(&uart);
/* IRQs 0-7 are at 0xb0 instead of 8 now */
/* we are using IRQ 4 for COM1 */
old1 = G_intloc[(intno + 0xb0) * 2];
old2 = G_intloc[(intno + 0xb0) * 2 + 1];
intaddr = (unsigned long)hltinthit;

/* we are interested in this interrupt */
xch = PREADB(imr);
xch &= ~(1 << (intno % 8));
PWRITEB(imr, xch);

disable();
uartEnableGPO2(&uart);

uartEnableTBE(&uart);
/* uartEnableModem(&uart); */
/* uartRaiseDTR(&uart); */
/* uartRaiseRTS(&uart); */
/* uartCTS(&uart); */
intdesc1 = (0x8 << 16) | (intaddr & 0xffff);
intdesc2 = (intaddr & 0xffff0000)
| (1 << 15)
| (0 << 13)
| (0x0e << 8);
G_intloc[(intno + 0xb0) * 2] = intdesc1;
G_intloc[(intno + 0xb0) * 2 + 1] = intdesc2;
uartTxCh(&uart, ch);
hltintgo();
enable();
do
{
id = uartGetIntType(&uart);
} while (id != UART_NO_PENDING);
PWRITEB(0x20, 0x20);
uartDisableInts(&uart);
uartDisableGPO2(&uart);

xch = PREADB(imr);
xch |= (1 << (intno % 8));
PWRITEB(imr, xch);

uartReset(&uart);
uartTerm(&uart);

disable();
G_intloc[(intno + 0xb0) * 2] = old1;
G_intloc[(intno + 0xb0) * 2 + 1] = old2;
enable();
}


/ enable interrupts and then halt until interrupt hit
_hltintgo:
hloop:
/ I believe hlt will be interrupted by other interrupts, like
/ the timer interrupt, so we need to do it in a loop
sti
hlt
cli
jmp hloop
_hltinthit:
/ remove return address, segment and flags from the stack as we
/ do not intend to return to the jmp following the hlt instruction
/ that was likely interrupted
add $12, %esp
/ note that interrupts will be disabled again (I think) by virtue
/ of the fact that an interrupt occurred. The caller would have
/ disabled interrupts already, so we are returning to the same
/ disabled state.
ret
Paul Edwards
2023-04-15 21:13:50 UTC
Permalink
Post by Paul Edwards
I finally realized that since I'm getting an interrupt from
the TBE enable (for unknown reasons), then if I moved
the disable (cli) before that, then by the time I had
outputted a byte, the interrupt would still be pending
and even if I didn't get one for the outputted byte, it
was enough to get one for the TBE call.
And now it is working, with the simple design.
Note that it is working (and previously failing) under
Bochs. I haven't tried real hardware yet.

And now I realize there may be a problem with the
current code.

Let's say the serial port is slow.

The sequence I am doing is enabling TBE and then outputting
a byte.

Enabling TBE generates an interrupt, but outputting the byte
only randomly does (could also be a Bochs bug).

Because I am now relying on the TBE enable interrupt to get
me out of the HLT loop, I am no longer have the desired
constraint on the OUT instruction completing.

Meaning the second time through the loop, the second OUT
could be executed before the first one has completed.

If the UART discards the TBE interrupt when it realizes that
it is no longer the case that the transmit buffer is empty,
because there has been an OUT instruction issued since
then, then my current design should work.

Does anyone know what is happening?

Thanks. Paul.
Terje Mathisen
2023-04-15 21:42:17 UTC
Permalink
Post by Paul Edwards
Post by Paul Edwards
I finally realized that since I'm getting an interrupt from
the TBE enable (for unknown reasons), then if I moved
the disable (cli) before that, then by the time I had
outputted a byte, the interrupt would still be pending
and even if I didn't get one for the outputted byte, it
was enough to get one for the TBE call.
And now it is working, with the simple design.
Note that it is working (and previously failing) under
Bochs. I haven't tried real hardware yet.
And now I realize there may be a problem with the
current code.
Let's say the serial port is slow.
The sequence I am doing is enabling TBE and then outputting
a byte.
Enabling TBE generates an interrupt, but outputting the byte
only randomly does (could also be a Bochs bug).
Because I am now relying on the TBE enable interrupt to get
me out of the HLT loop, I am no longer have the desired
constraint on the OUT instruction completing.
Meaning the second time through the loop, the second OUT
could be executed before the first one has completed.
If the UART discards the TBE interrupt when it realizes that
it is no longer the case that the transmit buffer is empty,
because there has been an OUT instruction issued since
then, then my current design should work.
Does anyone know what is happening?
Thanks. Paul.
How do you guarantee that the interrupt is directed to your thread
that's sitting in a HLT state?

Terje
--
- <Terje.Mathisen at tmsw.no>
"almost all programming can be viewed as an exercise in caching"
Paul Edwards
2023-04-16 01:08:23 UTC
Permalink
Post by Terje Mathisen
How do you guarantee that the interrupt is directed to your thread
that's sitting in a HLT state?
This is single-threading PDOS/386.

Simple, and understandable.

And once you have this system, you can use it to create
a more complicated system of your own. (And I may
choose to do that myself one day, but I'm getting
pretty old and I'm still on the simple system(s)).

BFN. Paul.
Paul Edwards
2023-04-16 08:37:04 UTC
Permalink
Post by Paul Edwards
And now I realize there may be a problem with the
current code.
I have now managed to test on real hardware.

I was surprised to see that INT 14H to initialize the
port is apparently working, as I was able to transmit
characters.

I was expecting to have to update PDOS to replace
the INT 14H initialization functionality.

However, when I copied a file to com1, most characters
were dropped.

So that bit of the theory was correct.

If I just copied a 1-byte file to com1, then copied another
1-byte file to com1, the characters both went through.

I then tried moving the disable() below the TBE enable in
the hope that the old (default) interrupt handler would
take care of that, and I would get my character. But that
would have created a timing issue even if it had worked.
Instead there was no change in behavior.

And then I figured that what I needed was consistency,
and that there should be two interrupts.

So I did a second call to hltintgo.

And this time it hung.

Fortunately I realized I need to repeat the read of the uart
and do the write to 0x20 and it worked on my real hardware.

And the same code worked on Bochs too.

So working code now below.

Thanks everyone for your thoughts.

BFN. Paul.


static void writecomm(int port, int ch)
{
UART uart;
unsigned long old1;
unsigned long old2;
unsigned long intdesc1;
unsigned long intdesc2;
unsigned long intaddr;
int xch;
int intno = 4;
int a8259 = 0x20;
int imr = 0x21;
int id;

uartInit(&uart);
uartAddress(&uart, 0x3f8);
PREADB(a8259); /* we don't use the result of this */
uartDisableInts(&uart);
/* IRQs 0-7 are at 0xb0 instead of 8 now */
/* we are using IRQ 4 for COM1 */
old1 = G_intloc[(intno + 0xb0) * 2];
old2 = G_intloc[(intno + 0xb0) * 2 + 1];
intaddr = (unsigned long)hltinthit;

/* we are interested in this interrupt */
xch = PREADB(imr);
xch &= ~(1 << (intno % 8));
PWRITEB(imr, xch);

uartEnableGPO2(&uart);

/* uartEnableModem(&uart); */
/* uartRaiseDTR(&uart); */
/* uartRaiseRTS(&uart); */
/* uartCTS(&uart); */
intdesc1 = (0x8 << 16) | (intaddr & 0xffff);
intdesc2 = (intaddr & 0xffff0000)
| (1 << 15)
| (0 << 13)
| (0x0e << 8);
disable();
G_intloc[(intno + 0xb0) * 2] = intdesc1;
G_intloc[(intno + 0xb0) * 2 + 1] = intdesc2;
/* for some reason just enabling the interrupt causes
an interrupt. But transmitting a character doesn't
necessarily generate an interrupt for some reason.
But by disabling the interrupts while both enabling
TBE and sending a character, we 'guarantee' that we
will receive an interrupt from at least one of those
so that the hlt instruction will be interrupted. */
uartEnableTBE(&uart);
hltintgo();
enable();
do
{
id = uartGetIntType(&uart);
} while (id != UART_NO_PENDING);
PWRITEB(0x20, 0x20);
disable();
uartTxCh(&uart, ch);
hltintgo();
enable();
do
{
id = uartGetIntType(&uart);
} while (id != UART_NO_PENDING);
PWRITEB(0x20, 0x20);
uartDisableInts(&uart);
uartDisableGPO2(&uart);

xch = PREADB(imr);
xch |= (1 << (intno % 8));
PWRITEB(imr, xch);

uartReset(&uart);
uartTerm(&uart);

disable();
G_intloc[(intno + 0xb0) * 2] = old1;
G_intloc[(intno + 0xb0) * 2 + 1] = old2;
enable();
}
Paul Edwards
2023-04-17 00:44:53 UTC
Permalink
I have now added read functionality.

And I was able to reuse my existing assembler code.

So that's a great result.

But as per comments below, I did get two surprising
results attempting to get it to work, with a working
theory.

BFN. Paul.


static int readcomm(int port)
{
UART uart;
unsigned long old1;
unsigned long old2;
unsigned long intdesc1;
unsigned long intdesc2;
unsigned long intaddr;
int xch;
int intno = 4;
int a8259 = 0x20;
int imr = 0x21;
int id;
int ch;

uartInit(&uart);
uartAddress(&uart, 0x3f8);
PREADB(a8259); /* we don't use the result of this */
uartDisableInts(&uart);
/* IRQs 0-7 are at 0xb0 instead of 8 now */
/* we are using IRQ 4 for COM1 */
old1 = G_intloc[(intno + 0xb0) * 2];
old2 = G_intloc[(intno + 0xb0) * 2 + 1];
intaddr = (unsigned long)hltinthit;

/* we are interested in this interrupt */
xch = PREADB(imr);
xch &= ~(1 << (intno % 8));
PWRITEB(imr, xch);

uartEnableGPO2(&uart);

/* uartEnableModem(&uart); */
/* uartRaiseDTR(&uart); */
/* uartRaiseRTS(&uart); */
/* uartCTS(&uart); */
intdesc1 = (0x8 << 16) | (intaddr & 0xffff);
intdesc2 = (intaddr & 0xffff0000)
| (1 << 15)
| (0 << 13)
| (0x0e << 8);
disable();
G_intloc[(intno + 0xb0) * 2] = intdesc1;
G_intloc[(intno + 0xb0) * 2 + 1] = intdesc2;
uartEnableRxRDY(&uart);
hltintgo();
/* if I immediately disable UART interrupts, I can no
longer read the old pending id of RxRDY.
If I read the pending ids, RxRDY just gets reasserted,
presumably because I haven't actually read the
character yet.
If I try reading the character, a new character may
come in and I'll miss it.
So the safest thing to do is just disable interrupts
and assume that RxRDY was hit, since that was the only
thing actually enabled, and I don't bother reading the
interrupt ids. */
G_intloc[(intno + 0xb0) * 2] = old1;
G_intloc[(intno + 0xb0) * 2 + 1] = old2;
uartDisableInts(&uart);
enable();
ch = uartRecCh(&uart);
PWRITEB(0x20, 0x20);
uartDisableGPO2(&uart);

xch = PREADB(imr);
xch |= (1 << (intno % 8));
PWRITEB(imr, xch);

uartReset(&uart);
uartTerm(&uart);

return (ch);
}

s***@nospicedham.conman.org
2023-04-15 23:23:22 UTC
Permalink
I replied once with some code, but it seems you didn't see it, so I'm
replying again.
Post by Paul Edwards
And now it is working, with the simple design.
I will look into refinements now that the basics are working.
static void writecomm(int port, int ch)
{
...
Post by Paul Edwards
old1 = G_intloc[(intno + 0xb0) * 2];
old2 = G_intloc[(intno + 0xb0) * 2 + 1];
intaddr = (unsigned long)hltinthit;
...
Post by Paul Edwards
intdesc1 = (0x8 << 16) | (intaddr & 0xffff);
intdesc2 = (intaddr & 0xffff0000)
| (1 << 15)
| (0 << 13)
| (0x0e << 8);
G_intloc[(intno + 0xb0) * 2] = intdesc1;
G_intloc[(intno + 0xb0) * 2 + 1] = intdesc2;
...
Post by Paul Edwards
G_intloc[(intno + 0xb0) * 2] = old1;
G_intloc[(intno + 0xb0) * 2 + 1] = old2;
...
Post by Paul Edwards
}
Why are you installing, then uninstalling, the interrupt handler for each
character? And I think you are making this out to be more complicated that
it should be. As I wrote before, the interrupt handler for the UART can be
as simple as:

uart_irq_handler:
push ax ; or eax if 32-bit
mov al,20h ; sent end-of-interrupt to 8259 PIC
out 20h,al
pop ax
iret

That's it. No mucking with return addresses, or CLI/STI instructions or
anything like that. Let the interrupt happen, and inform the 8259 it's been
handled. Set the vector once, and be done with it.

And the code to transmit the character, which assumes the UART has been
initialized with the baud rate and bit settings:

; assume character to transmit is in AL
; also assume the transmitter register
; is empty

mov dx,[port] ; get port address
out dx,al ; send the character
inc dx ; point to interrupt enable register
in al,dx ; read current setting
or al,2 ; set transmitter empty IRQ
out dx,al ; tell UART
inc dx ; point to interrupt ID register

pause:

hlt ; halt CPU

; read interrupt ID reg; this also clears the IRQ on the serial chip
; and checks to see if we're the cause of the IRQ, and not something
; else like the keyboard.

in al,dx ; read UART interrupt register
and al,0eh ; isolate source of interrupt bits
cmp al,2 ; is it the transmitter empty IRQ?
bne pause ; if not, keep waiting
dec dx ; point to interrupt enable register
in al,dx ; read current setting
and al,0FDh ; disable transmit empty IRQ
out dx,al

; and we're done with transmitting the character

You need to tell both the 8259 and the UART that the interrupt has been
handled.

-spc
Paul Edwards
2023-04-16 01:19:12 UTC
Permalink
Post by s***@nospicedham.conman.org
I replied once with some code, but it seems you didn't see it, so I'm
replying again.
Sorry. I wasn't sure what to reply with, and I was still in
the process of debugging. Yours a different design,
and assembler-focused. I know this is an assembler group,
and I am indeed using assembler, but I'm trying to minimize it.
The required minimal assembler has already changed
numerous times to try to get this very basic functionality
working.

I'm not disputing that it wouldn't work or be better.

But I have a different design I am trying to get to work.
(and it should work - and the fact that it only half-works
is exactly what I want to see and understand).
Post by s***@nospicedham.conman.org
Why are you installing, then uninstalling, the interrupt handler for each
character?
So that I can see an understandable sequence until I
am happy that the sequence is right.

I can potentially move that later. But I probably won't,
because the interrupt may be shared. And code
elsewhere would use the same assembler routine,
same interrupt number, same simple logic, and still work.
Post by s***@nospicedham.conman.org
And I think you are making this out to be more complicated that
it should be.
It is more complicated for an assembler programmer, but
not more complicated for a (or at least, this) C programmer,
because almost all the logic is in C.
Post by s***@nospicedham.conman.org
As I wrote before, the interrupt handler for the UART can be
hlt ; halt CPU
...
Post by s***@nospicedham.conman.org
bne pause ; if not, keep waiting
And I can't move this code out into C, due to this requiring
the stack pointer isn't changed.

I don't wish to use inline assembler either - I want my
C code to be C90-compliant.
Post by s***@nospicedham.conman.org
You need to tell both the 8259 and the UART that the interrupt has been
handled.
I believe my code does that.

BFN. Paul.
Loading...