diff options
-rw-r--r-- | arch/parisc/kernel/time.c | 8 | ||||
-rw-r--r-- | drivers/parisc/led.c | 225 | ||||
-rw-r--r-- | include/asm-parisc/led.h | 3 |
3 files changed, 112 insertions, 124 deletions
diff --git a/arch/parisc/kernel/time.c b/arch/parisc/kernel/time.c index 163cdf39be20..bc979e1abdec 100644 --- a/arch/parisc/kernel/time.c +++ b/arch/parisc/kernel/time.c | |||
@@ -89,14 +89,6 @@ irqreturn_t timer_interrupt(int irq, void *dev_id, struct pt_regs *regs) | |||
89 | } | 89 | } |
90 | } | 90 | } |
91 | 91 | ||
92 | #ifdef CONFIG_CHASSIS_LCD_LED | ||
93 | /* Only schedule the led tasklet on cpu 0, and only if it | ||
94 | * is enabled. | ||
95 | */ | ||
96 | if (cpu == 0 && !atomic_read(&led_tasklet.count)) | ||
97 | tasklet_schedule(&led_tasklet); | ||
98 | #endif | ||
99 | |||
100 | /* check soft power switch status */ | 92 | /* check soft power switch status */ |
101 | if (cpu == 0 && !atomic_read(&power_tasklet.count)) | 93 | if (cpu == 0 && !atomic_read(&power_tasklet.count)) |
102 | tasklet_schedule(&power_tasklet); | 94 | tasklet_schedule(&power_tasklet); |
diff --git a/drivers/parisc/led.c b/drivers/parisc/led.c index 286902298e33..95bd07b8b61b 100644 --- a/drivers/parisc/led.c +++ b/drivers/parisc/led.c | |||
@@ -18,6 +18,9 @@ | |||
18 | * Changes: | 18 | * Changes: |
19 | * - Audit copy_from_user in led_proc_write. | 19 | * - Audit copy_from_user in led_proc_write. |
20 | * Daniele Bellucci <bellucda@tiscali.it> | 20 | * Daniele Bellucci <bellucda@tiscali.it> |
21 | * - Switch from using a tasklet to a work queue, so the led_LCD_driver | ||
22 | * can sleep. | ||
23 | * David Pye <dmp@davidmpye.dyndns.org> | ||
21 | */ | 24 | */ |
22 | 25 | ||
23 | #include <linux/config.h> | 26 | #include <linux/config.h> |
@@ -37,6 +40,7 @@ | |||
37 | #include <linux/proc_fs.h> | 40 | #include <linux/proc_fs.h> |
38 | #include <linux/ctype.h> | 41 | #include <linux/ctype.h> |
39 | #include <linux/blkdev.h> | 42 | #include <linux/blkdev.h> |
43 | #include <linux/workqueue.h> | ||
40 | #include <linux/rcupdate.h> | 44 | #include <linux/rcupdate.h> |
41 | #include <asm/io.h> | 45 | #include <asm/io.h> |
42 | #include <asm/processor.h> | 46 | #include <asm/processor.h> |
@@ -47,25 +51,30 @@ | |||
47 | #include <asm/uaccess.h> | 51 | #include <asm/uaccess.h> |
48 | 52 | ||
49 | /* The control of the LEDs and LCDs on PARISC-machines have to be done | 53 | /* The control of the LEDs and LCDs on PARISC-machines have to be done |
50 | completely in software. The necessary calculations are done in a tasklet | 54 | completely in software. The necessary calculations are done in a work queue |
51 | which is scheduled at every timer interrupt and since the calculations | 55 | task which is scheduled regularly, and since the calculations may consume a |
52 | may consume relatively much CPU-time some of the calculations can be | 56 | relatively large amount of CPU time, some of the calculations can be |
53 | turned off with the following variables (controlled via procfs) */ | 57 | turned off with the following variables (controlled via procfs) */ |
54 | 58 | ||
55 | static int led_type = -1; | 59 | static int led_type = -1; |
56 | static int led_heartbeat = 1; | 60 | static unsigned char lastleds; /* LED state from most recent update */ |
57 | static int led_diskio = 1; | 61 | static unsigned int led_heartbeat = 1; |
58 | static int led_lanrxtx = 1; | 62 | static unsigned int led_diskio = 1; |
63 | static unsigned int led_lanrxtx = 1; | ||
59 | static char lcd_text[32]; | 64 | static char lcd_text[32]; |
60 | static char lcd_text_default[32]; | 65 | static char lcd_text_default[32]; |
61 | 66 | ||
67 | |||
68 | static struct workqueue_struct *led_wq; | ||
69 | static void led_work_func(void *); | ||
70 | static DECLARE_WORK(led_task, led_work_func, NULL); | ||
71 | |||
62 | #if 0 | 72 | #if 0 |
63 | #define DPRINTK(x) printk x | 73 | #define DPRINTK(x) printk x |
64 | #else | 74 | #else |
65 | #define DPRINTK(x) | 75 | #define DPRINTK(x) |
66 | #endif | 76 | #endif |
67 | 77 | ||
68 | |||
69 | struct lcd_block { | 78 | struct lcd_block { |
70 | unsigned char command; /* stores the command byte */ | 79 | unsigned char command; /* stores the command byte */ |
71 | unsigned char on; /* value for turning LED on */ | 80 | unsigned char on; /* value for turning LED on */ |
@@ -116,12 +125,27 @@ lcd_info __attribute__((aligned(8))) = | |||
116 | #define LCD_DATA_REG lcd_info.lcd_data_reg_addr | 125 | #define LCD_DATA_REG lcd_info.lcd_data_reg_addr |
117 | #define LED_DATA_REG lcd_info.lcd_cmd_reg_addr /* LASI & ASP only */ | 126 | #define LED_DATA_REG lcd_info.lcd_cmd_reg_addr /* LASI & ASP only */ |
118 | 127 | ||
128 | #define LED_HASLCD 1 | ||
129 | #define LED_NOLCD 0 | ||
130 | |||
131 | /* The workqueue must be created at init-time */ | ||
132 | static int start_task(void) | ||
133 | { | ||
134 | /* Display the default text now */ | ||
135 | if (led_type == LED_HASLCD) lcd_print( lcd_text_default ); | ||
136 | |||
137 | /* Create the work queue and queue the LED task */ | ||
138 | led_wq = create_singlethread_workqueue("led_wq"); | ||
139 | queue_work(led_wq, &led_task); | ||
140 | |||
141 | return 0; | ||
142 | } | ||
143 | |||
144 | device_initcall(start_task); | ||
119 | 145 | ||
120 | /* ptr to LCD/LED-specific function */ | 146 | /* ptr to LCD/LED-specific function */ |
121 | static void (*led_func_ptr) (unsigned char); | 147 | static void (*led_func_ptr) (unsigned char); |
122 | 148 | ||
123 | #define LED_HASLCD 1 | ||
124 | #define LED_NOLCD 0 | ||
125 | #ifdef CONFIG_PROC_FS | 149 | #ifdef CONFIG_PROC_FS |
126 | static int led_proc_read(char *page, char **start, off_t off, int count, | 150 | static int led_proc_read(char *page, char **start, off_t off, int count, |
127 | int *eof, void *data) | 151 | int *eof, void *data) |
@@ -286,52 +310,35 @@ static void led_LASI_driver(unsigned char leds) | |||
286 | /* | 310 | /* |
287 | ** | 311 | ** |
288 | ** led_LCD_driver() | 312 | ** led_LCD_driver() |
289 | ** | ||
290 | ** The logic of the LCD driver is, that we write at every scheduled call | ||
291 | ** only to one of LCD_CMD_REG _or_ LCD_DATA_REG - registers. | ||
292 | ** That way we don't need to let this tasklet busywait for min_cmd_delay | ||
293 | ** milliseconds. | ||
294 | ** | ||
295 | ** TODO: check the value of "min_cmd_delay" against the value of HZ. | ||
296 | ** | 313 | ** |
297 | */ | 314 | */ |
298 | static void led_LCD_driver(unsigned char leds) | 315 | static void led_LCD_driver(unsigned char leds) |
299 | { | 316 | { |
300 | static int last_index; /* 0:heartbeat, 1:disk, 2:lan_in, 3:lan_out */ | 317 | static int i; |
301 | static int last_was_cmd;/* 0: CMD was written last, 1: DATA was last */ | 318 | static unsigned char mask[4] = { LED_HEARTBEAT, LED_DISK_IO, |
302 | struct lcd_block *block_ptr; | 319 | LED_LAN_RCV, LED_LAN_TX }; |
303 | int value; | ||
304 | |||
305 | switch (last_index) { | ||
306 | case 0: block_ptr = &lcd_info.heartbeat; | ||
307 | value = leds & LED_HEARTBEAT; | ||
308 | break; | ||
309 | case 1: block_ptr = &lcd_info.disk_io; | ||
310 | value = leds & LED_DISK_IO; | ||
311 | break; | ||
312 | case 2: block_ptr = &lcd_info.lan_rcv; | ||
313 | value = leds & LED_LAN_RCV; | ||
314 | break; | ||
315 | case 3: block_ptr = &lcd_info.lan_tx; | ||
316 | value = leds & LED_LAN_TX; | ||
317 | break; | ||
318 | default: /* should never happen: */ | ||
319 | return; | ||
320 | } | ||
321 | |||
322 | if (last_was_cmd) { | ||
323 | /* write the value to the LCD data port */ | ||
324 | gsc_writeb( value ? block_ptr->on : block_ptr->off, LCD_DATA_REG ); | ||
325 | } else { | ||
326 | /* write the command-byte to the LCD command register */ | ||
327 | gsc_writeb( block_ptr->command, LCD_CMD_REG ); | ||
328 | } | ||
329 | 320 | ||
330 | /* now update the vars for the next interrupt iteration */ | 321 | static struct lcd_block * blockp[4] = { |
331 | if (++last_was_cmd == 2) { /* switch between cmd & data */ | 322 | &lcd_info.heartbeat, |
332 | last_was_cmd = 0; | 323 | &lcd_info.disk_io, |
333 | if (++last_index == 4) | 324 | &lcd_info.lan_rcv, |
334 | last_index = 0; /* switch back to heartbeat index */ | 325 | &lcd_info.lan_tx |
326 | }; | ||
327 | |||
328 | /* Convert min_cmd_delay to milliseconds */ | ||
329 | unsigned int msec_cmd_delay = 1 + (lcd_info.min_cmd_delay / 1000); | ||
330 | |||
331 | for (i=0; i<4; ++i) | ||
332 | { | ||
333 | if ((leds & mask[i]) != (lastleds & mask[i])) | ||
334 | { | ||
335 | gsc_writeb( blockp[i]->command, LCD_CMD_REG ); | ||
336 | msleep(msec_cmd_delay); | ||
337 | |||
338 | gsc_writeb( leds & mask[i] ? blockp[i]->on : | ||
339 | blockp[i]->off, LCD_DATA_REG ); | ||
340 | msleep(msec_cmd_delay); | ||
341 | } | ||
335 | } | 342 | } |
336 | } | 343 | } |
337 | 344 | ||
@@ -356,7 +363,7 @@ static __inline__ int led_get_net_activity(void) | |||
356 | 363 | ||
357 | rx_total = tx_total = 0; | 364 | rx_total = tx_total = 0; |
358 | 365 | ||
359 | /* we are running as tasklet, so locking dev_base | 366 | /* we are running as a workqueue task, so locking dev_base |
360 | * for reading should be OK */ | 367 | * for reading should be OK */ |
361 | read_lock(&dev_base_lock); | 368 | read_lock(&dev_base_lock); |
362 | rcu_read_lock(); | 369 | rcu_read_lock(); |
@@ -405,7 +412,7 @@ static __inline__ int led_get_diskio_activity(void) | |||
405 | static unsigned long last_pgpgin, last_pgpgout; | 412 | static unsigned long last_pgpgin, last_pgpgout; |
406 | struct page_state pgstat; | 413 | struct page_state pgstat; |
407 | int changed; | 414 | int changed; |
408 | 415 | ||
409 | get_full_page_state(&pgstat); /* get no of sectors in & out */ | 416 | get_full_page_state(&pgstat); /* get no of sectors in & out */ |
410 | 417 | ||
411 | /* Just use a very simple calculation here. Do not care about overflow, | 418 | /* Just use a very simple calculation here. Do not care about overflow, |
@@ -413,86 +420,70 @@ static __inline__ int led_get_diskio_activity(void) | |||
413 | changed = (pgstat.pgpgin != last_pgpgin) || (pgstat.pgpgout != last_pgpgout); | 420 | changed = (pgstat.pgpgin != last_pgpgin) || (pgstat.pgpgout != last_pgpgout); |
414 | last_pgpgin = pgstat.pgpgin; | 421 | last_pgpgin = pgstat.pgpgin; |
415 | last_pgpgout = pgstat.pgpgout; | 422 | last_pgpgout = pgstat.pgpgout; |
416 | 423 | ||
417 | return (changed ? LED_DISK_IO : 0); | 424 | return (changed ? LED_DISK_IO : 0); |
418 | } | 425 | } |
419 | 426 | ||
420 | 427 | ||
421 | 428 | ||
422 | /* | 429 | /* |
423 | ** led_tasklet_func() | 430 | ** led_work_func() |
424 | ** | 431 | ** |
425 | ** is scheduled at every timer interrupt from time.c and | 432 | ** manages when and which chassis LCD/LED gets updated |
426 | ** updates the chassis LCD/LED | ||
427 | 433 | ||
428 | TODO: | 434 | TODO: |
429 | - display load average (older machines like 715/64 have 4 "free" LED's for that) | 435 | - display load average (older machines like 715/64 have 4 "free" LED's for that) |
430 | - optimizations | 436 | - optimizations |
431 | */ | 437 | */ |
432 | 438 | ||
433 | #define HEARTBEAT_LEN (HZ*6/100) | 439 | #define HEARTBEAT_LEN (HZ*10/100) |
434 | #define HEARTBEAT_2ND_RANGE_START (HZ*22/100) | 440 | #define HEARTBEAT_2ND_RANGE_START (HZ*28/100) |
435 | #define HEARTBEAT_2ND_RANGE_END (HEARTBEAT_2ND_RANGE_START + HEARTBEAT_LEN) | 441 | #define HEARTBEAT_2ND_RANGE_END (HEARTBEAT_2ND_RANGE_START + HEARTBEAT_LEN) |
436 | 442 | ||
437 | #define NORMALIZED_COUNT(count) (count/(HZ/100)) | 443 | #define LED_UPDATE_INTERVAL (1 + (HZ*19/1000)) |
438 | 444 | ||
439 | static void led_tasklet_func(unsigned long unused) | 445 | static void led_work_func (void *unused) |
440 | { | 446 | { |
441 | static unsigned char lastleds; | 447 | static unsigned long last_jiffies; |
442 | unsigned char currentleds; /* stores current value of the LEDs */ | ||
443 | static unsigned long count; /* static incremented value, not wrapped */ | ||
444 | static unsigned long count_HZ; /* counter in range 0..HZ */ | 448 | static unsigned long count_HZ; /* counter in range 0..HZ */ |
449 | unsigned char currentleds = 0; /* stores current value of the LEDs */ | ||
445 | 450 | ||
446 | /* exit if not initialized */ | 451 | /* exit if not initialized */ |
447 | if (!led_func_ptr) | 452 | if (!led_func_ptr) |
448 | return; | 453 | return; |
449 | 454 | ||
450 | /* increment the local counters */ | 455 | /* increment the heartbeat timekeeper */ |
451 | ++count; | 456 | count_HZ += jiffies - last_jiffies; |
452 | if (++count_HZ == HZ) | 457 | last_jiffies = jiffies; |
458 | if (count_HZ >= HZ) | ||
453 | count_HZ = 0; | 459 | count_HZ = 0; |
454 | 460 | ||
455 | currentleds = lastleds; | 461 | if (likely(led_heartbeat)) |
456 | |||
457 | if (led_heartbeat) | ||
458 | { | ||
459 | /* flash heartbeat-LED like a real heart (2 x short then a long delay) */ | ||
460 | if (count_HZ<HEARTBEAT_LEN || | ||
461 | (count_HZ>=HEARTBEAT_2ND_RANGE_START && count_HZ<HEARTBEAT_2ND_RANGE_END)) | ||
462 | currentleds |= LED_HEARTBEAT; | ||
463 | else | ||
464 | currentleds &= ~LED_HEARTBEAT; | ||
465 | } | ||
466 | |||
467 | /* look for network activity and flash LEDs respectively */ | ||
468 | if (led_lanrxtx && ((NORMALIZED_COUNT(count)+(8/2)) & 7) == 0) | ||
469 | { | 462 | { |
470 | currentleds &= ~(LED_LAN_RCV | LED_LAN_TX); | 463 | /* flash heartbeat-LED like a real heart |
471 | currentleds |= led_get_net_activity(); | 464 | * (2 x short then a long delay) |
465 | */ | ||
466 | if (count_HZ < HEARTBEAT_LEN || | ||
467 | (count_HZ >= HEARTBEAT_2ND_RANGE_START && | ||
468 | count_HZ < HEARTBEAT_2ND_RANGE_END)) | ||
469 | currentleds |= LED_HEARTBEAT; | ||
472 | } | 470 | } |
473 | 471 | ||
474 | /* avoid to calculate diskio-stats at same irq as netio-stats */ | 472 | if (likely(led_lanrxtx)) currentleds |= led_get_net_activity(); |
475 | if (led_diskio && (NORMALIZED_COUNT(count) & 7) == 0) | 473 | if (likely(led_diskio)) currentleds |= led_get_diskio_activity(); |
476 | { | ||
477 | currentleds &= ~LED_DISK_IO; | ||
478 | currentleds |= led_get_diskio_activity(); | ||
479 | } | ||
480 | 474 | ||
481 | /* blink all LEDs twice a second if we got an Oops (HPMC) */ | 475 | /* blink all LEDs twice a second if we got an Oops (HPMC) */ |
482 | if (oops_in_progress) { | 476 | if (unlikely(oops_in_progress)) |
483 | currentleds = (count_HZ<=(HZ/2)) ? 0 : 0xff; | 477 | currentleds = (count_HZ<=(HZ/2)) ? 0 : 0xff; |
484 | } | ||
485 | |||
486 | /* update the LCD/LEDs */ | ||
487 | if (currentleds != lastleds) { | ||
488 | led_func_ptr(currentleds); | ||
489 | lastleds = currentleds; | ||
490 | } | ||
491 | } | ||
492 | 478 | ||
493 | /* main led tasklet struct (scheduled from time.c) */ | 479 | if (currentleds != lastleds) |
494 | DECLARE_TASKLET_DISABLED(led_tasklet, led_tasklet_func, 0); | 480 | { |
481 | led_func_ptr(currentleds); /* Update the LCD/LEDs */ | ||
482 | lastleds = currentleds; | ||
483 | } | ||
495 | 484 | ||
485 | queue_delayed_work(led_wq, &led_task, LED_UPDATE_INTERVAL); | ||
486 | } | ||
496 | 487 | ||
497 | /* | 488 | /* |
498 | ** led_halt() | 489 | ** led_halt() |
@@ -522,9 +513,13 @@ static int led_halt(struct notifier_block *nb, unsigned long event, void *buf) | |||
522 | default: return NOTIFY_DONE; | 513 | default: return NOTIFY_DONE; |
523 | } | 514 | } |
524 | 515 | ||
525 | /* completely stop the LED/LCD tasklet */ | 516 | /* Cancel the work item and delete the queue */ |
526 | tasklet_disable(&led_tasklet); | 517 | if (led_wq) { |
527 | 518 | cancel_rearming_delayed_workqueue(led_wq, &led_task); | |
519 | destroy_workqueue(led_wq); | ||
520 | led_wq = NULL; | ||
521 | } | ||
522 | |||
528 | if (lcd_info.model == DISPLAY_MODEL_LCD) | 523 | if (lcd_info.model == DISPLAY_MODEL_LCD) |
529 | lcd_print(txt); | 524 | lcd_print(txt); |
530 | else | 525 | else |
@@ -559,7 +554,6 @@ int __init register_led_driver(int model, unsigned long cmd_reg, unsigned long d | |||
559 | printk(KERN_INFO "LCD display at %lx,%lx registered\n", | 554 | printk(KERN_INFO "LCD display at %lx,%lx registered\n", |
560 | LCD_CMD_REG , LCD_DATA_REG); | 555 | LCD_CMD_REG , LCD_DATA_REG); |
561 | led_func_ptr = led_LCD_driver; | 556 | led_func_ptr = led_LCD_driver; |
562 | lcd_print( lcd_text_default ); | ||
563 | led_type = LED_HASLCD; | 557 | led_type = LED_HASLCD; |
564 | break; | 558 | break; |
565 | 559 | ||
@@ -589,9 +583,11 @@ int __init register_led_driver(int model, unsigned long cmd_reg, unsigned long d | |||
589 | initialized++; | 583 | initialized++; |
590 | register_reboot_notifier(&led_notifier); | 584 | register_reboot_notifier(&led_notifier); |
591 | 585 | ||
592 | /* start the led tasklet for the first time */ | 586 | /* Ensure the work is queued */ |
593 | tasklet_enable(&led_tasklet); | 587 | if (led_wq) { |
594 | 588 | queue_work(led_wq, &led_task); | |
589 | } | ||
590 | |||
595 | return 0; | 591 | return 0; |
596 | } | 592 | } |
597 | 593 | ||
@@ -626,8 +622,8 @@ void __init register_led_regions(void) | |||
626 | ** lcd_print() | 622 | ** lcd_print() |
627 | ** | 623 | ** |
628 | ** Displays the given string on the LCD-Display of newer machines. | 624 | ** Displays the given string on the LCD-Display of newer machines. |
629 | ** lcd_print() disables the timer-based led tasklet during its | 625 | ** lcd_print() disables/enables the timer-based led work queue to |
630 | ** execution and enables it afterwards again. | 626 | ** avoid a race condition while writing the CMD/DATA register pair. |
631 | ** | 627 | ** |
632 | */ | 628 | */ |
633 | int lcd_print( char *str ) | 629 | int lcd_print( char *str ) |
@@ -637,12 +633,13 @@ int lcd_print( char *str ) | |||
637 | if (!led_func_ptr || lcd_info.model != DISPLAY_MODEL_LCD) | 633 | if (!led_func_ptr || lcd_info.model != DISPLAY_MODEL_LCD) |
638 | return 0; | 634 | return 0; |
639 | 635 | ||
640 | /* temporarily disable the led tasklet */ | 636 | /* temporarily disable the led work task */ |
641 | tasklet_disable(&led_tasklet); | 637 | if (led_wq) |
638 | cancel_rearming_delayed_workqueue(led_wq, &led_task); | ||
642 | 639 | ||
643 | /* copy display string to buffer for procfs */ | 640 | /* copy display string to buffer for procfs */ |
644 | strlcpy(lcd_text, str, sizeof(lcd_text)); | 641 | strlcpy(lcd_text, str, sizeof(lcd_text)); |
645 | 642 | ||
646 | /* Set LCD Cursor to 1st character */ | 643 | /* Set LCD Cursor to 1st character */ |
647 | gsc_writeb(lcd_info.reset_cmd1, LCD_CMD_REG); | 644 | gsc_writeb(lcd_info.reset_cmd1, LCD_CMD_REG); |
648 | udelay(lcd_info.min_cmd_delay); | 645 | udelay(lcd_info.min_cmd_delay); |
@@ -656,8 +653,10 @@ int lcd_print( char *str ) | |||
656 | udelay(lcd_info.min_cmd_delay); | 653 | udelay(lcd_info.min_cmd_delay); |
657 | } | 654 | } |
658 | 655 | ||
659 | /* re-enable the led tasklet */ | 656 | /* re-queue the work */ |
660 | tasklet_enable(&led_tasklet); | 657 | if (led_wq) { |
658 | queue_work(led_wq, &led_task); | ||
659 | } | ||
661 | 660 | ||
662 | return lcd_info.lcd_width; | 661 | return lcd_info.lcd_width; |
663 | } | 662 | } |
diff --git a/include/asm-parisc/led.h b/include/asm-parisc/led.h index 1ac8ab6c580d..efadfd543ec6 100644 --- a/include/asm-parisc/led.h +++ b/include/asm-parisc/led.h | |||
@@ -23,9 +23,6 @@ | |||
23 | 23 | ||
24 | #define LED_CMD_REG_NONE 0 /* NULL == no addr for the cmd register */ | 24 | #define LED_CMD_REG_NONE 0 /* NULL == no addr for the cmd register */ |
25 | 25 | ||
26 | /* led tasklet struct */ | ||
27 | extern struct tasklet_struct led_tasklet; | ||
28 | |||
29 | /* register_led_driver() */ | 26 | /* register_led_driver() */ |
30 | int __init register_led_driver(int model, unsigned long cmd_reg, unsigned long data_reg); | 27 | int __init register_led_driver(int model, unsigned long cmd_reg, unsigned long data_reg); |
31 | 28 | ||