/**
 * \file
 *
 * \brief SAM D20 Pin Multiplexer Driver
 *
 * Copyright (C) 2012-2013 Atmel Corporation. All rights reserved.
 *
 * \asf_license_start
 *
 * \page License
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * 3. The name of Atmel may not be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * 4. This software may only be redistributed and used in connection with an
 *    Atmel microcontroller product.
 *
 * THIS SOFTWARE IS PROVIDED BY ATMEL "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE
 * EXPRESSLY AND SPECIFICALLY DISCLAIMED. IN NO EVENT SHALL ATMEL BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * \asf_license_stop
 *
 */
#include <pinmux.h>

/**
 * \internal
 * Writes out a given configuration of a Port pin configuration to the
 * hardware module.
 *
 * \note If the pin direction is set as an output, the pull-up/pull-down input
 *       configuration setting is ignored.
 *
 * \param[in] port      Base of the PORT module to configure.
 * \param[in] pin_mask  Mask of the port pin to configure.
 * \param[in] config    Configuration settings for the pin.
 */
static void _system_pinmux_config(
		PortGroup *const port,
		const uint32_t pin_mask,
		const struct system_pinmux_config *const config)
{
	Assert(port);
	Assert(config);

	/* Track the configuration bits into a temporary variable before writing */
	uint32_t pin_cfg = 0;

	/* Enable the pin peripheral mux flag if non-GPIO selected (pin mux will
	 * be written later) and store the new mux mask */
	if (config->mux_position != SYSTEM_PINMUX_GPIO) {
		pin_cfg |= PORT_WRCONFIG_PMUXEN;
		pin_cfg |= (config->mux_position << PORT_WRCONFIG_PMUX_Pos);
	}

	/* Check if the user has requested that the input buffer be enabled */
	if ((config->direction == SYSTEM_PINMUX_PIN_DIR_INPUT) ||
			(config->direction == SYSTEM_PINMUX_PIN_DIR_OUTPUT_WITH_READBACK)) {
		/* Enable input buffer flag */
		pin_cfg |= PORT_WRCONFIG_INEN;

		/* Enable pull-up/pull-down control flag if requested */
		if (config->input_pull != SYSTEM_PINMUX_PIN_PULL_NONE) {
			pin_cfg |= PORT_WRCONFIG_PULLEN;
		}

		/* Clear the port DIR bits to disable the output buffer */
		port->DIRCLR.reg = pin_mask;
	}

	/* Check if the user has requested that the output buffer be enabled */
	if ((config->direction == SYSTEM_PINMUX_PIN_DIR_OUTPUT) ||
			(config->direction == SYSTEM_PINMUX_PIN_DIR_OUTPUT_WITH_READBACK)) {
		/* Cannot use a pullup if the output driver is enabled,
		 * if requested the input buffer can only sample the current
		 * output state */
		pin_cfg &= ~PORT_WRCONFIG_PULLEN;

		/* Set the port DIR bits to enable the output buffer */
		port->DIRSET.reg = pin_mask;
	}

	/* The Write Configuration register (WRCONFIG) requires the
	 * pins to to grouped into two 16-bit half-words - split them out here */
	uint32_t lower_pin_mask = (pin_mask & 0xFFFF);
	uint32_t upper_pin_mask = (pin_mask >> 16);

	/* Configure the lower 16-bits of the port to the desired configuration,
	 * including the pin peripheral multiplexer just in case it is enabled */
	port->WRCONFIG.reg
		= (lower_pin_mask << PORT_WRCONFIG_PINMASK_Pos) |
			pin_cfg | PORT_WRCONFIG_WRPMUX | PORT_WRCONFIG_WRPINCFG;

	/* Configure the upper 16-bits of the port to the desired configuration,
	 * including the pin peripheral multiplexer just in case it is enabled */
	port->WRCONFIG.reg
		= (upper_pin_mask << PORT_WRCONFIG_PINMASK_Pos) |
			pin_cfg | PORT_WRCONFIG_WRPMUX | PORT_WRCONFIG_WRPINCFG |
			PORT_WRCONFIG_HWSEL;

	/* Set the pull-up state once the port pins are configured if one was
	 * requested and it does not violate the valid set of port
	 * configurations */
	if (pin_cfg & PORT_WRCONFIG_PULLEN) {
		/* Set the OUT register bits to enable the pullup if requested,
		 * clear to enable pull-down */
		if (config->input_pull == SYSTEM_PINMUX_PIN_PULL_UP) {
			port->OUTSET.reg = pin_mask;
		} else {
			port->OUTCLR.reg = pin_mask;
		}
	}
}

/**
 * \brief Writes a Port pin configuration to the hardware module.
 *
 * Writes out a given configuration of a Port pin configuration to the hardware
 * module.
 *
 * \note If the pin direction is set as an output, the pull-up/pull-down input
 *       configuration setting is ignored.
 *
 * \param[in] gpio_pin  Index of the GPIO pin to configure.
 * \param[in] config    Configuration settings for the pin.
 */
