/* * Linux driver for ADM5120's GPIO * Copyright (C)2006 by Sergio Aguayo, Marcel Groothuis * * This file is covered by the GNU GPLv2. * * This driver is mostly compatible with the standard ADMtek * driver, but this doesn't use the major 166, only the major * 167, and things just work correctly. * * Differences between this and the old driver: * 1) Things work as expected: "LED ON" turns on the led and * "LED OFF" turns it off. In the old driver they are inverted * and due to an unknown bug inverting them again just messes * up the input pins. * 2) Input and output pins are mapped to the same major device * number, each identified with its mode. In te old one, major * 166 was used for output and 167 for input. That was the way * of reconfiguring a certain pin for input or output (writing * or reading to a 167:x or 166:x). * 3) Much cleaner code: this is no longer just a hack of a * probably larger driver. * 4) Support for the switch LEDs as GPIO * * Changes: * - 01/Feb/2006: Now uses spinlocks when writing to the GPIO_IO_CONF0 * register to avoid corruption. * - 25/April/2006: Added support for configuration of the ADM5120 switch * LEDs as GPIO. * - 11/Nov/2006: DL4HUF * Rewrite "concept code" for LED as GPIO, remove any bugs in LED code. * Add read back the real input and the real registersettings * from ADM5120 register. * Include Version in led.h, add any comment * - 06/Dez/2006: DL4HUF * Add support for all LED and GPIO, now 15 LED and 8 GPIO are available. * Add support for old ioctl-call. Now old demo "led_set" and userspace tool * to drive a Standard-LCD working again. * Add proc-interface to check the modes ("/proc/driver/led") * Only the ioctl-call "set led on/off" are available. The "get" call are not supported yet. * The numbering are changed ! All GPIO and LED are called sequential. * Number 0..7 are the real GPIO. * Number 8..22 are the LED from the Ethernet-switch in the order LED0, LED1, LED2 per port. * For old tools make a symlink from /dev/led0 to /dev/gpio0 ! * Clean the code for right logic among "mode" and "value". Big rewrite ! * Add support for blinking of LED. * Add support for setting default mode with all available modes for LED. * Read from /dev/gpiox prints the input-value 0/1 or the LED-Mode. * Write to /dev/gpiox can "led on","led off","led invert" to invert the logic for next on/off * and for the input reading * With "led switch x" x=mode you can now set all modes for led and set the gpio to input or output * No external tool needed anymore * - 28/Dez/2006 DL4HUF * Add support to read input (and mode) via ioctl. So usermode prgs can build own protocols like * seriall ADC, SPI and so on. */ #include #include #include #include #include #include #include #include #include "led.h" /* enable ONLY if you want to debug, as (specially during blinks) will generate lots of output */ #undef GPIO_DEBUG #define STATUS_BUF_LEN 30 struct adm5120gpio_data { int mode; int open_count; int status_num; int period; int invert; char status[STATUS_BUF_LEN]; char status_tmp[STATUS_BUF_LEN]; struct timer_list timer; }; /* default configuration for Edimax BR-6114WG/6104wg/6104K/6104KP */ static int adm5120gpio_init_data[] __initdata = { /* mode, value, invert */ GPIO_MODE_OUTPUT, 1, 0, /* 0 - Power LED */ GPIO_MODE_OUTPUT, 1, 1, /* 1 - First USB port (6104KP) */ GPIO_MODE_INPUT, 0, 0, /* 2 - Reset switch */ GPIO_MODE_OUTPUT, 1, 1, /* 3 - Second USB port (6104KP) */ GPIO_MODE_INPUT, 0, 0, /* 4 not on BR6104K(P) */ GPIO_MODE_OUTPUT, 1, 1, /* 5 - WLAN LED not on BR6104K(P) */ GPIO_MODE_INPUT, 0, 0, /* 6 not on BR6104K(P) */ GPIO_MODE_INPUT, 0, 0, /* 7 not on BR6104K(P) */ SW_LED_LINK_ACT, 0, 1, /* SW Port 0 LED 0 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 0 LED 1 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 0 LED 2 not connected on BR6104K(P) */ SW_LED_LINK_ACT, 0, 1, /* SW Port 1 LED 0 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 1 LED 1 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 1 LED 2 not connected on BR6104K(P) */ SW_LED_LINK_ACT, 0, 1, /* SW Port 2 LED 0 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 2 LED 1 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 2 LED 2 not connected on BR6104K(P) */ SW_LED_LINK_ACT, 0, 1, /* SW Port 3 LED 0 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 3 LED 1 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 3 LED 2 not connected on BR6104K(P) */ SW_LED_LINK_ACT, 0, 1, /* SW Port 4 LED 0 */ GPIO_MODE_OUTPUT, 0, 1, /* SW Port 4 LED 1 */ GPIO_MODE_OUTPUT, 0, 1 /* SW Port 4 LED 2 not connected on BR6104K(P) */ }; static struct adm5120gpio_data gpio_data[GPIO_NUM_DEV]; static spinlock_t adm5120gpio_spinlock; static char led_modes[13][STATUS_BUF_LEN]; static int mk_shift(int id) { int shift = id - 8; /* LED numbers 8 to 10 */ if (id > 10) shift = id - 11; /* LED numbers 11 to 13 */ if (id > 13) shift = id - 14; /* LED numbers 14 to 16 */ if (id > 16) shift = id - 17; /* LED numbers 17 to 19 */ if (id > 19) shift = id - 20; /* LED numbers 20 to 22 */ return shift; } static void adm5120_set_ledreg (int id, int value) /* set the LED-register */ { unsigned long data,shift,mask; shift = mk_shift(id); shift = shift * 4; /* 4 bit per LED */ data = (value & 0xF) << shift; /* shift value for LED0, LED1 or LED2 per port */ mask = ~(0x0F << shift); /* shift mask for LED0,LED1 or LED2 per port */ switch (id) { case 8: /* Port 0: LED0 */ case 9: /* Port 0: LED1 */ case 10: /* Port 0: LED2 */ SW_PORT0_LED &= mask; /* clear old bits first */ SW_PORT0_LED |= data; /* set new bits */ break; case 11: /* Port 1: LED0 */ case 12: /* Port 1: LED1 */ case 13: /* Port 1: LED2 */ SW_PORT1_LED &= mask; SW_PORT1_LED |= data; break; case 14: /* Port 2: LED0 */ case 15: /* Port 2: LED1 */ case 16: /* Port 2: LED2 */ SW_PORT2_LED &= mask; SW_PORT2_LED |= data; break; case 17: /* Port 3: LED0 */ case 18: /* Port 3: LED1 */ case 19: /* Port 3: LED2 */ SW_PORT3_LED &= mask; SW_PORT3_LED |= data; break; case 20: /* Port 4: LED0 */ case 21: /* Port 4: LED1 */ case 22: /* Port 4: LED2 */ SW_PORT4_LED &= mask; SW_PORT4_LED |= data; break; default: return; } #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: setreg id:%d mask:0x%x data:0x%x shift:%i\n",id, mask,data,shift); #endif } static int adm5120gpio_get_mode (int id) /* read the real ADM5120 mode bit/register */ { int val, shift; if (id < 8) { val = ((GPIO_IO_CONF0 & (1<<(id+16))) == 0 ? 0 : 1); /* read the output-enable-bit */ #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: get_mode: 0x%x from pin %i\n", val, id); #endif return val; } else { shift = mk_shift(id); /* LED0 = bit3:0, LED1 bit7:4, LED2 = bit11:8 */ shift = shift * 4; /* 4 bit per LED */ switch (id) { case 8: case 9: case 10: /* Port 0: LED0, LED1, LED2 */ val = (SW_PORT0_LED & (0xF<>shift; if (val == 1) val=13; /* pseudo-mode hardware-flash */ #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: get_mode: 0x%x from pin %i\n", val, id); #endif return (val); } } static int adm5120gpio_get_value (int id) /* read the real input bit */ { int val; int shift; if (id < 8) { val = ((GPIO_IO_CONF0 & (1<<(id+8))) == 0 ? 0 : 1); val = (gpio_data[id].invert == 0? val: !val); #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: input value: %i from pin %i\n", val, id); #endif return val; } else { shift = mk_shift(id); shift = shift + 12; /* input are on other location */ switch (id) { case 8: case 9: case 10: /* Port 0: LED0, LED1, LED2 */ val = (SW_PORT0_LED & (0x01<>shift; if (gpio_data[id].invert == 1) val = (val==1)? 0: 1; /* if invers , invert the input also */ #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: input value %i from pin %i\n", val, id); #endif return (val); } } static void adm5120gpio_set_value (int id, int value) { unsigned long data; int mode; gpio_data[id].status_num = value; mode = adm5120gpio_get_mode(id); if ((mode > GPIO_MODE_INPUT) && (mode < SW_LED_LINK)) { /* only set value if output-mode */ if ((gpio_data[id].invert == 1) && (value < 2)) value=(value==1)? 0:1; /* invert only output */ spin_lock(&adm5120gpio_spinlock); if (id<8) { data = 1 << (id+24); /* set the output-bit */ switch (value) { case 0: /* off */ GPIO_IO_CONF0 &= ~data; break; case 1: /* on */ GPIO_IO_CONF0 |= data; break; case 2: /* inverse (for blinks) */ GPIO_IO_CONF0 ^= data; break; default: ; } #ifdef GPIO_DEBUG /* for debug return all bits per led */ data |= 1 << id; /* if input or output (only datasheet V1.3) */ data |= 1 << (id+8); /* the real input */ data |= 1 << (id+16); /* output enable */ data = GPIO_IO_CONF0 &= data; /* output value mask from above */ #endif } else { data = value; switch (value) { case 0: /* off */ adm5120_set_ledreg(id,SW_LED_GPIO_OUTPUT_0); break; case 1: /* on */ adm5120_set_ledreg(id,SW_LED_GPIO_OUTPUT_1); break; case 2: data = adm5120gpio_get_mode(id); /* get the current mode */ value = data; if (data == SW_LED_GPIO_OUTPUT_0) value = SW_LED_GPIO_OUTPUT_1; if (data == SW_LED_GPIO_OUTPUT_1) value = SW_LED_GPIO_OUTPUT_0; adm5120_set_ledreg(id,value); /* invert the output if output mode */ break; default: ; /* nothing to do */ } } spin_unlock(&adm5120gpio_spinlock); #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: set_value: pin: %i value: 0x%x, mode: %i\n", id, data,gpio_data[id].mode); #endif } } static void adm5120gpio_set_mode(int id,int mode) { strcpy(gpio_data[id].status,led_modes[mode]); /* save the new mode */ gpio_data[id].mode = mode; #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: set_mode: pin %i to %s\n", id, led_modes[mode]); #endif spin_lock(&adm5120gpio_spinlock); if (id<8) { unsigned long bit = 1 << (id+16); /* output enable bit */ if (mode == GPIO_MODE_INPUT) GPIO_IO_CONF0 &= ~bit; /* clear the bit */ if (mode == GPIO_MODE_OUTPUT) GPIO_IO_CONF0 |= bit; /* set the bit */ #ifdef GPIO_DEBUG bit |= 1 << id; /* the real input/output-mode 1=input! */ bit &= GPIO_IO_CONF0; printk(KERN_INFO "adm5120gpio: set_mode: pin %i reg is: 0x%x\n", id, bit); #endif } else { /* SWITCH PxLED IO */ switch (mode) { case 1: adm5120_set_ledreg(id,SW_LED_GPIO_OUTPUT_0); break; case 13: adm5120_set_ledreg(id,SW_LED_GPIO_OUTPUT_FLASH); break; default: adm5120_set_ledreg(id,mode); /* all other mode direct */ } } spin_unlock(&adm5120gpio_spinlock); } static void adm5120gpio_tokenize (char *str, char token[][STATUS_BUF_LEN], int token_num) { int t, i; for ( t = 0 ; t < token_num ; t++ ) { memset(token[t], 0, STATUS_BUF_LEN); while ( *str == ' ' ) str++; for ( i = 0 ; str[i] ; i++ ) { if ( str[i] == '\t' || str[i] == ' ' || str[i] == '\n' ) break; if ( i < (STATUS_BUF_LEN-1) ) token[t][i] = str[i]; } str += i; } } static void adm5120gpio_blink (u_long id) { if (gpio_data[id].status_num == 2) { gpio_data[id].timer.expires = jiffies + (gpio_data[id].period * HZ /1000); adm5120gpio_set_value(id, 2); add_timer(&gpio_data[id].timer); } } static void adm5120gpio_set_string (int id) { char token[3][STATUS_BUF_LEN]; adm5120gpio_tokenize(gpio_data[id].status_tmp, token, 3); if (strcmp(token[0], "LED")) return; if (!strcmp(token[1], "ON")) goto _seton; /* LED ON */ if (!strcmp(token[1], "OFF")) goto _setoff; /* LED OFF */ if (!strcmp(token[1], "BLINK")) { /* LED BLINK xxxxxx */ int period = 0; char *p = token[2]; if (!strcmp(token[2], "OFF")) goto _setoff; /* LED BLINK OFF */ if (*p >= '0' && *p <= '9') { while (*p >= '0' && *p <= '9') period = period * 10 + (*p++) - '0'; if (period > 10000) period = 10000; if (period == 0) goto _setoff; } /* * Note: the original driver supports periods such as "fast", "slow" * and so on. We don't support them, but use 500ms for anything we don't * recognize. */ else period = 500; gpio_data[id].period = period; sprintf(gpio_data[id].status, "LED BLINK %d", period); adm5120gpio_set_value(id, 2); del_timer(&gpio_data[id].timer); /* Configure timer */ gpio_data[id].timer.function = adm5120gpio_blink; gpio_data[id].timer.data = id; init_timer(&gpio_data[id].timer); gpio_data[id].timer.expires = jiffies + ( period * HZ / 1000); add_timer(&gpio_data[id].timer); return; } if (!strcmp(token[1], "SWITCH")) { /* SW LED IO */ int value = 0; char *p = token[2]; if (*p >= '0' && *p <= '9') { while (*p >= '0' && *p <= '9') value = value * 10 + (*p++) - '0'; value &= 0xF; } else { value = GPIO_MODE_OUTPUT; } if ((id<8) && (value>1)) return; /* gpio has only input or output */ if (value > 13) return; /* only 0..13 are allowed for led */ printk(KERN_INFO "adm5120gpio: switch gpio %i to %s \n",id,led_modes[value]); strcpy(gpio_data[id].status,led_modes[value]); gpio_data[id].mode = value; adm5120gpio_set_mode(id,value); return; } if (!strcmp(token[1], "INVERT")) { gpio_data[id].invert= (gpio_data[id].invert == 1)? 0: 1; if (gpio_data[id].mode == GPIO_MODE_OUTPUT) adm5120gpio_set_value(id,2); return; } /* fall-through */ _seton: strcpy(gpio_data[id].status, "LED ON"); adm5120gpio_set_value(id, 1); return; _setoff: strcpy(gpio_data[id].status, "LED OFF"); adm5120gpio_set_value(id, 0); } static ssize_t adm5120gpio_write (struct file *file, const char *buffer, size_t length, loff_t *offset) { int id = (int)file->private_data; char *p = gpio_data[id].status_tmp; int nbytes = 0; char c; if (*offset >= STATUS_BUF_LEN) return -ENOSPC; p += *offset; while (length > 0 && *offset < STATUS_BUF_LEN) { c = *buffer++; p[nbytes] = c>='a' && c<='z' ? c-'a'+'A' : c; length--; (*offset)++; nbytes++; } return nbytes; } static ssize_t adm5120gpio_read (struct file *file, char *buffer, size_t length, loff_t *offset) { int id = (int)file->private_data; int rem, len, mode; char strTemp[40]; mode = adm5120gpio_get_mode(id); if (mode == 0) { /* if input-mode */ len = sprintf(strTemp, "%i\n", adm5120gpio_get_value(id)); /* print the value 0 or 1*/ } else { len = sprintf(strTemp, "%s (%i)\n",gpio_data[id].status, mode); /* print the mode */ } rem = len - *offset; if (rem <= 0) { *offset = len; return 0; } if (rem > length) rem = length; memcpy(buffer, &strTemp[*offset], rem); *offset += rem; return rem; } static int adm5120gpio_open (struct inode *inode, struct file *file) { int id = MINOR(inode->i_rdev); if (id >= GPIO_NUM_DEV) return -ENODEV; if (gpio_data[id].open_count && !(file->f_mode & FMODE_READ)) return -EBUSY; /* only allow one open, except for reading*/ /* count only opens for writing */ if (file->f_mode & FMODE_WRITE) gpio_data[id].open_count++; file->private_data = (void*)id; return 0; } static int adm5120gpio_release (struct inode *inode, struct file *file) { int id = (int)file->private_data; if (file->f_mode & FMODE_WRITE) { gpio_data[id].open_count--; adm5120gpio_set_string(id); /* here are the real write-operation */ memset(gpio_data[id].status_tmp, 0, STATUS_BUF_LEN); } return 0; } static int adm5120gpio_ioctl (struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { int mode[GPIO_NUM_DEV*2], err, i, id, val; id = (int)file->private_data; if ( arg <= GPIO_NUM_DEV) id = arg; #ifdef GPIO_DEBUG printk(KERN_INFO "adm5120gpio: cmd:%i, pin:%i\n", cmd,id); #endif switch (cmd) { case SIOCDEVSGPIOMODE: err = copy_from_user(&mode, (int*)arg, GPIO_NUM_DEV*sizeof(int)*2); if (err) return -EFAULT; for (i = 0; i < GPIO_NUM_DEV; i++) { if ((mode[i*2] < 0) || (mode[i*2] > 13)) return -EINVAL; gpio_data[i].invert = mode[(i*2)+1]; adm5120gpio_set_mode(i,mode[i*2]); } break; case SIOCDEVGGPIOMODE: for (i = 0; i < GPIO_NUM_DEV; i++) { mode[i*2] = adm5120gpio_get_mode(i); mode[(i*2)+1] = gpio_data[i].invert; } err = copy_to_user((int*)arg, mode, GPIO_NUM_DEV*sizeof(int)*2); if (err) return -EFAULT; break; case 0: /* set OFF (from old driver) */ adm5120gpio_set_value(id, 0); break; case 1: /* set ON (from old driver) */ adm5120gpio_set_value(id, 1); break; case 2: /* read the value (0/1) or mode (2..13) */ val = adm5120gpio_get_mode(id); if (val == 0) val = adm5120gpio_get_value(id); /* if input */ err = copy_to_user((int*)arg, val, sizeof(int)); if (err) return -EFAULT; break; default: return -EOPNOTSUPP; } return 0; } static int led_read_proc(char *buf, char **start, off_t fpos, int length, int *eof, void *data) { int len, id, value, mode; for ( len = id = 0 ; id < GPIO_NUM_DEV ; id++ ) { mode = adm5120gpio_get_mode(id); if (mode < 4 ) { if (mode == GPIO_MODE_INPUT) { value = adm5120gpio_get_value(id); len += sprintf(buf+len, "%d: %s: %i %s\n",id,led_modes[mode],value,(gpio_data[id].invert == 0)? "normal":"inverted"); } else { len += sprintf(buf+len, "%d: %s %s\n",id,led_modes[mode],(gpio_data[id].invert == 0)? "normal":"inverted"); } } else { len += sprintf(buf+len, "%d: %s\n",id,led_modes[mode]); } } len = strlen(buf) - fpos; if ( len <= 0 ) { *start = buf; *eof = 1; return 0; } *start = buf + fpos; if ( len <= length ) *eof = 1; return len < length ? len : length; } static struct file_operations gpio_fops = { .read = adm5120gpio_read, .write = adm5120gpio_write, .open = adm5120gpio_open, .ioctl = adm5120gpio_ioctl, .release = adm5120gpio_release, }; static int __init adm5120gpio_init(void) { int err,id,value; if (!(err = request_mem_region(GPIO_IO_BASE, GPIO_IO_LEN, "adm5120gpio"))) { printk(KERN_ERR "adm5120gpio: I/O region already used. Cannot proceed.\n"); return err; } err = register_chrdev(GPIO_DEV_MAJOR, "adm5120gpio", &gpio_fops); if (err < 0) { printk(KERN_ERR "adm5120gpio: can't register char device %d.\n",GPIO_DEV_MAJOR); return err; } err = create_proc_read_entry("driver/led", 0, 0, led_read_proc, NULL); if (!(err)) { printk(KERN_ERR "adm5120gpio: unable to register /proc/driver/led\n"); return err; } strcpy(led_modes[0], "INPUT"); strcpy(led_modes[1], "OUTPUT"); strcpy(led_modes[2], "OUTPUT_1"); /* spezial from led */ strcpy(led_modes[3], "OUTPUT_0"); /* spezial from led */ strcpy(led_modes[4], "LINK"); strcpy(led_modes[5], "SPEED"); strcpy(led_modes[6], "DUPLEX"); strcpy(led_modes[7], "ACTIVITY"); strcpy(led_modes[8], "COLLISION"); strcpy(led_modes[9], "LINK_ACT"); strcpy(led_modes[10], "DUPLEX_COL"); strcpy(led_modes[11], "10M_ACT"); strcpy(led_modes[12], "100M_ACT"); strcpy(led_modes[13], "FLASH"); /* pseudo mode, register setting = 1 */ spin_lock_init(&adm5120gpio_spinlock); memset(gpio_data, 0, sizeof(gpio_data)); printk(KERN_INFO "adm5120gpio: Initializing IO pins:\n"); for (id = 0; id < GPIO_NUM_DEV; id++) { adm5120gpio_set_mode(id,adm5120gpio_init_data[id*3]); gpio_data[id].invert = adm5120gpio_init_data[(id*3)+2]; /* strcpy(gpio_data[id].status,led_modes[gpio_data[id].mode]); */ if (gpio_data[id].mode == GPIO_MODE_OUTPUT) { value = adm5120gpio_init_data[(id*3)+1]; adm5120gpio_set_value(id,value); printk(KERN_INFO "Pin %i: is OUTPUT %s, %s\n",id,value==1?"on":"off",gpio_data[id].invert==0?"normal":"inverted"); } else { printk(KERN_INFO "Pin %i: is %s\n",id,gpio_data[id].status); } } printk(KERN_INFO "adm5120gpio: ADM5120 GPIO driver %s initialized.\n",version); return 0; } static void __exit adm5120gpio_exit(void) { unregister_chrdev(GPIO_DEV_MAJOR, "adm5120gpio"); remove_proc_entry("driver/led", NULL); release_mem_region(GPIO_IO_BASE, GPIO_IO_LEN); } module_init(adm5120gpio_init); module_exit(adm5120gpio_exit); EXPORT_NO_SYMBOLS; MODULE_LICENSE("GPLv2"); MODULE_AUTHOR("Sergio Aguayo (webmaster@qmailhosting.net), dl4huf (dl4huf@darc.de)"); MODULE_DESCRIPTION("Driver for ADM5120's GPIO ports");