331 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			C
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			331 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			C
		
	
	
		
			Executable File
		
	
	
	
	
| /*
 | |
|  * Copyright (c) 2006-2023, RT-Thread Development Team
 | |
|  *
 | |
|  * SPDX-License-Identifier: Apache-2.0
 | |
|  *
 | |
|  * Change Logs:
 | |
|  * Date           Author       Notes
 | |
|  * 2023-11-21     Shell        init ver.
 | |
|  */
 | |
| 
 | |
| #define DBG_TAG "drivers.serial"
 | |
| #define DBG_LVL DBG_INFO
 | |
| #include <rtdbg.h>
 | |
| 
 | |
| #include <rthw.h>
 | |
| #include <rtthread.h>
 | |
| #include <rtdevice.h>
 | |
| #include <terminal/terminal.h>
 | |
| 
 | |
| #define TTY_NAME_PREFIX "S" /* (S)erial */
 | |
| #define LWP_TTY_WORKQUEUE_PRIORITY 3
 | |
| 
 | |
| struct serial_tty_context
 | |
| {
 | |
|     struct rt_serial_device *parent;
 | |
|     struct rt_device_notify backup_notify;
 | |
|     struct rt_work work;
 | |
| };
 | |
| 
 | |
| static struct rt_workqueue *_ttyworkq; /* system work queue */
 | |
| 
 | |
| static rt_atomic_t _device_id_counter = 0;
 | |
| 
 | |
| static long get_dec_digits(rt_ubase_t val)
 | |
| {
 | |
|     long result = 1;
 | |
|     while (1)
 | |
|     {
 | |
|         if (val < 10)
 | |
|             return result;
 | |
|         if (val < 100)
 | |
|             return result + 1;
 | |
|         if (val < 1000)
 | |
|             return result + 2;
 | |
|         if (val < 10000)
 | |
|             return result + 3;
 | |
|         val /= 10000U;
 | |
|         result += 4;
 | |
|     }
 | |
|     return result;
 | |
| }
 | |
| 
 | |
| static char *alloc_device_name(void)
 | |
| {
 | |
|     char *tty_dev_name;
 | |
|     unsigned int devid = rt_atomic_add(&_device_id_counter, 1);
 | |
|     long digits_len = (sizeof(TTY_NAME_PREFIX) - 1) /* raw prefix */
 | |
|                       + get_dec_digits(devid) + 1;  /* tailing \0 */
 | |
| 
 | |
|     tty_dev_name = rt_malloc(digits_len);
 | |
|     if (tty_dev_name)
 | |
|         rt_sprintf(tty_dev_name, "%s%u", TTY_NAME_PREFIX, devid);
 | |
|     return tty_dev_name;
 | |
| }
 | |
| 
 | |
| static void _tty_rx_notify(struct rt_device *device)
 | |
| {
 | |
|     lwp_tty_t tp;
 | |
|     struct serial_tty_context *softc;
 | |
| 
 | |
|     tp = rt_container_of(device, struct lwp_tty, parent);
 | |
|     RT_ASSERT(tp);
 | |
| 
 | |
|     softc = tty_softc(tp);
 | |
| 
 | |
|     if (_ttyworkq)
 | |
|         rt_workqueue_submit_work(_ttyworkq, &softc->work, 0);
 | |
| }
 | |
| 
 | |
| static void _tty_rx_worker(struct rt_work *work, void *data)
 | |
| {
 | |
|     char input;
 | |
|     rt_ssize_t readbytes;
 | |
|     lwp_tty_t tp = data;
 | |
|     struct serial_tty_context *softc;
 | |
|     struct rt_serial_device *serial;
 | |
| 
 | |
|     tty_lock(tp);
 | |
| 
 | |
|     while (1)
 | |
|     {
 | |
|         softc = tty_softc(tp);
 | |
|         serial = softc->parent;
 | |
|         readbytes = rt_device_read(&serial->parent, -1, &input, 1);
 | |
|         if (readbytes != 1)
 | |
|         {
 | |
|             break;
 | |
|         }
 | |
| 
 | |
|         ttydisc_rint(tp, input, 0);
 | |
|     }
 | |
| 
 | |
|     ttydisc_rint_done(tp);
 | |
|     tty_unlock(tp);
 | |
| }
 | |
| 
 | |
| rt_inline void _setup_serial(struct rt_serial_device *serial, lwp_tty_t tp,
 | |
|                              struct serial_tty_context *softc)
 | |
