1/* 2 * linux/drivers/video/metronomefb.c -- FB driver for Metronome controller 3 * 4 * Copyright (C) 2008, Jaya Kumar 5 * 6 * This file is subject to the terms and conditions of the GNU General Public 7 * License. See the file COPYING in the main directory of this archive for 8 * more details. 9 * 10 * Layout is based on skeletonfb.c by James Simmons and Geert Uytterhoeven. 11 * 12 * This work was made possible by help and equipment support from E-Ink 13 * Corporation. http://www.eink.com/ 14 * 15 * This driver is written to be used with the Metronome display controller. 16 * It is intended to be architecture independent. A board specific driver 17 * must be used to perform all the physical IO interactions. An example 18 * is provided as am200epd.c 19 * 20 */ 21#include <linux/module.h> 22#include <linux/kernel.h> 23#include <linux/errno.h> 24#include <linux/string.h> 25#include <linux/mm.h> 26#include <linux/vmalloc.h> 27#include <linux/delay.h> 28#include <linux/interrupt.h> 29#include <linux/fb.h> 30#include <linux/init.h> 31#include <linux/platform_device.h> 32#include <linux/list.h> 33#include <linux/firmware.h> 34#include <linux/dma-mapping.h> 35#include <linux/uaccess.h> 36#include <linux/irq.h> 37 38#include <video/metronomefb.h> 39 40#include <asm/unaligned.h> 41 42/* Display specific information */ 43#define DPY_W 832 44#define DPY_H 622 45 46static int user_wfm_size; 47 48/* frame differs from image. frame includes non-visible pixels */ 49struct epd_frame { 50 int fw; /* frame width */ 51 int fh; /* frame height */ 52 u16 config[4]; 53 int wfm_size; 54}; 55 56static struct epd_frame epd_frame_table[] = { 57 { 58 .fw = 832, 59 .fh = 622, 60 .config = { 61 15 /* sdlew */ 62 | 2 << 8 /* sdosz */ 63 | 0 << 11 /* sdor */ 64 | 0 << 12 /* sdces */ 65 | 0 << 15, /* sdcer */ 66 42 /* gdspl */ 67 | 1 << 8 /* gdr1 */ 68 | 1 << 9 /* sdshr */ 69 | 0 << 15, /* gdspp */ 70 18 /* gdspw */ 71 | 0 << 15, /* dispc */ 72 599 /* vdlc */ 73 | 0 << 11 /* dsi */ 74 | 0 << 12, /* dsic */ 75 }, 76 .wfm_size = 47001, 77 }, 78 { 79 .fw = 1088, 80 .fh = 791, 81 .config = { 82 0x0104, 83 0x031f, 84 0x0088, 85 0x02ff, 86 }, 87 .wfm_size = 46770, 88 }, 89 { 90 .fw = 1200, 91 .fh = 842, 92 .config = { 93 0x0101, 94 0x030e, 95 0x0012, 96 0x0280, 97 }, 98 .wfm_size = 46770, 99 }, 100}; 101 102static struct fb_fix_screeninfo metronomefb_fix = { 103 .id = "metronomefb", 104 .type = FB_TYPE_PACKED_PIXELS, 105 .visual = FB_VISUAL_STATIC_PSEUDOCOLOR, 106 .xpanstep = 0, 107 .ypanstep = 0, 108 .ywrapstep = 0, 109 .line_length = DPY_W, 110 .accel = FB_ACCEL_NONE, 111}; 112 113static struct fb_var_screeninfo metronomefb_var = { 114 .xres = DPY_W, 115 .yres = DPY_H, 116 .xres_virtual = DPY_W, 117 .yres_virtual = DPY_H, 118 .bits_per_pixel = 8, 119 .grayscale = 1, 120 .nonstd = 1, 121 .red = { 4, 3, 0 }, 122 .green = { 0, 0, 0 }, 123 .blue = { 0, 0, 0 }, 124 .transp = { 0, 0, 0 }, 125}; 126 127/* the waveform structure that is coming from userspace firmware */ 128struct waveform_hdr { 129 u8 stuff[32]; 130 131 u8 wmta[3]; 132 u8 fvsn; 133 134 u8 luts; 135 u8 mc; 136 u8 trc; 137 u8 stuff3; 138 139 u8 endb; 140 u8 swtb; 141 u8 stuff2a[2]; 142 143 u8 stuff2b[3]; 144 u8 wfm_cs; 145} __attribute__ ((packed)); 146 147/* main metronomefb functions */ 148static u8 calc_cksum(int start, int end, u8 *mem) 149{ 150 u8 tmp = 0; 151 int i; 152 153 for (i = start; i < end; i++) 154 tmp += mem[i]; 155 156 return tmp; 157} 158 159static u16 calc_img_cksum(u16 *start, int length) 160{ 161 u16 tmp = 0; 162 163 while (length--) 164 tmp += *start++; 165 166 return tmp; 167} 168 169/* here we decode the incoming waveform file and populate metromem */ 170static int load_waveform(u8 *mem, size_t size, int m, int t, 171 struct metronomefb_par *par) 172{ 173 int tta; 174 int wmta; 175 int trn = 0; 176 int i; 177 unsigned char v; 178 u8 cksum; 179 int cksum_idx; 180 int wfm_idx, owfm_idx; 181 int mem_idx = 0; 182 struct waveform_hdr *wfm_hdr; 183 u8 *metromem = par->metromem_wfm; 184 struct device *dev = par->info->dev; 185 186 if (user_wfm_size) 187 epd_frame_table[par->dt].wfm_size = user_wfm_size; 188 189 if (size != epd_frame_table[par->dt].wfm_size) { 190 dev_err(dev, "Error: unexpected size %Zd != %d\n", size, 191 epd_frame_table[par->dt].wfm_size); 192 return -EINVAL; 193 } 194 195 wfm_hdr = (struct waveform_hdr *) mem; 196 197 if (wfm_hdr->fvsn != 1) { 198 dev_err(dev, "Error: bad fvsn %x\n", wfm_hdr->fvsn); 199 return -EINVAL; 200 } 201 if (wfm_hdr->luts != 0) { 202 dev_err(dev, "Error: bad luts %x\n", wfm_hdr->luts); 203 return -EINVAL; 204 } 205 cksum = calc_cksum(32, 47, mem); 206 if (cksum != wfm_hdr->wfm_cs) { 207 dev_err(dev, "Error: bad cksum %x != %x\n", cksum, 208 wfm_hdr->wfm_cs); 209 return -EINVAL; 210 } 211 wfm_hdr->mc += 1; 212 wfm_hdr->trc += 1; 213 for (i = 0; i < 5; i++) { 214 if (*(wfm_hdr->stuff2a + i) != 0) { 215 dev_err(dev, "Error: unexpected value in padding\n"); 216 return -EINVAL; 217 } 218 } 219 220 /* calculating trn. trn is something used to index into 221 the waveform. presumably selecting the right one for the 222 desired temperature. it works out the offset of the first 223 v that exceeds the specified temperature */ 224 if ((sizeof(*wfm_hdr) + wfm_hdr->trc) > size) 225 return -EINVAL; 226 227 for (i = sizeof(*wfm_hdr); i <= sizeof(*wfm_hdr) + wfm_hdr->trc; i++) { 228 if (mem[i] > t) { 229 trn = i - sizeof(*wfm_hdr) - 1; 230 break; 231 } 232 } 233 234 /* check temperature range table checksum */ 235 cksum_idx = sizeof(*wfm_hdr) + wfm_hdr->trc + 1; 236 if (cksum_idx > size) 237 return -EINVAL; 238 cksum = calc_cksum(sizeof(*wfm_hdr), cksum_idx, mem); 239 if (cksum != mem[cksum_idx]) { 240 dev_err(dev, "Error: bad temperature range table cksum" 241 " %x != %x\n", cksum, mem[cksum_idx]); 242 return -EINVAL; 243 } 244 245 /* check waveform mode table address checksum */ 246 wmta = get_unaligned_le32(wfm_hdr->wmta) & 0x00FFFFFF; 247 cksum_idx = wmta + m*4 + 3; 248 if (cksum_idx > size) 249 return -EINVAL; 250 cksum = calc_cksum(cksum_idx - 3, cksum_idx, mem); 251 if (cksum != mem[cksum_idx]) { 252 dev_err(dev, "Error: bad mode table address cksum" 253 " %x != %x\n", cksum, mem[cksum_idx]); 254 return -EINVAL; 255 } 256 257 /* check waveform temperature table address checksum */ 258 tta = get_unaligned_le32(mem + wmta + m * 4) & 0x00FFFFFF; 259 cksum_idx = tta + trn*4 + 3; 260 if (cksum_idx > size) 261 return -EINVAL; 262 cksum = calc_cksum(cksum_idx - 3, cksum_idx, mem); 263 if (cksum != mem[cksum_idx]) { 264 dev_err(dev, "Error: bad temperature table address cksum" 265 " %x != %x\n", cksum, mem[cksum_idx]); 266 return -EINVAL; 267 } 268 269 /* here we do the real work of putting the waveform into the 270 metromem buffer. this does runlength decoding of the waveform */ 271 wfm_idx = get_unaligned_le32(mem + tta + trn * 4) & 0x00FFFFFF; 272 owfm_idx = wfm_idx; 273 if (wfm_idx > size) 274 return -EINVAL; 275 while (wfm_idx < size) { 276 unsigned char rl; 277 v = mem[wfm_idx++]; 278 if (v == wfm_hdr->swtb) { 279 while (((v = mem[wfm_idx++]) != wfm_hdr->swtb) && 280 wfm_idx < size) 281 metromem[mem_idx++] = v; 282 283 continue; 284 } 285 286 if (v == wfm_hdr->endb) 287 break; 288 289 rl = mem[wfm_idx++]; 290 for (i = 0; i <= rl; i++) 291 metromem[mem_idx++] = v; 292 } 293 294 cksum_idx = wfm_idx; 295 if (cksum_idx > size) 296 return -EINVAL; 297 cksum = calc_cksum(owfm_idx, cksum_idx, mem); 298 if (cksum != mem[cksum_idx]) { 299 dev_err(dev, "Error: bad waveform data cksum" 300 " %x != %x\n", cksum, mem[cksum_idx]); 301 return -EINVAL; 302 } 303 par->frame_count = (mem_idx/64); 304 305 return 0; 306} 307 308static int metronome_display_cmd(struct metronomefb_par *par) 309{ 310 int i; 311 u16 cs; 312 u16 opcode; 313 static u8 borderval; 314 315 /* setup display command 316 we can't immediately set the opcode since the controller 317 will try parse the command before we've set it all up 318 so we just set cs here and set the opcode at the end */ 319 320 if (par->metromem_cmd->opcode == 0xCC40) 321 opcode = cs = 0xCC41; 322 else 323 opcode = cs = 0xCC40; 324 325 /* set the args ( 2 bytes ) for display */ 326 i = 0; 327 par->metromem_cmd->args[i] = 1 << 3 /* border update */ 328 | ((borderval++ % 4) & 0x0F) << 4 329 | (par->frame_count - 1) << 8; 330 cs += par->metromem_cmd->args[i++]; 331 332 /* the rest are 0 */ 333 memset((u8 *) (par->metromem_cmd->args + i), 0, (32-i)*2); 334 335 par->metromem_cmd->csum = cs; 336 par->metromem_cmd->opcode = opcode; /* display cmd */ 337 338 return par->board->met_wait_event_intr(par); 339} 340 341static int metronome_powerup_cmd(struct metronomefb_par *par) 342{ 343 int i; 344 u16 cs; 345 346 /* setup power up command */ 347 par->metromem_cmd->opcode = 0x1234; /* pwr up pseudo cmd */ 348 cs = par->metromem_cmd->opcode; 349 350 /* set pwr1,2,3 to 1024 */ 351 for (i = 0; i < 3; i++) { 352 par->metromem_cmd->args[i] = 1024; 353 cs += par->metromem_cmd->args[i]; 354 } 355 356 /* the rest are 0 */ 357 memset((u8 *) (par->metromem_cmd->args + i), 0, (32-i)*2); 358 359 par->metromem_cmd->csum = cs; 360 361 msleep(1); 362 par->board->set_rst(par, 1); 363 364 msleep(1); 365 par->board->set_stdby(par, 1); 366 367 return par->board->met_wait_event(par); 368} 369 370static int metronome_config_cmd(struct metronomefb_par *par) 371{ 372 /* setup config command 373 we can't immediately set the opcode since the controller 374 will try parse the command before we've set it all up */ 375 376 memcpy(par->metromem_cmd->args, epd_frame_table[par->dt].config, 377 sizeof(epd_frame_table[par->dt].config)); 378 /* the rest are 0 */ 379 memset((u8 *) (par->metromem_cmd->args + 4), 0, (32-4)*2); 380 381 par->metromem_cmd->csum = 0xCC10; 382 par->metromem_cmd->csum += calc_img_cksum(par->metromem_cmd->args, 4); 383 par->metromem_cmd->opcode = 0xCC10; /* config cmd */ 384 385 return par->board->met_wait_event(par); 386} 387 388static int metronome_init_cmd(struct metronomefb_par *par) 389{ 390 int i; 391 u16 cs; 392 393 /* setup init command 394 we can't immediately set the opcode since the controller 395 will try parse the command before we've set it all up 396 so we just set cs here and set the opcode at the end */ 397 398 cs = 0xCC20; 399 400 /* set the args ( 2 bytes ) for init */ 401 i = 0; 402 par->metromem_cmd->args[i] = 0; 403 cs += par->metromem_cmd->args[i++]; 404 405 /* the rest are 0 */ 406 memset((u8 *) (par->metromem_cmd->args + i), 0, (32-i)*2); 407 408 par->metromem_cmd->csum = cs; 409 par->metromem_cmd->opcode = 0xCC20; /* init cmd */ 410 411 return par->board->met_wait_event(par); 412} 413 414static int metronome_init_regs(struct metronomefb_par *par) 415{ 416 int res; 417 418 res = par->board->setup_io(par); 419 if (res) 420 return res; 421 422 res = metronome_powerup_cmd(par); 423 if (res) 424 return res; 425 426 res = metronome_config_cmd(par); 427 if (res) 428 return res; 429 430 res = metronome_init_cmd(par); 431 432 return res; 433} 434 435static void metronomefb_dpy_update(struct metronomefb_par *par) 436{ 437 int fbsize; 438 u16 cksum; 439 unsigned char *buf = (unsigned char __force *)par->info->screen_base; 440 441 fbsize = par->info->fix.smem_len; 442 /* copy from vm to metromem */ 443 memcpy(par->metromem_img, buf, fbsize); 444 445 cksum = calc_img_cksum((u16 *) par->metromem_img, fbsize/2); 446 *((u16 *)(par->metromem_img) + fbsize/2) = cksum; 447 metronome_display_cmd(par); 448} 449 450static u16 metronomefb_dpy_update_page(struct metronomefb_par *par, int index) 451{ 452 int i; 453 u16 csum = 0; 454 u16 *buf = (u16 __force *)(par->info->screen_base + index); 455 u16 *img = (u16 *)(par->metromem_img + index); 456 457 /* swizzle from vm to metromem and recalc cksum at the same time*/ 458 for (i = 0; i < PAGE_SIZE/2; i++) { 459 *(img + i) = (buf[i] << 5) & 0xE0E0; 460 csum += *(img + i); 461 } 462 return csum; 463} 464 465/* this is called back from the deferred io workqueue */ 466static void metronomefb_dpy_deferred_io(struct fb_info *info, 467 struct list_head *pagelist) 468{ 469 u16 cksum; 470 struct page *cur; 471 struct fb_deferred_io *fbdefio = info->fbdefio; 472 struct metronomefb_par *par = info->par; 473 474 /* walk the written page list and swizzle the data */ 475 list_for_each_entry(cur, &fbdefio->pagelist, lru) { 476 cksum = metronomefb_dpy_update_page(par, 477 (cur->index << PAGE_SHIFT)); 478 par->metromem_img_csum -= par->csum_table[cur->index]; 479 par->csum_table[cur->index] = cksum; 480 par->metromem_img_csum += cksum; 481 } 482 483 metronome_display_cmd(par); 484} 485 486static void metronomefb_fillrect(struct fb_info *info, 487 const struct fb_fillrect *rect) 488{ 489 struct metronomefb_par *par = info->par; 490 491 sys_fillrect(info, rect); 492 metronomefb_dpy_update(par); 493} 494 495static void metronomefb_copyarea(struct fb_info *info, 496 const struct fb_copyarea *area) 497{ 498 struct metronomefb_par *par = info->par; 499 500 sys_copyarea(info, area); 501 metronomefb_dpy_update(par); 502} 503 504static void metronomefb_imageblit(struct fb_info *info, 505 const struct fb_image *image) 506{ 507 struct metronomefb_par *par = info->par; 508 509 sys_imageblit(info, image); 510 metronomefb_dpy_update(par); 511} 512 513/* 514 * this is the slow path from userspace. they can seek and write to 515 * the fb. it is based on fb_sys_write 516 */ 517static ssize_t metronomefb_write(struct fb_info *info, const char __user *buf, 518 size_t count, loff_t *ppos) 519{ 520 struct metronomefb_par *par = info->par; 521 unsigned long p = *ppos; 522 void *dst; 523 int err = 0; 524 unsigned long total_size; 525 526 if (info->state != FBINFO_STATE_RUNNING) 527 return -EPERM; 528 529 total_size = info->fix.smem_len; 530 531 if (p > total_size) 532 return -EFBIG; 533 534 if (count > total_size) { 535 err = -EFBIG; 536 count = total_size; 537 } 538 539 if (count + p > total_size) { 540 if (!err) 541 err = -ENOSPC; 542 543 count = total_size - p; 544 } 545 546 dst = (void __force *)(info->screen_base + p); 547 548 if (copy_from_user(dst, buf, count)) 549 err = -EFAULT; 550 551 if (!err) 552 *ppos += count; 553 554 metronomefb_dpy_update(par); 555 556 return (err) ? err : count; 557} 558 559static struct fb_ops metronomefb_ops = { 560 .owner = THIS_MODULE, 561 .fb_write = metronomefb_write, 562 .fb_fillrect = metronomefb_fillrect, 563 .fb_copyarea = metronomefb_copyarea, 564 .fb_imageblit = metronomefb_imageblit, 565}; 566 567static struct fb_deferred_io metronomefb_defio = { 568 .delay = HZ, 569 .deferred_io = metronomefb_dpy_deferred_io, 570}; 571 572static int metronomefb_probe(struct platform_device *dev) 573{ 574 struct fb_info *info; 575 struct metronome_board *board; 576 int retval = -ENOMEM; 577 int videomemorysize; 578 unsigned char *videomemory; 579 struct metronomefb_par *par; 580 const struct firmware *fw_entry; 581 int i; 582 int panel_type; 583 int fw, fh; 584 int epd_dt_index; 585 586 /* pick up board specific routines */ 587 board = dev->dev.platform_data; 588 if (!board) 589 return -EINVAL; 590 591 /* try to count device specific driver, if can't, platform recalls */ 592 if (!try_module_get(board->owner)) 593 return -ENODEV; 594 595 info = framebuffer_alloc(sizeof(struct metronomefb_par), &dev->dev); 596 if (!info) 597 goto err; 598 599 /* we have two blocks of memory. 600 info->screen_base which is vm, and is the fb used by apps. 601 par->metromem which is physically contiguous memory and 602 contains the display controller commands, waveform, 603 processed image data and padding. this is the data pulled 604 by the device's LCD controller and pushed to Metronome. 605 the metromem memory is allocated by the board driver and 606 is provided to us */ 607 608 panel_type = board->get_panel_type(); 609 switch (panel_type) { 610 case 6: 611 epd_dt_index = 0; 612 break; 613 case 8: 614 epd_dt_index = 1; 615 break; 616 case 97: 617 epd_dt_index = 2; 618 break; 619 default: 620 dev_err(&dev->dev, "Unexpected panel type. Defaulting to 6\n"); 621 epd_dt_index = 0; 622 break; 623 } 624 625 fw = epd_frame_table[epd_dt_index].fw; 626 fh = epd_frame_table[epd_dt_index].fh; 627 628 /* we need to add a spare page because our csum caching scheme walks 629 * to the end of the page */ 630 videomemorysize = PAGE_SIZE + (fw * fh); 631 videomemory = vzalloc(videomemorysize); 632 if (!videomemory) 633 goto err_fb_rel; 634 635 info->screen_base = (char __force __iomem *)videomemory; 636 info->fbops = &metronomefb_ops; 637 638 metronomefb_fix.line_length = fw; 639 metronomefb_var.xres = fw; 640 metronomefb_var.yres = fh; 641 metronomefb_var.xres_virtual = fw; 642 metronomefb_var.yres_virtual = fh; 643 info->var = metronomefb_var; 644 info->fix = metronomefb_fix; 645 info->fix.smem_len = videomemorysize; 646 par = info->par; 647 par->info = info; 648 par->board = board; 649 par->dt = epd_dt_index; 650 init_waitqueue_head(&par->waitq); 651 652 /* this table caches per page csum values. */ 653 par->csum_table = vmalloc(videomemorysize/PAGE_SIZE); 654 if (!par->csum_table) 655 goto err_vfree; 656 657 /* the physical framebuffer that we use is setup by 658 * the platform device driver. It will provide us 659 * with cmd, wfm and image memory in a contiguous area. */ 660 retval = board->setup_fb(par); 661 if (retval) { 662 dev_err(&dev->dev, "Failed to setup fb\n"); 663 goto err_csum_table; 664 } 665 666 /* after this point we should have a framebuffer */ 667 if ((!par->metromem_wfm) || (!par->metromem_img) || 668 (!par->metromem_dma)) { 669 dev_err(&dev->dev, "fb access failure\n"); 670 retval = -EINVAL; 671 goto err_csum_table; 672 } 673 674 info->fix.smem_start = par->metromem_dma; 675 676 /* load the waveform in. assume mode 3, temp 31 for now 677 a) request the waveform file from userspace 678 b) process waveform and decode into metromem */ 679 retval = request_firmware(&fw_entry, "metronome.wbf", &dev->dev); 680 if (retval < 0) { 681 dev_err(&dev->dev, "Failed to get waveform\n"); 682 goto err_csum_table; 683 } 684 685 retval = load_waveform((u8 *) fw_entry->data, fw_entry->size, 3, 31, 686 par); 687 release_firmware(fw_entry); 688 if (retval < 0) { 689 dev_err(&dev->dev, "Failed processing waveform\n"); 690 goto err_csum_table; 691 } 692 693 retval = board->setup_irq(info); 694 if (retval) 695 goto err_csum_table; 696 697 retval = metronome_init_regs(par); 698 if (retval < 0) 699 goto err_free_irq; 700 701 info->flags = FBINFO_FLAG_DEFAULT | FBINFO_VIRTFB; 702 703 info->fbdefio = &metronomefb_defio; 704 fb_deferred_io_init(info); 705 706 retval = fb_alloc_cmap(&info->cmap, 8, 0); 707 if (retval < 0) { 708 dev_err(&dev->dev, "Failed to allocate colormap\n"); 709 goto err_free_irq; 710 } 711 712 /* set cmap */ 713 for (i = 0; i < 8; i++) 714 info->cmap.red[i] = (((2*i)+1)*(0xFFFF))/16; 715 memcpy(info->cmap.green, info->cmap.red, sizeof(u16)*8); 716 memcpy(info->cmap.blue, info->cmap.red, sizeof(u16)*8); 717 718 retval = register_framebuffer(info); 719 if (retval < 0) 720 goto err_cmap; 721 722 platform_set_drvdata(dev, info); 723 724 dev_dbg(&dev->dev, 725 "fb%d: Metronome frame buffer device, using %dK of video" 726 " memory\n", info->node, videomemorysize >> 10); 727 728 return 0; 729 730err_cmap: 731 fb_dealloc_cmap(&info->cmap); 732err_free_irq: 733 board->cleanup(par); 734err_csum_table: 735 vfree(par->csum_table); 736err_vfree: 737 vfree(videomemory); 738err_fb_rel: 739 framebuffer_release(info); 740err: 741 module_put(board->owner); 742 return retval; 743} 744 745static int metronomefb_remove(struct platform_device *dev) 746{ 747 struct fb_info *info = platform_get_drvdata(dev); 748 749 if (info) { 750 struct metronomefb_par *par = info->par; 751 752 unregister_framebuffer(info); 753 fb_deferred_io_cleanup(info); 754 fb_dealloc_cmap(&info->cmap); 755 par->board->cleanup(par); 756 vfree(par->csum_table); 757 vfree((void __force *)info->screen_base); 758 module_put(par->board->owner); 759 dev_dbg(&dev->dev, "calling release\n"); 760 framebuffer_release(info); 761 } 762 return 0; 763} 764 765static struct platform_driver metronomefb_driver = { 766 .probe = metronomefb_probe, 767 .remove = metronomefb_remove, 768 .driver = { 769 .name = "metronomefb", 770 }, 771}; 772module_platform_driver(metronomefb_driver); 773 774module_param(user_wfm_size, uint, 0); 775MODULE_PARM_DESC(user_wfm_size, "Set custom waveform size"); 776 777MODULE_DESCRIPTION("fbdev driver for Metronome controller"); 778MODULE_AUTHOR("Jaya Kumar"); 779MODULE_LICENSE("GPL"); 780