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 | |
Vivien Didelot | 1d24087 | 2014-07-08 18:57:47 -0400 | [diff] [blame^] | 150 | static ssize_t id_show(struct device *dev, struct device_attribute *attr, |
| 151 | char *buf) |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 152 | { |
| 153 | struct ts5500_sbc *sbc = dev_get_drvdata(dev); |
| 154 | |
| 155 | return sprintf(buf, "0x%.2x\n", sbc->id); |
| 156 | } |
Vivien Didelot | 1d24087 | 2014-07-08 18:57:47 -0400 | [diff] [blame^] | 157 | static DEVICE_ATTR_RO(id); |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 158 | |
Vivien Didelot | 1d24087 | 2014-07-08 18:57:47 -0400 | [diff] [blame^] | 159 | static ssize_t jumpers_show(struct device *dev, struct device_attribute *attr, |
| 160 | char *buf) |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 161 | { |
| 162 | struct ts5500_sbc *sbc = dev_get_drvdata(dev); |
| 163 | |
| 164 | return sprintf(buf, "0x%.2x\n", sbc->jumpers >> 1); |
| 165 | } |
Vivien Didelot | 1d24087 | 2014-07-08 18:57:47 -0400 | [diff] [blame^] | 166 | static DEVICE_ATTR_RO(jumpers); |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 167 | |
Vivien Didelot | 1d24087 | 2014-07-08 18:57:47 -0400 | [diff] [blame^] | 168 | #define TS5500_ATTR_BOOL(_field) \ |
| 169 | static ssize_t _field##_show(struct device *dev, \ |
| 170 | struct device_attribute *attr, char *buf) \ |
| 171 | { \ |
| 172 | struct ts5500_sbc *sbc = dev_get_drvdata(dev); \ |
| 173 | \ |
| 174 | return sprintf(buf, "%d\n", sbc->_field); \ |
| 175 | } \ |
| 176 | static DEVICE_ATTR_RO(_field) |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 177 | |
Vivien Didelot | 1d24087 | 2014-07-08 18:57:47 -0400 | [diff] [blame^] | 178 | TS5500_ATTR_BOOL(sram); |
| 179 | TS5500_ATTR_BOOL(rs485); |
| 180 | TS5500_ATTR_BOOL(adc); |
| 181 | TS5500_ATTR_BOOL(ereset); |
| 182 | TS5500_ATTR_BOOL(itr); |
Vivien Didelot | 7d02912 | 2013-01-04 16:18:14 -0500 | [diff] [blame] | 183 | |
| 184 | static struct attribute *ts5500_attributes[] = { |
| 185 | &dev_attr_id.attr, |
| 186 | &dev_attr_jumpers.attr, |
| 187 | &dev_attr_sram.attr, |
| 188 | &dev_attr_rs485.attr, |
| 189 | &dev_attr_adc.attr, |
| 190 | &dev_attr_ereset.attr, |
| 191 | &dev_attr_itr.attr, |
| 192 | NULL |
| 193 | }; |
| 194 | |
| 195 | static const struct attribute_group ts5500_attr_group = { |
| 196 | .attrs = ts5500_attributes, |
| 197 | }; |
| 198 | |
| 199 | static struct resource ts5500_dio1_resource[] = { |
| 200 | DEFINE_RES_IRQ_NAMED(7, "DIO1 interrupt"), |
| 201 | }; |
| 202 | |
| 203 | static struct platform_device ts5500_dio1_pdev = { |
| 204 | .name = "ts5500-dio1", |
| 205 | .id = -1, |
| 206 | .resource = ts5500_dio1_resource, |
| 207 | .num_resources = 1, |
| 208 | }; |
| 209 | |
| 210 | static struct resource ts5500_dio2_resource[] = { |
| 211 | DEFINE_RES_IRQ_NAMED(6, "DIO2 interrupt"), |
| 212 | }; |
| 213 | |
| 214 | static struct platform_device ts5500_dio2_pdev = { |
| 215 | .name = "ts5500-dio2", |
| 216 | .id = -1, |
| 217 | .resource = ts5500_dio2_resource, |
| 218 | .num_resources = 1, |
| 219 | }; |
| 220 | |
| 221 | static void ts5500_led_set(struct led_classdev *led_cdev, |
| 222 | enum led_brightness brightness) |
| 223 | { |
| 224 | outb(!!brightness, TS5500_LED_JP_ADDR); |
| 225 | } |
| 226 | |
| 227 | static enum led_brightness ts5500_led_get(struct led_classdev *led_cdev) |
| 228 | { |
| 229 | return (inb(TS5500_LED_JP_ADDR) & TS5500_LED) ? LED_FULL : LED_OFF; |
| 230 | } |
| 231 | |
| 232 | static struct led_classdev ts5500_led_cdev = { |
| 233 | .name = "ts5500:green:", |
| 234 | .brightness_set = ts5500_led_set, |
| 235 | .brightness_get = ts5500_led_get, |
| 236 | }; |
| 237 | |
| 238 | static int ts5500_adc_convert(u8 ctrl) |
| 239 | { |
| 240 | u8 lsb, msb; |
| 241 | |
| 242 | /* Start conversion (ensure the 3 MSB are set to 0) */ |
| 243 | outb(ctrl & 0x1f, TS5500_ADC_CONV_INIT_LSB_ADDR); |
| 244 | |
| 245 | /* |
| 246 | * The platform has CPLD logic driving the A/D converter. |
| 247 | * The conversion must complete within 11 microseconds, |
| 248 | * otherwise we have to re-initiate a conversion. |
| 249 | */ |
| 250 | udelay(TS5500_ADC_CONV_DELAY); |
| 251 | if (inb(TS5500_ADC_CONV_BUSY_ADDR) & TS5500_ADC_CONV_BUSY) |
| 252 | return -EBUSY; |
| 253 | |
| 254 | /* Read the raw data */ |
| 255 | lsb = inb(TS5500_ADC_CONV_INIT_LSB_ADDR); |
| 256 | msb = inb(TS5500_ADC_CONV_MSB_ADDR); |
| 257 | |
| 258 | return (msb << 8) | lsb; |
| 259 | } |
| 260 | |
| 261 | static struct max197_platform_data ts5500_adc_pdata = { |
| 262 | .convert = ts5500_adc_convert, |
| 263 | }; |
| 264 | |
| 265 | static struct platform_device ts5500_adc_pdev = { |
| 266 | .name = "max197", |
| 267 | .id = -1, |
| 268 | .dev = { |
| 269 | .platform_data = &ts5500_adc_pdata, |
| 270 | }, |
| 271 | }; |
| 272 | |
| 273 | static int __init ts5500_init(void) |
| 274 | { |
| 275 | struct platform_device *pdev; |
| 276 | struct ts5500_sbc *sbc; |
| 277 | int err; |
| 278 | |
| 279 | /* |
| 280 | * There is no DMI available or PCI bridge subvendor info, |
| 281 | * only the BIOS provides a 16-bit identification call. |
| 282 | * It is safer to find a signature in the BIOS shadow RAM. |
| 283 | */ |
| 284 | err = ts5500_check_signature(); |
| 285 | if (err) |
| 286 | return err; |
| 287 | |
| 288 | pdev = platform_device_register_simple("ts5500", -1, NULL, 0); |
| 289 | if (IS_ERR(pdev)) |
| 290 | return PTR_ERR(pdev); |
| 291 | |
| 292 | sbc = devm_kzalloc(&pdev->dev, sizeof(struct ts5500_sbc), GFP_KERNEL); |
| 293 | if (!sbc) { |
| 294 | err = -ENOMEM; |
| 295 | goto error; |
| 296 | } |
| 297 | |
| 298 | err = ts5500_detect_config(sbc); |
| 299 | if (err) |
| 300 | goto error; |
| 301 | |
| 302 | platform_set_drvdata(pdev, sbc); |
| 303 | |
| 304 | err = sysfs_create_group(&pdev->dev.kobj, &ts5500_attr_group); |
| 305 | if (err) |
| 306 | goto error; |
| 307 | |
| 308 | ts5500_dio1_pdev.dev.parent = &pdev->dev; |
| 309 | if (platform_device_register(&ts5500_dio1_pdev)) |
| 310 | dev_warn(&pdev->dev, "DIO1 block registration failed\n"); |
| 311 | ts5500_dio2_pdev.dev.parent = &pdev->dev; |
| 312 | if (platform_device_register(&ts5500_dio2_pdev)) |
| 313 | dev_warn(&pdev->dev, "DIO2 block registration failed\n"); |
| 314 | |
| 315 | if (led_classdev_register(&pdev->dev, &ts5500_led_cdev)) |
| 316 | dev_warn(&pdev->dev, "LED registration failed\n"); |
| 317 | |
| 318 | if (sbc->adc) { |
| 319 | ts5500_adc_pdev.dev.parent = &pdev->dev; |
| 320 | if (platform_device_register(&ts5500_adc_pdev)) |
| 321 | dev_warn(&pdev->dev, "ADC registration failed\n"); |
| 322 | } |
| 323 | |
| 324 | return 0; |
| 325 | error: |
| 326 | platform_device_unregister(pdev); |
| 327 | return err; |
| 328 | } |
| 329 | device_initcall(ts5500_init); |
| 330 | |
| 331 | MODULE_LICENSE("GPL"); |
| 332 | MODULE_AUTHOR("Savoir-faire Linux Inc. <kernel@savoirfairelinux.com>"); |
| 333 | MODULE_DESCRIPTION("Technologic Systems TS-5500 platform driver"); |