/*
 * Copyright (c) 2019, Arm Limited and affiliates.
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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.
 */

#ifndef MBED_TESTER_H
#define MBED_TESTER_H

#include "DynamicPinList.h"
#include "platform/FileHandle.h"
#include "platform/Callback.h"
#include "drivers/DigitalInOut.h"

/**
 * The base class for controlling the FPGA CI Test Shield
 *
 * This is the primary interface to the FPGA CI Test Shield. It contains
 * all the code to communicate with the FPGA. It also provides high level
 * helper functions, such as functions to setup pin multiplexing, to
 * select the currently active peripheral and to perform software updates.
 *
 * Subclasses can inherit from this class and provide further functionality,
 * such as the ability to test SPI.
 *
 * @note Synchronization level: Not protected
 *
 * Example of how to toggle Arduino pin D6 from the FPGA cI Test Shield:
 * @code
 * #include "mbed.h"
 * #include "MbedTester.h"
 *
 * const PinList *form_factor = pinmap_ff_default_pins();
 * const PinList *restricted = pinmap_restricted_pins();
 * MbedTester tester(form_factor, restricted);
 *
 * int main() {
 *     // Reset the FPGA CI Test Shield to put it into a known state
 *     tester.reset();
 *
 *     // Select the GPIO peripheral
 *     tester.select_peripheral(MbedTester::PeripheralGPIO);
 *
 *     // Map D6 to LogicalPinGPIO0
 *     tester.pin_map_set(D6, MbedTester::LogicalPinGPIO0);
 *
 *     // Toggle pin D6
 *     int toggle = 0;
 *     while (1) {
 *         tester.gpio_write(MbedTester::LogicalPinGPIO0, toggle, true);
 *         wait(0.5);
 *         toggle = !toggle;
 *     }
 * }
 * @endcode
 */
class MbedTester {
public:

    /*
     * This type represents the index of a physical pin on the FPGA.
     * This can be any value from 0 to 127. A form factor is used to
     * map a PinName to a physical pin index.
     */
    typedef uint8_t PhysicalIndex;

    /*
     * There are 8 logical pins connected to the peripherals inside the FPGA.
     * These are logical pins 0 through 7. All peripherals can read from these
     * pins and the currently active peripheral can output on these pins.
     *
     * Each logical pin has a fixed function for a given peripheral which is
     * why the same value is defined multiple times. For example logical pin
     * 2 is SPI clock (SPISclk) when SPI is the active peripheral and
     * UART clear to send (UARTCts) when UART is the active peripheral.
     *
     * A logic pin can be mapped to any physical pin (PinName type) by calling
     * the function MbedTester::pin_map_set.
     */
    enum LogicalPin {

        LogicalPinGPIO0 = 0,
        LogicalPinGPIO1 = 1,
        LogicalPinGPIO2 = 2,
        LogicalPinGPIO3 = 3,
        LogicalPinGPIO4 = 4,
        LogicalPinGPIO5 = 5,
        LogicalPinGPIO6 = 6,
        LogicalPinGPIO7 = 7,

        LogicalPinSPIMosi = 0,
        LogicalPinSPIMiso = 1,
        LogicalPinSPISclk = 2,
        LogicalPinSPISsel = 3,

        LogicalPinIOMetrics0 = 0,
        LogicalPinIOMetrics1 = 1,
        LogicalPinIOMetrics2 = 2,
        LogicalPinIOMetrics3 = 3,
        LogicalPinIOMetrics4 = 4,
        LogicalPinIOMetrics5 = 5,
        LogicalPinIOMetrics6 = 6,
        LogicalPinIOMetrics7 = 7,

        LogicalPinUARTTx = 0,
        LogicalPinUARTRx = 1,
        LogicalPinUARTCts = 2,
        LogicalPinUARTRts = 3,

        LogicalPinI2CSda = 0,
        LogicalPinI2CScl = 1,

        LogicalPinCount = 8,
        LogicalPinBanks = 2,
        LogicalPinTotal = LogicalPinCount * LogicalPinBanks
    };

    /*
     * These are the peripherals internal to the FPGA. A peripheral can be
     * selected by calling MbedTester::select_peripheral.
     */
    enum Peripheral {
        PeripheralGPIO = 1,
        PeripheralSPI = 2,
        PeripheralUART = 4,
        PeripheralI2C = 5,
        PeripheralSPISlave = 6
    };

