发博文
正文 字体大小:

Linux串口通信编程(Linux Serial Programming HOWTO

(2008-11-27 14:26:33)
标签:

linux

串口编程

it

分类: 编程技术
译者: 曾元佑

最好的侦错你程式码的方法是建构另一台 Linux box, 并把两台电脑用 null-modem 缆线连接. 用 miniterm以传送字元到你的 Linux box. Miniterm 很容易编译而它会把所有输入到键盘的字元透过序列埠传送. 只有这个宣告定义会被检查 #define MODEMDEVICE "/dev/ttyS0". 如果是 COM1 设定为 ttyS0, 如果是 COM2 设定为 ttyS1 等等.. 先前的测试是必要的, 所有的 字元都将以 raw 方式 (不经任何处理) 直接传送. 测试是否连接正确, 在两台电脑上都启动 miniterm 然後随便在键盘上乱按. 在其中一台上输入的字元应该会显示在另一台电脑上反之亦同. 但输入的字元不会回应到与之相连的萤幕上. 

要自制 null-modem 的电缆, 你必需要把 TxD (传送) 及 RxD (接收) 两线对调.

当然也可以只用一台电脑来作相同的测试, 只要电脑上有两个未使用的序列埠. 当然你也就要执行两个 miniterm 来当虚拟控制台. 如果你是藉由拔去滑鼠来取得另一个序列埠, 记得要把 /dev/mouse 装置重新导向, 如果它存在的话. 如果你使用多埠的序列埠控制卡, 请确定它已设定正确. 当我在我的电脑上测试时也曾经因为设定错误而出过槌. 当我连到另一台电脑, 通讯埠开始传送字元. 就因为刚好这不是完整的非同步式传输, 所以可在同一台电脑上执行两个程式.

连接埠设定 
/dev/ttyS* 装置会被当成连接到你的 Linux box 的终端机, 并且在启动後就设定好了. 这个观念在你写 raw 装置的通讯程式时必需记住. 也就是说这个连接埠被设定为回应所有自这个装置送出的字元, 而用在资料传输时通常这种要改变这种工作模式. 

所有的参数可以由一个小程式简单的完成. 设定参数被放在一个结构体内 struct termios, 他的定义档在 : 

#define NCCS 19
struct termios {
        tcflag_t c_iflag;              
        tcflag_t c_oflag;              
        tcflag_t c_cflag;              
        tcflag_t c_lflag;              
        cc_t c_line;                   
        cc_t c_cc[NCCS];               
};

这个档案也包含所有的旗标定义. 输入模式旗标在 c_iflag 掌管所有的输入处理, 这就意谓著由装置上传来的字元在还没用 read 功能读取前可以先处理过. 同理 c_oflag 掌管所有的输出处理. c_cflag 包含连接埠的设定, 如 鲍率, 每字元多少位元, 停止位元, 等等.. 区域模式旗标放在 c_lflag 用来侦测字元是否回应, 而讯号会送到你的程式, 等等.. 最後 c_cc 阵列定义了档案终了的控制字元, 停止, 等等.. 预设的控制字元值放在 . 有关旗标的细节摆在使用手册 termios(3). termios 结构体内的 c_line 行控制 (line discipline) 元素, 不能在 POSIX 相容的系统下使用译者注:这里所说的 line discipline 虽然我翻成 行控制但还是很难说出那是舍. 如果想知道请看看 kernel :( .


序列装置的输入观念 
有三个输入的观念要说明. 按照所要写的应用程式选用适合的观念. 尽量避免使用回圈来读取单一的字元再组成字串. 我曾这样做过, 会掉字元, 且对 read 而言不会显示任何错误. 


标准输入程序
这是终端机的标准处理程序, 但用来与其他 dl 型式的以行为单位的输入通讯也很有用, 也就是 read 会传回一整行完整的输入资料. 行预设的终止字元是 NL (ASCII LF), 档案结束符, 或行终止字元. 预设环境下, CR (是 DOS/Windows 预设的行终止符) 不会终止一行的叙述. 

标准的输入处理程序还可以处理 清除, 删除字, 重印字元, 及转换 CR 为 NL 等等功能.. 


非标准输入程序
非标准输入程序可以用在需要每次读取固定数量字元的情况, 并允许使用字元输入时间的计时器. 这种模式可以用在读取固定字元数量的应用程式, 或者所连接的装置会突然送出大量字元的状况. 

 

非同步式输入
以上所叙述的两种模式都可以用在非同步与同步的传输模式. 预设是在同步的模式下工作, 也就是在尚未读取完之前, read 的状态会被阻断. 而非同步模式下 read 的状态会直接返回并送出讯号到所叫用的程式直到完成工作. 这个讯号可以由讯号的处理程式 handler...来接收. 


等待来自多个讯号来源的输入
这并不是一个不一样的输入模式. 如果你要透过序列埠连接并处理多个装置的话, 它是满有用的. 在我的应用程式中我必需在几乎同一时间内, 透过 TCP/IP socket 及序列埠处理来自其他电脑的输入讯号. 下面这个□例程式将等待来自两个不同输入源的讯号. 如果其中一个信号源出现, 他就会被处理, 而程式会继续等待新的输入讯号. 

以下这个方法看起来相当覆杂, 但请记住 Linux 是一个多工的作业系统. select 这个系统呼叫并不会在等待输入讯号时把 CPU 负载加重, 而如果你用回圈方式来等待输入讯号将使得其它同时执行的行程被拖慢. 


程序范例
所有的范例来源自 miniterm.c. The type ahead 暂存器被限制在 255 个字元, 就跟标准输入程序的最大字串长度相同 或 ). 

参考程式码中的注解它会解释不同输入模式的使用. 我希望这些程式码都能被了解. 标准输入程序的程式范例的注解写得最好, 其它的范例都只在不同於其它范例的地方做注解. 

叙述不是很完整, 但可以激励你对这范例做实验, 以延生出合於你所需应用程式的最佳解. 

别忘记要把序列埠的权限设定正确 (也就是: chmod a+rw /dev/ttyS1)! 



标准输入程序 

#include 
#include 
#include 
#include 
#include 


#define BAUDRATE B38400            

#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 

#define FALSE 0
#define TRUE 1

volatile int STOP=FALSE; 

main()
{
  int fd,c, res;
  struct termios oldtio,newtio;
  char buf[255];

 fd open(MODEMDEVICE, O_RDWR O_NOCTTY ); 
 if (fd <0) {perror(MODEMDEVICE); exit(-1); }

 tcgetattr(fd,&oldtio); 
 bzero(&newtio, sizeof(newtio)); 


 newtio.c_cflag BAUDRATE CRTSCTS CS8 CLOCAL CREAD;
 

 newtio.c_iflag IGNPAR ICRNL;
 

 newtio.c_oflag 0;
 

 newtio.c_lflag ICANON;
 

 newtio.c_cc[VINTR]    0;      
 newtio.c_cc[VQUIT]    0;     
 newtio.c_cc[VERASE]   0;     
 newtio.c_cc[VKILL]    0;     
 newtio.c_cc[VEOF]     4;     
 newtio.c_cc[VTIME]    0;     
 newtio.c_cc[VMIN]     1;     
 newtio.c_cc[VSWTC]    0;     
 newtio.c_cc[VSTART]   0;      
 newtio.c_cc[VSTOP]    0;     
 newtio.c_cc[VSUSP]    0;     
 newtio.c_cc[VEOL]     0;     
 newtio.c_cc[VREPRINT] 0;     
 newtio.c_cc[VDISCARD] 0;     
 newtio.c_cc[VWERASE]  0;     
 newtio.c_cc[VLNEXT]   0;     
 newtio.c_cc[VEOL2]    0;     


 tcflush(fd, TCIFLUSH);
 tcsetattr(fd,TCSANOW,&newtio);


 while (STOP==FALSE)     
 
    res read(fd,buf,255); 
    buf[res]=0;             
    printf(":%s:%d\n", buf, res);
    if (buf[0]=='z') STOP=TRUE;
 }
 
 tcsetattr(fd,TCSANOW,&oldtio);
}



