Linux终端设备驱动(二)

 

14.3终端设备驱动初始化与释放

14.3.1模块加载与卸载函数
    tty驱动的模块加载函数中通常需要分配、初始化tty_driver结构体并申请必要的硬件资源,代码清单14.4。tty驱动的模块卸载函数完成与模块加载函数完成相反的工作。
代码清单14.4 终端设备驱动模块加载函数范例
1  /* tty驱动模块加载函数 */
2  static int __init xxx_init(void)
3  {
4    ...
5    /* 分配tty_driver结构体 */
6    xxx_tty_driver = alloc_tty_driver(XXX_PORTS);
7    /* 初始化tty_driver结构体 */
8    xxx_tty_driver->owner = THIS_MODULE;
9    xxx_tty_driver->devfs_name = "tts/";
10   xxx_tty_driver->name = "ttyS";
11   xxx_tty_driver->major = TTY_MAJOR;
12   xxx_tty_driver->minor_start = 64;
13   xxx_tty_driver->type = TTY_DRIVER_TYPE_SERIAL;
14   xxx_tty_driver->subtype = SERIAL_TYPE_NORMAL;
15   xxx_tty_driver->init_termios = tty_std_termios;
16   xxx_tty_driver->init_termios.c_cflag = B9600 | CS8 | CREAD | HUPCL | CLOCAL;
17   xxx_tty_driver->flags = TTY_DRIVER_REAL_RAW;
18   tty_set_operations(xxx_tty_driver, &xxx_ops);
19 
20   ret = tty_register_driver(xxx_tty_driver);
21   if (ret)
22   {
23     printk(KERN_ERR "Couldn't register xxx serial driver/n");
24     put_tty_driver(xxx_tty_driver);
25     return ret;
26   }
27 
28   ...
29   ret = request_irq(...); /* 硬件资源申请 */
30   ...
31 }  
14.3.2打开与关闭函数
    当用户对tty驱动所分配的设备节点进行open()系统调用时,tty_driver中的open()成员函数将被tty核心调用。tty 驱动必须设置open()成员,否则,-ENODEV将被返回给调用open()的用户。open()成员函数的第1个参数为一个指向分配给这个设备的 tty_struct结构体的指针,第2个参数为文件指针。
tty_struct结构体被 tty核心用来保存当前tty端口的状态,它的大多数成员只被 tty核心使用。tty_struct中的几个重要成员如下:
•  flags标示tty 设备的当前状态,包括TTY_THROTTLED、TTY_IO_ERROR、TTY_OTHER_CLOSED、TTY_EXCLUSIVE、TTY_DEBUG、TTY_DO_WRITE_WAKEUP、TTY_PUSH、TTY_CLOSING、TTY_DONT_FLIP、TTY_HW_COOK_OUT、TTY_HW_COOK_IN、TTY_PTY_LOCK、TTY_NO_WRITE_SPLIT等。
•  ldisc为给 tty 设备的线路规程。
•  write_wait、read_wait为给tty写/读函数的等待队列,tty驱动应当在合适的时机唤醒对应的等待队列。
•  termios为指向 tty 设备的当前 termios 设置的指针。
•  stopped:1指示是否停止tty设备,tty 驱动可以设置这个值;hw_stopped:1指示是否tty设备已经被停止,tty 驱动可以设置这个值;flow_stopped:1指示是否 tty 设备数据流停止。
•  driver_data、disc_data为数据指针,用于存储tty驱动和线路规程的“私有”数据。
驱动中可以定义1个设备相关的结构体,并在open()函数中将其赋值给tty_struct的driver_data成员,如代码清单14.5。
代码清单14.5 在tty驱动打开函数中赋值tty_struct的driver_data成员
1  /* 设备“私有”数据结构体 */
2  struct xxx_tty
3  {
4    struct tty_struct *tty; /* tty_struct指针 */
5    int open_count; /* 打开次数 */
6    struct semaphore sem; /* 结构体锁定信号量 */
7    int xmit_buf; /* 传输缓冲区 */
8    ...
9  }
10 
11 /* 打开函数 */
12 static int xxx_open(struct tty_struct *tty, struct file *file)
13 {
14   struct xxx_tty *xxx;
15 
16   /* 分配xxx_tty */
17   xxx = kmalloc(sizeof(*xxx), GFP_KERNEL);
18   if (!xxx)
19     return  - ENOMEM;
20   /* 初始化xxx_tty中的成员 */
21   init_MUTEX(&xxx->sem);
22   xxx->open_count = 0;
23   ...
24   /* 让tty_struct中的driver_data指向xxx_tty */
25   tty->driver_data = xxx;
26   xxx->tty = tty;
27   ...
28   return 0;
29 }
    在用户对前面使用 open()系统调用而创建的文件句柄进行close()系统调用时,tty_driver中的close()成员函数将被tty核心调用。


