| /* drivers/char/dcc_tty.c |
| * |
| * Copyright (C) 2007 Google, Inc. |
| * |
| * This software is licensed under the terms of the GNU General Public |
| * License version 2, as published by the Free Software Foundation, and |
| * may be copied, distributed, and modified under those terms. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| */ |
| |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/delay.h> |
| #include <linux/console.h> |
| #include <linux/hrtimer.h> |
| #include <linux/tty.h> |
| #include <linux/tty_driver.h> |
| #include <linux/tty_flip.h> |
| |
| MODULE_DESCRIPTION("DCC TTY Driver"); |
| MODULE_LICENSE("GPL"); |
| MODULE_VERSION("1.0"); |
| |
| static DEFINE_SPINLOCK(g_dcc_tty_lock); |
| static struct hrtimer g_dcc_timer; |
| static char g_dcc_buffer[16]; |
| static int g_dcc_buffer_head; |
| static int g_dcc_buffer_count; |
| static unsigned g_dcc_write_delay_usecs = 1; |
| static struct tty_driver *g_dcc_tty_driver; |
| static struct tty_struct *g_dcc_tty; |
| static int g_dcc_tty_open_count; |
| |
| static void dcc_poll_locked(void) |
| { |
| char ch; |
| int rch; |
| int written; |
| |
| while (g_dcc_buffer_count) { |
| ch = g_dcc_buffer[g_dcc_buffer_head]; |
| asm( |
| "mrc 14, 0, r15, c0, c1, 0\n" |
| "mcrcc 14, 0, %1, c0, c5, 0\n" |
| "movcc %0, #1\n" |
| "movcs %0, #0\n" |
| : "=r" (written) |
| : "r" (ch) |
| ); |
| if (written) { |
| if (ch == '\n') |
| g_dcc_buffer[g_dcc_buffer_head] = '\r'; |
| else { |
| g_dcc_buffer_head = (g_dcc_buffer_head + 1) % ARRAY_SIZE(g_dcc_buffer); |
| g_dcc_buffer_count--; |
| if (g_dcc_tty) |
| tty_wakeup(g_dcc_tty); |
| } |
| g_dcc_write_delay_usecs = 1; |
| } else { |
| if (g_dcc_write_delay_usecs > 0x100) |
| break; |
| g_dcc_write_delay_usecs <<= 1; |
| udelay(g_dcc_write_delay_usecs); |
| } |
| } |
| |
| if (g_dcc_tty && !test_bit(TTY_THROTTLED, &g_dcc_tty->flags)) { |
| asm( |
| "mrc 14, 0, %0, c0, c1, 0\n" |
| "tst %0, #(1 << 30)\n" |
| "moveq %0, #-1\n" |
| "mrcne 14, 0, %0, c0, c5, 0\n" |
| : "=r" (rch) |
| ); |
| if (rch >= 0) { |
| ch = rch; |
| tty_insert_flip_string(g_dcc_tty, &ch, 1); |
| tty_flip_buffer_push(g_dcc_tty); |
| } |
| } |
| |
| |
| if (g_dcc_buffer_count) |
| hrtimer_start(&g_dcc_timer, ktime_set(0, g_dcc_write_delay_usecs * NSEC_PER_USEC), HRTIMER_MODE_REL); |
| else |
| hrtimer_start(&g_dcc_timer, ktime_set(0, 20 * NSEC_PER_MSEC), HRTIMER_MODE_REL); |
| } |
| |
| static int dcc_tty_open(struct tty_struct * tty, struct file * filp) |
| { |
| int ret; |
| unsigned long irq_flags; |
| |
| spin_lock_irqsave(&g_dcc_tty_lock, irq_flags); |
| if (g_dcc_tty == NULL || g_dcc_tty == tty) { |
| g_dcc_tty = tty; |
| g_dcc_tty_open_count++; |
| ret = 0; |
| } else |
| ret = -EBUSY; |
| spin_unlock_irqrestore(&g_dcc_tty_lock, irq_flags); |
| |
| printk("dcc_tty_open, tty %p, f_flags %x, returned %d\n", tty, filp->f_flags, ret); |
| |
| return ret; |
| } |
| |
| static void dcc_tty_close(struct tty_struct * tty, struct file * filp) |
| { |
| printk("dcc_tty_close, tty %p, f_flags %x\n", tty, filp->f_flags); |
| if (g_dcc_tty == tty) { |
| if (--g_dcc_tty_open_count == 0) |
| g_dcc_tty = NULL; |
| } |
| } |
| |
| static int dcc_write(const unsigned char *buf_start, int count) |
| { |
| const unsigned char *buf = buf_start; |
| unsigned long irq_flags; |
| int copy_len; |
| int space_left; |
| int tail; |
| |
| if (count < 1) |
| return 0; |
| |
| spin_lock_irqsave(&g_dcc_tty_lock, irq_flags); |
| do { |
| tail = (g_dcc_buffer_head + g_dcc_buffer_count) % ARRAY_SIZE(g_dcc_buffer); |
| copy_len = ARRAY_SIZE(g_dcc_buffer) - tail; |
| space_left = ARRAY_SIZE(g_dcc_buffer) - g_dcc_buffer_count; |
| if (copy_len > space_left) |
| copy_len = space_left; |
| if (copy_len > count) |
| copy_len = count; |
| memcpy(&g_dcc_buffer[tail], buf, copy_len); |
| g_dcc_buffer_count += copy_len; |
| buf += copy_len; |
| count -= copy_len; |
| if (copy_len < count && copy_len < space_left) { |
| space_left -= copy_len; |
| copy_len = count; |
| if (copy_len > space_left) { |
| copy_len = space_left; |
| } |
| memcpy(g_dcc_buffer, buf, copy_len); |
| buf += copy_len; |
| count -= copy_len; |
| g_dcc_buffer_count += copy_len; |
| } |
| dcc_poll_locked(); |
| space_left = ARRAY_SIZE(g_dcc_buffer) - g_dcc_buffer_count; |
| } while(count && space_left); |
| spin_unlock_irqrestore(&g_dcc_tty_lock, irq_flags); |
| return buf - buf_start; |
| } |
| |
| static int dcc_tty_write(struct tty_struct * tty, const unsigned char *buf, int count) |
| { |
| int ret; |
| /* printk("dcc_tty_write %p, %d\n", buf, count); */ |
| ret = dcc_write(buf, count); |
| if (ret != count) |
| printk("dcc_tty_write %p, %d, returned %d\n", buf, count, ret); |
| return ret; |
| } |
| |
| static int dcc_tty_write_room(struct tty_struct *tty) |
| { |
| int space_left; |
| unsigned long irq_flags; |
| |
| spin_lock_irqsave(&g_dcc_tty_lock, irq_flags); |
| space_left = ARRAY_SIZE(g_dcc_buffer) - g_dcc_buffer_count; |
| spin_unlock_irqrestore(&g_dcc_tty_lock, irq_flags); |
| return space_left; |
| } |
| |
| static int dcc_tty_chars_in_buffer(struct tty_struct *tty) |
| { |
| int ret; |
| asm( |
| "mrc 14, 0, %0, c0, c1, 0\n" |
| "mov %0, %0, LSR #30\n" |
| "and %0, %0, #1\n" |
| : "=r" (ret) |
| ); |
| return ret; |
| } |
| |
| static void dcc_tty_unthrottle(struct tty_struct * tty) |
| { |
| unsigned long irq_flags; |
| |
| spin_lock_irqsave(&g_dcc_tty_lock, irq_flags); |
| dcc_poll_locked(); |
| spin_unlock_irqrestore(&g_dcc_tty_lock, irq_flags); |
| } |
| |
| static enum hrtimer_restart dcc_tty_timer_func(struct hrtimer *timer) |
| { |
| unsigned long irq_flags; |
| |
| spin_lock_irqsave(&g_dcc_tty_lock, irq_flags); |
| dcc_poll_locked(); |
| spin_unlock_irqrestore(&g_dcc_tty_lock, irq_flags); |
| return HRTIMER_NORESTART; |
| } |
| |
| void dcc_console_write(struct console *co, const char *b, unsigned count) |
| { |
| #if 1 |
| dcc_write(b, count); |
| #else |
| /* blocking printk */ |
| while (count > 0) { |
| int written; |
| written = dcc_write(b, count); |
| if (written) { |
| b += written; |
| count -= written; |
| } |
| } |
| #endif |
| } |
| |
| static struct tty_driver *dcc_console_device(struct console *c, int *index) |
| { |
| *index = 0; |
| return g_dcc_tty_driver; |
| } |
| |
| static int __init dcc_console_setup(struct console *co, char *options) |
| { |
| if (co->index != 0) |
| return -ENODEV; |
| return 0; |
| } |
| |
| |
| static struct console dcc_console = |
| { |
| .name = "ttyDCC", |
| .write = dcc_console_write, |
| .device = dcc_console_device, |
| .setup = dcc_console_setup, |
| .flags = CON_PRINTBUFFER, |
| .index = -1, |
| }; |
| |
| static struct tty_operations dcc_tty_ops = { |
| .open = dcc_tty_open, |
| .close = dcc_tty_close, |
| .write = dcc_tty_write, |
| .write_room = dcc_tty_write_room, |
| .chars_in_buffer = dcc_tty_chars_in_buffer, |
| .unthrottle = dcc_tty_unthrottle, |
| }; |
| |
| static int __init dcc_tty_init(void) |
| { |
| int ret; |
| |
| hrtimer_init(&g_dcc_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); |
| g_dcc_timer.function = dcc_tty_timer_func; |
| |
| g_dcc_tty_driver = alloc_tty_driver(1); |
| if (!g_dcc_tty_driver) { |
| printk(KERN_ERR "dcc_tty_probe: alloc_tty_driver failed\n"); |
| ret = -ENOMEM; |
| goto err_alloc_tty_driver_failed; |
| } |
| g_dcc_tty_driver->owner = THIS_MODULE; |
| g_dcc_tty_driver->driver_name = "dcc"; |
| g_dcc_tty_driver->name = "ttyDCC"; |
| g_dcc_tty_driver->major = 0; // auto assign |
| g_dcc_tty_driver->minor_start = 0; |
| g_dcc_tty_driver->type = TTY_DRIVER_TYPE_SERIAL; |
| g_dcc_tty_driver->subtype = SERIAL_TYPE_NORMAL; |
| g_dcc_tty_driver->init_termios = tty_std_termios; |
| g_dcc_tty_driver->flags = TTY_DRIVER_RESET_TERMIOS | TTY_DRIVER_REAL_RAW | TTY_DRIVER_DYNAMIC_DEV; |
| tty_set_operations(g_dcc_tty_driver, &dcc_tty_ops); |
| ret = tty_register_driver(g_dcc_tty_driver); |
| if (ret) { |
| printk(KERN_ERR "dcc_tty_probe: tty_register_driver failed, %d\n", ret); |
| goto err_tty_register_driver_failed; |
| } |
| tty_register_device(g_dcc_tty_driver, 0, NULL); |
| |
| register_console(&dcc_console); |
| hrtimer_start(&g_dcc_timer, ktime_set(0, 0), HRTIMER_MODE_REL); |
| |
| return 0; |
| |
| err_tty_register_driver_failed: |
| put_tty_driver(g_dcc_tty_driver); |
| g_dcc_tty_driver = NULL; |
| err_alloc_tty_driver_failed: |
| return ret; |
| } |
| |
| static void __exit dcc_tty_exit(void) |
| { |
| int ret; |
| |
| tty_unregister_device(g_dcc_tty_driver, 0); |
| ret = tty_unregister_driver(g_dcc_tty_driver); |
| if (ret < 0) { |
| printk(KERN_ERR "dcc_tty_remove: tty_unregister_driver failed, %d\n", ret); |
| } else { |
| put_tty_driver(g_dcc_tty_driver); |
| } |
| g_dcc_tty_driver = NULL; |
| } |
| |
| module_init(dcc_tty_init); |
| module_exit(dcc_tty_exit); |
| |
| |