| /* |
| * 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/module.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); |
| |
| MODULE_LICENSE("GPL"); |
| MODULE_AUTHOR("Savoir-faire Linux Inc. <kernel@savoirfairelinux.com>"); |
| MODULE_DESCRIPTION("Technologic Systems TS-5500 platform driver"); |