/*++

Copyright (c) 2015 Minoca Corp. All Rights Reserved

Module Name:

    ns16550.c

Abstract:

    This module implements the firmware serial port interface on a 16550
    standard UART.

Author:

    Chris Stevens 10-Jul-2015

Environment:

    Firmware

--*/

//
// ------------------------------------------------------------------- Includes
//

#include "uefifw.h"
#include "dev/ns16550.h"

//
// --------------------------------------------------------------------- Macros
//

//
// Macros to read from and write to 16550 registers.
//

#define NS16550_READ8(_Device, _Register) \
    ((PNS16550_READ8)(_Device)->Read8)((_Device), (_Register))

#define NS16550_WRITE8(_Device, _Register, _Value) \
    ((PNS16550_WRITE8)(_Device)->Write8)((_Device), (_Register), (_Value))

//
// This macro returns the offset of a given register from its base.
//

#define NS16550_REGISTER_OFFSET(_Device, _Register) \
    ((_Device)->RegisterOffset + ((_Register) << (_Device)->RegisterShift))

//
// ---------------------------------------------------------------- Definitions
//

//
// Define the bits for the PC UART Line Status register.
//

#define NS16550_LINE_STATUS_DATA_READY     0x01
#define NS16550_LINE_STATUS_TRANSMIT_EMPTY 0x20
#define NS16550_LINE_STATUS_ERRORS         0x8E

//
// ------------------------------------------------------ Data Type Definitions
//

typedef enum _NS16550_REGISTER {
    Ns16550Data            = 0,
    Ns16550DivisorLow      = 0,
    Ns16550InterruptEnable = 1,
    Ns16550DivisorHigh     = 1,
    Ns16550InterruptStatus = 2,
    Ns16550FifoControl     = 2,
    Ns16550LineControl     = 3,
    Ns16550ModemControl    = 4,
    Ns16550LineStatus      = 5,
    Ns16550ModemStatus     = 6,
    Ns16550Scratch         = 7
} NS16550_REGISTER, *PNS16550_REGISTER;

typedef
UINT8
(*PNS16550_READ8) (
    PNS16550_CONTEXT Context,
    NS16550_REGISTER Register
    );

/*++

Routine Description:

    This routine reads a 16550 register.

Arguments:

    Context - Supplies a pointer to the device context.

    Register - Supplies the register to read.

Return Value:

    Returns the value at the register.

--*/

typedef
VOID
(*PNS16550_WRITE8) (
    PNS16550_CONTEXT Context,
    NS16550_REGISTER Register,
    UINT8 Value
    );

/*++

Routine Description:

    This routine writes to a 16550 register.

Arguments:

    Context - Supplies a pointer to the device context.

    Register - Supplies the register to write to.

    Value - Supplies the value to write.

Return Value:

    None.

--*/

//
// ----------------------------------------------- Internal Function Prototypes
//

UINT8
EfipNs16550ReadIo8 (
    PNS16550_CONTEXT Device,
    NS16550_REGISTER Register
    );

VOID
EfipNs16550WriteIo8 (
    PNS16550_CONTEXT Device,
    NS16550_REGISTER Register,
    UINT8 Value
    );

UINT8
EfipNs16550ReadMemory8 (
    PNS16550_CONTEXT Device,
    NS16550_REGISTER Register
    );

VOID
EfipNs16550WriteMemory8 (
    PNS16550_CONTEXT Device,
    NS16550_REGISTER Register,
    UINT8 Value
    );

//
// -------------------------------------------------------------------- Globals
//

//
// ------------------------------------------------------------------ Functions
//

//
// --------------------------------------------------------- Internal Functions
//

EFI_STATUS
EfipNs16550ComputeDivisor (
    UINT32 BaseBaud,
    UINT32 BaudRate,
    UINT16 *Divisor
    )

/*++

Routine Description:

    This routine computes the divisor rates for a NS 16550 UART at a given baud
    rate.

Arguments:

    BaseBaud - Supplies the baud rate for a divisor of 1.

    BaudRate - Supplies the desired baud rate.

    Divisor - Supplies a pointer where the divisor will be returned on success.

Return Value:

    EFI_SUCCESS on success.

    EFI_UNSUPPORTED if the given baud rate cannot be achieved.

--*/

{

    UINT32 CurrentBaud;
    UINT32 LocalDivisor;

    //
    // Compute the baud rate divisor.
    //

    if (BaudRate > BaseBaud) {
        return EFI_UNSUPPORTED;
    }

    LocalDivisor = 1;
    while (TRUE) {
        CurrentBaud = BaseBaud / LocalDivisor;
        if ((CurrentBaud <= BaudRate) || (CurrentBaud == 0)) {
            break;
        }

        LocalDivisor += 1;
    }

    if ((CurrentBaud == 0) || (LocalDivisor > MAX_UINT16)) {
        return EFI_UNSUPPORTED;
    }

    *Divisor = (UINT16)LocalDivisor;
    return EFI_SUCCESS;
}

EFI_STATUS
EfipNs16550Initialize (
    PNS16550_CONTEXT Context
    )

