EverDrive-64 X7

From N64brew Wiki
Revision as of 05:50, 17 September 2020 by Murachue (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

The EverDrive-64 X7 is a flash cart made by Krikzz.


Serial Communication

The EverDrive-64 X7 supports serial over USB communication to a host PC. This can be used to load ROMs and allows running ROMs to send an receive data from the host PC. Krikzz provides a reference implementation here.

Serial Registers

The EverDrive-64 X7 provides various registers starting at the address 0x1F800000 in the cartridge address space. The following registers are used for USB communcation.

List of Registers
Name Description Address relative to 0x1F800000 Size (bytes)
REG_USB_CFG Reads USB status and sends read and write commands 0x0004 4
REG_USB_DATA The temporary data buffer used to read and write to USB 0x0400 512
REG_KEY Set to 0xAA55 on initialization 0x8004 4
REG_SYS_CFG Set to 0 on initialization 0x8000 4

The REG_USB_CFG register contains various bits as described below

REG_USB_CFG bits
Name Description Bits (15-0) Hex Mask
USB_LE_CFG Set to a 1 when reading and writing data 15 0x8000
USB_LE_CTR Set to a 1 when reading and writing data 14 0x4000
USB_STA_BSY Behavior not known 13 0x2000
USB_STA_PWR Set to when 1 when data can be read or written 12 0x1000
USB_STA_TXE Set to 0 when data can be written 11 0x0800
USB_CFG_RD/USB_STA_RXF Writing a 1 switches the controller to read mode. Writing a 0 switches the controller to write mode.

Reads as a 0 when data is available to be read.

10 0x0400
USB_CFG_ACT/USB_STA_ACT Writing a 1 starts reading or writing to REG_USB_DATA. The direction of data is specified by the value of USB_CFG_RD. The amount of data written is specified by baddr.

When reading this bit, a 1 indicates the USB port is busy reading or writing data.

9 0x0200
baddr These 5 bits give the address into REG_USB_DAT where USB data is read from and written to. Data is read starting at baddr offset until the end of the REG_USB_DAT buffer. This means the amount of data read is 512 - baddr. Because of this, when copying data to and from REG_USB_DAT, you don't always start copying at the beginning of the buffer. 8..0 0x01FF

Initializing EverDrive

Below is the code used to initialize the EverDrive to be used for USB IO from krikzz's reference code with comments

/*************************************************************************
 * Peripheral Interface (PI) Registers 
 */
#define PI_BASE_REG		0x04600000

/* PI dom1 latency (R/W): [7:0] domain 1 device latency */
#define PI_BSD_DOM1_LAT_REG	(PI_BASE_REG+0x14)

/* PI dom1 pulse width (R/W): [7:0] domain 1 device R/W strobe pulse width */
#define PI_BSD_DOM1_PWD_REG	(PI_BASE_REG+0x18)

#define	IO_WRITE(addr,data)	(*(vu32 *)PHYS_TO_K1(addr)=(u32)(data))

void bi_init() {
    // sets the PI manager latency
    IO_WRITE(PI_BSD_DOM1_LAT_REG, 0x04);
    // sets the PI manager pulse
    IO_WRITE(PI_BSD_DOM1_PWD_REG, 0x0C);
    
    // the specific value of 0xAA55 needs to be written here to enable USB IO
    bi_reg_wr(REG_KEY, 0xAA55);
    bi_reg_wr(REG_SYS_CFG, 0);
    // this flushes serial buffer of any pending data
    bi_usb_init();

    // sets the save type. Not needed for USB IO
    bi_set_save_type(SAVE_OFF);
}

The example code sets latency and pulse for the values expected for USB communication. Different devices such as flash, sram, or cartridge would expect different values. You should change these to the appropriate values for the device before doing a DMA to that device. If you are using libultra there is a function designed to configure the PI manager for you before starting a DMA using osEPiStartDma. The first parameter of this function accepts a pointer to a OSPiHandle which contains the latency and pulse values that should be used by the PI manager.

Sending Data to the Host

Below is the example code for sending data to the host PC in the function bi_usb_wr. Keep in mind that src should be aligned on 8 byte boundaries since it is used in a DMA transfer. For the same reason the length should be a multiple of 2.

#define USB_LE_CFG      0x8000
#define USB_LE_CTR      0x4000

#define USB_CFG_ACT     0x0200
#define USB_CFG_RD      0x0400
#define USB_CFG_WR      0x0000

#define USB_STA_ACT     0x0200
#define USB_STA_RXF     0x0400
#define USB_STA_TXE     0x0800
#define USB_STA_PWR     0x1000
#define USB_STA_BSY     0x2000

#define USB_CMD_RD_NOP  (USB_LE_CFG | USB_LE_CTR | USB_CFG_RD)
#define USB_CMD_RD      (USB_LE_CFG | USB_LE_CTR | USB_CFG_RD | USB_CFG_ACT)
#define USB_CMD_WR_NOP  (USB_LE_CFG | USB_LE_CTR | USB_CFG_WR)
#define USB_CMD_WR      (USB_LE_CFG | USB_LE_CTR | USB_CFG_WR | USB_CFG_ACT)

#define REG_BASE        0x1F800000
#define KSEG1           0xA0000000

#define REG_ADDR(reg)   (KSEG1 | REG_BASE | (reg))

u8 bi_usb_busy() {
    u32 tout = 0;

    // wait for the USB_STA_ACT bit to clear in REG_USB_CFG
    // indicating the USB port is no longer busy
    while ((bi_reg_rd(REG_USB_CFG) & USB_STA_ACT) != 0) {
        // check to see if the operation has timed out
        if (tout++ != 8192)continue;
        // Turn off USB serial communication
        bi_reg_wr(REG_USB_CFG, USB_CMD_RD_NOP);
        return BI_ERR_USB_TOUT;
    }

    return 0;
}

u8 bi_usb_wr(void *src, u32 len) {
    u8 resp = 0;
    u16 blen, baddr;

    // switch USB to write mode but don't start the transfer yet
    bi_reg_wr(REG_USB_CFG, USB_CMD_WR_NOP);

    while (len) {
        // data can only be sent in chunks of 512 bytes
        blen = 512;
        if (blen > len)blen = len;
        // calculate the offset into REG_USB_DAT from the length of data
        baddr = 512 - blen;

        // DMA the data from ram into the temporary buffer on the cartridge
        // Note that sending less than 512 bytes will result it baddr being
        // a positive value making the start of the DMA copy be somewhere
        // in the middle of REG_USB_DAT instead of always starting at the
        // beginning 
        sysPI_wr(src, REG_ADDR(REG_USB_DAT + baddr), blen);
        src += 512;

        // Start sending data from REG_USB_DAT over the usb starting at
        // baddr and ending at the end of the 512 byte buffer.
        bi_reg_wr(REG_USB_CFG, USB_CMD_WR | baddr);

        // Wait for the data to finish sending over USB
        resp = bi_usb_busy();
        if (resp)break;

        len -= blen;
    }

    return resp;
}

Reading Data from the Host

Below is an example of reading data from the host PC. Since dst is used in a DMA transfer it must be aligned to 8 bytes. For the same reason, len should be multiple of 2. The EverDrive-64 X7 also has the limitation that at least 16 bytes must be read. For this reason you should pad data sent to the EverDrive to a multiple of 16 bytes. You also must not read more data than what as been send from the host PC otherwise the read operation will time out while waiting for data that never comes. To solve this you can either prefix data sent to the EverDrive with a header indicating the amount of data being sent or you read data 16 bytes at a time calling bi_usb_can_wr each time to check if there is more data.

// returns 1 if there is data waiting to be read
u8 bi_usb_can_wr() {
    // check if USB_STA_PWR is 1 and USB_STA_TXE is 0 in REG_USB_CFG 
    u32 status = bi_reg_rd(REG_USB_CFG) & (USB_STA_PWR | USB_STA_TXE);
    if (status == USB_STA_PWR)return 1;
    return 0;
}

u8 bi_usb_rd(void *dst, u32 len) {

    u8 resp = 0;
    u16 blen, baddr;

    while (len) {
        // data cannot be read in chunks larger than 512 bytes
        blen = 512;
        if (blen > len)blen = len;
        baddr = 512 - blen;

        // tell the EverDrive to write data received from USB to
        // to REG_USB_DAT starting at address baddr. It will continue
        // to write data until the end of the 512 byte buffer. If
        // there is not enough data to fill the buffer it will wait 
        // until that data is received
        bi_reg_wr(REG_USB_CFG, USB_CMD_RD | baddr);
        // wait for the serial data to be copied into REG_USB_DAT
        // if there is not enough data to write to the end of the
        // buffer this operation will time out
        resp = bi_usb_busy();
        if (resp)break;
      
        // copy data from REG_USB_DAT into ram
        sysPI_rd(dst, REG_ADDR(REG_USB_DAT + baddr), blen);

        dst += blen;
        len -= blen;
    }

    return resp;
}

Common Code

Code used for both reading and writing

void sysPI_rd(void *ram, unsigned long pi_address, unsigned long len) {

    pi_address &= 0x1FFFFFFF;

    // invalidates the cache so the new values in ram after reading
    // wont mismatch what is in the cache 
    // this is part of libdragon, for the libultra equivalent use osInvalDCache
    data_cache_hit_writeback_invalidate(ram, len);

    // the rest of the code is for a DMA transfer from the cartridge into RAM. 
    // use osEPiStartDma for libultra
    disable_interrupts();

    while (dma_busy());
    IO_WRITE(PI_STATUS_REG, 3);
    PI_regs->ram_address = ram;
    PI_regs->pi_address = pi_address; //(pi_address | 0x10000000) & 0x1FFFFFFF;
    PI_regs->write_length = len - 1;
    while (dma_busy());

    enable_interrupts();
}

void sysPI_wr(void *ram, unsigned long pi_address, unsigned long len) {

    pi_address &= 0x1FFFFFFF;

    // Writes the cache back to ram so the DMA gets the correct value
    // this is part of libdragon, for the libultra equivalent use osWritebackDCache
    data_cache_hit_writeback(ram, len);

    // the rest of the code is for a DMA transfer from the RAM into the cartridge. 
    // use osEPiStartDma for libultra
    disable_interrupts();

    while (dma_busy());
    IO_WRITE(PI_STATUS_REG, 3);
    PI_regs->ram_address = ram;
    PI_regs->pi_address = pi_address; //(pi_address | 0x10000000) & 0x1FFFFFFF;
    PI_regs->read_length = len - 1;
    while (dma_busy());

    enable_interrupts();

}

// Writes a given value to the given register on the EverDrive
void bi_reg_wr(u16 reg, u32 val) {
    sysPI_wr(&val, REG_ADDR(reg), 4);
}

// Reads the given register value
u32 bi_reg_rd(u16 reg) {
    u32 val;
    sysPI_rd(&val, REG_ADDR(reg), 4);
    return val;
}