Search code examples
assemblysleep16-bit

How to sleep in bare-metal 16 bit x86 assembly?


I want to halt the execution about 0.1 seconds, regardless of the clock speed of the CPU. The code should run directly from a boot device, it should therefore not use DOS interrupts.

I'm currently using int 15h, but this seems to conflict with the beep tones I'm modulating with channel 2 of the PIT. I heard of channel 0, but I have no clue about how to set this up.

The accuracy is not that important, however, it should run on old and modern computers at the same speed. So just looping instructions is not an option.

The beep code and the sleep are just a bunch of macros for changing the frequency and turning the speaker on and off. The beep seems to not stop if I call sleep right before beepoff.

Here are the beep macros:

%macro beepinit 0
    mov al, 182
    out 43h, al
%endmacro

%macro beepfreq 0
    out 42h, al
    mov al, ah
    out 42h, al
%endmacro

%macro beepon 0
    in al, 61h
    or al, 00000011b
    out 61h, al
%endmacro

%macro beepoff 0
    in al, 61h
    and al, 11111100b
    out 61h, al
%endmacro

and the sleep one:

%macro sleep 2
    push dx
    mov ah, 86h
    mov cx, %1
    mov dx, %2
    int 15h
    pop dx
%endmacro

I'm using the NASM assembler.

This is not a duplicate of How can I create a sleep function in 16bit MASM Assembly x86?, because this one is for bare-metal assembly outside of Windows or DOS.


Solution

  • The programmable interval timer is the way to go. If you will only deal with newer systems, look into HPET. For PIT there are a few things to know. Firstly, to set it up you need to use port 0x43 as the control/command port to configure the channel zero timer. The byte that we want to send there is a bit mapped field:

      7  |  6  |  5  |  4  |  3  |  2  |  1  |  0 
    +----------------------------------------------+
    | Channel  |  RW Mode  |   Channel Mode  | BCD?|
    +----------------------------------------------+
    

    Channel will be cleared to select channel zero.

    The RW Model can be 1-LSB, 2-MSB or 3-LSB followed by MSB. We want both bits on (bit pattern of 3, 011) because we need to send a 16 bit value (LSB then MSB)

    For channel mode we want a square wave. This is a bit pattern of 3 (011)

    We want to send a 16 bit divisor for the counter, not a BCD value, so the lowest bit is cleared.

    This gives us: 000110110 in binary or 0x36 in hex. Now we set it up:

    mov al, 0x36    ; 0x36 from our work above
    out 0x43, al    ; Send that byte to the control port
    
    mov ax, 11931   ; The timer ticks at  1193182. 100hz would be 1193182/11931
    out 0x40, al    ; send low byte
    out 0x40, ah    ; send high byte
    

    At this point you need to decide if you are going to react to an interrupt (IRQ 0) or simply want to read the timer. I'll point you to this excellent reference on OSDev which has a brilliant writeup on both with example code.