Linux UART驱动分析及测试

1.Linux TTY驱动程序框架

Linux TTY驱动程序代码位于/drivers/tty下面。TTY的层次接口包括TTY应用层、TTY文件层、TTY线路规程层、TTY驱动层、TTY设备驱动层。TTY应用层负责应用逻辑,TTY文件层负责文件接口,TTY线路规程负责串行通信协议处理,包括特定协议的封装与解封,TTY驱动层对各种TTY设备进行分类和抽象。TTY设备驱动层实现具体的TTY设备(芯片或者控制器)驱动,即设备配置与数据收发。具体的TTY设备有串口、USB串口、VT设备、PTY设备等,最常见的是串口,即UART。

2.Linux UART驱动框架

UART驱动没有主机端和设备端之分,只有控制器驱动。imx6ull的UART驱动已由厂家写好,位于/drivers/tty/serial/imx.c文件中。剩下的工作主要是在设备树中配置串口节点信息,当UART驱动和设备树匹配成功后,相应的UART被驱动起来,在/dev/目录下生成ttymxcX(x=0…n)文件。

2.1.UART驱动结构体

正如前面分析的一样,每个设备驱动都有一个xxx_driver结构体,串口也不例外,struct uart_driver结构体描述UART驱动。struct uart_driver结构体与前面的xxx_driver结构体有所不同,struct uart_driver结构体本身并不包含底层UART硬件的操作方法,其是所有串口设备驱动的抽象和封装,起到了连接硬件设备驱动和TTY驱动的作用。注册了struct uart_driver后还不能使用UART设备,还需要关联具体的UART设备。

    include<linux/serial_core.h>
    struct uart_driver {
        struct module		*owner;
        const char		*driver_name;  // 驱动名称
        const char		*dev_name;     // 设备名称
        int			 major;            // 主设备号
        int			 minor;            // 次设备号
        int			 nr;               // 设备数量
        struct console		*cons;     // 控制台
        /*
        * these are private; the low level driver should not
        * touch these; they should be initialised to NULL
        */
        struct uart_state	*state;
        struct tty_driver	*tty_driver;
    };

加载和卸载模块的时候要注册和注销驱动,使用下面的函数注册和注销struct uart_driver结构体。uart_register_driver函数将struct uart_driver与TTY驱动层关联起来。

    // 注册uart驱动,参数为`struct uart_driver`结构体指针
    int uart_register_driver(struct uart_driver *uart)
    // 注销uart驱动,参数为`struct uart_driver`结构体指针
    void uart_unregister_driver(struct uart_driver *uart)
2.1.UART端口

