/** @file ksamplefeedbackloop.c
 *
 * @brief Kernel module for ADDI-DATA XPCI-3XXX analog I/O feedback loop with signal processing
 *
 * This module implements a high-performance feedback loop that reads analog input
 * values from XPCI-3XXX PCI cards, optionally applies a user-defined transformation
 * function, and writes the results to analog outputs. It uses the board's auto-refresh
 * feature for efficient input sampling combined with a high-resolution timer (hrtimer)
 * for precise periodic updates, avoiding the scheduling latency issues of traditional
 * kernel threads.
 *
 * @section Configuration
 * - @b NUMBER_OF_CHANNELS: Number of analog input channels to use (default: 4)
 * - @b FIRST_CHANNEL: First channel index to start from (default: 0)
 * - @b INPUT_POLARITY: Input signal polarity (ADDIDATA_BIPOLAR or ADDIDATA_UNIPOLAR)
 * - @b INPUT_GAIN: Input gain setting (ADDIDATA_1_GAIN, ADDIDATA_2_GAIN, etc.)
 * - @b INPUT_CONVERT_TIME_US: Conversion time per sample in microseconds (default: 25)
 * - @b FEEDBACK_FN: Macro to transform input values before output (default: identity function)
 * - @b FEEDBACK_PERIOD_NS: Timer period in nanoseconds (default: 100000 = 100μs = 10kHz)
 *
 * @par Signal Processing
 * The FEEDBACK_FN macro allows real-time signal processing.
 * @note The Analog input/output 16-bit signal represents, in most 3XXX cards, a voltage
 *       range of -10V to +10V, where 0V is encoded as 0x7FFF (32767). Account for this offset
 *       in your transformations.
 *
 * Example transformations (for bipolar ±10V range):
 * @code
 *   // Identity (pass-through)
 *   #define FEEDBACK_FN(X) (X)
 *
 *   // Invert signal around 0V
 *   #define FEEDBACK_FN(X) (0x7FFE - (X))
 *
 *   // Scale signal by 2x (amplify)
 *   #define FEEDBACK_FN(X) (((uint32_t)(X) - 0x7FFF) * 2 + 0x7FFF)
 * @endcode
 * @warning FEEDBACK_FN must execute quickly and deterministically to maintain
 *          consistent timing. Complex operations may introduce latency.
 * @note The above FEEDBACK_FN are voluntary simple, and don't do any boundary checks.
 *       Warping is expected if input voltage is above certain values.
 *
 * @note The actual number of usable channels depends on your XPCI-3XXX card model.
 *       The module automatically detects the number of available output channels
 *       and only writes to existing outputs. Channel mapping starts from FIRST_CHANNEL.
 * @note INPUT_CONVERT_TIME_US * NUMBER_OF_CHANNELS must be less than FEEDBACK_PERIOD_NS/1000
 *       (enforced at compile time).
 * @note For best timing precision in real-time applications, consider using a
 *       PREMPT_RT patched kernel (https://www.kernel.org/doc/html/next/core-api/real-time/theory.html).
 *
 * @section Usage
 * 1. Configure the desired parameters at the top of this file
 * 2. Build the module from the parent directory: @code make -f _makefile @endcode
 * 3. Load the module: @code insmod ksamplefeedbackloop.ko @endcode
 * 4. Verify operation: @code dmesg | grep ksamplefeedbackloop @endcode
 * 5. Unload the module: @code rmmod ksamplefeedbackloop @endcode
 *
 * @section Requirements
 * - Linux kernel ≥ 2.6.16 (hrtimer support)
 * - ADDI-DATA XPCI-3XXX series PCI card with analog I/O
 * - xpci3xxx kernel API drivers installed
 *
 * @author ADDI-DATA GmbH
 * @license GPL
 */

#include <linux/version.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,19)
    #include <linux/config.h>
#else
    #if LINUX_VERSION_CODE < KERNEL_VERSION(2,6,33)
        #include <linux/autoconf.h>
    #else
        #include <generated/autoconf.h>
    #endif
#endif
#include <linux/spinlock.h>
#include <linux/ioctl.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/pci.h>
#include <asm/io.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(3,4,0)
    #include <asm/system.h>
#endif
#include <asm/uaccess.h>
#include <asm/bitops.h>
#include <linux/sched.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>

#include <xpci3xxx.h>
#include <xpci3xxx-kapi.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ADDI-DATA");
MODULE_DESCRIPTION("XPCI-3XXX Analog input to output feedback loop with hrtimer");

#if LINUX_VERSION_CODE < KERNEL_VERSION(5,12,0)
MODULE_SUPPORTED_DEVICE("XPCI-3XXX");
#endif

EXPORT_NO_SYMBOLS;

