root/arch/x86/platform/ts5500/ts5500.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Technologic Systems TS-5500 Single Board Computer support
 *
 * Copyright (C) 2013-2014 Savoir-faire Linux Inc.
 *      Vivien Didelot <vivien.didelot@savoirfairelinux.com>
 *
 * This driver registers the Technologic Systems TS-5500 Single Board Computer
 * (SBC) and its devices, and exposes information to userspace such as jumpers'
 * state or available options. For further information about sysfs entries, see
 * Documentation/ABI/testing/sysfs-platform-ts5500.
 *
 * This code may be extended to support similar x86-based platforms.
 * Actually, the TS-5500 and TS-5400 are supported.
 */

#include <linux/delay.h>
#include <linux/io.h>
#include <linux/kernel.h>
#include <linux/leds.h>
#include <linux/init.h>
#include <linux/platform_data/max197.h>
#include <linux/platform_device.h>
#include <linux/slab.h>

/* Product code register */
#define TS5500_PRODUCT_CODE_ADDR        0x74
#define TS5500_PRODUCT_CODE             0x60    /* TS-5500 product code */
#define TS5400_PRODUCT_CODE             0x40    /* TS-5400 product code */

/* SRAM/RS-485/ADC options, and RS-485 RTS/Automatic RS-485 flags register */
#define TS5500_SRAM_RS485_ADC_ADDR      0x75
#define TS5500_SRAM                     BIT(0)  /* SRAM option */
#define TS5500_RS485                    BIT(1)  /* RS-485 option */
#define TS5500_ADC                      BIT(2)  /* A/D converter option */
#define TS5500_RS485_RTS                BIT(6)  /* RTS for RS-485 */
#define TS5500_RS485_AUTO               BIT(7)  /* Automatic RS-485 */

/* External Reset/Industrial Temperature Range options register */
#define TS5500_ERESET_ITR_ADDR          0x76
#define TS5500_ERESET                   BIT(0)  /* External Reset option */
#define TS5500_ITR                      BIT(1)  /* Indust. Temp. Range option */

/* LED/Jumpers register */
#define TS5500_LED_JP_ADDR              0x77
#define TS5500_LED                      BIT(0)  /* LED flag */
#define TS5500_JP1                      BIT(1)  /* Automatic CMOS */
#define TS5500_JP2                      BIT(2)  /* Enable Serial Console */
#define TS5500_JP3                      BIT(3)  /* Write Enable Drive A */
#define TS5500_JP4                      BIT(4)  /* Fast Console (115K baud) */
#define TS5500_JP5                      BIT(5)  /* User Jumper */
#define TS5500_JP6                      BIT(6)  /* Console on COM1 (req. JP2) */
#define TS5500_JP7                      BIT(7)  /* Undocumented (Unused) */

/* A/D Converter registers */
#define TS5500_ADC_CONV_BUSY_ADDR       0x195   /* Conversion state register */
#define TS5500_ADC_CONV_BUSY            BIT(0)
#define TS5500_ADC_CONV_INIT_LSB_ADDR   0x196   /* Start conv. / LSB register */
#define TS5500_ADC_CONV_MSB_ADDR        0x197   /* MSB register */
#define TS5500_ADC_CONV_DELAY           12      /* usec */

/**
 * struct ts5500_sbc - TS-5500 board description
 * @name:       Board model name.
 * @id:         Board product ID.
 * @sram:       Flag for SRAM option.
 * @rs485:      Flag for RS-485 option.
 * @adc:        Flag for Analog/Digital converter option.
 * @ereset:     Flag for External Reset option.
 * @itr:        Flag for Industrial Temperature Range option.
 * @jumpers:    Bitfield for jumpers' state.
 */
struct ts5500_sbc {
        const char *name;
        int     id;
        bool    sram;
        bool    rs485;
        bool    adc;
        bool    ereset;
        bool    itr;
        u8      jumpers;
};

/* Board signatures in BIOS shadow RAM */
static const struct {
        const char * const string;
        const ssize_t offset;
} ts5500_signatures[] __initconst = {
        { "TS-5x00 AMD Elan", 0xb14 },
};

