UNIX中(从而也在Linux中),同一种设备分配一个主设备号,同属一种设备的不同设备分配不同的子设备号以示区别。原则上讲,设备文件可以存在于任何目录中,但在标准的UNIX(也在Linux)中,设备文件总是存在于/dev目录中。串口的设备文件为/dev/ttyS0、/dev/ttyS1等,代表串口1、2等。IDE硬盘设备文件为/dev/hda、/dev/hdb等代表IDE硬盘1、2等,在PC系统中只能存在4块IDE硬盘。第一个IDE硬盘的第一个分区为/dev/hda1,依次类推。另外,SCSI硬盘的设备文件为/dev/sda、/dev/sdb等。软盘的设备文件为/dev/fd0、/dev/fd1等。Linux的虚拟终端设备也是按文件管理,分别是/dev/tty1、/dev/tty2等。
由于对设备文件的操作,直接对应地层的I/O,有时会带来安全问题。比如,允许一个普通用户对磁盘设备有读写权限,那么他就会利用低层的读写获取或修改任何文件的内容。这听起来很难,但很容易作到。在比如,一个用户如果取得了对中断文件的读写权限,那么他可以编一个象中断登录一样的程序骗取别的用户的用户名和密码,再做的好一点,可以等到输入用户名和密码后再传给真正的登录会话程序完成登录,使受损害的用户无法察觉。总之,在一个很多人使用的重要的系统中,由设备文件引起的安全问题要足够的重视。
设备文件的操作和普通文件类似,open、close、read、write等函数同样可以使用。但对字符设备,改变文件当前指针位置的操作显然是不允许的。设备也只有在确实可用的情况下才能打开,设备真实存在,并且安装了响应的驱动程序。低层设备出来普通的操作外,还需要对设备进行控制、配置等和硬件相关的操作,这需要由ioctl函数来完成。下面介绍这个函数。
9.3.1 设备文件控制函数
尽管多数情况下硬件设备的操作可以通过文件的read、write、lseek等操作实现,但总有一些特例,比如弹出光盘、让磁带机倒带、设置声卡的采样率等,需要一些特别的手段实现。这就是ioctl函数,它可以算得上是控制设备的“瑞士军刀”,所有这些特殊的操作,它都能实现。它的原型包含在sys/ioctl.h头文件中。
int ioctl(int filedes,int command, ...);
函数的第一个参数是打开的文件的描述符,第二个参数是命令。第三个参数往往是需要的,它表示完成命令的操作需要的参数或返回的结果。它的意义取决于命令参数,可以是单个数,也可以是指向复杂的数据结构的指针。实际上,由于ioctl函数面向所有设备文件,不同的设备又是千差万别,所以,第三个参数的意义、函数的返回值、错误代码等等都取决于command。并且,不同的设备,即使是相同的command也有不同的含义,需要的参数和返回值、错误代码等也不同。所以,对一个未知的设备文件无法知道怎样使用ioctl函数。这只能查阅每一个特殊设备的编程文档。
下面我们将举两个实际的例子说明设备文件的编程方法。
9.3.2 串行口的编程
串行通讯是常用的系统互连的手段,不仅限于计算机和外设之间通讯。在现代的操作系统中,一般不允许用户直接用机器指令直接操作系统的硬件。Linux操作系统也是这样。Linux系统中是通过对设备特殊文件的读写来进行通讯口的输入输出操作。通过它,用户的程序能够和操作系统内核中的设备驱动程序通讯,从而控制硬件的行为。串行口的设备文件是/dev/ttySn,其中n=0,1等,代表串口1和2。文中我们以/dev/ttyS0为例。
要对串行口设备文件进行读写,首先需要打开设备文件。打开串口的函数调用应该写成:
fd=open(“/dev/ttyS0”,O_RDWR|O_NDELAY|O_NCTTY);
属性参数中的O_RDWR和O_NDELAY和普通文件相同,另外能用于普通文件也能用于串口设备文件的属性还有O_RDONLY、O_WRONLY、O_NODELAY等,属性O_NCTTY是串口中才使用的,打开普通文件不用。它的意思是不要把打开的串口作为打开进程的控制终端。函数调用成功,返回打开串口的句柄,以后串口的读写都通过该句柄实现。如果不成功,函数返回-1,并通过变量errno返回错误原因,程序中应该检查该变量。
设备文件打开后,串行口的输入输出操作是通过文件读写函数read和write进行的。当串口使用完成后,应当关闭。关闭串口用close函数。
改变Linux系统中串行口的设置,需要存取termios结构。这有两种方法可以选择,一种是通过ioctl函数,另一种是符合POSIX1.0的函数tcgetattr()和tcsetattr()方法。由于Linux系统很好的遵循POSIX1.0规范,因此我们选择规范中的方法进行介绍。
Linux系统中串行口的模式由一个数据结构描述,这就是termios结构。它至少包含下面的成员:
tcflag_t c_iflag:位掩码,用来表明输入的模式。
tcflag_t c_oflag:位掩码,用来表明输出的模式。
tcflag_t c_cflag:位掩码,用来表明控制的模式。
tcflag_t c_lflag:位掩码,用来表明高级本地模式。
cc_t c_cc[NCCS]:字符数组,用来表明那些字符和控制函数相联系。
上面的tcflag_t是一个无符号整型,位掩码的意思就是每个属性用一个位表示,从而可以用“或”运算把各种性质组合起来。
c_iflag用来表明输入模式,可取值如下:
tcflag_t INPCK:是否使能输入奇偶校验。如果置位,表示奇偶校验,否则无奇偶校验。
tcflag_t IGNPAR:如果置位,输入的任何超越错和奇偶错都被忽略。只有INPCK也被设置时才有用。
tcflag_t PARMAK:该位被置位,表明任何奇偶错或超越错的接收字符都要被标记然后才传送给程序。它只有在INPCK被设置和IGNPAR没有设置时才有意义。详情参看有关资料。
tcflag_t ISTRIP:该位被置位,任何错误字符都被剥成7位,使之成为可读ASCII字符。
另外还有许多有意义的值,由于并不常用,这里不在赘述,请查阅有关资料。
输出模式常用取值只有一个,为:
tcflag_t OPOST,它被置位表明要求系统输出时将回车符变成回车、换行。
本地模式有如下常用的取值:
tcflag_t ICANON,设置是否采用正则输入模式。正则输入模式下,输入组织成“行”,每行用回车'n'和换行符结尾。用户必须输入一正行读操作才能执行,并且每次只能读一行。在这种情况下,实际上是操作系统缓存了一行的内容,并且允许在标志行尾的回车、换行符输入之前通过控制字符编辑已输入的内容。在非正则模式下,输入不用行来组织,可以读取任意个字符。
tcflag_t ECHO,它用来设置输入是否有回声,即输入一个字符同时又把它输出,这个功能对终端特别有用,是你键盘输入的字符同时在屏幕上能够显示。
tcflag_t ISIG,是否识别特殊字符INTR、QUIT、SUSP,这些字符一旦被识别,就会产生响应的信号,用来产生对程序的控制。
控制模式设置最重要,它负责控制串口通讯的波特率、停止位、数据宽度等,重要参数如下:
tcflag_t CSTOPB,设置停止位的个数,如果该位被设置,表明有两个停止位,否则有一个停止位。
tcflag_t PARENB,表明是否具有奇偶校验位,如果该位被设置,表明有奇偶校验,否则没有。
tcflag_t PARODD,该位被设置,表明奇校验,否则为偶校验。只有PARENB被设置该位才有意义。
tcflag_t CSIZE,用来设置字符宽度。
tcflag_t CS5,每字符5个bit。
tcflag_t CS6,每字符6个bit。
tcflag_t CS7,每字符7个bit。
tcflag_t CS8,每字符8个bit。
设置串行通讯的输入、输出速率,需要改变结构termios的某些成员的值。不需要了解这些成员的具体情况,因为用函数cfsetispeed和cfsetospeed可以实现这些功能,它们分别设置输入和输出的速率。其原型如下:
cfsetispeed(struct termios *termios_p,speed_t speed);
cfsetospeed(struct termios *termios_p, speed_t speed);
其中termios_p是指向termios结构的指针,speed是速度值,它的取值应该为一个集合中的元素,每个元素名字都和一个速度对应,比如,B1200对应波特率1200。取值为:B0、B50、B75、B110、B134、B150、B200、B300、B600、B1200、B1800、B2400、B4800、B9600、B19200、B38400、B57600、B115200、B230400、B460800。其中B19200和B38400还有别名为EXTA和EXTB。另外,B0表示断开连接。
获取当前的串行口设置用tcgetattr()函数,其原形为:
int tcgetattr(int filedes, struct termios *termios_p);
调用成功返回0,失败返回-1。filedes是打开的串行口的文件句柄,termios_p是termios的指针,返回时指向代表当前设置的结构。取得的termios结构经过改变后,用函数tcsetattr()进行设置:
int tcsetattr(int filedes, int when, const struct termios *termios_p);
其它参数和tcgetattr()一样,只是多了一个when参数。它表明设置什么时候开始起作用。它的取值如下:
TCSANOW,立即起作用。
TCSADRAIN,所有输出队列中的字符都发送之后才起作用。
TCSAFLUSH,和TCSADRAIN功能相似,只是同时丢弃输入队列中的所有字符。
下面的函数打开串口后设置格式,并返回串口的文件句柄:
int opencom1(void)
{
int fd;
struct termios options;
if((fd=open("/dev/ttyS0",O_RDWR|O_NOCTTY|O_NDELAY))==-1)
{
perror("/dev/ttyS0n");
return -1;
}
tcgetattr(fd,&options);
cfsetispeed(&options,B2400);
cfsetospeed(&options,B2400);
options.c_cflag|=(CLOCAL|CREAD); //忽略控制信号线和使能读功能
options.c_cflag|=PARENB; //奇偶检验
options.c_cflag&=~PARODD; //偶校验
options.c_cflag|=CSTOPB; //两个停止位
options.c_cflag&=~CSIZE;
options.c_cflag|=CS8; //8个数据位
options.c_lflag&=~(ICANON|ECHO|ISIG); //原始输入模式
options.c_oflag&=~OPOST; //原始输出
options.c_cc[VMIN]=0;
options.c_cc[VTIME]=10;
tcsetattr(fd,TCSANOW,&options);
return fd;
}
下面的程序用上面的函数打开一个串口,对它进行读写:
int
main(void)
{
char c;
int fd;
fd=opencom1();
if(fd=-1) exit(-1);
while(1)
{
read(fd,&c,1);
if(c=='04')
break;
else
write(fd,&c,1);
}
return 0;
}
9.3.3 声卡的编程
声卡是普通个人电脑的标准设备。在Linux下,有几种设备文件用来控制声卡的功能。一种是声音混合设备,/dev/mixer,用来控制各个声道的音量。有一个应用程序,aumix可以用来通过/dev/mixer设备控制各个声道的音量。另一种设备是声音的采集和播放设备,包括/dev/audio、/dev/dsp等,它们之间区别不大,/dev/audio提供了和SUN的声音系统的兼容。还有一种是音乐设备,/dev/midi、/dev/sequencer等,它提供了播放音乐的一种途径。在这一节里,我们着重介绍通过对/dev/audio编程进行声音的录制和回放。
声音编程必须包含sys/ioctl.h、unistd.h和sys/soundcard.h头文件,因为这些文件中包含了必须的函数声明和变量说明。程序的开头应该是这样的:
/*
* Standard includes
*/
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/soundcard.h>
/*
* Mandatory variables.
*/
#define BUF_SIZE 1024
int audio_fd;
unsigned char audio_buffer[BUF_SIZE];
打开设备文件用open函数,属性参数必须是O_WRONLY,O_RDONLY和O_RDWR之一,其它的属性值这里没有定义。推荐在尽可能的情况下使用O_WRONLY,O_RDONLY打开设备文件,这样效率高。只有在必要的情况下才使用O_RDWR打开文件。代码如下:
if ((audio_fd = open("/dev/audio", open_mode, 0)) == -1) {
/* Open of device failed */
perror(DEVICE_NAME);
exit(1);
}
普通的声音录制用read函数即可。下面的程序段实现:
int len;
if ((len = read(fd, audio_buffer, count)) == -1) {
perror("audio read");
exit(1);
}
这里读数据的长度count推荐使用2的整数次密,如:8,16,32等。这样做效果很好,程序会更加稳定的工作。由于声卡的采样速率是精确的,所以读取一定的字节需要的时间很精确,可以利用这一点进行定时。
声音回放使用write函数,但是要求写的数据和读的数据格式相同。
对声卡的参数设置,主要包括三个方面:采样格式,声道数和采样频率。进行参数设置是必须按照上面的顺序依次进行设置。否则会发生错误。
采样格式很多,列表如下:
在硬件水平,有的声卡只支持8位采样率,一些高端的声卡则只支持16位采样率。有时16为或更高分辨率的采样率是软件模拟出来的,这时的效果不如直接使用较低的8为采样率效果好一些。
设置采样率用ioctl函数SNDCTL_DSP_SETFMT命令。并不是所有的采样率都被支持。设置采样率后要检查是否真正设置完成了。比如,你设置一个16位长度的采样格式,两个字节表示一个声音数据。如果没有设置成功,而是设置成了缺省的8位采样格式,你的声音数据就会变成噪音。有可能损坏耳机、扬声器等设备或损伤人的耳朵。
下面是实现的代码例子:
int format;
format = AFMT_S16_LE;
if (ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format) == -1) {
/* fatal error */
perror("SNDCTL_DSP_SETFMT");
exit(1);
}
if (format != AFMT_S16_LE) {
/* The device doesn't support the requested audio format. The
program should use another format (for example the one returned
in "format") or alternatively it must display an error message
and to abort. */
}
上面的代码设置采样格式为AFMT_S16_LE并检查设置是否成功。上面的检查程序是很重要的。下面的代码可以检查系统是否支持某个采样格式:
int mask;
if (ioctl(audio_fd, SNDCTL_DSP_GETFMTS, &mask) == -1) {
/* Handle fatal error ... */
}
if (mask & AFMT_MPEG) {
/* The device supports MPEG format ... */
}
现代的声音系统大多是立体声(2声道)系统,声道数的设置用函数ioctl的命令SNDCTL_DSP_CHANNELS。有的系统不止有2个声道,有时有16个声道。
下面的代码实现设置立体声系统:
int channels = 2; /* 1=mono, 2=stereo */
if (ioctl(audio_fd, SNDCTL_DSP_CHANNELS, &channels) == -1) {
/* Fatal error */
perror("SNDCTL_DSP_CHANNELS");
exit(1);
}
if (channels != 2)
{
/* The device doesn't support stereo mode ... */
}
由于许多老SoundBlaster 1 and 2系统兼容系统不支持立体声,所以检查设置是否成功是必要的。
采样率是每秒采样的数据个数。采样率是由某个固定频率分频得到的,所以,采样率是一些特定的值,不同的硬件可能支持的采样率不同。缺省的采样率一般是8kHz。最小的采样率是5kHz,老式声卡一般支持的采样率是11.025,22.05,44.1kHz等,如果多个声道,采样率就是多个声道采样率的和。现代的声卡也支持96kHz,DVD的声音质量。
设置采样率用SNDCTL_DSP_SPEED命令实现,例子如下:
int speed = 11025;
if (ioctl(audio_fd, SNDCTL_DSP_SPEED, &speed)==-1) {
/* Fatal error */
perror("SNDCTL_DSP_SPEED");
exit(Error code);
}
if ( /* returned speed differs significantly from the requested one... */ ) {
/* The device doesn't support the requested speed... */
}
下面举一个完整的例子,简单的设置声卡,录制声音然后从扬声器播放出来:
/*
* Standard includes
*/
#include <sys/ioctl.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/soundcard.h>
#include <math.h>
/*
* Mandatory variables.
*/
#define BUF_SIZE 1024
#define SPEED 8000
int audio_fd;
unsigned char audio_buffer[BUF_SIZE];
int main(){
if ((audio_fd = open("/dev/audio", O_RDWR, 0)) == -1) {
/* Open of device failed */
perror("/dev/sound");
exit(1);
}
if(ioctl(audio_fd,SNDCTL_DSP_SETDUPLEX,0)==-1){
/* fatal error */
perror("SNDCTL_DSP_SETDUPLEX");
exit(-1);
}
int format;
format = AFMT_U8;
if (ioctl(audio_fd, SNDCTL_DSP_SETFMT, &format) == -1) {
/* fatal error */
perror("SNDCTL_DSP_SETFMT");
exit(1);
}
if (format != AFMT_U8) {
/* The device doesn't support the requested audio format. The
program should use another format (for example the one returned
in "format") or alternatively it must display an error message
and to abort. */
perror("AFMT_U8");
exit(-1);
}
int channels = 1; /* 1=mono, 2=stereo */
if (ioctl(audio_fd, SNDCTL_DSP_CHANNELS, &channels) == -1) {
/* Fatal error */
perror("SNDCTL_DSP_CHANNELS");
exit(1);
}
if (channels != 1)
{
/* The device do only support stereo mode ... */
perror("SNDCTL_DSP_CHANNELS");
exit(-1);
}
int speed = SPEED;
if (ioctl(audio_fd, SNDCTL_DSP_SPEED, &speed)==-1) {
/* Fatal error */
perror("SNDCTL_DSP_SPEED");
exit(-1);
}
if ( speed!=SPEED) {
/* The device doesn't support the requested speed... */
perror("speed error");
exit(-1);
}
int len;
while(1){
if ((len = read(audio_fd, audio_buffer, BUF_SIZE)) == -1) {
perror("audio read");
exit(1);
}
if((len = write(audio_fd,audio_buffer,BUF_SIZE)) == -1) {
perror("audio write");
exit(1);
}
}
}