14.4 数据发送和接收
    
图14.3给出了终端设备数据发送和接收过程中的数据流以及函数调用关系。用户在有数据发送给终端设备时,通过“write()系统调用――tty核心――线路规程”的层层调用,最终调用tty_driver结构体中的write()函数完成发送。
    因为速度和tty硬件缓冲区容量的原因,不是所有的写程序要求的字符都可以在调用写函数时被发送,因此写函数应当返回能够发送给硬件的字节数以便用户程序检查是否所有的数据被真正写入。如果在 wirte()调用期间发生任何错误,一个负的错误码应当被返回。
                    

用户空间

write()                      read()

tty核心

    tty_write()                   tty_ read()

线路规程

ldisc.write()                  ldisc. read()

                            ldisc.receive_buf()

tty驱动                         tty_flip_buffer_push()

driver.write()

                            中断处理函数

硬件层     

tty缓冲区

flip_buffer

 DB9

数据流

函数调用

           


                图14.3 终端设备数据发送和接收过程中的数据流和函数调用关系


    tty_driver 的write()函数接受3个参数tty_struct、发送数据指针及要发送的字节数,一般首先会通过tty_struct的driver_data成员得到设备私有信息结构体,然后依次进行必要的硬件操作开始发送,代码清单14.6给出了tty_driver的write()函数范例。
代码清单14.6 tty_driver结构体的write()成员函数范例
1  static int xxx_write(struct tty_struct *tty, const unsigned char *buf, int count)
2  {
3    /* 获得tty设备私有数据 */
4    struct xxx_tty *xxx = (struct xxx_tty*)tty->driver_data;
5    ...
6    /* 开始发送 */
7    while (1)
8    {
9      local_irq_save(flags);
10     c = min_t(int, count, min(SERIAL_XMIT_SIZE - xxx->xmit_cnt - 1,
11       SERIAL_XMIT_SIZE - xxx->xmit_head));

12     if (c <= 0)
13     {
14       local_irq_restore(flags);
15       break;
16     }
17     //拷贝到发送缓冲区
18     memcpy(xxx->xmit_buf + xxx->xmit_head, buf, c);
19     xxx->xmit_head = (xxx->xmit_head + c) &(SERIAL_XMIT_SIZE - 1);
20     xxx->xmit_cnt += c;
21     local_irq_restore(flags);
22 
23     buf += c;
24     count -= c;
25     total += c;
26   }
27 
28   if (xxx->xmit_cnt && !tty->stopped && !tty->hw_stopped)
29   {
30     start_xmit(xxx);//开始发送
31   }
32   return total; //返回发送的字节数
33 }      
    当tty子系统自己需要发送数据到 tty 设备时,如果没有实现 put_char()函数,write()函数将被调用,此时传入的count参数为1,通过对代码清单14.7的分析即可获知。