一个串口控制器或串口芯片上往往有多个串行端口(serial ports,对应于一个物理上的串口),这些串行端口具备相同的操作机制。Linux内核将这些串行端口用struct uart_port结构体描述。struct uart_port用于描述一个UART端口的中断、I/O内存地址、FIFO大小、端口类型等信息。

    struct uart_port {
        spinlock_t		lock;			/* 自旋锁 */
        unsigned long		iobase;			/* IO基地址 */
        unsigned char __iomem	*membase;		/* 内存地址 */
        unsigned int		(*serial_in)(struct uart_port *, int);
        void			(*serial_out)(struct uart_port *, int, int);
        void			(*set_termios)(struct uart_port *,struct ktermios *new,struct ktermios *old);
        void			(*set_mctrl)(struct uart_port *, unsigned int);
        int			(*startup)(struct uart_port *port);
        void			(*shutdown)(struct uart_port *port);
        void			(*throttle)(struct uart_port *port);
        void			(*unthrottle)(struct uart_port *port);
        int			(*handle_irq)(struct uart_port *);
        void			(*pm)(struct uart_port *, unsigned int state,
                        unsigned int old);
        void			(*handle_break)(struct uart_port *);
        int			(*rs485_config)(struct uart_port *,struct serial_rs485 *rs485);
        unsigned int		irq;			/* 中断号 */
        unsigned long		irqflags;		/* 中断标志  */
        unsigned int		uartclk;		/* 时钟 */
        unsigned int		fifosize;		/* 发送fifo大小 */
        unsigned char		x_char;			/* xon/xoff握手字节 */
        unsigned char		regshift;		/* 寄存器偏移 */
        unsigned char		iotype;			/* IO访问类型 */
        unsigned char		unused1;
        unsigned int		read_status_mask;	/* 读状态验码 */
        unsigned int		ignore_status_mask;	/* 忽略状态掩码 */
        struct uart_state	*state;			/* 指向父设备的状态 */
        struct uart_icount	icount;			/* statistics */

        int			hw_stopped;		/* sw-assisted CTS flow state */
        unsigned int		mctrl;			/* current modem ctrl settings */
        unsigned int		timeout;		/* character-based timeout */
        unsigned int		type;			/* port type */
        const struct uart_ops	*ops;       /* 此结构体很重要,uart硬件的操作函数 */
        unsigned int		custom_divisor;
        unsigned int		line;			/* port index */
        unsigned int		minor;
        resource_size_t		mapbase;		/* 内存映射 */
        resource_size_t		mapsize;        /* 内存映射大小 */
        struct device		*dev;			/* 父设备 */
        unsigned char		hub6;			/* this should be in the 8250 driver */
        unsigned char		suspended;
        unsigned char		irq_wake;
        unsigned char		unused[2];
        struct attribute_group	*attr_group;		/* port specific attributes */
        const struct attribute_group **tty_groups;	/* all attributes (serial core use only) */
        struct serial_rs485     rs485;
        void			*private_data;		/* generic platform data pointer */
    };
    // uart硬件操作函数集合,底层硬件驱动必须实现这个结构体,每个函数的具体作用可以参考Documentation/serial/driver文档
    struct uart_ops {
        // 发送fifo和发送移位寄存器是否为空,为空返回TIOCSER_TEMT,否则返回0
        unsigned int	(*tx_empty)(struct uart_port *);
        void		(*set_mctrl)(struct uart_port *, unsigned int mctrl);
        unsigned int	(*get_mctrl)(struct uart_port *);
        void		(*stop_tx)(struct uart_port *);  // 停止发送
        void		(*start_tx)(struct uart_port *); // 开始发送
        void		(*throttle)(struct uart_port *);
        void		(*unthrottle)(struct uart_port *);
        void		(*send_xchar)(struct uart_port *, char ch);
        void		(*stop_rx)(struct uart_port *);  // 停止接收
        void		(*enable_ms)(struct uart_port *);  // 使能模式状态中断
        void		(*break_ctl)(struct uart_port *, int ctl);
        int		(*startup)(struct uart_port *);
        void		(*shutdown)(struct uart_port *);
        void		(*flush_buffer)(struct uart_port *);
        void		(*set_termios)(struct uart_port *, struct ktermios *new,struct ktermios *old);
        void		(*set_ldisc)(struct uart_port *, struct ktermios *);
        void		(*pm)(struct uart_port *, unsigned int state,unsigned int oldstate);
        const char	*(*type)(struct uart_port *);
        void		(*release_port)(struct uart_port *);  // 释放port使用的内存和IO资源
        int		(*request_port)(struct uart_port *);  // 请求port需要的内存和IO资源,失败返回-EBUSY
        void		(*config_port)(struct uart_port *, int);
        int		(*verify_port)(struct uart_port *, struct serial_struct *);
        int		(*ioctl)(struct uart_port *, unsigned int, unsigned long);
    #ifdef CONFIG_CONSOLE_POLL
        int		(*poll_init)(struct uart_port *);
        void		(*poll_put_char)(struct uart_port *, unsigned char);
        int		(*poll_get_char)(struct uart_port *);
    #endif
    };
`uart_port`需要和`uart_driver`关联才能工作,关联的函数如下:

    // 添加端口
    int uart_add_one_port(struct uart_driver *drv, struct uart_port *uport)
    // 删除端口
    int uart_remove_one_port(struct uart_driver *drv, struct uart_port *uport)

3.imx6ull串口驱动分析