    /**
     * Construct a new MbedTester object
     *
     * The form factor pins passed into this class must match the
     * physical MbedTester shield connected to this board or testing
     * will not work correctly. The order of pins in this list must
     * match the order of the shield.
     *
     * The exclude pins list should be used to exclude pins which you either
     * don't want to be tested or are not suitable for use as a control
     * channel. This list is allowed to contain pins that are not in the
     * form factor.
     *
     * @param form_factor The pins that are available on this form factor
     * @param exclude_pins The pins that must not be used
     */
    MbedTester(const PinList *form_factor, const PinList *exclude_pins);

    /**
     * Destroy and cleanup resources associated with this object
     */
    ~MbedTester();

    /**
     * Enable automatic selection and update of control pins
     *
     * Calling this function configures MbedTester to automatically select
     * and update the control pins. The control pins are moved if the
     * function MbedTester::pin_map_set is called and maps a pin that is being used
     * for control.
     *
     * @note Automatic selection and update of control pins is the default.
     * Unless MbedTester::set_control_pins_manual has been called to manually
     * set the control pins this function has no effect.
     */
    void set_control_pins_auto();

    /**
     * Set the control pins to use for communication
     *
     * Manually set the control pins. Calling this function
     * disables automatic control pin selection and updates.
     * The function MbedTester::pin_map_set must not be used to map over
     * control pins when in this mode.
     *
     * @param clk Clock pin to use as the control channel
     * @param mosi Mosi pin to use as the control channel
     * @param miso Miso pin to use as the control channel
     * @param aux Auxillary pin to use as the control cannel
     */
    void set_control_pins_manual(PinName clk, PinName mosi, PinName miso, PinName aux);

    /**
     * Read FPGA CI Test Shield firmware
     *
     * Read the firmware on the FPGA CI Test Shield. An optional progress callback
     * can be supplied to display progress while the firmware is being read.
     *
     * @param dest File to write the firmware to. This file must have been opened as writeable
     * @param progress Optional progress callback called when the percent complete changes
     * @return true if firmware was successfully read, false otherwise
     */
    bool firmware_dump(mbed::FileHandle *dest, mbed::Callback<void(uint8_t)> progress = mbed::Callback<void(uint8_t)>());

    /**
     * Read FPGA CI Test Shield flash
     *
     * Read the entire flash contents of the FPGA CI Test Shield. An optional progress callback
     * can be supplied to display progress while the firmware is being read.
     *
     * @param dest File to write the firmware to. This file must have been opened as writeable
     * @param progress Optional progress callback called when the percent complete changes
     * @return true if firmware was successfully read, false otherwise
     */
    bool firmware_dump_all(mbed::FileHandle *dest, mbed::Callback<void(uint8_t)> progress = mbed::Callback<void(uint8_t)>());

    /**
     * Program new FPGA CI Test Shield firmware
     *
     * Program firmware from the file given. The binary bitstream must be in the correct format
     * and contain a correct CRC or the update will fail. To correctly format a bitstream binary
     * the post_process_bitstream.py script in the fpga-ci-test-shield repository should be used.
     *
     * Note - release binaries for the FPGA CI Test Shield have already been formatted and
     *        can be loaded directly with this function
     *
     * @param src File containing the new firmware to program
     * @param progress Optional progress callback called when the percent complete changes
     * @return true if firmware was successfully applied, false otherwise
     */
    bool firmware_update(mbed::FileHandle *src, mbed::Callback<void(uint8_t)> progress = mbed::Callback<void(uint8_t)>());

    /**
     * Map a physical pin to the given logical pin
     *
     * This function will automatically move the control channel
     * pins to avoid interfering with the mapped pin.
     *
     * @param physical Physical pin on the board
     * @param logical Logical pin to map to
     */
    void pin_map_set(PinName physical, LogicalPin logical);

    /**
     * Reset all pin mappings
     *
     * After this call all pins will be unmapped
     */
    void pin_map_reset();

    /**
     * Reset all peripherals
     *
     * This does not reset the pin mappings
     */
    void peripherals_reset();