3.2 非标准输入程序 
在非标准的输入程序模式下, 输入的资料不会被组合成一行而输入後的处理功能 (清除, 杀掉, 删除, 等等.) 都不能使用. 这个模式有两个功能控制参数: c_cc[VTIME] 设定字元输入时间计时器, 及 c_cc[VMIN] 设定满足读取功能的最低字元接收个数. 

如果 MIN 且 TIME 0, MIN 设定为满足读取功能的最低字元接收个数. 由於 TIME 是 零, 所以计时器将不被使用. 

如果 MIN 且 TIME 0, TIME 将被当做逾时设定值. 满足读取功能的情况为读取到单一字元, 或者超过 TIME 所定义的时间 (t TIME *0.1 s). 如果超过 TIME 所定义的时间, 则不会传回任何字元. 

如果 MIN 且 TIME 0, TIME 将被当做一个分割字元组的计时器. 满足读取功能的条件为 接收到  MIN 个数的字元, 或两个字元的间隔时间超过 TIME 所定义的值. 计时器会在每读到一个字元後重新计时, 且只会在第一个字元收到後才会启动. 

如果 MIN 且 TIME 0, 读取功能就马上被满足. 目前所存在的字元组个数, 或者 将回传的字元组个数. 根据  Antonino (参考 贡献) 所说, 你可以用 fcntl(fd, F_SETFL, FNDELAY); 在读取前得到相同的结果. 

藉由修改 newtio.c_cc[VTIME] 及 newtio.c_cc[VMIN] 上述的模式就可以测试了. 


#include 
#include 
#include 
#include 
#include 

#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 
#define FALSE 0
#define TRUE 1

volatile int STOP=FALSE; 