| {
 | |
|     struct rt_device_notify notify;
 | |
| 
 | |
|     softc->backup_notify = serial->rx_notify;
 | |
|     notify.dev = &tp->parent;
 | |
|     notify.notify = _tty_rx_notify;
 | |
| 
 | |
|     rt_device_init(&serial->parent);
 | |
| 
 | |
|     rt_work_init(&softc->work, _tty_rx_worker, tp);
 | |
|     rt_device_control(&serial->parent, RT_DEVICE_CTRL_NOTIFY_SET, ¬ify);
 | |
| }
 | |
| 
 | |
| rt_inline void _restore_serial(struct rt_serial_device *serial, lwp_tty_t tp,
 | |
|                                struct serial_tty_context *softc)
 | |
| {
 | |
|     rt_device_control(&serial->parent, RT_DEVICE_CTRL_NOTIFY_SET, &softc->backup_notify);
 | |
| }
 | |
| 
 | |
| static int _serial_isbusy(struct rt_serial_device *serial)
 | |
| {
 | |
|     rt_thread_t user_thread = rt_console_current_user();
 | |
|     rt_thread_t self_thread = rt_thread_self();
 | |
| 
 | |
|     return rt_console_get_device() == &serial->parent &&
 | |
|            (user_thread != RT_NULL && user_thread != self_thread);
 | |
| }
 | |
| 
 | |
| static void serial_tty_outwakeup(struct lwp_tty *tp)
 | |