代码清单14.7 put_char()函数的write()替代
1  int tty_register_driver(struct tty_driver *driver)
2  {
3    ...
4    if (!driver->put_char)//没有定义put_char()函数
5      driver->put_char = tty_default_put_char;
6    ...
7  }
8  static void tty_default_put_char(struct tty_struct *tty, unsigned char ch)
9  {
10   tty->driver->write(tty, &ch, 1);//调用tty_driver.write()函数
11 }
    读者朋友们可能注意到了,tty_driver结构体中没有提供 read()函数。因为发送是用户主动的,而接收即用户调read()则是读一片缓冲区中已放好的数据。tty 核心在一个称为struct tty_flip_buffer 的结构体中缓冲数据直到它被用户请求。因为tty核心提供了缓冲逻辑,因此每个 tty 驱动并非一定要实现它自身的缓冲逻辑。
    tty驱动不必过于关心tty_flip_buffer 结构体的细节,如果其count字段大于或等于TTY_FLIPBUF_SIZE,这个flip缓冲区就需要被刷新到用户,刷新通过对tty_flip_buffer_push()函数的调用来完成,代码清单代码清单14.8给出了范例。
代码清单14.8 tty_flip_buffer_push()范例
1 for (i = 0; i < data_size; ++i)
2 {
3   if (tty->flip.count >= TTY_FLIPBUF_SIZE)
4      tty_flip_buffer_push(tty);//数据填满向上层“推”
5   tty_insert_flip_char(tty, data[i], TTY_NORMAL);//把数据插入缓冲区
6 }
7 tty_flip_buffer_push(tty);
    从tty 驱动接收到字符通过tty_insert_flip_char()函数被插入到flip缓冲区。该函数的第1个参数是数据应当保存入的 tty_struct结构体,第 2 个参数是要保存的字符,第3个参数是应当为这个字符设置的标志,如果字符是一个接收到的常规字符,则设为TTY_NORMAL,如果是一个特殊类型的指示错误的字符,依据具体的错误类型,应当设为TTY_BREAK、 TTY_PARITY或TTY_OVERRUN。


14.5 TTY线路设置
14.5.1线路设置用户空间接口
 用户可用如下2种方式改变tty设备的线路设置或者获取当前线路设置:
 1、调用用户空间的termios库函数
     用户空间的应用程序需引用termios.h头文件,该头文件包含了终端设备的I/O接口,实际是由POSIX定义的标准方法。对终端设备操作模式的描述由termios结构体完成,从代码清单14.2可以看出,这个结构体包含c_iflag、c_oflag、c_cflag、c_lflag和c_cc []几个成员。
    termios的c_cflag主要包含如下位域信息:CSIZE(字长)、CSTOPB(2个停止位)、PARENB(奇偶校验位使能)、PARODD (奇校验位,当PARENB被使能时)、CREAD(字符接收使能,如果没有置位,仍然从端口接收字符,但这些字符都要被丢弃)、 CRTSCTS (如果被置位,使能CTS状态改变报告)、CLOCAL (如果没有置位,使能调制解调器状态改变报告)。
    termios的c_iflag主要包含如下位域信息:INPCK (使能帧和奇偶校验错误检查)、BRKINT(break将清除终端输入/输出队列,向该终端上前台的程序发出SIGINT信号)、 PARMRK (奇偶校验和帧错误被标记,在INPCK被设置且IGNPAR未被设置的情况下才有意义)、IGNPAR (忽略奇偶校验和帧错误)、IGNBRK (忽略break)。
    通过tcgetattr()、tcsetattr()函数即可完成对终端设备的操作模式的设置和获取,这2个函数的原型如下:
int tcgetattr (int fd, struct termios *termios_p);
int tcsetattr (int fd, int optional_actions, struct termios *termios_p);
例如,Raw模式的线路设置为:
• 非正规模式
• 关闭回显
• 禁止 CR 到 NL 的映射(ICRNL)、输入奇偶校验、输入第 8 位的截取(ISTRIP)以及输出流控制
• 8位字符(CS8),奇偶校验被禁止
• 禁止所有的输出处理
• 每次一个字节 (c_cc [VMIN] = 1、c_cc [VTIME] = 0)
则对应的对termios结构体的设置就为:
termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
                                   | INLCR | IGNCR | ICRNL | IXON);
termios_p->c_oflag &= ~OPOST;
termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
termios_p->c_cflag &= ~(CSIZE | PARENB);
termios_p->c_cflag |= CS8;
通过如下一组函数可完成输入/输出波特率的获取和设置:
speed_t cfgetospeed (struct termios *termios_p); //获得输出波特率
speed_t cfgetispeed (struct termios *termios_p); //获得输入波特率