3.1.imx6ull串口设备树节点
// uart3的设备树节点
uart3: serial@021ec000 {
    compatible = "fsl,imx6ul-uart",
                "fsl,imx6q-uart", "fsl,imx21-uart";  // 兼容属性,可利用兼容属性找到对应的驱动程序
    reg = <0x021ec000 0x4000>;
    interrupts = ;
    clocks = <&clks IMX6UL_CLK_UART3_IPG>,
            <&clks IMX6UL_CLK_UART3_SERIAL>;
    clock-names = "ipg", "per";
    dmas = <&sdma 29 4 0>, <&sdma 30 4 0>;
    dma-names = "rx", "tx";
    status = "disabled";
};
// uart3的的pinctrl节点
pinctrl_uart3: uart3grp {
    fsl,pins = <
        MX6UL_PAD_UART3_TX_DATA__UART3_DCE_TX	0x1b0b1
        MX6UL_PAD_UART3_RX_DATA__UART3_DCE_RX	0x1b0b1
    >;
};
// 引用uart3节点
&uart3{
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart3>;
    status = "okay";
};
3.2.imx6ull串口驱动分析
    // 用于和设备树匹配的表,和设备树中的兼容属性一致
    static const struct of_device_id imx_uart_dt_ids[] = {
        { .compatible = "fsl,imx6q-uart", .data = &imx_uart_devdata[IMX6Q_UART], },
        { .compatible = "fsl,imx1-uart", .data = &imx_uart_devdata[IMX1_UART], },
        { .compatible = "fsl,imx21-uart", .data = &imx_uart_devdata[IMX21_UART], },
        { /* sentinel */ }
    };
    // 平台驱动结构体
    static struct platform_driver serial_imx_driver = {
        .probe		= serial_imx_probe,  // probe函数,匹配成功后自动调用
        .remove		= serial_imx_remove,
        .suspend	= serial_imx_suspend,
        .resume		= serial_imx_resume,
        .id_table	= imx_uart_devtype,  // 传统的匹配表
        .driver		= {
            .name	= "imx-uart",  // 驱动名称
            .of_match_table = imx_uart_dt_ids,  // 设备树匹配表
        },
    };

模块初始化的时候调用uart_register_driver注册了uart_driver结构体,主要工作是关联TTY驱动层,设置一些参数。

    // 模块初始化函数
    static int __init imx_serial_init(void)
    {   // 注册uart_driver结构体
        int ret = uart_register_driver(&imx_reg);
        if (ret)
            return ret;
        // 注册为平台驱动程序,用于自动匹配和调用probe函数
        ret = platform_driver_register(&serial_imx_driver);
        if (ret != 0)
            uart_unregister_driver(&imx_reg);
        return ret;
    }
    // 模块退出函数
    static void __exit imx_serial_exit(void)
    {   // 注销平台驱动
        platform_driver_unregister(&serial_imx_driver);
        // 注销uart_driver结构体
        uart_unregister_driver(&imx_reg);
    }
    // 串口设备结构体指针数组,probe函数中会将分配的设备结构体指针保存到此数组中
    #define UART_NR 8
    static struct imx_port *imx_ports[UART_NR];
    // 驱动具体定义的uart_driver结构体,成员state指向的内存在uart_register_driver函数中根据uart port数量进行分配
    static struct uart_driver imx_reg = {
        .owner          = THIS_MODULE,
        .driver_name    = DRIVER_NAME,
        .dev_name       = DEV_NAME,
        .major          = SERIAL_IMX_MAJOR,
        .minor          = MINOR_START,
        .nr             = ARRAY_SIZE(imx_ports),
        .cons           = IMX_CONSOLE,
    };

串口模块加载的时候首先调用了uart_register_driver函数,首先分析uart_register_driver函数:

    uart_register_driver
      ->kzalloc(sizeof(struct uart_state) * drv->nr, GFP_KERNEL)    // 分配uart_state数组
      ->alloc_tty_driver    // 分配tty_driver结构体
      // 设置tty_driver,串口相关参数在打开设备初始化的时候会用到
      ->normal->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL  // 波特率等信息
      ->normal->init_termios.c_ispeed = normal->init_termios.c_ospeed = 9600
      ->tty_set_operations(normal, &uart_ops)  // 将tty驱动的操作函数和uart的操作函数关联起来
      ->tty_port_init    // 初始化tty_port
      ->tty_register_driver    // 注册tty驱动