#define MODULE_NAME "ksamplefeedbackloop"
#define NUMBER_OF_BOARDS     1
// NUMBER_OF_CHANNELS should be set to the number of inputs to use
#define NUMBER_OF_CHANNELS   4
#define FIRST_CHANNEL   0
#define INPUT_POLARITY ADDIDATA_BIPOLAR
#define INPUT_GAIN ADDIDATA_1_GAIN
#define INPUT_CONVERT_TIME_US 25

// Define FEEDBACK_FN to apply a correction on the input
#define FEEDBACK_FN(X) (X)

static struct pci_dev *_pdev[NUMBER_OF_BOARDS];
static struct hrtimer feedback_timer;
static struct pci_dev *timer_pdev;
static uint32_t prevCycleIndex = 1;
static uint8_t g_numberOfOutputs = 0;
#define FEEDBACK_PERIOD_NS  100000  // 100us period (10kHz)

#if (FEEDBACK_PERIOD_NS/1000 < (INPUT_CONVERT_TIME_US * NUMBER_OF_CHANNELS))
#error INPUT_CONVERT_TIME_US * NUMBER_OF_CHANNELS Should be inferior to timer period to work properly. \
    Either reduce the NUMBER_OF_CHANNELS/INPUT_CONVERT_TIME_US \
    or slow down timer by augmenting INPUT_CONVERT_TIME_US.
#endif
/* Timer callback */
static enum hrtimer_restart feedback_timer_callback(struct hrtimer *timer)
{
    unsigned long irqstate = 0;
    uint32_t val[ADDIDATA_MAX_AI] = {0};
    uint32_t cycleIndex = 0;
    int ch = 0;
    int error = 0;
    uint8_t numberOfOutputs = g_numberOfOutputs;

    xpci3xxx_lock(timer_pdev, &irqstate);
    // This function always read values for all channels
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,1,0)
    i_xpci3xxx_FastReadAnalogInputAutoRefreshValueAndCounter(timer_pdev, ADDIDATA_MAX_AI, val, &cycleIndex);
#else
    i_xpci3xxx_ReadAnalogInputAutoRefreshValueAndCounter(timer_pdev, val, &cycleIndex);
#endif
    xpci3xxx_unlock(timer_pdev, irqstate);

    // This loop will simply replicate the value read to the corresponding output.
    // Depending on the 3XXX card, the loop might not go through all inputs or all outputs,
    // Since some cards only have 16, 8, 4 or no output at all.
    // Number of outputs can be checked with i_xpci3xxx_GetNumberOfAnalogOutputChannels
    // The loop start with the first read analog input, which is set by preproc FIRST_CHANNEL
    if (cycleIndex != prevCycleIndex) {
        for (ch = FIRST_CHANNEL; ch < (NUMBER_OF_CHANNELS + FIRST_CHANNEL); ch++) {
            if (ch < numberOfOutputs) {
                xpci3xxx_lock(timer_pdev, &irqstate);
                // Example of applying a feedback function (we can reuse val[ch], since it's a local variable)
                // IMPORTANT : the function should be quick enough, with a known and consistant processing time,
                // To avoid output delay
                val[ch] = FEEDBACK_FN(val[ch]);
                error = i_xpci3xxx_WriteAnalogOutputValue(timer_pdev, ch, val[ch]);
                xpci3xxx_unlock(timer_pdev, irqstate);
                if (error != 0)
                    printk(KERN_ERR "Error writing to output channel %d: %d\n", ch, error);
            }
        }
        prevCycleIndex = cycleIndex;
    }

    hrtimer_forward_now(timer, ns_to_ktime(FEEDBACK_PERIOD_NS));
    return HRTIMER_RESTART;
}