int cfsetospeed (struct termios *termios_p, speed_t speed); //设置输出波特率
int cfsetispeed (struct termios *termios_p, speed_t speed); //设置输入波特率
如下一组函数则完成线路控制:
int tcdrain (int fd); //等待所有输出都被发送
int tcflush (int fd, int queue_selector); //flush输入/输出缓存
int tcflow (int fd, int action); // 对输入和输出流进行控制
int tcsendbreak (int fd, int duration);//发送break
    tcflush函数刷清(抛弃)输入缓存(终端驱动程序已接收到,但用户程序尚未读取)或输出缓存(用户程序已经写,但驱动尚未发送),queue参数可取TCIFLUSH(刷清输入队列)、TCOFLUSH(刷清输出队列)或TCIOFLUSH(刷清输入、输出队列)。
    tcflow()对输入输出进行流控制,action参数可取TCOOFF(输出被挂起)、TCOON(重新启动以前被挂起的输出)、TCIOFF(发送1个STOP字符,使终端设备暂停发送数据)、TCION(发送1个START字符,使终端恢复发送数据)。
   tcsendbreak()函数在一个指定的时间区间内发送连续的0二进位流。若duration参数为0,则此种发送延续0.25-0.5秒之间。POSIX.1说明若duration非0,则发送时间依赖于实现。
2、对tty设备节点进行ioctl()调用
     大部分termios库函数会被转化为对tty设备节点的ioctl()调用,例如tcgetattr()、tcsetattr()函数对应着TCGETS、TCSETS IO控制命令。TIOCMGET (获得MODEM状态位)、TIOCMSET(设置MODEM状态位)、TIOCMBIC       (清除指示MODEM位)、TIOCMBIS(设置指示MODEM位)这4个IO控制命令用于获取和设置MODEM握手,如RTS、CTS、DTR、 DSR、RI、CD等。
14.5.2 tty驱动set_termios函数
    大部分 termios 用户空间函数被库转换为对驱动节点的 ioctl()调用,而tty ioctl中的大部分命令会被tty核心转换为对tty驱动的set_termios()函数的调用。set_termios()函数需要根据用户对 termios的设置(termios设置包括字长、奇偶校验位、停止位、波特率等)完成实际的硬件设置。
tty_operations中的set_termios()函数原型为:
void(*set_termios)(struct tty_struct *tty, struct termios *old);
    新的设置被保存在tty_struct中,旧的设置被保存在old参数中,若新旧参数相同,则什么都不需要做,对于被改变的设置,需完成硬件上的设置,代码清单14.9给出了set_termios()函数的例子。