接着分析serial_imx_probe函数:

    serial_imx_probe()
      ->devm_kzalloc()    // 分配imx_port结构体内存
      ->serial_imx_probe_dt()  // 获取设备树信息
        ->of_match_device()    // 将设备树匹配表与设备进行匹配,获取设备对应的设备树匹配表
        ->of_alias_get_id()    // 获取uart别名id,与aliases设备树节点有关
        ->sport->port.line = ret;  // 将uart别名id保存到uart_port结构体line成员中
        ->of_get_property()  // 获取fsl,uart-has-rtscts属性,有就设置have_rtscts=1
        ->of_get_property()  // 获取fsl,dte-mode属性,有就设置dte_mode=1
        ->sport->devdata = of_id->data  // 设置设备结构体的驱动数据成员,指向设备树匹配表中的date成员
      ->platform_get_resource()  // 获取内存地址信息,即设备树中的reg属性
      ->devm_ioremap_resource()  // 映射获取的内存地址
      ->platform_get_irq()   // 获取接收中断(中断索引号为0),设备树中配置了此中断
      ->platform_get_irq()   // 获取发送中断(中断索引号为1),设备树中没有配置此中断
      ->platform_get_irq()   // 获取rts中断(中断索引号为2),设备树中没有配置此中断
      /*==================设置uart_port结构体,重要====================*/
      ->sport->port.dev = &pdev->dev;
      ->sport->port.mapbase = res->start;  // 设置映射前的基地址
      ->sport->port.membase = base;        // 设置映射后的基地址 
      ->sport->port.type = PORT_IMX,
      ->sport->port.iotype = UPIO_MEM;     // IO类型为IO内存
      ->sport->port.irq = rxirq;           // 设置接收中断
      ->sport->port.fifosize = 32;         // 设置fifo大小
      ->sport->port.ops = &imx_pops;       // 设置uart硬件操作函数集合,很重要
      ->sport->port.rs485_config = imx_rs485_config;  // 设置rs485配置函数
      ->sport->port.rs485.flags = SER_RS485_RTS_ON_SEND | SER_RS485_RX_DURING_TX;
      ->sport->port.flags = UPF_BOOT_AUTOCONF;
      ->init_timer(&sport->timer);    // 初始化定时器
      ->sport->timer.function = imx_timeout;  // 设置定时器超时函数
      ->sport->timer.data     = (unsigned long)sport;  // 设置定时器超时函数的参数

      ->devm_clk_get    // 获取ipg时钟
      ->devm_clk_get    // 获取per时钟
      ->clk_get_rate    // uart的时钟频率
      ->devm_request_irq    // 请求接收中断,中断服务函数为imx_int
      ->imx_ports[sport->port.line] = sport    // 保存设备结构体指针,数组索引为uart别名id
      ->platform_set_drvdata    // 设置设备驱动中的私有数据

      ->uart_add_one_port       // 关联uart_driver(imx_reg)和uart_port(port)
        ->if (uport->line >= drv->nr)    // 检查uart端口数量是否超过了uart_driver中规定的数量
        ->state = drv->state + uport->line    // 根据串口编号找到具体串口的state地址
        ->state->uart_port = uport    // 关联state中的port和具体的port
        ->uport->state = state        // 关联具体port中的state和uart_driver中的state
        ->uart_configure_port    // 配置串口
          ->ops->config_port()   // 调用imx_config_port配置串口
            ->port.type = PORT_IMX    // 设置port类型为Motorola i.MX SoC
          ->uart_report_port    // 打印串口配置信息
          ->ops->set_mctrl()    // 调用imx_set_mctrl函数设置串口
            ->imx_set_mctrl     // 设置串口模式
      ->tty_port_register_device_attr    // 注册tty设备

注册过程分析完,还需要分析底层的操作函数,底层操作函数定义如下,这些函数与常用的IO操作函数都有对应关系,数据接收函数需要在中断中分析。

    static struct uart_ops imx_pops = {
        .tx_empty	= imx_tx_empty,    // 发送FIFO是否为空
        .set_mctrl	= imx_set_mctrl,
        .get_mctrl	= imx_get_mctrl,
        .stop_tx	= imx_stop_tx,     // 停止发送
        .start_tx	= imx_start_tx,    // 发送数据时调用
        .stop_rx	= imx_stop_rx,     // 停止接收
        .enable_ms	= imx_enable_ms,
        .break_ctl	= imx_break_ctl,
        .startup	= imx_startup,    // 启动串口,open串口设备时调用
        .shutdown	= imx_shutdown,   // 关闭串口,close串口设备调用
        .flush_buffer	= imx_flush_buffer,
        .set_termios	= imx_set_termios,  // 设置串口参数
        .type		= imx_type,
        .config_port	= imx_config_port,
        .verify_port	= imx_verify_port,
    #if defined(CONFIG_CONSOLE_POLL)
        .poll_init      = imx_poll_init,
        .poll_get_char  = imx_poll_get_char,
        .poll_put_char  = imx_poll_put_char,
    #endif
    };

