b.liu | e958203 | 2025-04-17 19:18:16 +0800 | [diff] [blame^] | 1 | // SPDX-License-Identifier: GPL-2.0-or-later |
| 2 | /* |
| 3 | * ASR built-in touchscreen driver |
| 4 | * |
| 5 | * Copyright (C) 2023 ASR |
| 6 | */ |
| 7 | |
| 8 | #include <linux/platform_device.h> |
| 9 | #include <linux/input.h> |
| 10 | #include <linux/interrupt.h> |
| 11 | #include <linux/module.h> |
| 12 | #include <linux/clk.h> |
| 13 | #include <linux/io.h> |
| 14 | #include <linux/slab.h> |
| 15 | #include <linux/of.h> |
| 16 | #include <linux/timer.h> |
| 17 | |
| 18 | /* |
| 19 | * Touchscreen controller register offsets |
| 20 | */ |
| 21 | #define ASR_TSCR 0x0 |
| 22 | #define ASR_TSSR 0x4 |
| 23 | #define ASR_XYDR(x) (0x8 + (x) * 0x4) |
| 24 | |
| 25 | #define ASR_TSSR_DOWN 0 |
| 26 | #define ASR_TSSR_UP 1 |
| 27 | |
| 28 | #define ASR_TSC_MIN_XY_VAL 0 |
| 29 | #define ASR_TSC_MAX_XY_VAL 4095 |
| 30 | #define ASR_TSC_DOT_CNT_MAX 8 |
| 31 | |
| 32 | #define MOD_NAME "ts-asr" |
| 33 | |
| 34 | #define tsc_readl(dev, reg) \ |
| 35 | __raw_readl((dev)->tsc_base + (reg)) |
| 36 | #define tsc_writel(dev, reg, val) \ |
| 37 | __raw_writel((val), (dev)->tsc_base + (reg)) |
| 38 | |
| 39 | struct asr_tp_info { |
| 40 | int x; |
| 41 | int y; |
| 42 | int last_x; |
| 43 | int last_y; |
| 44 | }; |
| 45 | |
| 46 | struct asr_tsc { |
| 47 | struct timer_list move_timer; |
| 48 | struct input_dev *dev; |
| 49 | void __iomem *tsc_base; |
| 50 | int rdy_irq; |
| 51 | int up_irq; |
| 52 | int down_irq; |
| 53 | struct clk *clk; |
| 54 | struct asr_tp_info tp; |
| 55 | int dummpy_points; |
| 56 | }; |
| 57 | |
| 58 | #define ASR_TSC_MOVE_TIMEOUT 10 /* ms */ |
| 59 | #define ASR_TSC_DUMMY_POINTS 10 |
| 60 | #define ASR_TSC_TP_MARGIN 10 |
| 61 | |
| 62 | enum { |
| 63 | TSC_MODE_IDLE = 0, |
| 64 | TSC_MODE_WAIT_PRESSDOWN, |
| 65 | TSC_MODE_WAIT_RISEUP, |
| 66 | TSC_MODE_WAIT_AUTOMEASURE, |
| 67 | }; |
| 68 | static void asr_ts_set_mode(struct asr_tsc *tsc, int mode); |
| 69 | |
| 70 | static void asr_ts_movement_detect(struct timer_list *t) |
| 71 | { |
| 72 | struct asr_tsc *tsc = from_timer(tsc, t, move_timer); |
| 73 | u32 val; |
| 74 | val = tsc_readl(tsc, ASR_TSSR); |
| 75 | if (val & ASR_TSSR_UP) { |
| 76 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| 77 | return; |
| 78 | } |
| 79 | |
| 80 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_AUTOMEASURE); |
| 81 | } |
| 82 | |
| 83 | static void asr_ts_adc_init(void) |
| 84 | { |
| 85 | void __iomem* apb_spare; |
| 86 | u32 val; |
| 87 | |
| 88 | apb_spare = ioremap(0xD4090000, SZ_4K); |
| 89 | if (!apb_spare) { |
| 90 | pr_err("%s: error to ioremap apb_spare base\n", __func__); |
| 91 | return; |
| 92 | } |
| 93 | |
| 94 | val = readl(apb_spare + 0x12c); |
| 95 | val |= (1 << 8) | (1 << 20) | (1 << 22) | (0x4 << 24); |
| 96 | writel(val, apb_spare + 0x12c); |
| 97 | iounmap(apb_spare); |
| 98 | } |
| 99 | |
| 100 | static void asr_ts_set_mode(struct asr_tsc *tsc, int mode) |
| 101 | { |
| 102 | u32 val; |
| 103 | val = tsc_readl(tsc, ASR_TSCR); |
| 104 | val &= ~(0x3 << 1); |
| 105 | switch (mode) { |
| 106 | case TSC_MODE_WAIT_PRESSDOWN: |
| 107 | val &= ~((1 << 8) | (1 << 7)); |
| 108 | val |= (1 << 6) | (1 << 9); |
| 109 | break; |
| 110 | case TSC_MODE_WAIT_RISEUP: |
| 111 | val &= ~((1 << 8) | (1 << 6)); |
| 112 | val |= (1 << 7) | (1 << 9); |
| 113 | break; |
| 114 | case TSC_MODE_WAIT_AUTOMEASURE: |
| 115 | val &= ~((1 << 6) | (1 << 7) | (1 << 9)); |
| 116 | val |= (1 << 8); |
| 117 | break; |
| 118 | } |
| 119 | val |= (mode << 1); |
| 120 | tsc_writel(tsc, ASR_TSCR, val); |
| 121 | } |
| 122 | |
| 123 | static void asr_ts_measure(struct asr_tsc *tsc) |
| 124 | { |
| 125 | int i; |
| 126 | u32 val, meas_x, meas_y, sum_x = 0, sum_y = 0; |
| 127 | u16 min_x, max_x, min_y, max_y; |
| 128 | u16 x[ASR_TSC_DOT_CNT_MAX] = {0}; |
| 129 | u16 y[ASR_TSC_DOT_CNT_MAX] = {0}; |
| 130 | struct asr_tp_info *tp = (struct asr_tp_info *)&tsc->tp; |
| 131 | |
| 132 | for (i = 0; i < ASR_TSC_DOT_CNT_MAX; i++) { |
| 133 | val = tsc_readl(tsc, ASR_XYDR(i)); |
| 134 | x[i] = val & 0xffff; |
| 135 | sum_x += x[i]; |
| 136 | if (i == 0 ) { |
| 137 | min_x = x[0]; |
| 138 | max_x = x[0]; |
| 139 | } |
| 140 | if (min_x > x[i]) |
| 141 | min_x = x[i]; |
| 142 | if (max_x < x[i]) |
| 143 | max_x = x[i]; |
| 144 | |
| 145 | y[i] = (val >> 16) & 0xffff; |
| 146 | sum_y += y[i]; |
| 147 | |
| 148 | if (i == 0 ) { |
| 149 | min_y = y[0]; |
| 150 | max_y = y[0]; |
| 151 | } |
| 152 | if (min_y > y[i]) |
| 153 | min_y = y[i]; |
| 154 | if(max_y < y[i]) |
| 155 | max_y = y[i]; |
| 156 | pr_debug(" %s: [%d] (%d, %d)\n", __func__, i, x[i], y[i]); |
| 157 | } |
| 158 | |
| 159 | meas_x = (sum_x - max_x - min_x) / (ASR_TSC_DOT_CNT_MAX - 2); |
| 160 | meas_y = (sum_y - max_y - min_y) / (ASR_TSC_DOT_CNT_MAX - 2); |
| 161 | |
| 162 | pr_debug("meas_x: %d, meas_y: %d\n", meas_x, meas_y); |
| 163 | tp->x = meas_x; |
| 164 | tp->y = meas_y; |
| 165 | } |
| 166 | |
| 167 | static irqreturn_t asr_ts_down_isr(int irq, void *dev_id) |
| 168 | { |
| 169 | u32 val; |
| 170 | struct asr_tsc *tsc = (struct asr_tsc *)dev_id; |
| 171 | val = tsc_readl(tsc, ASR_TSSR); |
| 172 | |
| 173 | if (val & (1 << 6)) { |
| 174 | val |= (1 << 6); |
| 175 | tsc_writel(tsc, ASR_TSSR, val); |
| 176 | if (val & ASR_TSSR_UP) { |
| 177 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| 178 | return IRQ_HANDLED; |
| 179 | } |
| 180 | |
| 181 | tsc->dummpy_points = 1; |
| 182 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_AUTOMEASURE); |
| 183 | pr_debug("%s: got down irq.\n", __func__); |
| 184 | } else { |
| 185 | pr_warn("%s: mismatch irq?\n", __func__); |
| 186 | } |
| 187 | return IRQ_HANDLED; |
| 188 | } |
| 189 | |
| 190 | static irqreturn_t asr_ts_rdy_isr(int irq, void *dev_id) |
| 191 | { |
| 192 | u32 val; |
| 193 | int x, y; |
| 194 | struct asr_tsc *tsc = (struct asr_tsc *)dev_id; |
| 195 | struct input_dev *input = tsc->dev; |
| 196 | |
| 197 | val = tsc_readl(tsc, ASR_TSSR); |
| 198 | if (val & (1 << 8)) { |
| 199 | val |= (1 << 8); |
| 200 | tsc_writel(tsc, ASR_TSSR, val); |
| 201 | #if 0 |
| 202 | if (val & ASR_TSSR_UP) { |
| 203 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| 204 | return IRQ_HANDLED; |
| 205 | } |
| 206 | #endif |
| 207 | asr_ts_measure(tsc); |
| 208 | if (tsc->dummpy_points) { |
| 209 | tsc->dummpy_points++; |
| 210 | if (tsc->dummpy_points > ASR_TSC_DUMMY_POINTS) { |
| 211 | tsc->dummpy_points = 0; |
| 212 | x = tsc->tp.last_x = tsc->tp.x; |
| 213 | y = tsc->tp.last_y = tsc->tp.y; |
| 214 | } else |
| 215 | goto skip; |
| 216 | } else { |
| 217 | if (abs(tsc->tp.x - tsc->tp.last_x) < ASR_TSC_TP_MARGIN && |
| 218 | abs(tsc->tp.y - tsc->tp.last_y) < ASR_TSC_TP_MARGIN) |
| 219 | goto skip; |
| 220 | } |
| 221 | |
| 222 | x = tsc->tp.last_x = tsc->tp.x; |
| 223 | y = tsc->tp.last_y = tsc->tp.y; |
| 224 | |
| 225 | input_report_abs(input, ABS_X, x); |
| 226 | input_report_abs(input, ABS_Y, y); |
| 227 | input_report_key(input, BTN_TOUCH, 1); |
| 228 | input_sync(input); |
| 229 | skip: |
| 230 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_RISEUP); |
| 231 | mod_timer(&tsc->move_timer, jiffies + msecs_to_jiffies(ASR_TSC_MOVE_TIMEOUT)); |
| 232 | pr_debug("%s: got rdy irq. 0x%x, (%d, %d)\n", __func__, val, tsc->tp.x, tsc->tp.y); |
| 233 | } else { |
| 234 | pr_warn("%s: mismatch irq?\n", __func__); |
| 235 | } |
| 236 | return IRQ_HANDLED; |
| 237 | } |
| 238 | |
| 239 | static irqreturn_t asr_ts_up_isr(int irq, void *dev_id) |
| 240 | { |
| 241 | u32 val; |
| 242 | struct asr_tsc *tsc = (struct asr_tsc *)dev_id; |
| 243 | struct input_dev *input = tsc->dev; |
| 244 | |
| 245 | val = tsc_readl(tsc, ASR_TSSR); |
| 246 | if (val & (1 << 7)) { |
| 247 | val |= (1 << 7); |
| 248 | tsc_writel(tsc, ASR_TSSR, val); |
| 249 | if ((val & 1) == ASR_TSSR_DOWN) |
| 250 | return IRQ_HANDLED; |
| 251 | |
| 252 | pr_debug("%s: got up irq. (%d, %d)\n", __func__, tsc->tp.x, tsc->tp.y); |
| 253 | input_report_key(input, BTN_TOUCH, 0); |
| 254 | input_sync(input); |
| 255 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| 256 | } else { |
| 257 | pr_warn("%s: mismatch irq?\n", __func__); |
| 258 | } |
| 259 | return IRQ_HANDLED; |
| 260 | } |
| 261 | |
| 262 | static void asr_stop_tsc(struct asr_tsc *tsc) |
| 263 | { |
| 264 | tsc_writel(tsc, ASR_TSCR, 0); |
| 265 | clk_disable_unprepare(tsc->clk); |
| 266 | } |
| 267 | |
| 268 | static int asr_setup_tsc(struct asr_tsc *tsc) |
| 269 | { |
| 270 | u32 tmp; |
| 271 | int err; |
| 272 | asr_ts_adc_init(); |
| 273 | |
| 274 | err = clk_prepare_enable(tsc->clk); |
| 275 | if (err) |
| 276 | return err; |
| 277 | |
| 278 | tmp = tsc_readl(tsc, ASR_TSCR); |
| 279 | tmp |= (1 << 0); /* enabled tsc */ |
| 280 | tmp |= 0x7 << 16; /* YD ADC channel slection */ |
| 281 | tmp |= 0x6 << 11; /* XL ADC channel slection */ |
| 282 | tmp |= (1 << 8); /* INT_RDY_EN */ |
| 283 | tmp |= (0x20 << 24); |
| 284 | tmp |= (0 << 21); /* XL_PU_RES_SEL */ |
| 285 | tmp |= ((ASR_TSC_DOT_CNT_MAX - 1) << 3); /* DOT_CNT */ |
| 286 | tsc_writel(tsc, ASR_TSCR, tmp); |
| 287 | asr_ts_set_mode(tsc, TSC_MODE_WAIT_PRESSDOWN); |
| 288 | return 0; |
| 289 | } |
| 290 | |
| 291 | static int asr_ts_open(struct input_dev *dev) |
| 292 | { |
| 293 | struct asr_tsc *tsc = input_get_drvdata(dev); |
| 294 | return asr_setup_tsc(tsc); |
| 295 | } |
| 296 | |
| 297 | static void asr_ts_close(struct input_dev *dev) |
| 298 | { |
| 299 | struct asr_tsc *tsc = input_get_drvdata(dev); |
| 300 | asr_stop_tsc(tsc); |
| 301 | } |
| 302 | |
| 303 | static int asr_ts_probe(struct platform_device *pdev) |
| 304 | { |
| 305 | struct asr_tsc *tsc; |
| 306 | struct input_dev *input; |
| 307 | struct resource *res; |
| 308 | resource_size_t size; |
| 309 | int irq0, irq1, irq2; |
| 310 | int error; |
| 311 | |
| 312 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| 313 | if (!res) { |
| 314 | dev_err(&pdev->dev, "Can't get memory resource\n"); |
| 315 | return -ENOENT; |
| 316 | } |
| 317 | |
| 318 | irq0 = platform_get_irq(pdev, 0); |
| 319 | irq1 = platform_get_irq(pdev, 1); |
| 320 | irq2 = platform_get_irq(pdev, 2); |
| 321 | if (irq0 < 0 || irq1 < 0 || irq2 < 0) { |
| 322 | dev_err(&pdev->dev, "err irq:%d, %d, %d\n", irq0, irq1, irq2); |
| 323 | return -ENOENT; |
| 324 | } |
| 325 | |
| 326 | tsc = kzalloc(sizeof(*tsc), GFP_KERNEL); |
| 327 | input = input_allocate_device(); |
| 328 | if (!tsc || !input) { |
| 329 | dev_err(&pdev->dev, "failed allocating memory\n"); |
| 330 | error = -ENOMEM; |
| 331 | goto err_free_mem; |
| 332 | } |
| 333 | |
| 334 | tsc->dev = input; |
| 335 | tsc->down_irq = irq0; |
| 336 | tsc->rdy_irq = irq1; |
| 337 | tsc->up_irq = irq2; |
| 338 | |
| 339 | size = resource_size(res); |
| 340 | |
| 341 | if (!request_mem_region(res->start, size, pdev->name)) { |
| 342 | dev_err(&pdev->dev, "TSC registers are not free\n"); |
| 343 | error = -EBUSY; |
| 344 | goto err_free_mem; |
| 345 | } |
| 346 | |
| 347 | tsc->tsc_base = ioremap(res->start, size); |
| 348 | if (!tsc->tsc_base) { |
| 349 | dev_err(&pdev->dev, "Can't map memory\n"); |
| 350 | error = -ENOMEM; |
| 351 | goto err_release_mem; |
| 352 | } |
| 353 | |
| 354 | tsc->clk = clk_get(&pdev->dev, NULL); |
| 355 | if (IS_ERR(tsc->clk)) { |
| 356 | dev_err(&pdev->dev, "failed getting clock\n"); |
| 357 | error = PTR_ERR(tsc->clk); |
| 358 | goto err_unmap; |
| 359 | } |
| 360 | |
| 361 | input->name = MOD_NAME; |
| 362 | input->phys = "asr/input0"; |
| 363 | input->id.bustype = BUS_HOST; |
| 364 | input->id.vendor = 0x0001; |
| 365 | input->id.product = 0x0002; |
| 366 | input->id.version = 0x0100; |
| 367 | input->dev.parent = &pdev->dev; |
| 368 | input->open = asr_ts_open; |
| 369 | input->close = asr_ts_close; |
| 370 | |
| 371 | __set_bit(EV_ABS, input->evbit); |
| 372 | __set_bit(ABS_X, input->absbit); |
| 373 | __set_bit(ABS_Y, input->absbit); |
| 374 | __set_bit(EV_SYN, input->evbit); |
| 375 | __set_bit(EV_KEY, input->evbit); |
| 376 | __set_bit(BTN_TOUCH, input->keybit); |
| 377 | |
| 378 | input_set_abs_params(input, ABS_X, ASR_TSC_MIN_XY_VAL, |
| 379 | ASR_TSC_MAX_XY_VAL, 0, 0); |
| 380 | input_set_abs_params(input, ABS_Y, ASR_TSC_MIN_XY_VAL, |
| 381 | ASR_TSC_MAX_XY_VAL, 0, 0); |
| 382 | |
| 383 | input_set_drvdata(input, tsc); |
| 384 | |
| 385 | error = request_irq(tsc->down_irq, asr_ts_down_isr, |
| 386 | IRQF_SHARED, pdev->name, tsc); |
| 387 | if (error) { |
| 388 | dev_err(&pdev->dev, "failed requesting down irq\n"); |
| 389 | goto err_put_clock; |
| 390 | } |
| 391 | |
| 392 | error = request_irq(tsc->rdy_irq, asr_ts_rdy_isr, |
| 393 | IRQF_SHARED, pdev->name, tsc); |
| 394 | if (error) { |
| 395 | dev_err(&pdev->dev, "failed requesting rdy irq\n"); |
| 396 | goto err_free_irq0; |
| 397 | } |
| 398 | |
| 399 | error = request_irq(tsc->up_irq, asr_ts_up_isr, |
| 400 | IRQF_SHARED, pdev->name, tsc); |
| 401 | if (error) { |
| 402 | dev_err(&pdev->dev, "failed requesting up irq\n"); |
| 403 | goto err_free_irq1; |
| 404 | } |
| 405 | |
| 406 | error = input_register_device(input); |
| 407 | if (error) { |
| 408 | dev_err(&pdev->dev, "failed registering input device\n"); |
| 409 | goto err_free_irq2; |
| 410 | } |
| 411 | |
| 412 | platform_set_drvdata(pdev, tsc); |
| 413 | device_init_wakeup(&pdev->dev, 1); |
| 414 | timer_setup(&tsc->move_timer, asr_ts_movement_detect, 0); |
| 415 | dev_info(&pdev->dev, "bringup ok.\n"); |
| 416 | return 0; |
| 417 | |
| 418 | err_free_irq2: |
| 419 | free_irq(tsc->up_irq, tsc); |
| 420 | err_free_irq1: |
| 421 | free_irq(tsc->rdy_irq, tsc); |
| 422 | err_free_irq0: |
| 423 | free_irq(tsc->down_irq, tsc); |
| 424 | err_put_clock: |
| 425 | clk_put(tsc->clk); |
| 426 | err_unmap: |
| 427 | iounmap(tsc->tsc_base); |
| 428 | err_release_mem: |
| 429 | release_mem_region(res->start, size); |
| 430 | err_free_mem: |
| 431 | input_free_device(input); |
| 432 | kfree(tsc); |
| 433 | |
| 434 | return error; |
| 435 | } |
| 436 | |
| 437 | static int asr_ts_remove(struct platform_device *pdev) |
| 438 | { |
| 439 | struct asr_tsc *tsc = platform_get_drvdata(pdev); |
| 440 | struct resource *res; |
| 441 | |
| 442 | free_irq(tsc->down_irq, tsc); |
| 443 | free_irq(tsc->rdy_irq, tsc); |
| 444 | free_irq(tsc->up_irq, tsc); |
| 445 | |
| 446 | input_unregister_device(tsc->dev); |
| 447 | |
| 448 | clk_put(tsc->clk); |
| 449 | |
| 450 | iounmap(tsc->tsc_base); |
| 451 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); |
| 452 | release_mem_region(res->start, resource_size(res)); |
| 453 | |
| 454 | kfree(tsc); |
| 455 | |
| 456 | return 0; |
| 457 | } |
| 458 | |
| 459 | #ifdef CONFIG_PM |
| 460 | static int asr_ts_suspend(struct device *dev) |
| 461 | { |
| 462 | struct asr_tsc *tsc = dev_get_drvdata(dev); |
| 463 | struct input_dev *input = tsc->dev; |
| 464 | |
| 465 | /* |
| 466 | * Suspend and resume can be called when the device hasn't been |
| 467 | * enabled. If there are no users that have the device open, then |
| 468 | * avoid calling the TSC stop and start functions as the TSC |
| 469 | * isn't yet clocked. |
| 470 | */ |
| 471 | mutex_lock(&input->mutex); |
| 472 | |
| 473 | if (input->users) { |
| 474 | if (device_may_wakeup(dev)) { |
| 475 | enable_irq_wake(tsc->down_irq); |
| 476 | enable_irq_wake(tsc->rdy_irq); |
| 477 | enable_irq_wake(tsc->up_irq); |
| 478 | } else |
| 479 | asr_stop_tsc(tsc); |
| 480 | } |
| 481 | |
| 482 | mutex_unlock(&input->mutex); |
| 483 | |
| 484 | return 0; |
| 485 | } |
| 486 | |
| 487 | static int asr_ts_resume(struct device *dev) |
| 488 | { |
| 489 | struct asr_tsc *tsc = dev_get_drvdata(dev); |
| 490 | struct input_dev *input = tsc->dev; |
| 491 | |
| 492 | mutex_lock(&input->mutex); |
| 493 | |
| 494 | if (input->users) { |
| 495 | if (device_may_wakeup(dev)) { |
| 496 | disable_irq_wake(tsc->down_irq); |
| 497 | disable_irq_wake(tsc->rdy_irq); |
| 498 | disable_irq_wake(tsc->up_irq); |
| 499 | } else |
| 500 | asr_setup_tsc(tsc); |
| 501 | } |
| 502 | |
| 503 | mutex_unlock(&input->mutex); |
| 504 | |
| 505 | return 0; |
| 506 | } |
| 507 | |
| 508 | static const struct dev_pm_ops asr_ts_pm_ops = { |
| 509 | .suspend = asr_ts_suspend, |
| 510 | .resume = asr_ts_resume, |
| 511 | }; |
| 512 | #define ASR_TS_PM_OPS (&asr_ts_pm_ops) |
| 513 | #else |
| 514 | #define ASR_TS_PM_OPS NULL |
| 515 | #endif |
| 516 | |
| 517 | #ifdef CONFIG_OF |
| 518 | static const struct of_device_id asr_tsc_of_match[] = { |
| 519 | { .compatible = "asr,tsc", }, |
| 520 | { }, |
| 521 | }; |
| 522 | MODULE_DEVICE_TABLE(of, asr_tsc_of_match); |
| 523 | #endif |
| 524 | |
| 525 | static struct platform_driver asr_ts_driver = { |
| 526 | .probe = asr_ts_probe, |
| 527 | .remove = asr_ts_remove, |
| 528 | .driver = { |
| 529 | .name = MOD_NAME, |
| 530 | .pm = ASR_TS_PM_OPS, |
| 531 | .of_match_table = of_match_ptr(asr_tsc_of_match), |
| 532 | }, |
| 533 | }; |
| 534 | module_platform_driver(asr_ts_driver); |
| 535 | |
| 536 | MODULE_AUTHOR("Mark Zhang <markzhang@asrmicro.com>"); |
| 537 | MODULE_DESCRIPTION("ASR TSC Driver"); |
| 538 | MODULE_LICENSE("GPL"); |
| 539 | MODULE_ALIAS("platform:asr_ts"); |
| 540 | |