代码清单14.9 tty驱动程序set_termios()函数范例
1  static void xxx_set_termios(struct tty_struct *tty, struct termios *old_termios)
2  {
3    struct xxx_tty *info = (struct cyclades_port*)tty->driver_data;
4    /* 新设置等同于老设置,什么也不做 */
5    if (tty->termios->c_cflag == old_termios->c_cflag)
6      return ;
7    ...
8  
9    /* 关闭CRTSCTS硬件流控制 */
10   if ((old_termios->c_cflag &CRTSCTS) && !(cflag &CRTSCTS))
11   {
12     ...
13   }
14 
15   /* 打开CRTSCTS硬件流控制 */
16   if (!(old_termios->c_cflag &CRTSCTS) && (cflag &CRTSCTS))
17   {
18     ...
19   }
20 
21   /* 设置字节大小 */
22   switch (tty->termios->c_cflag &CSIZE)
23   {
24 
25     case CS5:
26     ...
27     case CS6:
28     ...
29     case CS7:
30     ...
31     case CS8:
32     ...
33   }
34 
35   /* 设置奇偶校验 */
36   if (tty->termios->c_cflag &PARENB)
37     if (tty->termios->c_cflag &PARODD)  //奇校验
38     ...
39     else  //偶校验
40     ...
41   else //无校验
42   ...
43 }
14.5.3 tty驱动 tiocmget和tiocmset函数
对TIOCMGET、 TIOCMSET、TIOCMBIC和TIOCMBIS IO控制命令的调用将被tty核心转换为对tty驱动tiocmget()函数和tiocmset()函数的调用,TIOCMGET对应tiocmget ()函数,TIOCMSET、TIOCMBIC和TIOCMBIS 对应tiocmset()函数,分别用于读取Modem控制的设置和进行Modem的设置。代码清单14.10给出了tiocmget()函数的范例,代 码清单14.11则给出了tiocmset()函数的范例。
代码清单14.10 tty驱动程序tiocmget()函数范例
1  static int xxx_tiocmget(struct tty_struct *tty, struct file *file)
2  {
3    struct xxx_tty *info = tty->driver_ data;
4    unsigned int result = 0;
5    unsigned int msr = info->msr;
6    unsigned int mcr = info->mcr;
7    result = ((mcr &MCR_DTR) ? TIOCM_DTR : 0) |  /* DTR 被设置 */
8    ((mcr &MCR_RTS) ? TIOCM_RTS : 0) |  /* RTS 被设置 */
9    ((mcr &MCR_LOOP) ? TIOCM_LOOP : 0) |  /* LOOP 被设置 */
10   ((msr &MSR_CTS) ? TIOCM_CTS : 0) |  /* CTS 被设置 */
11   ((msr &MSR_CD) ? TIOCM_CAR : 0) |  /* CD 被设置*/
12   ((msr &MSR_RI) ? TIOCM_RI : 0) |  /* 振铃指示被设置 */
13   ((msr &MSR_DSR) ? TIOCM_DSR : 0); /* DSR 被设置 */
14   return result;
15 }
代码清单14.11 tty驱动程序tiocmset()函数范例
1  static int xxx_tiocmset(struct tty_struct *tty, struct file *file, unsigned
2    int set, unsigned int clear)
3  {
4    struct xxx_tty *info = tty->driver_data;
5    unsigned int mcr = info->mcr;
6  
7    if (set &TIOCM_RTS) /* 设置RTS */
8      mcr |= MCR_RTS;
9    if (set &TIOCM_DTR) /* 设置DTR */
10     mcr |= MCR_RTS;
11 
12   if (clear &TIOCM_RTS) /* 清除RTS */
13     mcr &= ~MCR_RTS;
14   if (clear &TIOCM_DTR) /* 清除DTR */
15     mcr &= ~MCR_RTS;
16 
17   /* 设置设备新的MCR值 */
18   tiny->mcr = mcr;
19   return 0;
20 }
tiocmget()函数会访问MODEM状态寄存器(MSR),而tiocmset()函数会访问MODEM控制寄存器(MCR)。
14.5.3 tty驱动ioctl函数
    当用户在tty设备节点上进行ioctl(2) 调用时,tty_operations中的 ioctl()函数会被tty核心调用。如果 tty 驱动不知道如何处理传递给它的 ioctl 值,它返回 –ENOIOCTLCMD,之后tty 核心会执行一个通用的操作。
驱动中常见的需处理 的IO控制命令包括TIOCSERGETLSR(获得这个 tty 设备的线路状态寄存器LSR 的值)、TIOCGSERIAL(获得串口线信息)、TIOCMIWAIT(等待 MSR 改变)、TIOCGICOUNT(获得中断计数)等。代码清单14.12给出了tty驱动程序ioctl()函数的范例。
代码清单14.12 tty驱动程序ioctl()函数范例
1  static int xxx_ioctl(struct tty_struct *tty, struct file *filp, unsigned int
2    cmd, unsigned long arg)
3  {
4    struct xxx_tty *info = tty->driver_data;
5    ...
6    /* 处理各种命令 */
7    switch (cmd)
8    {
9      case TIOCGSERIAL:
10       ...
11     case TIOCSSERIAL:
12       ...
13     case TIOCSERCONFIG:
14       ...
15     case TIOCMIWAIT:
16     ...
17     case TIOCGICOUNT:
18     ...
19     case TIOCSERGETLSR:
20     ...
21   }
22   ...

23 }

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值