    /**
     * Reset everything
     *
     * This function resets the state of both the FPGA CI Test Shield
     * and the MbedTester object itself.
     *
     * Reset effects on the FPGA CI Test Shield include:
     * - All pins are tristated
     * - All pin mappings are reset
     * - All pullup/pulldown settings are reset
     * - All peripherals are reset
     *
     * Reset effects on the MbedTester object include
     * - Control channels tristated and freed
     * - Control channel selection set to automatic
     * - Most internal state reinitialized
     */
    void reset();

    /**
     * Reprogram the FPGA
     *
     * This function causes the FPGA to reboot and reload RAM contents.
     * This should be used after MbedTester::firmware_update to load the
     * new image.
     */
    void reprogram();

    /**
     * Get the running FPGA firmware version
     *
     * @return The version of firmware running on the FPGA.
     *
     */
    uint32_t version();

    /**
     * Select the currently active peripheral
     *
     * @param peripheral Active peripheral
     */
    void select_peripheral(Peripheral peripheral);

    /* **GPIO peripheral functions** */

    /**
     * Read a gpio pin
     *
     * @param gpio Logical pin to read from
     * @return 1 if the pin is high 0 if the pin is low
     */
    int gpio_read(LogicalPin gpio);

    /**
     * Set value and drive of a gpio pin
     *
     * @param gpio Logical pin to write to
     * @param value 0 to set the pin low or non-zero to set it high
     * @param driver 0 to set the pin to Hi-Z or non-zero to drive the pin
     */
    void gpio_write(LogicalPin gpio, int value, bool drive);

    /* **IO Metrics functions** */

    /**
     * Start recording metrics on all logical pins
     *
     * This function resets all past metrics to 0. To
     * preserve these call io_metrics_continue instead.
     */
    void io_metrics_start();

    /**
     * Stop recording metrics on all logical pins
     *
     * This function should be called before any metrics
     * are read to ensure the value does not change while
     * they are being read.
     */
    void io_metrics_stop();

    /**
     * Continue recording metrics on all logical pins
     *
     * Resume recording metrics.
     */
    void io_metrics_continue();

    /**
     * Get the shortest low pulse recorded
     *
     * @param pin Pin to read the metrics for
     * @return The shortest number of 100MHz clock cycles the pin was low
     */
    uint32_t io_metrics_min_pulse_low(LogicalPin pin);

    /**
     * Get the shortest high pulse recorded
     *
     * @param pin Pin to read the metrics for
     * @return The shortest number of 100MHz clock cycles the pin was high
     */
    uint32_t io_metrics_min_pulse_high(LogicalPin pin);

    /**
     * Get the longest low pulse recorded
     *
     * @param pin Pin to read the metrics for
     * @return The longest number of 100MHz clock cycles the pin was low
     */
    uint32_t io_metrics_max_pulse_low(LogicalPin pin);

    /**
     * Get the longest high pulse recorded
     *
     * @param pin Pin to read the metrics for
     * @return The longest number of 100MHz clock cycles the pin was high
     */
    uint32_t io_metrics_max_pulse_high(LogicalPin pin);

    /**
     * Get the number of rising edges
     *
     * @param pin Pin to read the metrics for
     * @return The number of rising edges
     */
    uint32_t io_metrics_rising_edges(LogicalPin pin);

    /**
     * Get the number of falling edges
     *
     * @param pin Pin to read the metrics for
     * @return The number of falling edges
     */
    uint32_t io_metrics_falling_edges(LogicalPin pin);

    /**
     * Reset the IO expander modules
     *
     */
    void pin_pull_reset_all();

    /**
     * FPGA Pullup mode
     */
    enum PullMode {
        PullUp,
        PullDown,
        PullNone
    };

    /**
     * Configure an Mbed pin for a pulldown resistor, pullup resistor, or tristate mode via PinName
     *
     * @param pin Mbed pin whose mode is being set
     * @param mode (MbedTester::PullUp, MbedTester::PullDown, or MbedTester::PullNone)
     * @return 0 on success, nonzero on failure
     */
    int pin_set_pull(PinName pin, PullMode mode);