分析imx_startup函数:

    imx_startup
      ->clk_prepare_enable    // 使能per时钟
      ->clk_prepare_enable    // 使能ipg时钟
      ->imx_setup_ufcr
        ->rx_fifo_trig = RXTL_UART    // 设置接收FIFO的触发字节数为16字节
        ->val |= TXTL << UFCR_TXTL_SHF | rx_fifo_trig  // 设置发送FIFO的触发字节数为2
      ->temp &= ~UCR2_SRST    // 软件复位uart控制器,FIFO等被清空
      ->temp |= UCR1_RRDYEN   // 开启接收中断
      ->temp |= UCR1_UARTEN   // 使能串口
      ->temp |= UCR4_OREN     // 接收溢出中断使能
      ->temp |= (UCR2_RXEN | UCR2_TXEN)    // 接受和发送使能

分析imx_start_tx函数:

    imx_start_tx
      ->writel(temp | UCR1_TXMPTYEN, sport->port.membase + UCR1)  // 开启发送中断

串口的发送和接收都在中断中进行,下面分析在serial_imx_probe函数中注册的中断服务函数imx_int:

    imx_int
      // 收到了数据,进接收中断服务函数
      ->if ((sts & USR1_RRDY || sts & USR1_AGTIM) &&!sport->dma_is_enabled)
        ->imx_rxint
          ->while (readl(sport->port.membase + USR2) & USR2_RDR)  // 循环读数据,直到读完
          ->readl(sport->port.membase + URXD0)  // 读取数据
          ->tty_insert_flip_char    // 回调tty层的函数,将收到的数据提交上去
            ->tty_insert_flip_string_flags
              ->tty_buffer_request_room    // 分配空间
              ->memcpy    // 将数据拷贝到tty层
      // 发送FIFO空或发送完成,进发送中断服务函数
      ->if ((sts & USR1_TRDY && readl(sport->port.membase + UCR1) & UCR1_TXMPTYEN) ||
	       (sts2 & USR2_TXDC && readl(sport->port.membase + UCR4) & UCR4_TCEN))
        ->imx_txint
          ->imx_transmit_buffer
            // 判断数据是否发送完或是否要停止发送
            ->if (uart_circ_empty(xmit) || uart_tx_stopped(&sport->port))
              imx_stop_tx  // 上述条件成立,停止发送
            // 发送缓冲区未空且发送FIFO未满就继续填充数据
            ->while (!uart_circ_empty(xmit) && !(readl(sport->port.membase + uts_reg(sport)) & UTS_TXFULL)) 
            ->writel(xmit->buf[xmit->tail], sport->port.membase + URTX0)
            ->imx_stop_tx(&sport->port)  // 发送缓冲区空,停止发送

4.调试说明

stty -F /dev/ttymxc2 -a // 查看某个串口的设置信息
stty -a < /dev/ttymxc2 // 查看某个串口的设置信息
// 设置串口,波特率115200,8位数据,无校验,1位停止位,禁止回显
stty -F /dev/ttymxc2 speed 115200 cs8 -parenb -cstopb -echo -icanon
// 取消规范模式,读请求直接从输入队列读取字符。使用cat时可直接显示字符,不需要数据中有回车
stty -F /dev/ttymxc2 -icanon