/*++

Routine Description:

    This routine initializes the NS 16550 serial port hardware. The caller
    should have initialized at least some of the context structure.

Arguments:

    Context - Supplies the pointer to the port's initialized context.

Return Value:

    EFI Status code.

--*/

{

    UINT8 Value;

    //
    // Determine the correct register access function.
    //

    if (Context->MemoryBase != NULL) {
        Context->Read8 = EfipNs16550ReadMemory8;
        Context->Write8 = EfipNs16550WriteMemory8;

    } else {
        Context->Read8 = EfipNs16550ReadIo8;
        Context->Write8 = EfipNs16550WriteIo8;
    }

    //
    // Begin programming the 16550 controller. The topmost bit in the line
    // control register turns the DLAB (Data Latch Address Byte) on. This
    // changes the meanings of the registers, allowing us to program the baud
    // rate divisor values.
    //

    Value = NS16550_READ8(Context, Ns16550LineControl);
    Value |= 0x80;
    NS16550_WRITE8(Context, Ns16550LineControl, Value);

    //
    // Set the divisor bytes. This programs the baud rate generator.
    //

    NS16550_WRITE8(Context,
                   Ns16550DivisorLow,
                   (UINT8)(Context->BaudRateDivisor & 0x00FF));

    NS16550_WRITE8(Context,
                   Ns16550DivisorHigh,
                   (UINT8)((Context->BaudRateDivisor >> 8) & 0x00FF));

    //
    // Now program the FIFO queue configuration. It is assumed that the FIFOs
    // are operational, which is not true on certain machines with very old
    // UARTs. Setting bit 0 enables the FIFO. Setting bits 1 and 2 clears both
    // FIFOs. Clearing bit 3 disables DMA mode. The top 4 bits vary depending
    // on the version. Setting bit 5 enables the 64 byte FIFO, which is only
    // available on 16750s. Bit 4 is reserved. Otherwise bits 4 and 5 are
    // either reserved or dictate the transmit FIFO's empty trigger. Bits 6 and
    // 7 set the receive FIFO's trigger, where setting both bits means that
    // "2 less than full", which for the default 16 byte FIFO means 14 bytes
    // are in the buffer.
    //

    Value = 0xC7;
    if ((Context->Flags & NS16550_FLAG_TRANSMIT_TRIGGER_2_CHARACTERS) != 0) {
        Value |= 0x10;

    } else if ((Context->Flags & NS16550_FLAG_64_BYTE_FIFO) != 0) {
        Value |= 0x20;
    }

    NS16550_WRITE8(Context, Ns16550FifoControl, Value);

    //
    // Now program the Line Control register again. Setting bits 0 and 1 sets
    // 8 data bits. Clearing bit 2 sets one stop bit. Clearing bit 3 sets no
    // parity. Additionally, clearing bit 7 turns the DLAB latch off, changing
    // the meaning of the registers back and allowing other control registers to
    // be accessed.
    //

    NS16550_WRITE8(Context, Ns16550LineControl, 0x03);

    //
    // Setting the Modem Control register to zero disables all hardware flow
    // control.
    //

    NS16550_WRITE8(Context, Ns16550ModemControl, 0x00);
    return EFI_SUCCESS;
}

EFI_STATUS
EfipNs16550Transmit (
    PNS16550_CONTEXT Context,
    VOID *Data,
    UINT32 Size
    )

/*++

Routine Description:

    This routine writes data out the serial port. This routine should busily
    spin if the previously sent byte has not finished transmitting.

Arguments:

    Context - Supplies the pointer to the port context.

    Data - Supplies a pointer to the data to write.

    Size - Supplies the size to write, in bytes.

Return Value:

    EFI_SUCCESS on success.

    EFI_DEVICE_ERROR if a device error occurred.

--*/

{

    UINT32 ByteIndex;
    UINT8 *Bytes;
    UINT8 StatusRegister;

    Bytes = Data;
    for (ByteIndex = 0; ByteIndex < Size; ByteIndex += 1) {

        //
        // Spin waiting for the buffer to become ready to send. If an error is
        // detected, bail out and report to the caller.
        //

        do {
            StatusRegister = NS16550_READ8(Context, Ns16550LineStatus);
            if ((StatusRegister & NS16550_LINE_STATUS_ERRORS) != 0) {
                return EFI_DEVICE_ERROR;
            }

        } while ((StatusRegister & NS16550_LINE_STATUS_TRANSMIT_EMPTY) == 0);

        //
        // Send the byte and return.
        //

        NS16550_WRITE8(Context, Ns16550Data, Bytes[ByteIndex]);
    }

    return EFI_SUCCESS;
}

EFI_STATUS
EfipNs16550Receive (
    PNS16550_CONTEXT Context,
    VOID *Data,
    UINT32 *Size
    )

/*++

Routine Description:

    This routine reads bytes from the serial port.

Arguments:

    Context - Supplies the pointer to the port context.

    Data - Supplies a pointer where the read data will be returned on success.

    Size - Supplies a pointer that on input contains the size of the receive
        buffer. On output, returns the number of bytes read.

Return Value:

    EFI_SUCCESS on success.

    EFI_NOT_READY if there was no data to be read at the current time.

    EFI_DEVICE_ERROR if a device error occurred.

--*/