    /**
     * Configure an Mbed pin for a pulldown resistor, pullup resistor, or tristate mode via pin index
     *
     * @param index Mbed pin index whose mode is being set
     * @param mode (MbedTester::PullUp, MbedTester::PullDown, or MbedTester::PullNone)
     * @return 0 on success, nonzero on failure
     */
    int pin_set_pull_index(int index, PullMode mode);

    /*
     * Register of the IO expander
     */
    enum IOExpanderReg {
        RegInput,
        RegOutput,
        RegConfig
    };

    /**
     * Read a bit for a specific Mbed pin that is set in the input, output, or configuration registers inside of the IO expander via PinName
     *
     * @param pin Mbed pin whose register bit is being read
     * @param reg_type Pin register to access, options are: MbedTester::RegInput, MbedTester::RegOutput, or MbedTester::RegConfig
     * @return The value of the bit read
     */
    uint8_t io_expander_read(PinName pin, IOExpanderReg reg_type);

    /**
     * Read a bit for a specific Mbed pin that is set in the input, output, or configuration registers inside of the IO expander via pin index
     *
     * @param index Mbed pin index whose register bit is being read
     * @param reg_type Pin register to access, options are: MbedTester::RegInput, MbedTester::RegOutput, or MbedTester::RegConfig
     * @return The value of the bit read
     */
    uint8_t io_expander_read_index(int index, IOExpanderReg reg_type);

    /**
     * Configure an Mbed pin for a pulldown resistor, pullup resistor, or tristate mode
     * (this version of the function uses io_expander_i2c_read_bb and io_expander_i2c_write_bb)
     *
     * @param pin Mbed pin whose mode is being set
     * @param mode (MbedTester::PullUp, MbedTester::PullDown, or MbedTester::PullNone)
     * @return 0 on success, nonzero on failure
     */
    int pin_set_pull_bb(PinName pin, PullMode mode);

    /**
     * Read a bit for a specific Mbed pin that is set in the input, output, or configuration registers inside of the IO expander
     * (this version of the function uses io_expander_i2c_read_bb)
     *
     * @param pin Mbed pin whose register bit is being read
     * @param reg_type Pin register to access, options are: MbedTester::RegInput, MbedTester::RegOutput, or MbedTester::RegConfig
     * @return The value of the bit read
     */
    uint8_t io_expander_read_bb(PinName pin, IOExpanderReg reg_type);

    /**
     * Create an analog voltage via the FPGA sys pwm in order to test Mbed AnalogIn
     *
     * @param enable Enable the FPGA system PWM (false: of, true: on)
     * @param voltage The analog voltage that will be created by the FPGA CI test shield (float: 0.0 to 1.0)
     */
    void set_analog_out(bool enable, float voltage);

    /**
     * Turn the FPGA ADC on and off (power management data will be collected while the ADC is on)
     *
     * @param val FPGA ADC enable bit (false: off, true: on)
     */
    void set_sample_adc(bool val);

    /**
     * Get the result of the analog to digital conversion computed on the FPGA in the form of a voltage reading. The FPGA ADC operates on 0V-1V, which means this function will only ever return a float ranging from 0.0-1.0.
     *
     * @return The conversion result in the form of a voltage measurement for AnalogMuxIn, Eg. a return value of 0.7 means the ADC on the FPGA read 0.7 volts on its analog input
     */
    float get_analog_in();

    /**
     * Similar to 'get_analog_in' but returns a voltage reading from ANIN0-3
     *
     * @param index ANIN pin to read (0-3)
     * @return The conversion result in the form of a voltage measurement for ANIN0-3
     */
    float get_anin_voltage(int index);

    /* **Mid level system access** */

    /*
     * These are the pins on the FPGA that are not connected to the Mbed board.
     * These can be controlled by using MbedTester::sys_pin_read and MbedTester::sys_pin_write.
     * These should not be used directly when using the FPGA CI Test Shield. The higher layer
     * APIs should be used instead.
     */
    enum SystemPin {
        Reset,
        Reprogram,

        DigitalID0,
        DigitalID1,
        DigitalID2,

        Led0,
        Led1,
        Led2,
        Led3,

        SPIIO0,
        SPIIO1,
        SPIIO2,
        SPIIO3,
        SPIClk,
        SPICS,

