λ

furix

cute os

settings

arch/x86_64/serial.c

3251 bytes raw

#include <serial.h>

#include <io.h>
#include <types.h>

#define COM_PORT 0x3F8

#define REG_DATA 0
#define REG_IER 1
#define REG_FCR 2
#define REG_LCR 3
#define REG_MCR 4
#define REG_LSR 5

#define LSR_THR_EMPTY 0x20

/*
 * serial output lock.
 *
 * the 16550 has a single transmit holding register and no way to read back what
 * is still queued, so two CPUs writing at once interleave mid-line and produce
 * garbled output. that is not cosmetic here: the AP bring-up log is the only
 * evidence available when a core fails to start, so a scrambled "ap: 3 online"
 * is indistinguishable from a core that never came up.
 *
 * a test-and-set flag is enough rather than a queue: output is strictly
 * synchronous (we spin until the holding register drains), so the critical
 * section is short and bounded by a few dozen I/O cycles.
 *
 * the wait is a plain spin, never a halt. an AP that arrived here with
 * interrupts off would deadlock against a `hlt`, and the BSP must stay able to
 * report the stall.
 */
static volatile uint8_t serial_lock;

static void serial_lock_acquire(void) {
  while (atomic_test_and_set_u8(&serial_lock) != 0) {
    /* another CPU owns the port; keep retrying */
  }
}

static void serial_lock_release(void) {
  /*
   * plain store rather than a locked release. the xchg in the acquire path is
   * a full barrier, so everything written before this store is already visible
   * to the next acquirer's locked read of the flag.
   */
  serial_lock = 0;
}

static uint8_t serial_read(uint16_t reg) {
  return inb((uint16_t)(COM_PORT + reg));
}

static void serial_write(uint16_t reg, uint8_t value) {
  outb((uint16_t)(COM_PORT + reg), value);
}

static void serial_write_byte(uint8_t byte) {
  while ((serial_read(REG_LSR) & LSR_THR_EMPTY) == 0) {
    /* wait for the transmit holding register to drain */
  }
  serial_write(REG_DATA, byte);
}

void serial_init(void) {
  serial_write(REG_IER, 0x00); /* no interrupts */
  serial_write(REG_LCR, 0x80); /* enable DLAB */
  serial_write(REG_DATA, 0x03); /* divisor low (38400 baud) */
  serial_write(REG_IER, 0x00);  /* divisor high */
  serial_write(REG_LCR, 0x03);  /* 8 bits, no parity, one stop bit */
  serial_write(REG_FCR, 0xC7);  /* enable + clear FIFOs, 14-byte threshold */
  serial_write(REG_MCR, 0x0B);  /* IRQs enabled, RTS/DSR set */
}

void serial_putc(char c) {
  serial_lock_acquire();

  if (c == '\n') {
    serial_write_byte((uint8_t)'\r');
  }
  serial_write_byte((uint8_t)c);

  serial_lock_release();
}

/*
 * the lock is taken once per string rather than per character: a 40-character
 * line then costs one atomic instead of forty, and the output stays atomic
 * across the whole line, which is what the log reader needs.
 */
void serial_puts(const char *s) {
  serial_lock_acquire();

  while (*s != '\0') {
    if (*s == '\n') {
      serial_write_byte((uint8_t)'\r');
    }
    serial_write_byte((uint8_t)*s);
    s++;
  }

  serial_lock_release();
}

void serial_put_hex(uint64_t value) {
  int shift;

  serial_puts("0x");
  for (shift = 60; shift >= 0; shift -= 4) {
    uint8_t nibble = (uint8_t)((value >> (unsigned)shift) & 0xF);
    char c = (nibble < 10) ? (char)('0' + nibble) : (char)('a' + (nibble - 10));
    serial_putc(c);
  }
}