static int ksamplefeedbackloop_init_board(struct pci_dev *pdev)
{
    int error = 0;
    unsigned long irqstate;
    int i = 0;
    uint8_t pb_ChannelList[NUMBER_OF_CHANNELS];
    uint8_t pb_Gain[NUMBER_OF_CHANNELS];
    uint8_t pb_Polarity[NUMBER_OF_CHANNELS];
    int ch = 0;
    uint8_t numberOfOutputs = 0;

    i_xpci3xxx_GetNumberOfAnalogOutputChannels(pdev, &numberOfOutputs);
    g_numberOfOutputs = numberOfOutputs;

    for (i = 0; i < NUMBER_OF_CHANNELS; i++) {
        pb_ChannelList[i] = i + FIRST_CHANNEL;
        pb_Polarity[i] = INPUT_POLARITY;
        pb_Gain[i] = INPUT_GAIN;
    }

    printk(KERN_INFO "Initializing analog input autorefresh...\n");

    xpci3xxx_lock(pdev, &irqstate);
    error = i_xpci3xxx_InitAnalogInputAutoRefresh(pdev,
            NUMBER_OF_CHANNELS, pb_ChannelList, pb_Gain, pb_Polarity,
            ADDIDATA_DISABLE, 0, ADDIDATA_DISABLE, 0, 0);
    xpci3xxx_unlock(pdev, irqstate);

    if (error) {
        printk(KERN_ERR "i_xpci3xxx_InitAnalogInputAutoRefresh failed: %d\n", error);
        return -1;
    }

    xpci3xxx_lock(pdev, &irqstate);
    error = i_xpci3xxx_StartAnalogInputAutoRefresh(pdev, ADDIDATA_MICRO_SECOND, INPUT_CONVERT_TIME_US);
    xpci3xxx_unlock(pdev, irqstate);

    if (error) {
        printk(KERN_ERR "i_xpci3xxx_StartAnalogInputAutoRefresh failed: %d\n", error);
        return -1;
    }

    printk(KERN_INFO "Initializing analog output...\n");
    for (ch = 0; ch < NUMBER_OF_CHANNELS && ch < numberOfOutputs; ch++) {
        xpci3xxx_lock(pdev, &irqstate);
        error = i_xpci3xxx_InitAnalogOutput(pdev, ch, INPUT_POLARITY);
        xpci3xxx_unlock(pdev, irqstate);
        if (error != 0) {
            printk(KERN_ERR "i_xpci3xxx_InitAnalogOutput channel %d failed: %d\n", ch, error);
            return -1;
        }
    }

    /* Initialize and start hrtimer using modern API */
    timer_pdev = pdev;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,1,0)
    hrtimer_setup(&feedback_timer, feedback_timer_callback, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
#else
    hrtimer_init(&feedback_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    feedback_timer.function = feedback_timer_callback;
#endif
    hrtimer_start(&feedback_timer, ns_to_ktime(FEEDBACK_PERIOD_NS), HRTIMER_MODE_REL);

    printk(KERN_INFO
           "ksamplefeedbackloop: board=%s, channels=%d, first_ch=%d, "
           "polarity=%s, gain=%s, convert_time=%dus, period=%dns, "
           "outputs=%d\n",
           pci_name(pdev),
           NUMBER_OF_CHANNELS, FIRST_CHANNEL,
           INPUT_POLARITY == ADDIDATA_BIPOLAR ? "BIPOLAR" : "UNIPOLAR",
           INPUT_GAIN == ADDIDATA_1_GAIN ? "1x" :
           INPUT_GAIN == ADDIDATA_2_GAIN ? "2x" :
           INPUT_GAIN == ADDIDATA_5_GAIN ? "5x" :
           INPUT_GAIN == ADDIDATA_10_GAIN ? "10x" : "unknown",
           INPUT_CONVERT_TIME_US, FEEDBACK_PERIOD_NS,
           numberOfOutputs);

    printk(KERN_INFO "ksamplefeedbackloop: Feedback loop started using hrtimer (period: %d ns)\n", FEEDBACK_PERIOD_NS);
    return 0;
}

static void ksamplefeedbackloop_release(struct pci_dev *pdev)
{
    int error = 0;
    unsigned long irqstate;

    if (timer_pdev == pdev) {
        hrtimer_cancel(&feedback_timer);
        timer_pdev = NULL;
    }

    xpci3xxx_lock(pdev, &irqstate);
    error = i_xpci3xxx_StopAnalogInputAutoRefresh(pdev);
    xpci3xxx_unlock(pdev, irqstate);

    xpci3xxx_lock(pdev, &irqstate);
    error = i_xpci3xxx_ReleaseAnalogInputAutoRefresh(pdev);
    xpci3xxx_unlock(pdev, irqstate);
    printk(KERN_INFO "ksamplefeedbackloop: Stopped\n");
}

static int __init _init_module(void)
{
    int index = 0;
    for (index = 0; index < NUMBER_OF_BOARDS; index++) {
        if ((_pdev[index] = xpci3xxx_lookup_board_by_index(index)) == NULL) {
            printk(KERN_DEBUG "Board index %i not found\n", index);
            return -ENODEV;
        }
        printk(KERN_DEBUG "Board index %i found (slot: %s)\n", index, pci_name(_pdev[index]));
    }

    for (index = 0; index < NUMBER_OF_BOARDS; index++) {
        if (ksamplefeedbackloop_init_board(_pdev[index])) {
            return -EIO;
        }
    }
    return 0;
}

static void __exit _cleanup_module(void)
{
    int index = 0;
    for (index = 0; index < NUMBER_OF_BOARDS; index++)
        ksamplefeedbackloop_release(_pdev[index]);
}

module_init(_init_module);
module_exit(_cleanup_module);