static int __init ts5500_check_signature(void)
{
        void __iomem *bios;
        int i, ret = -ENODEV;

        bios = ioremap(0xf0000, 0x10000);
        if (!bios)
                return -ENOMEM;

        for (i = 0; i < ARRAY_SIZE(ts5500_signatures); i++) {
                if (check_signature(bios + ts5500_signatures[i].offset,
                                    ts5500_signatures[i].string,
                                    strlen(ts5500_signatures[i].string))) {
                        ret = 0;
                        break;
                }
        }

        iounmap(bios);
        return ret;
}

static int __init ts5500_detect_config(struct ts5500_sbc *sbc)
{
        u8 tmp;
        int ret = 0;

        if (!request_region(TS5500_PRODUCT_CODE_ADDR, 4, "ts5500"))
                return -EBUSY;

        sbc->id = inb(TS5500_PRODUCT_CODE_ADDR);
        if (sbc->id == TS5500_PRODUCT_CODE) {
                sbc->name = "TS-5500";
        } else if (sbc->id == TS5400_PRODUCT_CODE) {
                sbc->name = "TS-5400";
        } else {
                pr_err("ts5500: unknown product code 0x%x\n", sbc->id);
                ret = -ENODEV;
                goto cleanup;
        }

        tmp = inb(TS5500_SRAM_RS485_ADC_ADDR);
        sbc->sram = tmp & TS5500_SRAM;
        sbc->rs485 = tmp & TS5500_RS485;
        sbc->adc = tmp & TS5500_ADC;

        tmp = inb(TS5500_ERESET_ITR_ADDR);
        sbc->ereset = tmp & TS5500_ERESET;
        sbc->itr = tmp & TS5500_ITR;

        tmp = inb(TS5500_LED_JP_ADDR);
        sbc->jumpers = tmp & ~TS5500_LED;

cleanup:
        release_region(TS5500_PRODUCT_CODE_ADDR, 4);
        return ret;
}

static ssize_t name_show(struct device *dev, struct device_attribute *attr,
                char *buf)
{
        struct ts5500_sbc *sbc = dev_get_drvdata(dev);

        return sprintf(buf, "%s\n", sbc->name);
}
static DEVICE_ATTR_RO(name);

static ssize_t id_show(struct device *dev, struct device_attribute *attr,
                char *buf)
{
        struct ts5500_sbc *sbc = dev_get_drvdata(dev);

        return sprintf(buf, "0x%.2x\n", sbc->id);
}
static DEVICE_ATTR_RO(id);

static ssize_t jumpers_show(struct device *dev, struct device_attribute *attr,
                char *buf)
{
        struct ts5500_sbc *sbc = dev_get_drvdata(dev);

        return sprintf(buf, "0x%.2x\n", sbc->jumpers >> 1);
}
static DEVICE_ATTR_RO(jumpers);

#define TS5500_ATTR_BOOL(_field)                                        \
        static ssize_t _field##_show(struct device *dev,                \
                        struct device_attribute *attr, char *buf)       \
        {                                                               \
                struct ts5500_sbc *sbc = dev_get_drvdata(dev);          \
                                                                        \
                return sprintf(buf, "%d\n", sbc->_field);               \
        }                                                               \
        static DEVICE_ATTR_RO(_field)

TS5500_ATTR_BOOL(sram);
TS5500_ATTR_BOOL(rs485);
TS5500_ATTR_BOOL(adc);
TS5500_ATTR_BOOL(ereset);
TS5500_ATTR_BOOL(itr);

static struct attribute *ts5500_attributes[] = {
        &dev_attr_id.attr,
        &dev_attr_name.attr,
        &dev_attr_jumpers.attr,
        &dev_attr_sram.attr,
        &dev_attr_rs485.attr,
        &dev_attr_adc.attr,
        &dev_attr_ereset.attr,
        &dev_attr_itr.attr,
        NULL
};

static const struct attribute_group ts5500_attr_group = {
        .attrs = ts5500_attributes,
};

static struct resource ts5500_dio1_resource[] = {
        DEFINE_RES_IRQ_NAMED(7, "DIO1 interrupt"),
};