        I2CReset,
        I2CSda0,
        I2CSda1,
        I2CSda2,
        I2CScl0,
        I2CScl1,
        I2CScl2,

        AnalogMuxEnable,
        AnalogMuxPwmOut,
        AnalogMuxIn,
        AnalogMuxAddr0,
        AnalogMuxAddr1,
        AnalogMuxAddr2,
        AnalogMuxAddr3,
        AnalogMuxAddr4,
        AnalogMuxAddr5,
        AnalogMuxAddr6,
        AnalogMuxAddr7,

        AnalogInP0,
        AnalogInP1,
        AnalogInP2,
        AnalogInP3,
        AnalogInN0,
        AnalogInN1,
        AnalogInN2,
        AnalogInN3,

        SystemPinCount
    };

    /**
     * Read from the given system pin
     *
     * @param pin The pin to read from
     * @return true if 1 was read, false if 0
     */
    bool sys_pin_read(SystemPin pin);

    /**
     * Write to the given system pin
     *
     * @param pin The pin to write to
     * @param value The value to output on the pin when driven
     * @param true to drive the output, false to set the output high-z
     * @return true if 1 was read, false if 0
     */
    void sys_pin_write(SystemPin pin, int value, bool drive);

    /**
     * I2C read on I2C system channels 0, 1, or 2
     *
     * @param i2c_index The number corresponding to the system i2c bus being used (0, 1, or 2)
     * @param dev_addr The I2C address of the device being read from
     * @param start_reg The internal device address where the read will start
     * @param data Data buffer for data to be read into
     * @param length The number of bytes to read
     * @return 0 on success (ACK), nonzero on failure (NACK)
     */
    int io_expander_i2c_read(uint8_t i2c_index, uint8_t dev_addr, uint8_t start_reg, uint8_t *data, int length);

    /**
     * I2C write on I2C system channels 0, 1, or 2
     *
     * @param i2c_index The number corresponding to the system i2c bus being used (0, 1, or 2)
     * @param dev_addr The I2C address of the device being written to
     * @param data The data to be written
     * @param length The number of bytes to be written
     * @return 0 on success (ACK), nonzero on failure (NACK)
     */
    int io_expander_i2c_write(uint8_t i2c_index, uint8_t dev_addr, uint8_t *data, int length);

    /**
     * I2C read on I2C system channels 0, 1, or 2
     * (bit banged version of function, bit banged over control channel)
     *
     * @param sda System pin used for sda
     * @param scl System pin used for scl
     * @param dev_addr The I2C address of the device being read from
     * @param start_reg The internal device address where the read will start
     * @param data Data buffer for data to be read into
     * @param length The number of bytes to read
     * @return 0 on success (ACK), nonzero on failure (NACK)
     */
    int io_expander_i2c_read_bb(SystemPin sda, SystemPin scl, uint8_t dev_addr, uint8_t start_reg, uint8_t *data, int length);

    /**
     * I2C write on I2C system channels 0, 1, or 2
     * (bit banged version of function, bit banged over control channel)
     *
     * @param sda System pin used for sda
     * @param scl System pin used for scl
     * @param dev_addr The I2C address of the device being written to
     * @param data The data to be written
     * @param length The number of bytes to be written
     * @return 0 on success (ACK), nonzero on failure (NACK)
     */
    int io_expander_i2c_write_bb(SystemPin sda, SystemPin scl, uint8_t dev_addr, uint8_t *data, int length);

    /**
     * Set the AnalogMuxAddr pins on the FPGA via PinName
     *
     * @param pin The Mbed pin that the analog signal will be routed to
     * @return 0 on success, nonzero on failure
     */
    int set_mux_addr(PinName pin);

    /**
     * Set the AnalogMuxAddr pins on the FPGA via pin index
     *
     * @param index The Mbed pin index that the analog signal will be routed to
     * @return 0 on success, nonzero on failure
     */
    int set_mux_addr_index(int index);

    /* **AnalogIn peripheral functions** */

    /**
     * Turn on/off the analog muxes
     *
     * @param val false: off, true: on
     */
    void set_mux_enable(bool val);

    /**
     * Turn on/off pwm output on FPGA to test Mbed AnalogIn
     *
     * @param val false: off, true: on
     */
    void set_pwm_enable(bool val);