| {
 | |
|     char out_char;
 | |
|     int len;
 | |
|     struct serial_tty_context *context = tty_softc(tp);
 | |
|     struct rt_serial_device *device;
 | |
| 
 | |
|     if (!context || !context->parent)
 | |
|     {
 | |
|         LOG_E("%s: Data corruption", __func__);
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     device = context->parent;
 | |
| 
 | |
|     if (_serial_isbusy(device))
 | |
|     {
 | |
|         return ;
 | |
|     }
 | |
| 
 | |
|     while ((len = ttydisc_getc(tp, &out_char, sizeof(out_char))) != 0)
 | |
|     {
 | |
|         device->ops->putc(device, out_char);
 | |
| 
 | |
|         /* discard remaining if emergency output is happened */
 | |
|         if (_serial_isbusy(device))
 | |
|         {
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| static int serial_tty_open(struct lwp_tty *tp)
 | |
| {
 | |
|     struct serial_tty_context *softc;
 | |
|     struct rt_serial_device *serial;
 | |
|     rt_err_t error;
 | |
|     int oflags;
 | |
| 
 | |
|     softc = tty_softc(tp);
 | |
|     serial = softc->parent;
 | |
| 
 | |
|     LOG_D("%s", __func__);
 | |
| 
 | |
|     rt_device_control(&serial->parent, RT_DEVICE_CTRL_CONSOLE_OFLAG, &oflags);
 | |
| 
 | |
|     error = rt_device_open(&serial->parent, oflags);
 | |
| 
 | |
|     if (!error)
 | |
|     {
 | |
|         /**
 | |
|          * to avoid driver accesssing null data,
 | |
|          * these are setup only after tty is registered
 | |
|          */
 | |
|         _setup_serial(serial, tp, softc);
 | |
|     }
 | |
|     return error;
 | |
| }
 | |
| 
 | |
| static void serial_tty_close(struct lwp_tty *tp)
 | |
| {
 | |
|     struct serial_tty_context *softc;
 | |
|     struct rt_serial_device *serial;
 | |
|     softc = tty_softc(tp);
 | |
|     serial = softc->parent;
 | |
| 
 | |
|     LOG_D("%s", __func__);
 | |
| 
 | |
|     _restore_serial(serial, tp, softc);
 | |
|     rt_device_close(&serial->parent);
 | |
| }
 | |
| 
 | |
| static int serial_tty_ioctl(struct lwp_tty *tp, rt_ubase_t cmd, rt_caddr_t data,
 | |
|                             struct rt_thread *td)
 | |
| {
 | |
|     int error;
 | |
|     switch (cmd)
 | |
|     {
 | |
|         case TCSETS:
 | |
|         case TCSETSW:
 | |
|         case TCSETSF:
 | |
|             RT_ASSERT(tp->t_devswsoftc);
 | |
|             struct serial_tty_context *softc = (struct serial_tty_context *)(tp->t_devswsoftc);
 | |
|             struct rt_serial_device *serial = softc->parent;
 | |
|             struct termios *termios = (struct termios *)data;
 | |
|             rt_device_control(&(serial->parent), cmd, termios);
 | |
|             error = -ENOIOCTL;
 | |
|         default:
 | |
|             /**
 | |
|              * Note: for the most case, we don't let serial layer handle ioctl,
 | |
|              * for that they can't act properly regarding to the process
 | |
|              * management system, since it is unawared of that. So a ENOSYS is
 | |
|              * returned and caused the TTY layer to handle ioctl itself.
 | |
|              */
 | |
|             error = -ENOSYS;
 | |
|             break;
 | |
|     }
 | |
|     return error;
 | |
| }
 | |
| 
 | |
| static struct lwp_ttydevsw serial_ttydevsw = {
 | |
|     .tsw_open = serial_tty_open,
 | |
|     .tsw_close = serial_tty_close,
 | |
|     .tsw_ioctl = serial_tty_ioctl,
 | |
|     .tsw_outwakeup = serial_tty_outwakeup,
 | |
| };
 | |
| 
 | |
| rt_err_t rt_hw_serial_register_tty(struct rt_serial_device *serial)
 | |
| {
 | |
|     rt_err_t rc;
 | |
|     lwp_tty_t tty;
 | |
|     char *dev_name;
 | |
|     struct serial_tty_context *softc;
 | |
| 
 | |
|     if (serial->rx_notify.dev)
 | |
|     {
 | |
|         return -RT_EBUSY;
 | |
|     }
 | |
| 
 | |
|     softc = rt_malloc(sizeof(struct serial_tty_context));
 | |
|     if (softc)
 | |
|     {
 | |
|         dev_name = alloc_device_name();
 | |
|         if (dev_name)
 | |
|         {
 | |
|             softc->parent = serial;
 | |
|             tty = lwp_tty_create(&serial_ttydevsw, softc);
 | |
|             if (tty)
 | |
|             {
 | |
|                 rc = lwp_tty_register(tty, dev_name);
 | |
| 
 | |
|                 if (rc != RT_EOK)
 | |
|                 {
 | |
|                     rt_free(tty);
 | |
|                     rt_free(softc);
 | |
|                 }
 | |
|             }
 | |
|             else
 | |
|             {
 | |
|                 rt_free(softc);
 | |
|                 rc = -RT_ENOMEM;
 | |
|             }
 | |
| 
 | |
|             rt_free(dev_name);
 | |
|         }
 | |
|         else
 | |
|         {
 | |
|             rt_free(softc);
 | |
|             rc = -RT_ENOMEM;
 | |
|         }
 | |
|     }
 | |
|     else
 | |
|     {
 | |
|         rc = -RT_ENOMEM;
 | |
|     }
 | |
| 
 | |
|     return rc;
 | |
| }
 | |
| 
 | |
| rt_err_t rt_hw_serial_unregister_tty(struct rt_serial_device *serial)
 | |
| {
 | |
|     rt_device_t tty_dev;
 | |
|     lwp_tty_t tp;
 | |
|     struct serial_tty_context *softc;
 | |
| 
 | |
|     tty_dev = serial->rx_notify.dev;
 | |
|     tp = rt_container_of(tty_dev, struct lwp_tty, parent);
 | |
| 
 | |
|     /* restore serial setting */
 | |
|     softc = tty_softc(tp);
 | |
|     serial->rx_notify = softc->backup_notify;
 | |
| 
 | |
|     tty_rel_gone(tp);
 | |
| 
 | |
|     /* device unregister? */
 | |
|     rt_device_destroy(&tp->parent);
 | |
|     /* resource free? */
 | |
|     lwp_tty_delete(tp);
 | |
| 
 | |
|     return RT_EOK;
 | |
| }
 | |
| 
 | |
| static int _tty_workqueue_init(void)
 | |
| {
 | |
|     if (_ttyworkq != RT_NULL)
 | |
|         return RT_EOK;
 | |
| 
 | |
|     _ttyworkq = rt_workqueue_create("ttyworkq", RT_SYSTEM_WORKQUEUE_STACKSIZE,
 | |
|                                     LWP_TTY_WORKQUEUE_PRIORITY);
 | |
|     RT_ASSERT(_ttyworkq != RT_NULL);
 | |
| 
 | |
|     return RT_EOK;
 | |
| }
 | |
| INIT_PREV_EXPORT(_tty_workqueue_init);
 |