#include <x86emu/x86emu_i8254.h>
#ifndef _KERNEL
#include <assert.h>
#define KASSERT(x) assert(x)
#endif
#define I8254_FREQ 1193182
static uint16_t
bcd2bin(uint16_t bcd_val)
{
return bcd_val % 0x10 + (bcd_val / 0x10 % 0x10 * 10) +
(bcd_val / 0x100 % 0x10 * 100) + (bcd_val / 0x1000 % 0x10 * 1000);
}
static uint16_t
bin2bcd(uint16_t bin_val)
{
return (bin_val % 10) + (bin_val / 10 % 10 * 0x10) +
(bin_val / 100 % 10 * 0x100) + (bin_val / 1000 % 10 * 0x1000);
}
static uint64_t
x86emu_i8254_gettick(struct x86emu_i8254 *sc)
{
struct timespec curtime;
uint64_t tick;
(*sc->gettime)(&curtime);
tick = (curtime.tv_sec - sc->base_time.tv_sec) * I8254_FREQ;
tick += (uint64_t)(curtime.tv_nsec - sc->base_time.tv_nsec) * I8254_FREQ / 1000000000;
return tick;
}
static uint16_t
x86emu_i8254_counter(struct x86emu_i8254_timer *timer, uint64_t curtick)
{
uint16_t maxtick;
if (timer->gate_high || timer->start_tick > curtick)
return timer->active_counter;
if (timer->active_is_bcd)
maxtick = 9999;
else
maxtick = 0xffff;
curtick -= timer->start_tick;
if (curtick <= timer->active_counter)
return timer->active_counter - curtick;
if (timer->active_counter == 0 && curtick - 1 <= maxtick)
return maxtick + 1 - curtick;
if (timer->active_mode & 2)
return timer->active_counter - curtick % timer->active_counter;
curtick -= maxtick + 1;
return maxtick - curtick % maxtick + 1;
}
static bool
x86emu_i8254_out(struct x86emu_i8254_timer *timer, uint64_t curtick)
{
if (timer->gate_high || timer->start_tick > curtick)
return (timer->active_mode != 0);
curtick -= timer->start_tick;
if (timer->active_mode == 0 || timer->active_mode == 1)
return curtick >= timer->start_tick;
if (timer->active_mode == 5 || timer->active_mode == 7)
return curtick != timer->start_tick;
if (timer->active_mode == 2 || timer->active_mode == 3) {
curtick %= timer->active_counter;
return curtick + 1 != timer->active_counter;
}
curtick %= timer->active_counter;
return curtick < (timer->active_counter + 1) / 2;
}
static void
x86emu_i8254_latch_status(struct x86emu_i8254_timer *timer, uint64_t curtick)
{
if (timer->status_is_latched)
return;
timer->latched_status = timer->active_is_bcd ? 1 : 0;
timer->latched_status |= timer->active_mode << 1;
timer->latched_status |= timer->rw_status;
timer->latched_status |= timer->null_count ? 0x40 : 0;
}
static void
x86emu_i8254_latch_counter(struct x86emu_i8254_timer *timer, uint64_t curtick)
{
if (!timer->counter_is_latched)
return;
timer->latched_counter = x86emu_i8254_counter(timer, curtick);
timer->counter_is_latched = true;
}
static void
x86emu_i8254_write_command(struct x86emu_i8254 *sc, uint8_t val)
{
struct x86emu_i8254_timer *timer;
int i;
if ((val >> 6) == 3) {
uint64_t curtick;
curtick = x86emu_i8254_gettick(sc);
for (i = 0; i < 3; ++i) {
timer = &sc->timer[i];
if ((val & (2 << i)) == 0)
continue;
if ((val & 0x10) != 0)
x86emu_i8254_latch_status(timer, curtick);
if ((val & 0x20) != 0)
x86emu_i8254_latch_counter(timer, curtick);
}
return;
}
timer = &sc->timer[val >> 6];
switch (val & 0x30) {
case 0:
x86emu_i8254_latch_counter(timer, x86emu_i8254_gettick(sc));
return;
case 1:
timer->write_lsb = timer->read_lsb = true;
timer->write_msb = timer->read_msb = false;
break;
case 2:
timer->write_lsb = timer->read_lsb = false;
timer->write_msb = timer->read_msb = true;
break;
case 3:
timer->write_lsb = timer->read_lsb = true;
timer->write_msb = timer->read_msb = true;
break;
}
timer->rw_status = val & 0x30;
timer->null_count = true;
timer->new_mode = (val >> 1) & 0x7;
timer->new_is_bcd = (val & 1) == 1;
}
static uint8_t
x86emu_i8254_read_counter(struct x86emu_i8254 *sc,
struct x86emu_i8254_timer *timer)
{
uint16_t val;
uint8_t output;
if (timer->status_is_latched) {
timer->status_is_latched = false;
return timer->latched_status;
}
if (timer->counter_is_latched)
val = timer->latched_counter;
else
val = x86emu_i8254_counter(&sc->timer[2],
x86emu_i8254_gettick(sc));
if (timer->active_is_bcd)
val = bin2bcd(val);
if (timer->read_lsb) {
output = val & 0xff;
timer->read_lsb = false;
} else if (timer->read_msb) {
output = val >> 8;
timer->read_msb = false;
} else
output = 0;
if (!timer->read_lsb && !timer->read_msb)
timer->counter_is_latched = false;
return output;
}
static void
x86emu_i8254_write_counter(struct x86emu_i8254 *sc,
struct x86emu_i8254_timer *timer, uint8_t val)
{
if (!timer->write_lsb && !timer->write_msb)
return;
if (timer->write_lsb) {
timer->new_counter &= ~0xff;
timer->new_counter |= val;
timer->write_lsb = false;
} else {
KASSERT(timer->write_msb);
timer->new_counter &= ~0xff00;
timer->new_counter |= val << 8;
timer->write_msb = false;
}
if (!timer->write_lsb && !timer->write_msb) {
timer->null_count = false;
timer->counter_is_latched = false;
timer->status_is_latched = false;
timer->active_is_bcd = timer->new_is_bcd;
timer->active_mode = timer->new_mode;
timer->start_tick = x86emu_i8254_gettick(sc) + 1;
if (timer->new_is_bcd)
timer->active_counter = bcd2bin(timer->new_counter);
}
}
static uint8_t
x86emu_i8254_read_nmi(struct x86emu_i8254 *sc)
{
uint8_t val;
val = (sc->timer[2].gate_high) ? 1 : 0;
if (x86emu_i8254_out(&sc->timer[2], x86emu_i8254_gettick(sc)))
val |= 0x20;
return val;
}
static void
x86emu_i8254_write_nmi(struct x86emu_i8254 *sc, uint8_t val)
{
bool old_gate;
old_gate = sc->timer[2].gate_high;
sc->timer[2].gate_high = (val & 1) == 1;
if (!old_gate && sc->timer[2].gate_high)
sc->timer[2].start_tick = x86emu_i8254_gettick(sc) + 1;
}
void
x86emu_i8254_init(struct x86emu_i8254 *sc, void (*gettime)(struct timespec *))
{
struct x86emu_i8254_timer *timer;
int i;
sc->gettime = gettime;
(*sc->gettime)(&sc->base_time);
for (i = 0; i < 3; ++i) {
timer = &sc->timer[i];
timer->gate_high = false;
timer->start_tick = 0;
timer->active_counter = 0;
timer->active_mode = 0;
timer->active_is_bcd = false;
timer->counter_is_latched = false;
timer->read_lsb = false;
timer->read_msb = false;
timer->status_is_latched = false;
timer->null_count = false;
}
}
uint8_t
x86emu_i8254_inb(struct x86emu_i8254 *sc, uint16_t port)
{
KASSERT(x86emu_i8254_claim_port(sc, port));
if (port == 0x40)
return x86emu_i8254_read_counter(sc, &sc->timer[0]);
if (port == 0x41)
return x86emu_i8254_read_counter(sc, &sc->timer[1]);
if (port == 0x42)
return x86emu_i8254_read_counter(sc, &sc->timer[2]);
if (port == 0x43)
return 0xff;
return x86emu_i8254_read_nmi(sc);
}
void
x86emu_i8254_outb(struct x86emu_i8254 *sc, uint16_t port, uint8_t val)
{
KASSERT(x86emu_i8254_claim_port(sc, port));
if (port == 0x40)
x86emu_i8254_write_counter(sc, &sc->timer[0], val);
else if (port == 0x41)
x86emu_i8254_write_counter(sc, &sc->timer[1], val);
else if (port == 0x42)
x86emu_i8254_write_counter(sc, &sc->timer[2], val);
else if (port == 0x43)
x86emu_i8254_write_command(sc, val);
else
x86emu_i8254_write_nmi(sc, val);
}
bool
x86emu_i8254_claim_port(struct x86emu_i8254 *sc, uint16_t port)
{
if (port >= 0x40 && port < 0x44)
return true;
if (port == 0x61)
return true;
return false;
}