main()
{
  int fd,c, res;
  struct termios oldtio,newtio;
  char buf[255];

 fd open(MODEMDEVICE, O_RDWR O_NOCTTY ); 
 if (fd <0) {perror(MODEMDEVICE); exit(-1); }

 tcgetattr(fd,&oldtio); 

 bzero(&newtio, sizeof(newtio));
 newtio.c_cflag BAUDRATE CRTSCTS CS8 CLOCAL CREAD;
 newtio.c_iflag IGNPAR;
 newtio.c_oflag 0;

 
 newtio.c_lflag 0;
 
 newtio.c_cc[VTIME]    0;   
 newtio.c_cc[VMIN]     5;   

 tcflush(fd, TCIFLUSH);
 tcsetattr(fd,TCSANOW,&newtio);


 while (STOP==FALSE)       
   res read(fd,buf,255);   
   buf[res]=0;               
   printf(":%s:%d\n", buf, res);
   if (buf[0]=='z') STOP=TRUE;
 }
 tcsetattr(fd,TCSANOW,&oldtio);
}


3.3 非同步式输入 

#include 
#include 
#include 
#include 
#include 
#include 

#define BAUDRATE B38400
#define MODEMDEVICE "/dev/ttyS1"
#define _POSIX_SOURCE 
#define FALSE 0
#define TRUE 1

volatile int STOP=FALSE; 

void signal_handler_IO (int status);   
int wait_flag=TRUE;                    

main()
{
  int fd,c, res;
  struct termios oldtio,newtio;
  struct sigaction saio;           
  char buf[255];

  
  fd open(MODEMDEVICE, O_RDWR O_NOCTTY O_NONBLOCK);
  if (fd <0) {perror(MODEMDEVICE); exit(-1); }

  
  saio.sa_handler signal_handler_IO;
  saio.sa_mask 0;
  saio.sa_flags 0;
  saio.sa_restorer NULL;
  sigaction(SIGIO,&saio,NULL);
  
  
  fcntl(fd, F_SETOWN, getpid());
  
  fcntl(fd, F_SETFL, FASYNC);

  tcgetattr(fd,&oldtio); 
  
  newtio.c_cflag BAUDRATE CRTSCTS CS8 CLOCAL CREAD;
  newtio.c_iflag IGNPAR ICRNL;
  newtio.c_oflag 0;
  newtio.c_lflag ICANON;
  newtio.c_cc[VMIN]=1;
  newtio.c_cc[VTIME]=0;
  tcflush(fd, TCIFLUSH);
  tcsetattr(fd,TCSANOW,&newtio);
 
   
  while (STOP==FALSE) {
    printf(".\n");usleep(100000);
    
    if (wait_flag==FALSE) 
      res read(fd,buf,255);
      buf[res]=0;
      printf(":%s:%d\n", buf, res);
      if (res==1) STOP=TRUE; 
      wait_flag TRUE;      
    }
  }
  
  tcsetattr(fd,TCSANOW,&oldtio);
}



void signal_handler_IO (int status)
{
  printf("received SIGIO signal.\n");
  wait_flag FALSE;
}


3.4 等待来自多个讯号来源的输入 
这一段很短. 它只能被拿来当成写程式时的提示, 故□例程式也很简短. 但这个□例不只能用在序列埠上, 还可以用在被当成档案来使用的装置上. 

select 呼叫及伴随它所引发的巨集共用 fd_set. fd_set 则是一个位元阵列, 而其中每一个位元代表一个有效的档案叙述结构. select 呼叫接受一个有效的档案叙述结构并传回 fd_set 位元阵列, 而该位元阵列中若有某一个位元为 1, 就表示相对映的档案叙述结构的档案发生了输入, 输出或有例外事件. 而这些巨集提供了所有处理 fd_set 的功能. 亦可参考手册 select(2). 


#include 
#include 
#include 

main()
{
   int    fd1, fd2;  
   fd_set readfs;    
   int    maxfd;     
   int    loop=1;     

   
   fd1 open_input_source("/dev/ttyS1");   
   if (fd1<0) exit(0);
   fd2 open_input_source("/dev/ttyS2");   
   if (fd2<0) exit(0);
   maxfd MAX (fd1, fd2)+1;  

   
   while (loop) {
     FD_SET(fd1, &readfs);  
     FD_SET(fd2, &readfs);  
     
     select(maxfd, &readfs, NULL, NULL, NULL);
     if (FD_ISSET(fd1))         
       handle_input_from_source1();
     if (FD_ISSET(fd2))         
       handle_input_from_source2();
   }

  

这个□例程式在等待输入讯号出现前, 不能确定它会停顿下来. 如果你需要在输入时加入逾时功能, 只需把 select 呼叫换成: 

int res;
struct timeval Timeout;


Timeout.tv_usec 0;  
Timeout.tv_sec  1;  
res select(maxfd, &readfs, NULL, NULL, &Timeout);
if (res==0)
 

这个程式会在 秒钟後逾时. 如果超过时间, select 会传回 0, 但是应该留意 Timeout 的时间递减是由 select 所等待输入讯号的时间为基准. 如果逾时的值是 0, select 会马上结束返回. 

阅读 评论 收藏 转载 打印举报
已投稿到:
  • 评论加载中,请稍候...

       

    验证码: 请点击后输入验证码 收听验证码

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 不良信息反馈 电话:4006900000 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有