/* 
 | 
 * Technologic Systems TS-5500 Single Board Computer support 
 | 
 * 
 | 
 * Copyright (C) 2013-2014 Savoir-faire Linux Inc. 
 | 
 *    Vivien Didelot <vivien.didelot@savoirfairelinux.com> 
 | 
 * 
 | 
 * This program is free software; you can redistribute it and/or modify it under 
 | 
 * the terms of the GNU General Public License as published by the Free Software 
 | 
 * Foundation; either version 2 of the License, or (at your option) any later 
 | 
 * version. 
 | 
 * 
 | 
 * 
 | 
 * 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/gpio-ts5500.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); 
 |