Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 1 | /* |
| 2 | * Technologic Systems TS-5500 Single Board Computer support |
| 3 | * |
| 4 | * Copyright (C) 2013 Savoir-faire Linux Inc. |
| 5 | * Vivien Didelot <vivien.didelot@savoirfairelinux.com> |
| 6 | * |
| 7 | * This program is free software; you can redistribute it and/or modify it under |
| 8 | * the terms of the GNU General Public License as published by the Free Software |
| 9 | * Foundation; either version 2 of the License, or (at your option) any later |
| 10 | * version. |
| 11 | * |
| 12 | * |
| 13 | * This driver registers the Technologic Systems TS-5500 Single Board Computer |
| 14 | * (SBC) and its devices, and exposes information to userspace such as jumpers' |
| 15 | * state or available options. For further information about sysfs entries, see |
| 16 | * Documentation/ABI/testing/sysfs-platform-ts5500. |
| 17 | * |
| 18 | * This code actually supports the TS-5500 platform, but it may be extended to |
| 19 | * support similar Technologic Systems x86-based platforms, such as the TS-5600. |
| 20 | */ |
| 21 | |
| 22 | #include <linux/delay.h> |
| 23 | #include <linux/io.h> |
| 24 | #include <linux/kernel.h> |
| 25 | #include <linux/leds.h> |
| 26 | #include <linux/module.h> |
| 27 | #include <linux/platform_data/gpio-ts5500.h> |
| 28 | #include <linux/platform_data/max197.h> |
| 29 | #include <linux/platform_device.h> |
| 30 | #include <linux/slab.h> |
| 31 | |
| 32 | /* Product code register */ |
| 33 | #define TS5500_PRODUCT_CODE_ADDR 0x74 |
| 34 | #define TS5500_PRODUCT_CODE 0x60 /* TS-5500 product code */ |
| 35 | |
| 36 | /* SRAM/RS-485/ADC options, and RS-485 RTS/Automatic RS-485 flags register */ |
| 37 | #define TS5500_SRAM_RS485_ADC_ADDR 0x75 |
| 38 | #define TS5500_SRAM BIT(0) /* SRAM option */ |
| 39 | #define TS5500_RS485 BIT(1) /* RS-485 option */ |
| 40 | #define TS5500_ADC BIT(2) /* A/D converter option */ |
| 41 | #define TS5500_RS485_RTS BIT(6) /* RTS for RS-485 */ |
| 42 | #define TS5500_RS485_AUTO BIT(7) /* Automatic RS-485 */ |
| 43 | |
| 44 | /* External Reset/Industrial Temperature Range options register */ |
| 45 | #define TS5500_ERESET_ITR_ADDR 0x76 |
| 46 | #define TS5500_ERESET BIT(0) /* External Reset option */ |
| 47 | #define TS5500_ITR BIT(1) /* Indust. Temp. Range option */ |
| 48 | |
| 49 | /* LED/Jumpers register */ |
| 50 | #define TS5500_LED_JP_ADDR 0x77 |
| 51 | #define TS5500_LED BIT(0) /* LED flag */ |
| 52 | #define TS5500_JP1 BIT(1) /* Automatic CMOS */ |
| 53 | #define TS5500_JP2 BIT(2) /* Enable Serial Console */ |
| 54 | #define TS5500_JP3 BIT(3) /* Write Enable Drive A */ |
| 55 | #define TS5500_JP4 BIT(4) /* Fast Console (115K baud) */ |
| 56 | #define TS5500_JP5 BIT(5) /* User Jumper */ |
| 57 | #define TS5500_JP6 BIT(6) /* Console on COM1 (req. JP2) */ |
| 58 | #define TS5500_JP7 BIT(7) /* Undocumented (Unused) */ |
| 59 | |
| 60 | /* A/D Converter registers */ |
| 61 | #define TS5500_ADC_CONV_BUSY_ADDR 0x195 /* Conversion state register */ |
| 62 | #define TS5500_ADC_CONV_BUSY BIT(0) |
| 63 | #define TS5500_ADC_CONV_INIT_LSB_ADDR 0x196 /* Start conv. / LSB register */ |
| 64 | #define TS5500_ADC_CONV_MSB_ADDR 0x197 /* MSB register */ |
| 65 | #define TS5500_ADC_CONV_DELAY 12 /* usec */ |
| 66 | |
| 67 | /** |
| 68 | * struct ts5500_sbc - TS-5500 board description |
| 69 | * @id: Board product ID. |
| 70 | * @sram: Flag for SRAM option. |
| 71 | * @rs485: Flag for RS-485 option. |
| 72 | * @adc: Flag for Analog/Digital converter option. |
| 73 | * @ereset: Flag for External Reset option. |
| 74 | * @itr: Flag for Industrial Temperature Range option. |
| 75 | * @jumpers: Bitfield for jumpers' state. |
| 76 | */ |
| 77 | struct ts5500_sbc { |
| 78 | int id; |
| 79 | bool sram; |
| 80 | bool rs485; |
| 81 | bool adc; |
| 82 | bool ereset; |
| 83 | bool itr; |
| 84 | u8 jumpers; |
| 85 | }; |
| 86 | |
| 87 | /* Board signatures in BIOS shadow RAM */ |
| 88 | static const struct { |
| 89 | const char * const string; |
| 90 | const ssize_t offset; |
Andi Kleen | 634676c | 2014-02-08 08:52:10 +0100 | [diff] [blame] | 91 | } ts5500_signatures[] __initconst = { |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 92 | { "TS-5x00 AMD Elan", 0xb14 }, |
| 93 | }; |
| 94 | |
| 95 | static int __init ts5500_check_signature(void) |
| 96 | { |
| 97 | void __iomem *bios; |
| 98 | int i, ret = -ENODEV; |
| 99 | |
| 100 | bios = ioremap(0xf0000, 0x10000); |
| 101 | if (!bios) |
| 102 | return -ENOMEM; |
| 103 | |
| 104 | for (i = 0; i < ARRAY_SIZE(ts5500_signatures); i++) { |
| 105 | if (check_signature(bios + ts5500_signatures[i].offset, |
| 106 | ts5500_signatures[i].string, |
| 107 | strlen(ts5500_signatures[i].string))) { |
| 108 | ret = 0; |
| 109 | break; |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | iounmap(bios); |
| 114 | return ret; |
| 115 | } |
| 116 | |
| 117 | static int __init ts5500_detect_config(struct ts5500_sbc *sbc) |
| 118 | { |
| 119 | u8 tmp; |
| 120 | int ret = 0; |
| 121 | |
| 122 | if (!request_region(TS5500_PRODUCT_CODE_ADDR, 4, "ts5500")) |
| 123 | return -EBUSY; |
| 124 | |
| 125 | tmp = inb(TS5500_PRODUCT_CODE_ADDR); |
| 126 | if (tmp != TS5500_PRODUCT_CODE) { |
| 127 | pr_err("This platform is not a TS-5500 (found ID 0x%x)\n", tmp); |
| 128 | ret = -ENODEV; |
| 129 | goto cleanup; |
| 130 | } |
| 131 | sbc->id = tmp; |
| 132 | |
| 133 | tmp = inb(TS5500_SRAM_RS485_ADC_ADDR); |
| 134 | sbc->sram = tmp & TS5500_SRAM; |
| 135 | sbc->rs485 = tmp & TS5500_RS485; |
| 136 | sbc->adc = tmp & TS5500_ADC; |
| 137 | |
| 138 | tmp = inb(TS5500_ERESET_ITR_ADDR); |
| 139 | sbc->ereset = tmp & TS5500_ERESET; |
| 140 | sbc->itr = tmp & TS5500_ITR; |
| 141 | |
| 142 | tmp = inb(TS5500_LED_JP_ADDR); |
| 143 | sbc->jumpers = tmp & ~TS5500_LED; |
| 144 | |
| 145 | cleanup: |
| 146 | release_region(TS5500_PRODUCT_CODE_ADDR, 4); |
| 147 | return ret; |
| 148 | } |
| 149 | |
| 150 | static ssize_t ts5500_show_id(struct device *dev, |
| 151 | struct device_attribute *attr, char *buf) |
| 152 | { |
| 153 | struct ts5500_sbc *sbc = dev_get_drvdata(dev); |
| 154 | |
| 155 | return sprintf(buf, "0x%.2x\n", sbc->id); |
| 156 | } |
| 157 | |
| 158 | static ssize_t ts5500_show_jumpers(struct device *dev, |
| 159 | struct device_attribute *attr, |
| 160 | char *buf) |
| 161 | { |
| 162 | struct ts5500_sbc *sbc = dev_get_drvdata(dev); |
| 163 | |
| 164 | return sprintf(buf, "0x%.2x\n", sbc->jumpers >> 1); |
| 165 | } |
| 166 | |
| 167 | #define TS5500_SHOW(field) \ |
| 168 | static ssize_t ts5500_show_##field(struct device *dev, \ |
| 169 | struct device_attribute *attr, \ |
| 170 | char *buf) \ |
| 171 | { \ |
| 172 | struct ts5500_sbc *sbc = dev_get_drvdata(dev); \ |
| 173 | return sprintf(buf, "%d\n", sbc->field); \ |
| 174 | } |
| 175 | |
| 176 | TS5500_SHOW(sram) |
| 177 | TS5500_SHOW(rs485) |
| 178 | TS5500_SHOW(adc) |
| 179 | TS5500_SHOW(ereset) |
| 180 | TS5500_SHOW(itr) |
| 181 | |
| 182 | static DEVICE_ATTR(id, S_IRUGO, ts5500_show_id, NULL); |
| 183 | static DEVICE_ATTR(jumpers, S_IRUGO, ts5500_show_jumpers, NULL); |
| 184 | static DEVICE_ATTR(sram, S_IRUGO, ts5500_show_sram, NULL); |
| 185 | static DEVICE_ATTR(rs485, S_IRUGO, ts5500_show_rs485, NULL); |
| 186 | static DEVICE_ATTR(adc, S_IRUGO, ts5500_show_adc, NULL); |
| 187 | static DEVICE_ATTR(ereset, S_IRUGO, ts5500_show_ereset, NULL); |
| 188 | static DEVICE_ATTR(itr, S_IRUGO, ts5500_show_itr, NULL); |
| 189 | |
| 190 | static struct attribute *ts5500_attributes[] = { |
| 191 | &dev_attr_id.attr, |
| 192 | &dev_attr_jumpers.attr, |
| 193 | &dev_attr_sram.attr, |
| 194 | &dev_attr_rs485.attr, |
| 195 | &dev_attr_adc.attr, |
| 196 | &dev_attr_ereset.attr, |
| 197 | &dev_attr_itr.attr, |
| 198 | NULL |
| 199 | }; |
| 200 | |
| 201 | static const struct attribute_group ts5500_attr_group = { |
| 202 | .attrs = ts5500_attributes, |
| 203 | }; |
| 204 | |
| 205 | static struct resource ts5500_dio1_resource[] = { |
| 206 | DEFINE_RES_IRQ_NAMED(7, "DIO1 interrupt"), |
| 207 | }; |
| 208 | |
| 209 | static struct platform_device ts5500_dio1_pdev = { |
| 210 | .name = "ts5500-dio1", |
| 211 | .id = -1, |
| 212 | .resource = ts5500_dio1_resource, |
| 213 | .num_resources = 1, |
| 214 | }; |
| 215 | |
| 216 | static struct resource ts5500_dio2_resource[] = { |
| 217 | DEFINE_RES_IRQ_NAMED(6, "DIO2 interrupt"), |
| 218 | }; |
| 219 | |
| 220 | static struct platform_device ts5500_dio2_pdev = { |
| 221 | .name = "ts5500-dio2", |
| 222 | .id = -1, |
| 223 | .resource = ts5500_dio2_resource, |
| 224 | .num_resources = 1, |
| 225 | }; |
| 226 | |
| 227 | static void ts5500_led_set(struct led_classdev *led_cdev, |
| 228 | enum led_brightness brightness) |
| 229 | { |
| 230 | outb(!!brightness, TS5500_LED_JP_ADDR); |
| 231 | } |
| 232 | |
| 233 | static enum led_brightness ts5500_led_get(struct led_classdev *led_cdev) |
| 234 | { |
| 235 | return (inb(TS5500_LED_JP_ADDR) & TS5500_LED) ? LED_FULL : LED_OFF; |
| 236 | } |
| 237 | |
| 238 | static struct led_classdev ts5500_led_cdev = { |
| 239 | .name = "ts5500:green:", |
| 240 | .brightness_set = ts5500_led_set, |
| 241 | .brightness_get = ts5500_led_get, |
| 242 | }; |
| 243 | |
| 244 | static int ts5500_adc_convert(u8 ctrl) |
| 245 | { |
| 246 | u8 lsb, msb; |
| 247 | |
| 248 | /* Start conversion (ensure the 3 MSB are set to 0) */ |
| 249 | outb(ctrl & 0x1f, TS5500_ADC_CONV_INIT_LSB_ADDR); |
| 250 | |
| 251 | /* |
| 252 | * The platform has CPLD logic driving the A/D converter. |
| 253 | * The conversion must complete within 11 microseconds, |
| 254 | * otherwise we have to re-initiate a conversion. |
| 255 | */ |
| 256 | udelay(TS5500_ADC_CONV_DELAY); |
| 257 | if (inb(TS5500_ADC_CONV_BUSY_ADDR) & TS5500_ADC_CONV_BUSY) |
| 258 | return -EBUSY; |
| 259 | |
| 260 | /* Read the raw data */ |
| 261 | lsb = inb(TS5500_ADC_CONV_INIT_LSB_ADDR); |
| 262 | msb = inb(TS5500_ADC_CONV_MSB_ADDR); |
| 263 | |
| 264 | return (msb << 8) | lsb; |
| 265 | } |
| 266 | |
| 267 | static struct max197_platform_data ts5500_adc_pdata = { |
| 268 | .convert = ts5500_adc_convert, |
| 269 | }; |
| 270 | |
| 271 | static struct platform_device ts5500_adc_pdev = { |
| 272 | .name = "max197", |
| 273 | .id = -1, |
| 274 | .dev = { |
| 275 | .platform_data = &ts5500_adc_pdata, |
| 276 | }, |
| 277 | }; |
| 278 | |
| 279 | static int __init ts5500_init(void) |
| 280 | { |
| 281 | struct platform_device *pdev; |
| 282 | struct ts5500_sbc *sbc; |
| 283 | int err; |
| 284 | |
| 285 | /* |
| 286 | * There is no DMI available or PCI bridge subvendor info, |
| 287 | * only the BIOS provides a 16-bit identification call. |
| 288 | * It is safer to find a signature in the BIOS shadow RAM. |
| 289 | */ |
| 290 | err = ts5500_check_signature(); |
| 291 | if (err) |
| 292 | return err; |
| 293 | |
| 294 | pdev = platform_device_register_simple("ts5500", -1, NULL, 0); |
| 295 | if (IS_ERR(pdev)) |
| 296 | return PTR_ERR(pdev); |
| 297 | |
| 298 | sbc = devm_kzalloc(&pdev->dev, sizeof(struct ts5500_sbc), GFP_KERNEL); |
| 299 | if (!sbc) { |
| 300 | err = -ENOMEM; |
| 301 | goto error; |
| 302 | } |
| 303 | |
| 304 | err = ts5500_detect_config(sbc); |
| 305 | if (err) |
| 306 | goto error; |
| 307 | |
| 308 | platform_set_drvdata(pdev, sbc); |
| 309 | |
| 310 | err = sysfs_create_group(&pdev->dev.kobj, &ts5500_attr_group); |
| 311 | if (err) |
| 312 | goto error; |
| 313 | |
| 314 | ts5500_dio1_pdev.dev.parent = &pdev->dev; |
| 315 | if (platform_device_register(&ts5500_dio1_pdev)) |
| 316 | dev_warn(&pdev->dev, "DIO1 block registration failed\n"); |
| 317 | ts5500_dio2_pdev.dev.parent = &pdev->dev; |
| 318 | if (platform_device_register(&ts5500_dio2_pdev)) |
| 319 | dev_warn(&pdev->dev, "DIO2 block registration failed\n"); |
| 320 | |
| 321 | if (led_classdev_register(&pdev->dev, &ts5500_led_cdev)) |
| 322 | dev_warn(&pdev->dev, "LED registration failed\n"); |
| 323 | |
| 324 | if (sbc->adc) { |
| 325 | ts5500_adc_pdev.dev.parent = &pdev->dev; |
| 326 | if (platform_device_register(&ts5500_adc_pdev)) |
| 327 | dev_warn(&pdev->dev, "ADC registration failed\n"); |
| 328 | } |
| 329 | |
| 330 | return 0; |
| 331 | error: |
| 332 | platform_device_unregister(pdev); |
| 333 | return err; |
| 334 | } |
| 335 | device_initcall(ts5500_init); |
| 336 | |
| 337 | MODULE_LICENSE("GPL"); |
| 338 | MODULE_AUTHOR("Savoir-faire Linux Inc. <kernel@savoirfairelinux.com>"); |
| 339 | MODULE_DESCRIPTION("Technologic Systems TS-5500 platform driver"); |