static struct platform_device ts5500_dio1_pdev = {
        .name = "ts5500-dio1",
        .id = -1,
        .resource = ts5500_dio1_resource,
        .num_resources = 1,
};

static struct resource ts5500_dio2_resource[] = {
        DEFINE_RES_IRQ_NAMED(6, "DIO2 interrupt"),
};

static struct platform_device ts5500_dio2_pdev = {
        .name = "ts5500-dio2",
        .id = -1,
        .resource = ts5500_dio2_resource,
        .num_resources = 1,
};

static void ts5500_led_set(struct led_classdev *led_cdev,
                           enum led_brightness brightness)
{
        outb(!!brightness, TS5500_LED_JP_ADDR);
}

static enum led_brightness ts5500_led_get(struct led_classdev *led_cdev)
{
        return (inb(TS5500_LED_JP_ADDR) & TS5500_LED) ? LED_FULL : LED_OFF;
}

static struct led_classdev ts5500_led_cdev = {
        .name = "ts5500:green:",
        .brightness_set = ts5500_led_set,
        .brightness_get = ts5500_led_get,
};

static int ts5500_adc_convert(u8 ctrl)
{
        u8 lsb, msb;

        /* Start conversion (ensure the 3 MSB are set to 0) */
        outb(ctrl & 0x1f, TS5500_ADC_CONV_INIT_LSB_ADDR);

        /*
         * The platform has CPLD logic driving the A/D converter.
         * The conversion must complete within 11 microseconds,
         * otherwise we have to re-initiate a conversion.
         */
        udelay(TS5500_ADC_CONV_DELAY);
        if (inb(TS5500_ADC_CONV_BUSY_ADDR) & TS5500_ADC_CONV_BUSY)
                return -EBUSY;

        /* Read the raw data */
        lsb = inb(TS5500_ADC_CONV_INIT_LSB_ADDR);
        msb = inb(TS5500_ADC_CONV_MSB_ADDR);

        return (msb << 8) | lsb;
}

static struct max197_platform_data ts5500_adc_pdata = {
        .convert = ts5500_adc_convert,
};

static struct platform_device ts5500_adc_pdev = {
        .name = "max197",
        .id = -1,
        .dev = {
                .platform_data = &ts5500_adc_pdata,
        },
};

static int __init ts5500_init(void)
{
        struct platform_device *pdev;
        struct ts5500_sbc *sbc;
        int err;

        /*
         * There is no DMI available or PCI bridge subvendor info,
         * only the BIOS provides a 16-bit identification call.
         * It is safer to find a signature in the BIOS shadow RAM.
         */
        err = ts5500_check_signature();
        if (err)
                return err;

        pdev = platform_device_register_simple("ts5500", -1, NULL, 0);
        if (IS_ERR(pdev))
                return PTR_ERR(pdev);

        sbc = devm_kzalloc(&pdev->dev, sizeof(struct ts5500_sbc), GFP_KERNEL);
        if (!sbc) {
                err = -ENOMEM;
                goto error;
        }

        err = ts5500_detect_config(sbc);
        if (err)
                goto error;

        platform_set_drvdata(pdev, sbc);

        err = sysfs_create_group(&pdev->dev.kobj, &ts5500_attr_group);
        if (err)
                goto error;

        if (sbc->id == TS5500_PRODUCT_CODE) {
                ts5500_dio1_pdev.dev.parent = &pdev->dev;
                if (platform_device_register(&ts5500_dio1_pdev))
                        dev_warn(&pdev->dev, "DIO1 block registration failed\n");
                ts5500_dio2_pdev.dev.parent = &pdev->dev;
                if (platform_device_register(&ts5500_dio2_pdev))
                        dev_warn(&pdev->dev, "DIO2 block registration failed\n");
        }

        if (led_classdev_register(&pdev->dev, &ts5500_led_cdev))
                dev_warn(&pdev->dev, "LED registration failed\n");

        if (sbc->adc) {
                ts5500_adc_pdev.dev.parent = &pdev->dev;
                if (platform_device_register(&ts5500_adc_pdev))
                        dev_warn(&pdev->dev, "ADC registration failed\n");
        }

        return 0;
error:
        platform_device_unregister(pdev);
        return err;
}
device_initcall(ts5500_init);