    /**
     * Check if FPGA pwm out is on or off
     *
     * @return FPGA enable bit (false: off, true: on)
     */
    bool get_pwm_enable();

    /**
     * Set the pwm output period and number of cycles high (duty cycle) on the FPGA
     *
     * @param period In units of clk cycles
     * @param cycles_high In units of clk cycles
     */
    void set_pwm_period_and_cycles_high(uint32_t period, uint32_t cycles_high);

    /**
     * Get the pwm output period of the FPGA
     *
     * @return FPGA pwm output period in units of clk cycles
     */
    uint32_t get_pwm_period();

    /**
     * Get the number of cycles that are high (duty cycle) from FPGA pwm
     *
     * @return FPGA pwm output cycles high
     */
    uint8_t get_pwm_cycles_high();

    /**
     * Get the 12-bit analog to digital conversion result from the FPGA
     *
     * @return 12-bit FPGA ADC result for AnalogMuxIn
     */
    uint16_t get_analogmuxin_measurement();

    /**
     * Similar to 'get_analogmuxin_measurement' but returns the current XADC measurement for ANIN0-3
     *
     * @param index ANIN pin to read (0-3)
     * @return 12-bit FPGA ADC result for ANIN0-3
     */
    uint16_t get_anin_measurement(int index);

    /**
     * Gets (by reference) the sum of all ANIN ADC results for any of the 4 ANIN pins specified by the index,
     * number of ADC sample sequences that have completed since the XADC was turned on, and the number of FPGA clk cycles
     * that have taken place since the ADC was turned on.
     *
     * @param index ANIN pin of which to get sum of results (0-3)
     * @param sum The sum of all specified ANIN pin's ADC results
     * @param samples The number of ADC sample sequences that have completed since the XADC was turned on
     * @param cycles The number of FPGA clk cycles that have taken place since the ADC was turned on
     */
    void get_anin_sum_samples_cycles(int index, uint64_t *sum, uint32_t *samples, uint64_t *cycles);

    /**
     * Allows safe reading of FPGA ADC related values while the FPGA ADC is on
     * If snapshot is set then the ADC values will be safely latched in the FPGA and safe to read.
     * The RTL will set snapshot to 0 after 1 clk cycle.
     */
    void set_snapshot();

    /**
     * Set the current system pin mode to disabled
     *
     * This releases any pin mappings that were set by the previous pin mode.
     */
    void sys_pin_mode_disabled();

    /**
     * Set the current system pin mode to serial flash
     *
     * Remap physical pins to the serial flash the FPGA boots from.
     * This is used for firmware updates.
     *
     * @param mosi The physical pin index to connect to serial flash mosi
     * @param miso The physical pin index to connect to serial flash miso
     * @param clk The physical pin index to connect to serial flash clk
     * @param ssel The physical pin index to connect to serial flash cs
     */
    void sys_pin_mode_spi_serial_flash(PhysicalIndex mosi, PhysicalIndex miso, PhysicalIndex clk, PhysicalIndex ssel);

    /**
     * Set the current system pin mode to io expander I2C bus
     *
     * Remap physical pins to the io expander I2C bus. The IO expanders
     * are used for setting pullups and pulldowns.
     *
     * @param index The index of the I2C bus to connect to
     * @param sda_in Physical pin index for the FPGA to output the state of SDA on
     * @param sda_val Physical pin index for the FPGA to read SDA from. When in this mode the Mbed board
     *                must always drive this pin. Driving a 0 causes the FPGA to pull the SDA on the I2C bus low. Setting
     *                a 1 causes the FPGA to let SDA on the I2C bus float (and get pulled to 1).
     * @param scl_in Physical pin index for the FPGA to output the state of SCL on
     * @param scl_val Physical pin index for the FPGA to read SCL from. When in this mode the Mbed board
     *                must always drive this pin. Driving a 0 causes the FPGA to pull the SCL on the I2C bus low. Setting
     *                a 1 causes the FPGA to let SDA on the SCL bus float (and get pulled to 1).
     */
    void sys_pin_mode_i2c_io_expander(int index, PhysicalIndex sda_in, PhysicalIndex sda_val, PhysicalIndex scl_in, PhysicalIndex scl_val);

