/* mbed Microcontroller Library
 * (C)Copyright TOSHIBA ELECTRONIC DEVICES & STORAGE CORPORATION 2018 All rights reserved
 *
 * 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.
 */
#include "pwmout_api.h"
#include "pinmap.h"
#include "gpio_include.h"

static const PinMap PinMap_PWM[] = {
    {PB0, PWM_0, PIN_DATA(5, 1)},
    {PC0, PWM_1, PIN_DATA(4, 1)},
    {PJ0, PWM_2, PIN_DATA(4, 1)},
    {PK2, PWM_3, PIN_DATA(4, 1)},
    {PN0, PWM_4, PIN_DATA(4, 1)},
    {PL5, PWM_5, PIN_DATA(4, 1)},
    {PG2, PWM_6, PIN_DATA(4, 1)},
    {NC,  NC,    0}
};

static const uint32_t prescale_tbl[] = {
    2, 8, 32, 128, 256, 512, 1024
};

void pwmout_init(pwmout_t* obj, PinName pin)
{
    uint16_t counter = 0;

    PWMName pwm = (PWMName)pinmap_peripheral(pin, PinMap_PWM);  // Determine the pwm channel
    MBED_ASSERT(pwm != (PWMName)NC);

    switch (pwm) {
        case PWM_0:
            obj->channel = TSB_T32A1;
            TSB_CG_FSYSENA_IPENA25 = ENABLE;
            TSB_CG_FSYSENA_IPENA01 = ENABLE;
            break;
        case PWM_1:
            obj->channel = TSB_T32A2;
            TSB_CG_FSYSENA_IPENA26 = ENABLE;
            TSB_CG_FSYSENA_IPENA02 = ENABLE;
            break;
        case PWM_2:
            obj->channel = TSB_T32A3;
            TSB_CG_FSYSENA_IPENA27 = ENABLE;
            TSB_CG_FSYSENA_IPENA08 = ENABLE;
            break;
        case PWM_3:
            obj->channel = TSB_T32A4;
            TSB_CG_FSYSENA_IPENA28 = ENABLE;
            TSB_CG_FSYSENA_IPENA09 = ENABLE;
            break;
        case PWM_4:
            obj->channel = TSB_T32A5;
            TSB_CG_FSYSENA_IPENA29 = ENABLE;
            TSB_CG_FSYSENA_IPENA12 = ENABLE;
            break;
        case PWM_5:
            obj->channel = TSB_T32A6;
            TSB_CG_FSYSENA_IPENA30 = ENABLE;
            TSB_CG_FSYSENA_IPENA10 = ENABLE;
            break;
        case PWM_6:
            obj->channel = TSB_T32A7;
            TSB_CG_FSYSENA_IPENA31 = ENABLE;
            TSB_CG_FSYSENA_IPENA06 = ENABLE;
            break;
        default:
            obj->channel = NULL;
            break;
    }

    pinmap_pinout(pin, PinMap_PWM);  // Set pin function as PWM
    obj->pin              = pin;
    obj->period           = DEFAULT_PERIOD;
    obj->divisor          = DEFAULT_CLOCK_DIVISION;
    obj->channel->MOD     = (T32A_MODE_32 | T32A_DBG_HALT_STOP);
    obj->channel->RUNC    = (T32A_COUNT_STOP | T32A_COUNT_DONT_START | T32A_RUN_DISABLE);
    obj->channel->CRC     = (T32A_PRSCLx_32 | T32A_WBF_ENABLE | T32A_RELOAD_TREGx);
    obj->channel->OUTCRC0 = T32A_OCR_DISABLE;
    obj->channel->OUTCRC1 = (T32A_OCRCMPx0_SET | T32A_OCRCMPx1_CLR);

    counter = ((DEFAULT_PERIOD * (SystemCoreClock)) / obj->divisor);
    obj->channel->RGC0    = counter;
    obj->channel->RGC1    = counter;
    obj->trailing_timing  = counter;
    obj->leading_timing   = counter;
}

