§ Для чего порты

Сами по себе эти инструкции несложные, их немного: IN - ввод данных из порта от 0 до 65535 и OUT - вывод данных в порт от 0 до 65535. Стоит заметить, что скорость работы портов крайне низкая. Буквально 100 наносекунд тратится на то, чтобы записать или прочитать из порта. А 100 нс - это невероятно медленная скорость для современных компьютеров.
Порты предназначены для общения с внешним миром. Процессор - это такая вещь в себе, но ему нужно знать, что вокруг происходит.

§ Реализация инструкции

Сначала надо написать две функции, для ввода данных из порта и вывода в порт
uint8_t io_ports[65536];

// Ввод данных
uint8_t port_in(uint16_t port) {
    return io_ports[port];
}

// Вывод
void port_out(uint16_t port, uint8_t data) {
    io_ports[port] = data;
}
Функции пока что не заполнены, но будут заполняться по мере написания для них устройств. Главные устройства это конечно таймер, видеоадаптер, клавиатура. Реализовано будет далее. А сейчас надо реализовать инструкции и это сделать очень просто.
// in
case 0xE4: case 0xE5:
case 0xEC: case 0xED:

    i_tmp = opcode_id & 8 ? regs16[REG_DX] : fetch(1);
    regs[REG_AL] = port_in(i_tmp);
    if (opcode_id & 1) regs[REG_AH] = port_in(i_tmp);
    break;

// out
case 0xE6: case 0xE7:
case 0xEE: case 0xEF:

    i_tmp = opcode_id & 8 ? regs16[REG_DX] : fetch(1);
    port_out(i_tmp, regs[REG_AL]);
    if (opcode_id & 1) port_out(i_tmp, regs[REG_AH]);
    break;

§ Таймер

Первым делом запрограммирую таймер. Для этого придется добавить несколько новых строк в заголовочные файлы:
int      io_hi_lo;
int      irq8_prevtime;
int      pit_frequency;
int      pit_interval;
И выполнить их инициализацию
io_hi_lo = 0;
pit_frequency = 65536;
pit_interval  = pit_frequency;
Переменная io_hi_lo принимает значение 0 - младший байт или 1 - старший байт, для записи в регистр pit_frequency, который является 16 битным и здесь записана скорость вызова таймера. При pit_frequency = 0 скорость таймера будет равна 18.2065 Гц (IRQ вызывается каждые 54.9254 с).
Также необходимо помнить, что существует программируемый контроллер прерываний (PIC), который тоже надо сделать, но пока сделаю все по-простому. Будем считать что таймер будет вызываться всегда, если есть IF=1. Просто дело в том, что можно заблокировать вызов IRQ 8 аппаратно.
// Вызов INT 8
void call_interrupt8() {

    // Каждые 1..65535 инструкции вызывать проверки
    if (--pit_interval <= 0) {

        ftime(&ms_clock);

        pit_interval  = pit_frequency;
        int time_curr = ms_clock.millitm;
        int time_diff = time_curr - irq8_prevtime;
        if (time_diff < 0) time_diff += 1000;

        // 54.9254 ms = 65536
        int time_pit = pit_frequency / 1191;

        // Вызов прерывания по истечению времени
        if (time_diff >= time_pit) {

            irq8_prevtime = time_curr;
            interrupt(8);
        }
    }
}
Это вспомогательная процедура, которая вызывается на каждой инструкции при включенном IF=1. Вызывается проверка истечения времени таймера каждые pit_frequency раз, при этом используя pit_interval, который уменьшается с каждой инструкцией на 1. После того как pit_interval достиг 0, происходит его обновление и запуск процедуры проверки истечения времени. Берется текущее время, вычисляется разность между предыдущим временем запроса INT8 и если она равна pit_frequency / 1191, то тогда обновляется новое время и делается запрос на INT8.