    /**
     * Map a physical pin index to the given logical pin
     *
     * This function will automatically move the control channel
     * pins to avoid interfering with the mapped pin. The physical
     * pin index does not need to be part of the form factor.
     *
     * @param physical_index Index of the physical pin on the board
     * @param logical Logical pin to map to
     */
    void pin_map_index(PhysicalIndex physical_index, LogicalPin logical);


    /* **Low level memory access** */

    /**
     * Write to tester memory
     *
     * @addr addr Address to write to
     * @param data Data to write
     * @param size Number of bytes to write
     */
    void write(uint32_t addr, const uint8_t *data, uint32_t size);

    /**
     * Read from tester memory
     *
     * @addr addr Address to read from
     * @param data Buffer to fill with data
     * @param size Number of bytes to read
     */
    void read(uint32_t addr, uint8_t *data, uint32_t size);

    /* **Self tests** */

    /**
     * Run all self tests
     *
     * @return true if all self tests pass, false otherwise
     */
    bool self_test_all();

    /**
     * Test that all allowed control channels can be used
     *
     * Check that all pairs of clk and mosi which aren't in
     * the restricted list can be used.
     *
     * @note CLK and MOSI lines are paired, where CLK is always
     * on an even index and MOSI is always on an oddd index:
     * clk_index_N = N * 2
     * mosi_index_N = N * 2 + 1
     *
     * @note This functions sets the control pin management mode
     * to automatic when it completes.
     *
     * @return true if all control channel pairs (clk and mosi) of this
     *         configuration can be used, false otherwise
     */
    bool self_test_control_channels();

    /**
     * Test that all allowed control miso lines can be used
     *
     * Check that every pin of this form factor aside from the
     * pins in the restricted list can be used as miso.
     *
     * @note This functions sets the control pin management mode
     * to automatic when it completes.
     *
     * @return true if all control channel miso lines of this
     *         configuration can be used, false otherwise
     */
    bool self_test_control_miso();

    /**
     * Test that the current control channel works
     *
     * @return true if communication is successful, false otherwise
     */
    bool self_test_control_current();

private:

    /*
     * Find suitable control pins
     *
     * This function finds the appropriate set of pins and test to ensure that communication with
     * the FPGA can be performed over them. Before calling this function MbedTester pins must be
     * freed by calling MbedTester::_free_control_pins.
     *
     * @param clk_out Clock pin to find and set
     * @param mosi_out Mosi pin to find and set
     * @param miso_out Miso pin to find and set
     * @param aux_out Aux pin to find and set
     * @return true if all pins were found, false otherwise
     */
    bool _find_control_indexes(PhysicalIndex &clk_out, PhysicalIndex &mosi_out, PhysicalIndex &miso_out, PhysicalIndex &aux_out);

    /*
     * Allocate control pins
     *
     * The pin indexes must already have been found
     */
    void _setup_control_pins();

    /*
     * Free control pins
     *
     * This function is safe to call even if the pins have been freed
     */
    void _free_control_pins();

    /*
     * Update the control channel if needed
     *
     * Open a control channel using allowed pins:
     * - Pin must be in the form factor
     * - Pin must not be in the exclude list
     * - Pin must not be mapped already
     */
    void _update_control_pins();

    /*
     * Check if this control channel needs to be updated
     *
     * @return true if the control channel needs to be updated, false otherwise
     */
    bool _update_needed();

    /*
     * Reset the state of the MbedTester class
     *
     * This does not effect the state of the FPGA in any way.
     */
    void _reset();

    PhysicalIndex _mapping[LogicalPinCount * (LogicalPinBanks + 1)];
    DynamicPinList _form_factor;
    DynamicPinList _exclude_pins;
    bool _control_auto;
    bool _control_valid;
    PhysicalIndex _clk_index;
    PhysicalIndex _mosi_index;
    PhysicalIndex _miso_index;
    PhysicalIndex _aux_index;
    mbed::DigitalInOut *_clk;
    mbed::DigitalInOut *_mosi;
    mbed::DigitalInOut *_miso;
    mbed::DigitalInOut *_aux;
    /*
     * Used to reset IO expander chips only once the first time
     * any IO expander is accessed as well as during an io_exp_test
     */
    int _init_io_exp_rst_flag;
};

#endif