5.测试程序源码

	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	#include 
	
	
	#define OK     (0)
	#define ERROR  (-1)
	#define PATH "/dev/ttymxc2"
	
	int set_opt(int fd, int nSpeed, int nBits, char nEvent, int nStop)
	{
	    struct termios newtio;
	    if (tcgetattr(fd, &newtio) != 0) {
	        perror("SetupSerial 1");
	        return -1;
	    }
	    newtio.c_cflag &= ~CSIZE; //字符长度掩码。取值为:CS5,CS6,CS7或CS8
	    switch(nBits)
	    {
	    case 5:
	        newtio.c_cflag |= CS5;
	        break;
	    case 6:
	        newtio.c_cflag |= CS6;
	        break;
	    case 7:
	        newtio.c_cflag |= CS7;
	        break;
	    case 8:
	        newtio.c_cflag |= CS8;
	        break;
	    }
	
	    switch(nEvent)
	    {
	    case 'O':
	        newtio.c_cflag |= PARENB;
	        newtio.c_cflag |= PARODD;  
	        newtio.c_iflag |= (INPCK | ISTRIP); 
	        break;
	    case 'E':
	        newtio.c_iflag |= (INPCK | ISTRIP);
	        newtio.c_cflag |= PARENB;
	        newtio.c_cflag &= ~PARODD;
	        break;
	    case 'N': 
	        newtio.c_cflag &= ~PARENB;
	        break;
	    }
	
	    switch(nSpeed)
	    {
	    case 2400:
	        cfsetispeed(&newtio, B2400);
	        cfsetospeed(&newtio, B2400);
	        break;
	    case 4800:
	        cfsetispeed(&newtio, B4800);
	        cfsetospeed(&newtio, B4800);
	        break;
	    case 9600:
	        cfsetispeed(&newtio, B9600);
	        cfsetospeed(&newtio, B9600);
	        break;
	    case 115200:
	        cfsetispeed(&newtio, B115200);
	        cfsetospeed(&newtio, B115200);
	        break;
	    case 460800:
	        cfsetispeed(&newtio, B460800);
	        cfsetospeed(&newtio, B460800);
	        break;
	    default:
	        cfsetispeed(&newtio, B115200);
	        cfsetospeed(&newtio, B115200);
	        break;
	    }
	
	    if(nStop == 1)
	        newtio.c_cflag &=  ~CSTOPB; 
	    else if (nStop == 2)
	        newtio.c_cflag |=  CSTOPB;
	
	    newtio.c_cc[VTIME]  = 0;  // 读取串口数据等待的时间
	    newtio.c_cc[VMIN] = 1;    // 最少读取到1个字节的数据才返回
	    newtio.c_cflag  |=  CLOCAL | CREAD;
	    newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
	    newtio.c_oflag &= ~OPOST;
	    newtio.c_oflag &= ~(ONLCR | OCRNL);
	    newtio.c_iflag &= ~(ICRNL | INLCR);
	    newtio.c_iflag &= ~(IXON | IXOFF | IXANY);
	    
	    tcflush(fd,TCIFLUSH);
	    if((tcsetattr(fd, TCSANOW, &newtio)) != 0) {
	        perror("com set error");
	        return -1;
	    }
	    printf("set done!\n\r");
	    return 0;
	}
	/*  选项
	 *  hex-- 十六进制
	 *  str-- 字符
	 */
	int main(int argc, char* argv[])
	{
	    int fd, ret, i = 0;
	    int hex;
	    char ch;
	
	    if (2 != argc) {
	        printf("input parameter error\n");
	        return ERROR;
	    }
	
	    if (0 == strcmp("hex", argv[1]))
	        hex = 1;
	    else if (0 == strcmp("str", argv[1]))
	        hex = 0;
	    else {
	        printf("option error\n");
	        return ERROR;
	    }
	    fd = open(PATH, O_RDWR | O_NOCTTY);
	    if (fd <= 0) {
	        printf("open error\n");
	        return ERROR;
	    }
	    printf("open %s success\n", PATH);
	    ret = set_opt(fd, 115200, 8, 'N', 1);
	    if (ret == -1) {
	        printf("set_opt error\n");
	        return ERROR;
	    }
	
	    while (1) { 
	        ret = read(fd, &ch, sizeof(ch));
	        if (ret < 0) {
	            printf("read error\n");
	            return ERROR;
	        }
	        // 读取到数据,打印并写回
	        if (ret == sizeof(ch)) {
	            if (1 == hex) {
	                printf("%2X ", (unsigned char)ch);
	                if (0 == ((i + 1) % 0xF))
	                    printf("\n");
	                ++i;
	            }
	            else
	                printf("%c", ch);
	            ret = write(fd, &ch, sizeof(ch));
	            if (ret < 0) {
	                printf("write error\n");
	                return ERROR;
	            }              
	        }          
	    }
	    close(fd);
	    return 0;
	}

你可能感兴趣的:(Linux驱动,Linux,UART驱动,TTY,Linux内核)