/* mbed USBHost Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#if defined(TARGET_RZ_A1H)

#include "mbed.h"
#include "USBHALHost.h"
#include "dbg.h"

#include "ohci_wrapp_RZ_A1.h"


#define HCCA_SIZE sizeof(HCCA)
#define ED_SIZE sizeof(HCED)
#define TD_SIZE sizeof(HCTD)

#define TOTAL_SIZE (HCCA_SIZE + (MAX_ENDPOINT*ED_SIZE) + (MAX_TD*TD_SIZE))
#define ALIGNE_MSK (0x0000000F)

static volatile uint8_t usb_buf[TOTAL_SIZE + ALIGNE_MSK];  //16 bytes aligned!

USBHALHost * USBHALHost::instHost;

USBHALHost::USBHALHost() {
    instHost = this;
    memInit();
    memset((void*)usb_hcca, 0, HCCA_SIZE);
    for (int i = 0; i < MAX_ENDPOINT; i++) {
        edBufAlloc[i] = false;
    }
    for (int i = 0; i < MAX_TD; i++) {
        tdBufAlloc[i] = false;
    }
}

void USBHALHost::init() {
    ohciwrapp_init(&_usbisr, 1);

    ohciwrapp_reg_w(OHCI_REG_CONTROL, 1);       // HARDWARE RESET
    ohciwrapp_reg_w(OHCI_REG_CONTROLHEADED, 0); // Initialize Control list head to Zero
    ohciwrapp_reg_w(OHCI_REG_BULKHEADED, 0);    // Initialize Bulk list head to Zero

    // Wait 100 ms before apply reset
    wait_ms(100);

    // software reset
    ohciwrapp_reg_w(OHCI_REG_COMMANDSTATUS, OR_CMD_STATUS_HCR);

    // Write Fm Interval and Largest Data Packet Counter
    ohciwrapp_reg_w(OHCI_REG_FMINTERVAL, DEFAULT_FMINTERVAL);
    ohciwrapp_reg_w(OHCI_REG_PERIODICSTART,  FI * 90 / 100);

    // Put HC in operational state
    ohciwrapp_reg_w(OHCI_REG_CONTROL, (ohciwrapp_reg_r(OHCI_REG_CONTROL) & (~OR_CONTROL_HCFS)) | OR_CONTROL_HC_OPER);
    // Set Global Power
    ohciwrapp_reg_w(OHCI_REG_RHSTATUS, OR_RH_STATUS_LPSC);

    ohciwrapp_reg_w(OHCI_REG_HCCA, (uint32_t)(usb_hcca));

    // Clear Interrrupt Status
    ohciwrapp_reg_w(OHCI_REG_INTERRUPTSTATUS, ohciwrapp_reg_r(OHCI_REG_INTERRUPTSTATUS));

    ohciwrapp_reg_w(OHCI_REG_INTERRUPTENABLE, OR_INTR_ENABLE_MIE | OR_INTR_ENABLE_WDH | OR_INTR_ENABLE_RHSC);

    // Enable the USB Interrupt
    ohciwrapp_reg_w(OHCI_REG_RHPORTSTATUS1, OR_RH_PORT_CSC);
    ohciwrapp_reg_w(OHCI_REG_RHPORTSTATUS1, OR_RH_PORT_PRSC);

    // Check for any connected devices
    if (ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_CCS) {
        //Device connected
        wait_ms(150);
        USB_DBG("Device connected (%08x)\n\r", ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1));
        deviceConnected(0, 1, ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_LSDA);
    }
}

uint32_t USBHALHost::controlHeadED() {
    return ohciwrapp_reg_r(OHCI_REG_CONTROLHEADED);
}

uint32_t USBHALHost::bulkHeadED() {
    return ohciwrapp_reg_r(OHCI_REG_BULKHEADED);
}

uint32_t USBHALHost::interruptHeadED() {
    return usb_hcca->IntTable[0];
}

void USBHALHost::updateBulkHeadED(uint32_t addr) {
    ohciwrapp_reg_w(OHCI_REG_BULKHEADED, addr);
}


void USBHALHost::updateControlHeadED(uint32_t addr) {
    ohciwrapp_reg_w(OHCI_REG_CONTROLHEADED, addr);
}

void USBHALHost::updateInterruptHeadED(uint32_t addr) {
    usb_hcca->IntTable[0] = addr;
}


void USBHALHost::enableList(ENDPOINT_TYPE type) {
    uint32_t wk_data;

    switch(type) {
        case CONTROL_ENDPOINT:
            ohciwrapp_reg_w(OHCI_REG_COMMANDSTATUS, OR_CMD_STATUS_CLF);
            wk_data = (ohciwrapp_reg_r(OHCI_REG_CONTROL) | OR_CONTROL_CLE);
            ohciwrapp_reg_w(OHCI_REG_CONTROL, wk_data);
            break;
        case ISOCHRONOUS_ENDPOINT:
            break;
        case BULK_ENDPOINT:
            ohciwrapp_reg_w(OHCI_REG_COMMANDSTATUS, OR_CMD_STATUS_BLF);
            wk_data = (ohciwrapp_reg_r(OHCI_REG_CONTROL) | OR_CONTROL_BLE);
            ohciwrapp_reg_w(OHCI_REG_CONTROL, wk_data);
            break;
        case INTERRUPT_ENDPOINT:
            wk_data = (ohciwrapp_reg_r(OHCI_REG_CONTROL) | OR_CONTROL_PLE);
            ohciwrapp_reg_w(OHCI_REG_CONTROL, wk_data);
            break;
    }
}


bool USBHALHost::disableList(ENDPOINT_TYPE type) {
    uint32_t wk_data;

    switch(type) {
        case CONTROL_ENDPOINT:
            wk_data = ohciwrapp_reg_r(OHCI_REG_CONTROL);
            if(wk_data & OR_CONTROL_CLE) {
                wk_data &= ~OR_CONTROL_CLE;
                ohciwrapp_reg_w(OHCI_REG_CONTROL, wk_data);
                return true;
            }
            return false;
        case ISOCHRONOUS_ENDPOINT:
            return false;
        case BULK_ENDPOINT:
            wk_data = ohciwrapp_reg_r(OHCI_REG_CONTROL);
            if(wk_data & OR_CONTROL_BLE) {
                wk_data &= ~OR_CONTROL_BLE;
                ohciwrapp_reg_w(OHCI_REG_CONTROL, wk_data);
                return true;
            }
            return false;
        case INTERRUPT_ENDPOINT:
            wk_data = ohciwrapp_reg_r(OHCI_REG_CONTROL);
            if(wk_data & OR_CONTROL_PLE) {
                wk_data &= ~OR_CONTROL_PLE;
                ohciwrapp_reg_w(OHCI_REG_CONTROL, wk_data);
                return true;
            }
            return false;
    }
    return false;
}


void USBHALHost::memInit() {
    volatile uint8_t *p_wk_buf = (uint8_t *)(((uint32_t)usb_buf + ALIGNE_MSK) & ~ALIGNE_MSK);

    usb_hcca = (volatile HCCA *)p_wk_buf;
    usb_edBuf = (volatile uint8_t *)(p_wk_buf + HCCA_SIZE);
    usb_tdBuf = (volatile uint8_t *)(p_wk_buf + HCCA_SIZE + (MAX_ENDPOINT*ED_SIZE));
}

volatile uint8_t * USBHALHost::getED() {
    for (int i = 0; i < MAX_ENDPOINT; i++) {
        if ( !edBufAlloc[i] ) {
            edBufAlloc[i] = true;
            return (volatile uint8_t *)(usb_edBuf + i*ED_SIZE);
        }
    }
    perror("Could not allocate ED\r\n");
    return NULL; //Could not alloc ED
}

volatile uint8_t * USBHALHost::getTD() {
    int i;
    for (i = 0; i < MAX_TD; i++) {
        if ( !tdBufAlloc[i] ) {
            tdBufAlloc[i] = true;
            return (volatile uint8_t *)(usb_tdBuf + i*TD_SIZE);
        }
    }
    perror("Could not allocate TD\r\n");
    return NULL; //Could not alloc TD
}


void USBHALHost::freeED(volatile uint8_t * ed) {
    int i;
    i = (ed - usb_edBuf) / ED_SIZE;
    edBufAlloc[i] = false;
}

void USBHALHost::freeTD(volatile uint8_t * td) {
    int i;
    i = (td - usb_tdBuf) / TD_SIZE;
    tdBufAlloc[i] = false;
}


void USBHALHost::resetRootHub() {
    // Initiate port reset
    ohciwrapp_reg_w(OHCI_REG_RHPORTSTATUS1, OR_RH_PORT_PRS);

    while (ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_PRS);

    // ...and clear port reset signal
    ohciwrapp_reg_w(OHCI_REG_RHPORTSTATUS1, OR_RH_PORT_PRSC);
}


void USBHALHost::_usbisr(void) {
    if (instHost) {
        instHost->UsbIrqhandler();
    }
}

void USBHALHost::UsbIrqhandler() {
    uint32_t int_status = ohciwrapp_reg_r(OHCI_REG_INTERRUPTSTATUS) & ohciwrapp_reg_r(OHCI_REG_INTERRUPTENABLE);

    if (int_status != 0) { //Is there something to actually process?
        // Root hub status change interrupt
        if (int_status & OR_INTR_STATUS_RHSC) {
            if (ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_CSC) {
                if (ohciwrapp_reg_r(OHCI_REG_RHSTATUS) & OR_RH_STATUS_DRWE) {
                    // When DRWE is on, Connect Status Change
                    // means a remote wakeup event.
                } else {

                    //Root device connected
                    if (ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_CCS) {

                        // wait 150ms to avoid bounce
                        wait_ms(150);

                        //Hub 0 (root hub), Port 1 (count starts at 1), Low or High speed
                        deviceConnected(0, 1, ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_LSDA);
                    }

                    //Root device disconnected
                    else {

                        if (!(int_status & OR_INTR_STATUS_WDH)) {
                            usb_hcca->DoneHead = 0;
                        }

                        // wait 200ms to avoid bounce
                        wait_ms(200);

                        deviceDisconnected(0, 1, NULL, usb_hcca->DoneHead & 0xFFFFFFFE);

                        if (int_status & OR_INTR_STATUS_WDH) {
                            usb_hcca->DoneHead = 0;
                            ohciwrapp_reg_w(OHCI_REG_INTERRUPTSTATUS, OR_INTR_STATUS_WDH);
                        }
                    }
                }
                ohciwrapp_reg_w(OHCI_REG_RHPORTSTATUS1, OR_RH_PORT_CSC);
            }
            if (ohciwrapp_reg_r(OHCI_REG_RHPORTSTATUS1) & OR_RH_PORT_PRSC) {
                ohciwrapp_reg_w(OHCI_REG_RHPORTSTATUS1, OR_RH_PORT_PRSC);
            }
            ohciwrapp_reg_w(OHCI_REG_INTERRUPTSTATUS, OR_INTR_STATUS_RHSC);
        }

        // Writeback Done Head interrupt
        if (int_status & OR_INTR_STATUS_WDH) {
            transferCompleted(usb_hcca->DoneHead & 0xFFFFFFFE);
            ohciwrapp_reg_w(OHCI_REG_INTERRUPTSTATUS, OR_INTR_STATUS_WDH);
        }
    }
}
#endif