{

    UINT32 ByteCount;
    UINT32 ByteIndex;
    UINT8 *Bytes;
    EFI_STATUS Status;
    UINT8 StatusRegister;

    ByteCount = *Size;
    Bytes = Data;
    Status = EFI_NOT_READY;
    for (ByteIndex = 0; ByteIndex < ByteCount; ByteIndex += 1) {
        StatusRegister = NS16550_READ8(Context, Ns16550LineStatus);
        if ((StatusRegister & NS16550_LINE_STATUS_ERRORS) != 0) {
            Status = EFI_DEVICE_ERROR;
            break;
        }

        if ((StatusRegister & NS16550_LINE_STATUS_DATA_READY) == 0) {
            break;
        }

        Bytes[ByteIndex] = NS16550_READ8(Context, Ns16550Data);
        Status = EFI_SUCCESS;
    }

    *Size = ByteIndex;
    return Status;
}

EFI_STATUS
EfipNs16550GetStatus (
    PNS16550_CONTEXT Context,
    BOOLEAN *ReceiveDataAvailable
    )

/*++

Routine Description:

    This routine returns the current device status.

Arguments:

    Context - Supplies a pointer to the serial port context.

    ReceiveDataAvailable - Supplies a pointer where a boolean will be returned
        indicating whether or not receive data is available.

Return Value:

    EFI_SUCCESS on success.

    EFI_DEVICE_ERROR if a device error occurred.

--*/

{

    UINT8 StatusRegister;

    *ReceiveDataAvailable = FALSE;
    StatusRegister = NS16550_READ8(Context, Ns16550LineStatus);
    if ((StatusRegister & NS16550_LINE_STATUS_DATA_READY) != 0) {
        *ReceiveDataAvailable = TRUE;
    }

    return EFI_SUCCESS;
}

UINT8
EfipNs16550ReadIo8 (
    PNS16550_CONTEXT Context,
    NS16550_REGISTER Register
    )

/*++

Routine Description:

    This routine reads a 16550 register from an I/O port.

Arguments:

    Context - Supplies a pointer to the device context.

    Register - Supplies the register to read.

Return Value:

    Returns the value at the register.

--*/

{

    UINT16 Port;

    Port = Context->IoBase + NS16550_REGISTER_OFFSET(Context, Register);
    return EfiIoPortIn8(Port);
}

VOID
EfipNs16550WriteIo8 (
    PNS16550_CONTEXT Context,
    NS16550_REGISTER Register,
    UINT8 Value
    )

/*++

Routine Description:

    This routine writes to an I/O port based 16550 register.

Arguments:

    Context - Supplies a pointer to the device context.

    Register - Supplies the register to write to.

    Value - Supplies the value to write.

Return Value:

    None.

--*/

{

    UINT16 Port;

    Port = Context->IoBase + NS16550_REGISTER_OFFSET(Context, Register);
    EfiIoPortOut8(Port, Value);
    return;
}

UINT8
EfipNs16550ReadMemory8 (
    PNS16550_CONTEXT Context,
    NS16550_REGISTER Register
    )

/*++

Routine Description:

    This routine reads a 16550 register from a memory mapped register.

Arguments:

    Context - Supplies a pointer to the device context.

    Register - Supplies the register to read.

Return Value:

    Returns the value at the register.

--*/

{

    VOID *Address;
    UINT8 Value;

    Address = Context->MemoryBase + NS16550_REGISTER_OFFSET(Context, Register);
    switch (Context->RegisterShift) {
    case NS16550_1_BYTE_REGISTER_SHIFT:
        Value = EfiReadRegister8(Address);
        break;

    case NS16550_2_BYTE_REGISTER_SHIFT:
        Value = EfiReadRegister16(Address);
        break;

    case NS16550_4_BYTE_REGISTER_SHIFT:
    default:
        Value = EfiReadRegister32(Address);
        break;
    }

    return Value;
}

VOID
EfipNs16550WriteMemory8 (
    PNS16550_CONTEXT Context,
    NS16550_REGISTER Register,
    UINT8 Value
    )

/*++

Routine Description:

    This routine writes to a memory mapped 16550 register.

Arguments:

    Context - Supplies a pointer to the device context.

    Register - Supplies the register to write to.

    Value - Supplies the value to write.

Return Value:

    None.

--*/

{

    VOID *Address;

    Address = Context->MemoryBase + NS16550_REGISTER_OFFSET(Context, Register);
    switch (Context->RegisterShift) {
    case NS16550_1_BYTE_REGISTER_SHIFT:
        EfiWriteRegister8(Address, Value);
        break;

    case NS16550_2_BYTE_REGISTER_SHIFT:
        EfiWriteRegister16(Address, (UINT16)Value);
        break;

    case NS16550_4_BYTE_REGISTER_SHIFT:
    default:
        EfiWriteRegister32(Address, (UINT32)Value);
        break;
    }

    return;
}