void pwmout_free(pwmout_t* obj)
{
    // Stops and clear count operation
    obj->channel->RUNC   = (T32A_RUN_DISABLE | T32A_COUNT_STOP);
    pwmout_write(obj, 0);
    obj->pin             = NC;
    obj->channel         = NULL;
    obj->trailing_timing = 0;
    obj->leading_timing  = 0;
    obj->divisor         = 0;
}

void pwmout_write(pwmout_t* obj, float value)
{
    obj->channel->RUNC  = (T32A_RUN_DISABLE | T32A_COUNT_STOP);  // Stop timer for setting clock again
    obj->leading_timing = (obj->trailing_timing -
                           (obj->trailing_timing * value));      // leading_timing value
    obj->channel->RGC0  = obj->leading_timing;                   // Setting TBxRG0 register
    obj->channel->RUNC  = (T32A_RUN_ENABLE | T32A_COUNT_START);  // Start count operation
}

float pwmout_read(pwmout_t* obj)
{
    float duty_cycle = ((float)(obj->trailing_timing - obj->leading_timing)
                        / obj->trailing_timing);
    return duty_cycle;
}

void pwmout_period(pwmout_t* obj, float seconds)
{
    pwmout_period_us(obj, (int)(seconds * 1000000.0f));
}

void pwmout_period_ms(pwmout_t* obj, int ms)
{
    pwmout_period_us(obj, ms * 1000);
}

void pwmout_period_us(pwmout_t* obj, int us)
{
    float    seconds    = 0;
    int      cycles     = 0;
    uint32_t clkdiv     = 0;
    int      i          = 0;
    float    duty_cycle = 0;

    seconds = (float)(us / 1000000.0f);

    // Select highest timer resolution
    for (i = 0; i < 7; ++i) {
        cycles = (int)(((SystemCoreClock) / prescale_tbl[i]) * seconds);
        if (cycles <= MAX_COUNTER_16B) {
            clkdiv =   i + 1; // range 1:7
            clkdiv <<= 28;
            break;
        } else {
            cycles =   MAX_COUNTER_16B;
            clkdiv =   7;
            clkdiv <<= 28;
        }
    }

    // Stop timer for setting clock again
    obj->channel->RUNC = (T32A_RUN_DISABLE | T32A_COUNT_STOP);
    // Restore the duty-cycle
    duty_cycle = ((float)(obj->trailing_timing - obj->leading_timing)
                  / obj->trailing_timing);

    obj->trailing_timing  = cycles;
    obj->leading_timing   = (cycles - (cycles * duty_cycle));
    // Change the source clock division and period
    obj->channel->MOD     = T32A_MODE_32;
    obj->channel->CRC     = (clkdiv | T32A_WBF_ENABLE | T32A_RELOAD_TREGx);
    obj->channel->OUTCRC0 = T32A_OCR_DISABLE;
    obj->channel->OUTCRC1 = (T32A_OCRCMPx0_SET | T32A_OCRCMPx1_CLR);
    obj->channel->RGC0    = obj->leading_timing;
    obj->channel->RGC1    = obj->trailing_timing;
    obj->channel->RUNC    = (T32A_RUN_ENABLE | T32A_COUNT_START);  // Start count operation
}

void pwmout_pulsewidth(pwmout_t* obj, float seconds)
{
    pwmout_pulsewidth_us(obj, seconds * 1000000.0f);
}

void pwmout_pulsewidth_ms(pwmout_t* obj, int ms)
{
    pwmout_pulsewidth_us(obj, ms * 1000);
}

void pwmout_pulsewidth_us(pwmout_t* obj, int us)
{
    float seconds = 0;
    float value   = 0;

    seconds = (float)(us / 1000000.0f);
    value = (((seconds / obj->period) * 100.0f) / 100.0f);

    pwmout_write(obj, value);
}