void system_pinmux_pin_set_config(
		const uint8_t gpio_pin,
		const struct system_pinmux_config *const config)
{
	PortGroup *const port = system_pinmux_get_group_from_gpio_pin(gpio_pin);
	uint32_t pin_mask = (1UL << (gpio_pin % 32));

	_system_pinmux_config(port, pin_mask, config);
}

/**
 * \brief Writes a Port pin group configuration to the hardware module.
 *
 * Writes out a given configuration of a Port pin group configuration to the
 * hardware module.
 *
 * \note If the pin direction is set as an output, the pull-up/pull-down input
 *       configuration setting is ignored.
 *
 * \param[in] port      Base of the PORT module to configure.
 * \param[in] mask      Mask of the port pin(s) to configure.
 * \param[in] config    Configuration settings for the pin.
 */
void system_pinmux_group_set_config(
		PortGroup *const port,
		const uint32_t mask,
		const struct system_pinmux_config *const config)
{
	Assert(port);

	for (int i = 0; i < 32; i++) {
		if (mask & (1UL << i)) {
			_system_pinmux_config(port, (1UL << i), config);
		}
	}
}

/**
 * \brief Configures the input sampling mode for a group of pins.
 *
 * Configures the input sampling mode for a group of pins, to
 * control when the physical I/O pin value is sampled and
 * stored inside the microcontroller.
 *
 * \param[in] port     Base of the PORT module to configure.
 * \param[in] mask     Mask of the port pin(s) to configure.
 * \param[in] mode     New pin sampling mode to configure.
 */
void system_pinmux_group_set_input_sample_mode(
		PortGroup *const port,
		const uint32_t mask,
		const enum system_pinmux_pin_sample mode)
{
	Assert(port);

	for (int i = 0; i < 32; i++) {
		if (mask & (1UL << i)) {
			uint32_t sample_quad_mask = (1UL << (i / 4));

			if (mode == SYSTEM_PINMUX_PIN_SAMPLE_ONDEMAND) {
				port->CTRL.reg |=  sample_quad_mask;
			}
			else {
				port->CTRL.reg &= ~sample_quad_mask;
			}
		}
	}
}

/**
 * \brief Configures the output driver strength mode for a group of pins.
 *
 * Configures the output drive strength for a group of pins, to
 * control the amount of current the pad is able to sink/source.
 *
 * \param[in] port     Base of the PORT module to configure.
 * \param[in] mask     Mask of the port pin(s) to configure.
 * \param[in] mode     New output driver strength mode to configure.
 */
void system_pinmux_group_set_output_strength(
		PortGroup *const port,
		const uint32_t mask,
		const enum system_pinmux_pin_strength mode)
{
	Assert(port);

	for (int i = 0; i < 32; i++) {
		if (mask & (1UL << i)) {
			if (mode == SYSTEM_PINMUX_PIN_STRENGTH_HIGH) {
				port->PINCFG[i].reg |=  PORT_PINCFG_DRVSTR;
			}
			else {
				port->PINCFG[i].reg &= ~PORT_PINCFG_DRVSTR;
			}
		}
	}
}

/**
 * \brief Configures the output slew rate mode for a group of pins.
 *
 * Configures the output slew rate mode for a group of pins, to
 * control the speed at which the physical output pin can react to
 * logical changes of the I/O pin value.
 *
 * \param[in] port     Base of the PORT module to configure.
 * \param[in] mask     Mask of the port pin(s) to configure.
 * \param[in] mode     New pin slew rate mode to configure.
 */
void system_pinmux_group_set_output_slew_rate(
		PortGroup *const port,
		const uint32_t mask,
		const enum system_pinmux_pin_slew_rate mode)
{
	Assert(port);

	for (int i = 0; i < 32; i++) {
		if (mask & (1UL << i)) {
			if (mode == SYSTEM_PINMUX_PIN_SLEW_RATE_LIMITED) {
				port->PINCFG[i].reg |=  PORT_PINCFG_SLEWLIM;
			}
			else {
				port->PINCFG[i].reg &= ~PORT_PINCFG_SLEWLIM;
			}
		}
	}
}

/**
 * \brief Configures the output driver mode for a group of pins.
 *
 * Configures the output driver mode for a group of pins, to
 * control the pad behavior.
 *
 * \param[in] port     Base of the PORT module to configure.
 * \param[in] mask     Mask of the port pin(s) to configure.
 * \param[in] mode     New pad output driver mode to configure.
 */
void system_pinmux_group_set_output_drive(
		PortGroup *const port,
		const uint32_t mask,
		const enum system_pinmux_pin_drive mode)
{
	Assert(port);

	for (int i = 0; i < 32; i++) {
		if (mask & (1UL << i)) {
			if (mode == SYSTEM_PINMUX_PIN_DRIVE_OPEN_DRAIN) {
				port->PINCFG[i].reg |=  PORT_PINCFG_ODRAIN;
			}
			else {
				port->PINCFG[i].reg &= ~PORT_PINCFG_ODRAIN;
			}
		}
	}
}
