summaryrefslogtreecommitdiff
path: root/arch/x86/platform/ts5500/ts5500.c
blob: 0b67da056fd927456a2826dad92208b2801143e4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
// 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);