1 设备驱动

VxWorks I/O系统管理着驱动程序,把应用程序的I/O请求转发给合适的驱动程序进行处理。如图3,I/O系统为驱动程序维护着三张表:

(1)文件描述符列表(FD TABLE):维护着已打开设备的句柄,驱动该设备的驱动程序号,设备相关参数的指针这三者的对应关系;

(2)设备描述链表(DEVICE LIST):维护着系统中存在的设备名,驱动该设备的驱动程序号,设备相关的参数的指针三者的对应关系;

(3)驱动程序列表(DRIVER TABLE):维护着驱动设备的驱动程序号,和该设备对应的七个基本I/O操作的函数(如 xxOpen(…)等)的入口地址这两者的对应关系。

clip_image001

图3 文件、设备和驱动表

I/O系统对这三个表的操作大致过程为:

当调用creat(…)或oPen(…)时,首先在设备描述链表中根据设备名找到相应的项(若为creat,则创建响应的表项),根据驱动程序号找到底层实现代码 xxCreate(…)或 xxOpen(…) ,I/O系统将驱动程序号及xxCreate(…)或xxOpen(…)返回的指向设备相关参数的指针添入文件描述符列表中,最后,返回在文件描述符列表中添入的项在该表中对应的序号,即文件句柄,提供给后续的I/O操作使用;

当调用read(…)或write(…)时,根据文件句柄,在文件描述符列表中找到相应的表项,得到操作该设备的驱动程序号,I/O系统再根据驱动程序列表得到相应的底层驱动实现函数xxReaD(…)或xxWrite(…)入口地址,调用相应的与设备相关的底层操作代码。

一般的,驱动程序包括三部分:初始化部分、函数功能部分和中断服务程序ISR。

初始化部分初始化硬件设备,分配设备所需的资源,完成系统相关的设置。对于字符设备来说,首先调用 iosDrvlnstall(…)安装驱动程序,把中断向量和中断服务程序挂上,然后调用iosDevAdd(…)加载驱动程序到I/O系统;对于块设备来说,首先把中断向量和中断服务程序挂上,在内存中分配一个设备结构并初始化,用户使用该块设备时,调用设备的初始化部分,再调用设备的创建函数返回一个BLK_DEV结构的指针,供文件系统的初始化函数使用。

函数功能部分完成系统指定的功能。对于字符设备,就是7个标准的 I/O函数;对块设备,则是BLK_DEV或SEQ_DEV结构中指定的函数。

中断服务程序是实时系统的重要组成部分,系统通过中断机制来了解外部事件,并做出响应。实时系统的反应速度取决于系统对中断的响应速度和中断处理程序的处理速度。因此,中断服务程序的处理时间应尽量短。所有的中断服务程序共享一个堆栈,没有任务控制块,所以,在中断服务程序中不能使用可导致阻塞的函数,如 printf(…)、semTake(…)等。如果希望在中断服务程序中输出一些信息,可以使用sysLOg(…),该函数是非阻塞的,可以接受7个参数。中断服务程序中可以使用semGive(…)与其它的非中断服务程序进行通信。理想的情况,一个中断服务程序仅调用一个semGive(…)系统调用,也就是说,中断服务程序的主要功能应该是发起一个任务来完成必要的处理。为提高中断服务程序与任务的合作性能,最好的机制是信号量。

1.1 字符设备

字符设备的驱动程序和I/O系统直接作用。字符设备调用驱动程序安装函数 iosDrvlnstall(…)在VxWorks中安装驱动程序。该函数有7个参数,分别指向驱动程序提供的7个标准I/O函数creat(…)、delete(…)、open(…)、close(…)、read(…)、write(…)、ioctl(…)。有些函数不一定要实现,对应的参数可以为NULL,iosDrvlnstall(…)只为驱动程序在驱动函数列表中分配了一个位置,要使用驱动函数还必须调用设备安装函数iosDevAdd(…),把设备名和驱动程序号写到数据结构 DEV_HDR中,并把它加入到系统的设备描述键表中。

并口打印机是典型的字符设备,其他的字符设备驱动都可以以它为模板进行修改,其驱动程序如下:

/* forward declarations */

LOCAL int lptOpen(LPT_DEV *pDev, char *name, int mode);

LOCAL int lptRead(LPT_DEV *pDev, char *pBuf, int size);

LOCAL int lptWrite(LPT_DEV *pDev, char *pBuf, int size);

LOCAL STATUS lptIoctl(LPT_DEV *pDev, int function, int arg);

LOCAL void lptIntr(LPT_DEV *pDev);

LOCAL void lptInit(LPT_DEV *pDev);

/*初始化设备驱动*/

STATUS lptDrv(int channels, /* LPT channels */

LPT_RESOURCE *pResource /* LPT resources */

)

{

int ix;

LPT_DEV *pDev;

/* check if driver already installed */

if (lptDrvNum > 0)

return (OK);

if (channels > N_LPT_CHANNELS)

return (ERROR);

for (ix = 0; ix < channels; ix++, pResource++)

{

pDev = &lptDev[ix];

pDev->created = FALSE;

pDev-&gt;autofeed = pResource-&gt;autofeed;

pDev-&gt;inservice = FALSE;

if (pResource-&gt;regDelta == 0)

pResource-&gt;regDelta = 1;

pDev-&gt;data = LPT_DATA_RES(pResource);

pDev-&gt;stat = LPT_STAT_RES(pResource);

pDev-&gt;ctrl = LPT_CTRL_RES(pResource);

pDev-&gt;intCnt = 0;

pDev-&gt;retryCnt = pResource-&gt;retryCnt;

pDev-&gt;busyWait = pResource-&gt;busyWait;

pDev-&gt;strobeWait = pResource-&gt;strobeWait;

pDev-&gt;timeout = pResource-&gt;timeout;

pDev-&gt;intLevel = pResource-&gt;intLevel;

pDev-&gt;syncSem = semBCreate(SEM_Q_FIFO, SEM_EMPTY);

pDev-&gt;muteSem = semMCreate(SEM_Q_PRIORITY | SEM_DELETE_SAFE |

SEM_INVERSION_SAFE);

(void)intConnect((VOIDFUNCPTR*)INUM_TO_IVEC(pResource-&gt;intVector),

(VOIDFUNCPTR)lptIntr, (int)pDev);

sysIntEnablePIC(pDev-&gt;intLevel); /* unmask the interrupt */

lptInit(&lptDev[ix]);

}

lptDrvNum = iosDrvInstall(lptOpen, (FUNCPTR)NULL, lptOpen, (FUNCPTR)NULL,

lptRead, lptWrite, lptIoctl);

return (lptDrvNum == ERROR ? ERROR : OK);

}

/*创建设备*/

STATUS lptDevCreate(char *name, /* name to use for this device */

int channel /* physical channel for this device (0 - 2) */

)

{

if (channel &gt;= N_LPT_CHANNELS)

return (ERROR);

if (lptDrvNum <= 0)

{

errnoSet(S_ioLib_NO_DRIVER);

return (ERROR);

}

/* if this device already exists, don't create it */

if (lptDev[channel].created)

return (ERROR);

/* mark the device as created, and add the device to the I/O system */

lptDev[channel].created = TRUE;

return (iosDevAdd(&lptDev[channel].devHdr, name, lptDrvNum));

}

/* lptOpen - open file to LPT */

LOCAL int lptOpen(LPT_DEV *pDev, char *name, int mode)

{

return ((int)pDev);

}

/* lptRead - read from the port */

LOCAL int lptRead(LPT_DEV *pDev, char *pBuf, int size)

{

return (ERROR); /* XXX would be supported in next release */

}

/* lptWrite - write to the port.

* RETURNS: The number of bytes written, or ERROR if the command didn't succeed.

*/

LOCAL int lptWrite(LPT_DEV *pDev, char *pBuf, int size)

{

int byteCnt = 0;

BOOL giveup = FALSE;

int retry;

int wait;

if (size == 0)

return (size);

semTake(pDev->muteSem, WAIT_FOREVER);

retry = 0;

while ((sysInByte(pDev-&gt;stat) &S_MASK) != (S_SELECT | S_NODERR | S_NOBUSY))

{

if (retry++ &gt; pDev-&gt;retryCnt)

{

if (giveup)

{

errnoSet(S_ioLib_DEVICE_ERROR);

semGive(pDev-&gt;muteSem);

return (ERROR);

}

else

{

lptInit(pDev);

giveup = TRUE;

}

}

wait = 0;

while (wait != pDev-&gt;busyWait)

wait++;

}

retry = 0;

do

{

wait = 0;

sysOutByte(pDev-&gt;data, *pBuf);

while (wait != pDev-&gt;strobeWait)wait++;

sysOutByte(pDev-&gt;ctrl, sysInByte(pDev-&gt;ctrl) | C_STROBE | C_ENABLE)

;

while (wait)

wait--;

sysOutByte(pDev-&gt;ctrl, sysInByte(pDev-&gt;ctrl) &~C_STROBE);

if (semTake(pDev-&gt;syncSem, pDev-&gt;timeout *sysClkRateGet()) == ERROR)

{

if (retry++ &gt; pDev-&gt;retryCnt)

{

errnoSet(S_ioLib_DEVICE_ERROR);

semGive(pDev-&gt;muteSem);

return (ERROR);

}

}

else

{

pBuf++;

byteCnt++;

}

}

while (byteCnt < size)

;

semGive(pDev->muteSem);

return (size);

}

/* lptIoctl - special device control */

LOCAL STATUS lptIoctl(LPT_DEV *pDev, /* device to control */

int function, /* request code */

int arg /* some argument */

)

{

int status = OK;

semTake(pDev-&gt;muteSem, WAIT_FOREVER);

switch (function)

{

case LPT_SETCONTROL:

sysOutByte(pDev-&gt;ctrl, arg);

break;

case LPT_GETSTATUS:

*(int*)arg = sysInByte(pDev-&gt;stat);

break;

default:

(void)errnoSet(S_ioLib_UNKNOWN_REQUEST);

status = ERROR;

break;

}

semGive(pDev-&gt;muteSem);

return (status);

}

/* lptIntr - handle an interrupt */

LOCAL void lptIntr(LPT_DEV *pDev)

{

pDev-&gt;inservice = TRUE;

pDev-&gt;intCnt++;

semGive(pDev-&gt;syncSem); //释放同步信号量

pDev-&gt;inservice = FALSE;

sysOutByte(pDev-&gt;ctrl, sysInByte(pDev-&gt;ctrl) &~C_ENABLE);

}

/* lptInit - initialize the specified LPT port */

LOCAL void lptInit(LPT_DEV *pDev)

{

sysOutByte(pDev-&gt;ctrl, 0); /* init */

taskDelay(sysClkRateGet() &gt;&gt; 3); /* hold min 50 mili sec */

if (pDev-&gt;autofeed)

sysOutByte(pDev-&gt;ctrl, C_NOINIT | C_SELECT | C_AUTOFEED);

else

sysOutByte(pDev-&gt;ctrl, C_NOINIT | C_SELECT);

}

相关的数据结构定义为:

typedef struct lptDev /* LPT_DEV */

{

DEV_HDR devHdr;

BOOL created; /* TRUE if this device has been created */

BOOL autofeed; /* TRUE if enable autofeed */

BOOL inservice; /* TRUE if interrupt in service */

ULONG data; /* data register */

ULONG stat; /* status register */

ULONG ctrl; /* control register */

int intCnt; /* interrupt count */

int retryCnt; /* retry count */

int busyWait; /* loop count for BUSY wait */

int strobeWait; /* loop count for STROBE wait */

int timeout; /* timeout second for syncSem */

int intLevel; /* interrupt level */

SEM_ID muteSem; /* mutex semaphore */

SEM_ID syncSem; /* sync semaphore */

} LPT_DEV;

typedef struct lptResource /* LPT_RESOURCE */

{

int ioBase; /* IO base address */

int intVector; /* interrupt vector */

int intLevel; /* interrupt level */

BOOL autofeed; /* TRUE if enable autofeed */

int busyWait; /* loop count for BUSY wait */

int strobeWait; /* loop count for STROBE wait */

int retryCnt; /* retry count */

int timeout; /* timeout second for syncSem */

int regDelta; /* register address spacing */

} LPT_RESOURCE;

1.2 串行设备

串行设备驱动的结构框图如图4,I/O不直接与串行设备驱动打交道,而是通过ttyDrv。ttyDrv是一个虚拟的设备驱动,与tylib一起,用于处理I/O系统与底层实际设备之间的通信。主要完成以下工作:

(1)处理I/O系统的各种需求,如在driver talbe中添加相应的驱动条目、创建设备标识符(device descriptor)。

(2)实现与上层标准I/O函数及实际驱动程序的无缝连接。其中,ttyDrv完成open和ioctl两项功能(ttyopen()和ttyioctl())。Tylib完成read和write两项功能(tyRead()和tyWrite())。

(3)管理输入/输出数据缓冲区。

clip_image003

图4 串行设备驱动的结构

下面,我们结合图4,以i8250为例,开始分析串行设备驱动的设计流程。用户在编写自己的驱动程序时,可以不按照系统函数命名的方法命名,也可以不按照系统给定的方法进行函数功能的划分,但其初始化及实现流程却不能改变。

⑴ i8250相关硬件设备的初始化。

编写驱动程序的第一步是完成相关硬件的初始化。与I8250相关的硬件初始化函数主要有以下三个:sysSerialHwInit()、i8250HrdInit()、i8250InitChannel(),其调用顺序是:sysSerialHwInit()ài8250HrdInit()ài8250InitChannel(),这条工作链的主要作用是,完成对I8250_CHAN数据结构的初始化。

下面对分别这几个函数的功能介绍一下:

sysSerialHwInit()

本函数完成的主要任务是初始化设备的中断向量、串口的通信模式及相关存储器,在函数的最后调用i8250HrdInit()对I8250_CHAN结构进一步初始化。

void sysSerialHwInit(void)

{

int i;

for (i = 0; i < N_UART_CHANNELS; i++)

{

i8250Chan[i].int_vec = devParas[i].vector; /*初始化中断向量*/

i8250Chan[i].channelMode = 0; /*初始化SIO_MODE 可以是INT或POLL*/

i8250Chan[i].lcr = UART_REG(UART_LCR, i); /*初始化line control register*/

//………………………

i8250Chan[i].outByte = sysOutByte;

/*挂接输出函数,此函数向指定的I/O地址写入1bye*/

i8250Chan[i].inByte = sysInByte;

/*挂接输出函数,此函数从指定的I/O地址读出1byte*/

i8250HrdInit(&i8250Chan[i]); /*调用i8250HrdInit()进一步完成初始化*/

}

}

i8250HrdInit()

本函数完成的主要工作是挂接相应的入口函数,具体说明如下:

void i8250HrdInit(I8250_CHAN *pChan /* 指向相应设备的指针*/)

{

if (i8250SioDrvFuncs.ioctl == NULL)

{

i8250SioDrvFuncs.ioctl = (int(*)())i8250Ioctl;

/*挂接用于处理控制I8250相关输入输出命令的函数*/

i8250SioDrvFuncs.txStartup = (int(*)())i8250Startup;

/*如果设备工作于中断模式下,启用此函数用于打开中断,使设备开始工作*/

i8250SioDrvFuncs.callbackInstall = i8250CallbackInstall;

/*安装上层提供的回调函数,本例中是安装的tyIRd()、tyITx()*/

i8250SioDrvFuncs.pollInput = (int(*)())i8250PRxChar; /*挂接输入轮询函数*/

i8250SioDrvFuncs.pollOutput = (int(*)(SIO_CHAN *, char))i8250PTxChar;

/*挂接输出轮询函数*/

}

pChan->pDrvFuncs = &i8250SioDrvFuncs; /*初始化CHAN结构,挂接接口函数列表*/

i8250InitChannel(pChan); /* reset the channel */

}

由上面挂接的函数可以看出,i8250驱动主要实现了三个功能:read、write、ioctl,而并没有实现所有的七项功能。同时,值的注意的是,对同一种设备的驱动只需挂接一次。

i8250CallbackInstall()函数为:

static int i8250CallbackInstall(SIO_CHAN *pSioChan, int callbackType, STATUS

(*callback)(), void *callbackArg)

{

I8250_CHAN *pChan = (I8250_CHAN*)pSioChan;

switch (callbackType)

{

case SIO_CALLBACK_GET_TX_CHAR:

pChan-&gt;getTxChar = callback;

pChan-&gt;getTxArg = callbackArg;

return (OK);

case SIO_CALLBACK_PUT_RCV_CHAR:

pChan-&gt;putRcvChar = callback;

pChan-&gt;putRcvArg = callbackArg;

return (OK);

default:

return (ENOSYS);

}

}

同时ttyDrv通过SIO_DRV_FUNCS使用xxDrv(i8250Drv)提供的服务,而xxDrv通过回调函数(本例中是由i8250CallbackInstall()安装的tyIRd()、tyITx())完成ttyDrv提出的请求。原理如图5所示:

clip_image005

图5 ttyDrv与xxDrv

i8250InitChannel()

本函数的主要作用是初始化特定的CHAN所描述的信道。具体分析如下。

static void i8250InitChannel(I8250_CHAN *pChan /* pointer to device */)

{

int oldLevel;

oldLevel = intLock(); /*关中断进入临界区*/

(void) i8250BaudSet(pChan, I8250_DEFAULT_BAUD);/*设置信道的波特率*/

//…………………………………

intUnlock(oldLevel); /*开中断响应,出临界区*/

}

⑵ 挂接中断服务程序

对i8250的硬件初始化完成后,接着挂接相关的中断服务程序。主要由sysSerialHwinit2()函数完成。需要注意的是,挂接中断应放在系统初始化的最后,主要是因为中断挂接函数intConnect()需要调用malloc()函数,如果在系统的内存分配还未初始化前调用,则会出错。下面请看源代码:

void sysSerialHwInit2(void)

{

int i;

for (i = 0; i < N_UART_CHANNELS; i++)

if (i8250Chan[i].int_vec)

{

(void)intConnect(INUM_TO_IVEC(i8250Chan[i].int_vec), i8250Int, (int)

&i8250Chan[i]);

sysIntEnablePIC(devParas[i].intLevel);

}

}

其中,宏INUM_TO_IVEC的作用是把中断号转为中断向量。i8250Int是指向输入/输出中断处理函数的指针。描述相应硬件的结构i8250Chan为函数i8250int()的入口参数。

至此,设备硬件的初始化、相关的低层函数的挂接、中断初始化基本完成。开始进行下一步,将设备的驱动函数安装在Driver Table 中。

⑶ 与上层标准输入/输出函数的挂接

在此处I/O系统通过调用ttyDrv()(在没有定义INCLUDE_TYCODRV_5_2的情况下)将相应驱动函数添加到Driver Table中,从而完成与上层标准输入/输出函数的挂接。

clip_image007

图6 串口驱动与I/O的挂接

由上图知,iosDrvInstall()函数在Driver Table中挂接的函数是tyWrite()和tyRead(),而不是我们实际编写的输入/输出函数。其具体的调用过程是:

? 当用户调用write函数进行写操作时,根据相应的fd调用在Driver Table中注册的函数tyWrite(),此函数的作用是将用户缓冲区的内容写入相应的输出ring buffer,当发现缓冲区内有内容时,开始调用回调函数tyITX(),从ring buffer读取字符,由I8250Startup()启动中断输出,最后由设备的输出中断服务程序(在本例中调用的是sysOutbyte())将字符发往指定的串口。

? 当串口接收到数据时会调用输入中断服务程序(在本例中是sysInbyte()),将输入的字符写入指定的缓冲区。然后由回调函数tyIRd()将缓冲区的内容读入ring buffer,当用户调用read函数进行写操作时,会根据相应的fd调用在Driver Table中注册的函数tyRead(),此函数会将ring buffer中的内容读入用户缓冲区。

i8250Startup()函数完成传输启动,源代码为:

LOCAL int i8250Startup(I8250_CHAN *pChan /* tty device to start up */

)

{

char ier = I8250_IER_RXRDY;

char mask;

if (pChan->channelMode == SIO_MODE_INT)

{

if (pChan-&gt;options &CLOCAL)

{

/* No modem control */

ier |= I8250_IER_TBE;

}

else

{

mask = ((*pChan-&gt;inByte)(pChan-&gt;msr)) &I8250_MSR_CTS;

/* if the CTS is asserted enable Tx interrupt */

if (mask &I8250_MSR_CTS)

ier |= (I8250_IER_TBE | I8250_IER_MSI);

else

ier |= (I8250_IER_MSI);

}

(*pChan-&gt;outByte)(pChan-&gt;ier, ier);

}

return (OK);

}

i8250中断服务程序为:

void i8250Int(I8250_CHAN *pChan)

{

char outChar; /* store a byte for transmission */

char interruptID; /* store contents of interrupt ID register */

char lineStatus; /* store contents of line status register */

char ier; /* store contents of interrupt enable register */

int ix = 0; /* allows us to return just in case IP never clears */

ier = (*pChan-&gt;inByte)(pChan-&gt;ier);

/* service UART interrupts until IP bit set or read counter expires */

FOREVER

{

interruptID = ((*pChan-&gt;inByte)(pChan-&gt;iid) &I8250_IIR_MASK);

if ((interruptID == I8250_IIR_IP) || (++ix &gt; I8250_IIR_READ_MAX))

{

break; /* interrupt cleared or loop counter expired */

}

/* filter possible anomalous interrupt ID from PC87307VUL (SPR 26117) */

interruptID &= 0x06; /* clear odd-bit to find interrupt ID */

if (interruptID == I8250_IIR_SEOB)

{

lineStatus = (*pChan-&gt;inByte)(pChan-&gt;lst);

}

else if (interruptID == I8250_IIR_RBRF)

{

if (pChan-&gt;putRcvChar != NULL)

(*pChan-&gt;putRcvChar)(pChan-&gt;putRcvArg, (*pChan-&gt;inByte)(pChan-&gt;data));

else

(*pChan-&gt;inByte)(pChan-&gt;data);

}

else if (interruptID == I8250_IIR_THRE)

{

if ((pChan-&gt;getTxChar != NULL) && (*pChan-&gt;getTxChar)(pChan-&gt;getTxArg,

&outChar) == OK)

{

(*pChan-&gt;outByte)(pChan-&gt;data, outChar);

}

/* There are no bytes available for transmission. Reading

* the IIR at the top of this loop will clear the interrupt.

*/

}

else if (interruptID == I8250_IIR_MSTAT)

/* modem status register */

{

char msr = (*(pChan)-&gt;inByte)(pChan-&gt;msr);

/* (|=) ... DON'T CLOBBER BITS ALREADY SET IN THE IER */

ier |= (I8250_IER_RXRDY | I8250_IER_MSI);

/* if CTS is asserted by modem, enable tx interrupt */

if ((msr &I8250_MSR_CTS) && (msr &I8250_MSR_DCTS))

{

(*pChan-&gt;outByte)(pChan-&gt;ier, (I8250_IER_TBE | ier));

}

else

/* turn off TBE interrupt until CTS from modem */

{

(*pChan-&gt;outByte)(pChan-&gt;ier, (ier &(~I8250_IER_TBE)));

}

}

} /* FOREVER */

}

对于输入/输出控制函数ioctl()的挂接,则是直接将命令传到由用户编写的i8250ioctl()函数,其具体的实现代码与驱动的设计思路无紧密的联系。

⑷ 具体设备与相关驱动的挂接

当Driver Table中相应的驱动函数挂接完成,开始编写驱动程序的最后一步:在Device Table中加入设备,完成具体设备与相关驱动的挂接。此项工作是由ttyDevCreat()函数完成的。本函数主要实现以下功能:

? 分配并初始化一个device descriptor。

? 通过调用tyDevInit()初始化tyLib。此处主要完成输入/输出ring buffer的创建、建立用与相关函数的信号量、初始化selectLib。

? 调用iosDevAdd()将串口设备加入Device Table。对于设备特性的描述信息是由sysSerialChanGet()函数得到,并以参数形式传入的。

? 为底层设备安装回调函数,在本例中是为i8250CHAN 安装tyIRd()、tyITx()两处回调函数。

? 开中断,设备开始以中断方式工作。

至此,串口驱动程序的分析全部完成。与挂接驱动函数不同,在安装设备的过程中,无论设备相同与否,有几个设备则上述过程需调用几次。以上各函数的加载主要在usrinit()函数中完成。

Bootrom和VxWorks启动过程中TTY设备的初始化过程如下:

#ifdef INCLUDE_TTY_DEV

if (NUM_TTY &gt; 0)

{

ttyDrv(); /* install console driver */

for (ix = 0; ix < NUM_TTY; ix++)

/* create serial devices */

{

sprintf(tyName, "%s%d", "/tyCo/", ix);

(void)ttyDevCreate(tyName, sysSerialChanGet(ix), 512, 512);

if (ix == CONSOLE_TTY)

/* init the tty console */

{

strcpy(consoleName, tyName);

consoleFd = open(consoleName, O_RDWR, 0);

(void)ioctl(consoleFd, FIOBAUDRATE, CONSOLE_BAUD_RATE);

(void)ioctl(consoleFd, FIOSETOPTIONS, OPT_TERMINAL);

}

}

}

#endif /* INCLUDE_TTY_DEV */

在用户应用程序中可如同文件一样操作串口:

int tty_send(void)

{

int fd;

int bytes_out;

char buff[] = "Serial commuciation pc com2"; //待发送的字符串

ttyDrv(); //把串口驱动程序添加到系统中

//下面语句把串口com2添加到系统中," tyC0/l"是设备名

ttyDevCreate("/tyCo/1", sysSerialChanGet(l), 512, 512);

//以可读写方式打开串口2,获得串口2的文件描述符

fd = "open"("/tyCo/1", O_RDWR);

//设置串口2的数据模式

ioctl(fd, FIOSETOPTIONS, OPT_PAW);

//设置串口 2的波特率

ioctl(fd, FIOBAUDP, ATE, 9600);

//把字符串buf反复的送往串口

while (1)

{

bytes_out = write(fd, buf, strlen(buf));

printf("output chars total: " %d\n", bytes_out);

taskDelay(60);

}

//关闭串口2

close(fd);

return 1;

}

另外,TTY设备有两种操作模式:raw模式和line模式。在raw模式下,每个刚从设备输入的字符对读者都是有效的;在line模式下所有输入字符被存储,直到NEWLINE字符输入。设备选项字使用带FIOSETOPTIONS 功能的ioctl()程序来设置模式,如:

/* put console in line mode */

(void)ioctl(consoleFd, FIOSETOPTIONS, OPT_TERMINAL);

OPT_TERMINAL意味着OPT_LINE、OPT_ECHO、OPT_CRMOD、OPT_TANDEM等标志全部被设置。

1.3 块设备

如图7所示,VxWorks中的块设备驱动程序不与I/O系统直接作用,它是通过文件系统与I/O系统作用的。文件系统把自己作为一个驱动程序装载到I/O系统中,并把请求转发给实际的设备驱动程序。块设备的驱动程序不使用iosDrvlnstsll(…)来安装驱动程序,而是通过初始化块设备描述结构BLK_DEV或顺序设备描述结构SEQ_DEV,来实现驱动程序提供给文件系统的功能。块设备也不调用设备安装函数iosDevAdd(…),而是调用文件系统的设备初始化函数如dosFsDevlnit(…)等。

clip_image009

图7 I/O、文件系统与块设备

作为一个块设备必须提供一个设备结构BLK_DEV来存取该设备。在该结构中应该定义一些与该设备相关的变量,如块大小、块数目。还应该定义一个函数列表,里面包括实现本设备的read, write, ioctl, reset, check state of device函数。通常还会定义一些成员来表示设备在文件系统上的特定条件,如磁盘改变等。当driver创建块设备时,设备没有名字或文件系统与其相联。在设备初始化函数中选择文件系统后才会使设备与文件系统相联。块设备的底层driver与字符设备driver不同,它并没有被安装到I/O driver table中,而是由每个文件系统作为一个driver安装到I/O driver table上。应用程序与设备的所有通讯都是先通过I/O系统&lt;-->文件系统<-->设备driver<-->设备。所以driver必须提供设备与VxWorks之间的接口。基本上这些接口都是与硬件相关的,每个设备都有其独特的处理过程,但是提供给VxWorks的接口应该是统一的。

Driver通常需要一个初始化函数,它所执行的操作应该只执行一次。它有一个原则就是初始化函数操作影响整个设备控制器,而其它后继的操作只影响特殊的设备。块设备的通用初始化函数包括:

? 初始化硬件;

? 分配和初始化设备数据结构;

? 创建互斥量(用于多个任务存取);

? 初始化中断向量表;

? 开设备中断。

块设备driver必须创建一个逻辑盘或连续的设备,所谓的逻辑盘设备可以仅仅是一个大的物理设备的一部分,设备driver必须可以跟踪任何一个块偏移值或其他可以将逻辑设备与物理设备的实际位置对上的方法。VxWorks文件系统的块序号通常以零开始,一个可以连续存取的设备的块大小通常是可变或多样的。大多数应用程序使用的都是可变的块大小。设备的create函数一般来说都会分配一个设备描述符结构,用于driver对设备的控制,这个结构的第一个成员必须是VxWorks定义的块设备结构(BLK_DEV or SEQ_DEV),因为文件系统在调用设备driver时需要使用这个块设备结构,下面定义的是BLK_DEV 和SEQ_DEV的成员:

clip_image010

clip_image011

对于直接存取的块设备的读函数原型是:

STATUS xxBlkRd

    (

     DEVICE *  pDev,       /* pointer to device descriptor */

    int       startBlk,   /* starting block to read */

     int       numBlks,    /* number of blocks to read */

    char *    pBuf        /* pointer to buffer to receive data */

    )

? pDev是指向块设备描述符的指针;

? startBlk是开始读的块的起始位置;

? numBlks指需要读的块的数目;

? PBuf是存储接收的数据的buffer的指针。

对于连续块设备的读原型是:

STATUS xxSeqRd

        (

        DEVICE *  pDev,         /* pointer to device descriptor */

        int       numBytes,     /* number of bytes to read */

        char *    buffer,       /* pointer to buffer to receive data */

        BOOL      fixed         /* TRUE =&gt; fixed block size */

        )

? pDev是指向块设备描述符的指针;

? numBytes指需要读的字节的数目;

? buffer是存储接收的数据的buffer的指针;

? Fixed是指是读函数是从连续设备的可变大小的块读还是从有多种大小的块读。

一般来说块设备driver有单独的函数对设备进行复位,它应该只复位指定的块设备而非所有的块设备,例如:

LOCAL STATUS ataReset(ATA_DEV *pDev)

{

ATA_CTRL *pCtrl = &ataCtrl[pDev-&gt;ctrl];

if (!pCtrl-&gt;installed)

return (ERROR);

semTake(&pCtrl-&gt;muteSem, WAIT_FOREVER);

(void)ataInit(pDev-&gt;ctrl, 0);

semGive(&pCtrl-&gt;muteSem);

return (OK);

}

一般来说块设备driver还应该有取设备状态的函数,它可以检测出当前设备的状态,这对可热插拔的设备来说很重要,当设备出错或被拔出时,这个函数返回Error,这时文件系统就不会继续往下操作,而当一个新的设备插上时,它设置BLK_DEV中的bd_readyChanged项,然后返回OK,这时open及create函数可以继续操作。新的设备可以自动地加入到系统中。例如:

LOCAL STATUS ataStatus(ATA_DEV *pDev)

{

ATA_CTRL *pCtrl = &ataCtrl[pDev-&gt;ctrl];

BLK_DEV *pBlkdev = &pDev-&gt;blkDev;

if (!pCtrl-&gt;installed)

return (ERROR);

if (pCtrl-&gt;changed)

{

pBlkdev-&gt;bd_readyChanged = TRUE;

pCtrl-&gt;changed = FALSE;

}

return (OK);

}

例如,块设备的创建函数非常关键,由它返回BLK_DEV数据结构:

BLK_DEV *ataDevCreate(int ctrl, /* ATA controller number, 0 is the primary

controller */

int drive, /* ATA drive number, 0 is the master drive */

int nBlocks, /* number of blocks on device, 0 = use entire disc */

int blkOffset /* offset BLK_DEV nBlocks from the start of the drive */

)

{

ATA_CTRL *pCtrl = &ataCtrl[ctrl];

ATA_TYPE *pType = &ataTypes[ctrl][drive];

ATA_DRIVE *pDrive = &pCtrl-&gt;drive[drive];

ATA_PARAM *pParam = &pDrive-&gt;param; /* move * */

ATA_DEV *pDev;

BLK_DEV *pBlkdev;

int maxBlks;

if ((ctrl &gt;= ATA_MAX_CTRLS) || (drive &gt;= ATA_MAX_DRIVES)

|| !ataDrvInstalled || !pCtrl-&gt;installed)

return (NULL);

if ((pDev = (ATA_DEV*)malloc(sizeof(ATA_DEV))) == NULL)

return (NULL);

pBlkdev = &pDev-&gt;blkDev;

if ((pDrive-&gt;state == ATA_DEV_OK) || (pDrive-&gt;state == ATA_DEV_MED_CH))

{

pDrive-&gt;state = ATA_DEV_MED_CH;

if (pDrive-&gt;type == ATA_TYPE_ATA)

{

/*

* if LBA is supported and ataLbaTotalSecs is not zero

* and ataLbaTotalSecs is greater than the product of

* CHS, then we should use the LBA value.

*/

if ((pDrive-&gt;okLba == TRUE) && (ataLbaTotalSecs[ctrl][drive] != 0) &&

(ataForceCHSonLBA != TRUE) && (ataLbaTotalSecs[ctrl][drive] &gt; (pType

-&gt;cylinders *pType-&gt;heads *pType-&gt;sectors)))

{

maxBlks = (ataLbaTotalSecs[ctrl][drive]) - blkOffset;

}

else

/* just use CHS */

{

maxBlks = ((pType-&gt;cylinders *pType-&gt;heads *pType-&gt;sectors) –

blkOffset);

}

if (nBlocks == 0)

nBlocks = maxBlks;

if (nBlocks &gt; maxBlks)

nBlocks = maxBlks;

pBlkdev-&gt;bd_nBlocks = nBlocks;

pBlkdev-&gt;bd_bytesPerBlk = pType-&gt;bytes;

pBlkdev-&gt;bd_blksPerTrack = pType-&gt;sectors;

pBlkdev-&gt;bd_nHeads = pType-&gt;heads;

pBlkdev-&gt;bd_removable = TRUE;

pBlkdev-&gt;bd_retry = 1;

pBlkdev-&gt;bd_mode = O_RDWR;

pBlkdev-&gt;bd_readyChanged = TRUE;

pBlkdev-&gt;bd_blkRd = ataBlkRd;

pBlkdev-&gt;bd_blkWrt = ataBlkWrt;

pBlkdev-&gt;bd_ioctl = ataIoctl;

pBlkdev-&gt;bd_reset = ataReset;

pBlkdev-&gt;bd_statusChk = ataStatus;

pBlkdev-&gt;bd_reset = NULL;

pBlkdev-&gt;bd_statusChk = NULL;

pDev-&gt;ctrl = ctrl;

pDev-&gt;drive = drive;

pDev-&gt;blkOffset = blkOffset;

}

else if (pDrive-&gt;type == ATA_TYPE_ATAPI)

{

pBlkdev-&gt;bd_nBlocks = 100;

pBlkdev-&gt;bd_bytesPerBlk = 2048;

pBlkdev-&gt;bd_blksPerTrack = pBlkdev-&gt;bd_nBlocks;

pBlkdev-&gt;bd_nHeads = 1;

if (pParam-&gt;config &CONFIG_REMOVABLE)

pBlkdev-&gt;bd_removable = TRUE;

else

pBlkdev-&gt;bd_removable = FALSE;

pBlkdev-&gt;bd_retry = 1;

pBlkdev-&gt;bd_mode = O_RDONLY;

pBlkdev-&gt;bd_readyChanged = TRUE;

pBlkdev-&gt;bd_blkRd = ataPiBlkRd;

pBlkdev-&gt;bd_blkWrt = ataStub;

pBlkdev-&gt;bd_ioctl = ataPiIoctl;

pBlkdev-&gt;bd_reset = ataPiReset;

pBlkdev-&gt;bd_statusChk = ataPiStatusChk;

pDev-&gt;ctrl = ctrl;

pDev-&gt;drive = drive;

pDev-&gt;blkOffset = blkOffset;

pDev-&gt;nBlocks = nBlocks;

}

ataDriveInit(ctrl, drive);

pDrive-&gt;state = ATA_DEV_OK;

}

return (&pDev-&gt;blkDev);

}

块设备创建及连接DOS文件系统的过程如下(下面这段代码来源于TMS,它将FLASH当作一个块设备看待):

if ((pBlkDev = flashDevCreate(offset, sectorSize, sectorCnt, 0)) != NULL)

{

/* Initialize the other units as DOS FS. */

if ((pDosVolDesc = dosFsDevInit(name, pBlkDev, NULL)) != NULL)

{

printf(" Success.\n\r");

}

else

{

printf(" No file system found.\n\rFormatting... ");

dosFsConfigInit(&volConfig, 0xff, 1, 1, 2, 2, 20, 0, DOS_OPT_LONGNAMES |

DOS_OPT_LOWERCASE);

if ((pDosVolDesc = dosFsDevInit(name, pBlkDev, &volConfig)) != NULL)

{

fd = creat(name, O_RDWR);

ioctl(fd, FIODISKINIT, 0);

printf("Success.\n\r");

}

else

{

printf("Failure.\n\r");

printf("%s:%d failed %08x\n", __FILE__, __LINE__, errno);

rc = ERROR;

}

}

}

1.4 网络设备

如图8,在Vxworks中,网卡驱动程序又分为END(Enhanced Network Driver,增强型网络驱动)和BSD两种。

clip_image013

图8 网络设备驱动结构

如图9,END驱动程序是基于MUX模式,网络驱动程序被划分为协议组件和硬件组件。MUX数据链路层和网络层之间的接口,它管理网络协议接口和低层硬件接口之间的交互;将硬件从网络协议的细节中隔离出来,使得网络协议层与硬件无关;避免使用输入钩子函数来过滤接收从协议来的数据包,也避免了使用输出钩子函数来过滤协议包的发送;并且链路层上的驱动程序需要访问网络层(IP或其他协议)时,也会调用相关的MUX例程。值得注意的是,网络层协议和数据链路层驱动程序不能直接通讯,它们必须通过MUX。

clip_image014

图9 END设备驱动

END网络设备的初始化主要通过定义在文件configNet.h中的一个数组表实现,初始化网络时muxDevLoad()会按这个表的定义把end初始化安装到VxWorks系统。

END设备驱动初始化的主要流程为:

VxWorks系统执行的第一个任务target\config\all\usrConfig.c文件中 usrRoot()----&gt;target\src\config\usrNetwork.c文件(该文件初始化TCP/IP)中 usrNetInit(BOOT_LINE_ADRS)(该函数作用是添加MUX END) ----&gt;pcooki = pCookie = muxDevLoad(pDevTbl-&gt;unit,.....)其中pDevTbl在BSP网络配置文件configNet.h中定义.END_TBL_ENTRY endDevTbl [] = {...} ,该表定义了网络设备的具体参数。

configNet.h部分定义如下所示:

#define DEC_LOAD_FUNC dec21x40EndLoad  /* 定义加载网络设备的入口程序 */

#define DEC_BUFF_LOAN 1

/* 网络设备硬件的物理定义数据串,一般BSP已经定义,不到必要时,无需更改 */

/*

* <devAdrs>:<PCIadrs>:<ivec>:<ilevel>:<numRds>:<numTds>:<memBase>: \

* <memSize>:<userFlags>

*/

# define DEC_LOAD_STRING

"0x81020000:0x80000000:0x12:0x12:-1:-1:-1:0:0x80800

000"

IMPORT END_OBJ* DEC_LOAD_FUNC (char*, void*);

/* 网络END设备表 */

END_TBL_ENTRY endDevTbl [] =

{ 0, DEC_LOAD_FUNC, DEC_LOAD_STRING, DEC_BUFF_LOAN, NULL, FALSE},

{ 0, END_TBL_END, NULL, 0, NULL, FALSE},

};

设置方法:

由以上可看出在VxWorks添加END网络驱动在文件config.h中添加"#define INCLUDE_NETWORK"和"#define INCLUDE_END";在configNet.h中加入END驱动的入口函数"#define xxx_LOAD_FUNC xxxxxEndLoad"和一些相关的初始化字符串。 这样就会在生成BSP包含END/MUX,系统网络初始化调用函数muxDevLoad()会更据这个表初始化END网络。

文件configNet.c关于END驱动初始化的主要内容:

#ifdef INCLUDE_END

IMPORT int  ipAttach ();

IMPORT END_TBL_ENTRY endDevTbl[]; /* 定义这个表 */

#endif /* INCLUDE_END */

#ifdef INCLUDE_END

int count;

END_TBL_ENTRY* pDevTbl;  /* END设备列表 */

END_OBJ* pCookie = NULL;

END_OBJ* pEnd;

#endif /* INCLUDE_END */

#if defined(INCLUDE_END)

  muxMaxBinds = MUX_MAX_BINDS; /* 初始化MUX接口 */

  if (muxLibInit() == ERROR)

..........

MUX数据包采用mBlk-clBlk-cluster数据结构处理网络协议栈传输的数据。其中,Cluster保存的是实际的数据,mBlk和clBlk中保存的信息是用来管理Cluster中保存的数据的。为了满足传输不同大小数据的需要,Cluster是一些大小不同的内存块;缺省情况下,VxWorks网络协议栈创建了大小从64~2048字节的6个不同的内存节点池。

clip_image016

图10 mBlk-clBlk-cluster数据结构

网卡的中断处理函数用于当网卡以中断方式工作时,如果设备产生中断,进行中断响应,进而根据中断状态寄存器的值来处理报文接收,出错处理等不同的中断。中断函数一般分为两部分。第一部分是一些耗时较小的响应中断的函数(如溢出错误处理、报文接收错误处理、报文发送错误处理等中断例程)。这些中断例程通过BSP中提供的系统函数sysintconnect()挂接到系统的中断结构上,一般在muxDevstart()完成此操作。第二部分是耗费时间的中断处理例程(如数据报处理例程)。在实时系统中驱动程序应避免对这些数据报处理例程直接调用,以减少中断关闭的时间,而是通过调用系统函数netjobAdd()将数据处理函数放在netjob任务的网络队列中,通过系统任务tNetTask来处理。

数据报的接收

设备直接将接收到的数据报放入内存池预先分配的cluster中并产生一个中断。如果设备不能完成上述功能,end驱动函数完成将数据从buffer到cluster中的拷贝。数据被放cluster中后,驱动程序将通过对netBuflib中函数的调用来完成mBlk-clBlk-cluster链的创建,从而为数据在网络协议各层之间的传递做好准备,创建此结构链一般需要以下四步:

? 调用函数netClblkGet从内存池中取cluster结构;

? 调用函数netClblkJoin将clBlk与存有数据的cluster联结起来;

? 调用函数netMblkGet从内存池中取mBlk结构;

? 调用函数netMblkClJoin将mBlk与clBlk-cluster结构连接起来;

? 为了向上层协议栈传递此结构链,end还将调用muxLib中所提供的函数receiveRtn()即muxReceive(), receiveRtn()调用stackRcvRtn(),上传数据包到service层。

数据报的接收过程如图11所示。

clip_image018

图11 接收数据过程

当一个数据报到来时会触发一个中断,中断服务程序xxInt将通过调用netjobAdd()任务队列添加一个网络任务,此网络任务为数据报接收函数(xxReceive),它通过系统任务tNettask来调用。然后接收函数调用MUX的接口函数muxReceive(),而muxReceive()又调用协议层提供的接口函数stackRcvRtn()将数据报传递到协议层,最终数据将通过协议层到达应用程序的缓冲区中,应用程序通过read()函数对其读取。从图11中我们可以看到,数据报经过物理层到达数据链路层,然后再通过mux层到达网络层,在通过TCP协议层到达应用层,完成了数据报接收的全过程。

接收函数的模板:

LOCAL void templateHandleRcvInt(END_DEVICE *pDrvCtrl /* interrupting device */

)

{

PKT *pPkt;

do

{

pDrvCtrl-&gt;rxHandling = TRUE;

while ((pPkt = templatePacketGet(pDrvCtrl)) != NULL)

templateRecv(pDrvCtrl, pPkt);

pDrvCtrl-&gt;rxHandling = FALSE;

}

while (templatePacketGet(pDrvCtrl) != NULL) ;

}

templateRecv()函数完成了mBlk-clBlk-cluster链的一系列处理,并通过END_RCV_RTN_CALL语句调用MUX层函数:

LOCAL STATUS templateRecv(END_DEVICE *pDrvCtrl, /* device structure */

PKT *pPkt /* packet to process */

)

{

int len = 0;

M_BLK_ID pMblk;

char *pCluster = NULL;

char *pNewCluster;

CL_BLK_ID pClBlk;

/* Add one to our unicast data. */

END_ERR_ADD(&pDrvCtrl-&gt;end, MIB2_IN_UCAST, + 1);

/*

* We implicitly are loaning here, if copying is necessary this

* step may be skipped, but the data must be copied before being

* passed up to the protocols.

*/

pNewCluster = netClusterGet(pDrvCtrl-&gt;end.pNetPool, pDrvCtrl-&gt;pClPoolId);

if (pNewCluster == NULL)

{

DRV_LOG(DRV_DEBUG_RX, "Cannot loan!\n", 1, 2, 3, 4, 5, 6);

END_ERR_ADD(&pDrvCtrl-&gt;end, MIB2_IN_ERRS, + 1);

goto cleanRXD;

}

/* Grab a cluster block to marry to the cluster we received. */

if ((pClBlk = netClBlkGet(pDrvCtrl-&gt;end.pNetPool, M_DONTWAIT)) == NULL)

{

netClFree(pDrvCtrl-&gt;end.pNetPool, (UCHAR*)pNewCluster);

DRV_LOG(DRV_DEBUG_RX, "Out of Cluster Blocks!\n", 1, 2, 3, 4, 5, 6);

END_ERR_ADD(&pDrvCtrl-&gt;end, MIB2_IN_ERRS, + 1);

goto cleanRXD;

}

if ((pMblk = mBlkGet(pDrvCtrl-&gt;end.pNetPool, M_DONTWAIT, MT_DATA)) == NULL)

{

netClBlkFree(pDrvCtrl-&gt;end.pNetPool, pClBlk);

netClFree(pDrvCtrl-&gt;end.pNetPool, (UCHAR*)pNewCluster);

DRV_LOG(DRV_DEBUG_RX, "Out of M Blocks!\n", 1, 2, 3, 4, 5, 6);

END_ERR_ADD(&pDrvCtrl-&gt;end, MIB2_IN_ERRS, + 1);

goto cleanRXD;

}

/* Join the cluster to the MBlock */

netClBlkJoin(pClBlk, pCluster, len, NULL, 0, 0, 0);

netMblkClJoin(pMblk, pClBlk);

/* TODO - Invalidate any RFD dma buffers */

/* TODO - Packet must be checked for errors. */

END_ERR_ADD(&pDrvCtrl-&gt;end, MIB2_IN_UCAST, + 1);

len = TEMPLATE_PKT_LEN_GET(pPkt);

pCluster = TEMPLATE_PKT_VIRT_GET(pPkt);

pMblk-&gt;mBlkHdr.mLen = len;

pMblk-&gt;mBlkHdr.mFlags |= M_PKTHDR;

pMblk-&gt;mBlkPktHdr.len = len;

/* make the packet data coherent */

END_CACHE_INVALIDATE(pMblk-&gt;mBlkHdr.mData, len);

DRV_LOG(DRV_DEBUG_RX, "Calling upper layer!\n", 1, 2, 3, 4, 5, 6);

/* TODO - Done with processing, clean up and pass it up. */

/* Call the upper layer's receive routine. */

END_RCV_RTN_CALL(&pDrvCtrl-&gt;end, pMblk);

cleanRXD:

return (OK);

}

数据报的发送基本上是数据报接收的反过程,如图12所示。应用程序通过write()函数调用,将要发送的数据放入应用程序数据缓冲区中。网络协议负责将bufer中的数据放入为其分配的内存池中,并以mBlk-clBlk-cluster链的形式来存储,这样实现了再往下层协议传递数据报时,传递的只是指向此数据链结构的指针,而代替了数据在各层协议之间的拷贝。当有数据要发送时,网络协议层通过其与mux层的接口调用muxSend()函数,而muxSend()函数又通过调用xxSend()函数负责将利用指针传递来的数据报送到发送FIFO队列中,然后起动网卡设备的发送功能,发送完后将随之产生中断信号,调用中断服务程序,清除设备缓冲区。

发送函数的模板:

LOCAL STATUS templateSend(END_DEVICE *pDrvCtrl, /* device ptr */

M_BLK_ID pMblk /* data to send */

)

{

int oldLevel = 0;

BOOL freeNow = TRUE;

/*

* Obtain exclusive access to transmitter. This is necessary because

* we might have more than one stack transmitting at once.

*/

if (!(pDrvCtrl-&gt;flags &TEMPLATE_POLLING))

END_TX_SEM_TAKE(&pDrvCtrl-&gt;end, WAIT_FOREVER);

/* TODO - Flush the data buffer(s), if it might be cached */

/* TODO - If necessary, get a Tx Frame Descriptor (TFD) */

/*

* TODO - If resources are not available,

* release the semaphore and return END_ERR_BLOCK.

* Do not free packet

*/

/* TODO - Translate buffer virtual address to a remove bus physical addr */

/* place a transmit request */

if (!(pDrvCtrl-&gt;flags &TEMPLATE_POLLING))

oldLevel = intLock();

/* protect templateInt */

/* TODO - initiate device transmit, FLUSH TFD */

/* Advance our management index(es) */

if (!(pDrvCtrl-&gt;flags &TEMPLATE_POLLING))

END_TX_SEM_GIVE(&pDrvCtrl-&gt;end);

if (!(pDrvCtrl-&gt;flags &TEMPLATE_POLLING))

intUnlock(oldLevel);

/* Bump the statistics counters. */

END_ERR_ADD(&pDrvCtrl-&gt;end, MIB2_OUT_UCAST, + 1);

/*

* Cleanup. If the driver copied the data from the mblks to a different

* buffer, then free the mblks now. Otherwise, free the mblk chain

* after the device is finished with the TFD.

*/

if (freeNow)

netMblkClChainFree(pMblk);

return (OK);

}

clip_image020

图12 发送数据过程

中断服务程序模板:

LOCAL void templateInt(END_DEVICE *pDrvCtrl /* interrupting device */

)

{

UCHAR stat;

DRV_LOG(DRV_DEBUG_INT, "Got an interrupt!\n", 1, 2, 3, 4, 5, 6);

/* Read the device status register */

stat = templateStatusRead(pDrvCtrl);

/* If false interrupt, return. */

if (!(stat &TEMPLATE_VALID_INT))

/* test for valid interrupt */

{

return ; /* return immediately, no error message */

}

/*

* enable interrupts, clear receive and/or transmit interrupts, and clear

* any errors that may be set.

*/

/* TODO - Check for errors */

/* Have netTask handle any input packets */

if ((stat &TEMPLATE_RINT) && (stat &TEMPLATE_RXON))

{

if (!(pDrvCtrl-&gt;rxHandling))

{

pDrvCtrl-&gt;rxHandling = TRUE;

netJobAdd((FUNCPTR)templateHandleRcvInt, (int)pDrvCtrl, 0, 0, 0, 0);

}

}

/* TODO - handle transmit interrupts */

}

xxLoad也是非常重要的函数,初始化END_OBJ结构,并返回其指针,它被muxDevLoad()调用:

END_OBJ *templateLoad(char *initString /* String to be parsed by the driver. */

)

{

END_DEVICE *pDrvCtrl;

DRV_LOG(DRV_DEBUG_LOAD, "Loading template...\n", 1, 2, 3, 4, 5, 6);

if (initString == NULL)

{

DRV_LOG(DRV_DEBUG_LOAD, "templateLoad: NULL initStr\r\n", 0, 0, 0, 0, 0, 0);

return NULL;

}

if (initString[0] == EOS)

{

bcopy((char*)DRV_NAME, initString, DRV_NAME_LEN);

return NULL;

}

/* else initString is not blank, pass two ... */

/* allocate the device structure */

pDrvCtrl = (END_DEVICE*)calloc(sizeof(END_DEVICE), 1);

if (pDrvCtrl == NULL)

goto errorExit;

/* parse the init string, filling in the device structure */

if (templateParse(pDrvCtrl, initString) == ERROR)

goto errorExit;

/* Ask the BSP for the ethernet address. */

SYS_ENET_ADDR_GET(pDrvCtrl);

/* initialize the END and MIB2 parts of the structure */

/*

* The M2 element must come from m2Lib.h

* This template is set up for a DIX type ethernet device.

*/

if (END_OBJ_INIT(&pDrvCtrl-&gt;end, NULL, "template", pDrvCtrl-&gt;unit,

&templateFuncTable, "END Template Driver.") == ERROR || END_MIB_INIT

(&pDrvCtrl-&gt;end, M2_ifType_ethernet_csmacd, &pDrvCtrl-&gt;enetAddr[0], 6,

END_BUFSIZ, END_SPEED) == ERROR)

goto errorExit;

/* Perform memory allocation/distribution */

if (templateMemInit(pDrvCtrl) == ERROR)

goto errorExit;

/* reset and reconfigure the device */

templateReset(pDrvCtrl);

templateConfig(pDrvCtrl);

/* set the flags to indicate readiness */

END_OBJ_READY(&pDrvCtrl-&gt;end, IFF_NOTRAILERS | IFF_BROADCAST | IFF_MULTICAST);

DRV_LOG(DRV_DEBUG_LOAD, "Done loading Template...", 1, 2, 3, 4, 5, 6);

return (&pDrvCtrl-&gt;end);

errorExit: if (pDrvCtrl != NULL)

free((char*)pDrvCtrl);

return NULL;

}

其他的重要函数包括:

? xxUnload():卸载一个设备驱动;

LOCAL STATUS templateUnload(END_DEVICE *pDrvCtrl /* device to be unloaded */)

{

END_OBJECT_UNLOAD(&pDrvCtrl-&gt;end);

/* TODO - Free any special allocated memory */

/* New: free the END_OBJ structure allocated during templateLoad() */

cfree((char*)pDrvCtrl);

return (OK);

}

? xxStart():启动设备;

/*******************************************************************************

*

* templateStart - start the device

*

* This function calls BSP functions to connect interrupts and start the

* device running in interrupt mode.

*

* RETURNS: OK or ERROR

*

*/

LOCAL STATUS templateStart(END_DEVICE *pDrvCtrl /* device ID */

)

{

STATUS result;

SYS_INT_CONNECT(pDrvCtrl, templateInt, (int)pDrvCtrl, &result);

if (result == ERROR)

return ERROR;

DRV_LOG(DRV_DEBUG_LOAD, "Interrupt connected.\n", 1, 2, 3, 4, 5, 6);

SYS_INT_ENABLE(pDrvCtrl);

DRV_LOG(DRV_DEBUG_LOAD, "interrupt enabled.\n", 1, 2, 3, 4, 5, 6);

END_FLAGS_SET(&pDrvCtrl-&gt;end, IFF_UP | IFF_RUNNING);

/* TODO - start the device, enabling interrupts */

return (OK);

}

? xxStop():停止设备;

? xxIoctl ():控制设备等。

1.5 PCI总线

PCI是外围设备互连(Peripheral Component Interconnect)的简称,作为一种通用的总线接口标准,它在目前的计算机系统中得到了非常广泛的应用。PCI提供了一组完整的总线接口规范,其目的是描述如何将计算机系统中的外围设备以一种结构化和可控化的方式连接在一起,同时它还刻画了外围设备在连接时的电气特性和行为规约,并且详细定义了计算机系统中的各个不同部件之间应该如何正确地进行交互。

无论是在基于Intel芯片的PC机中,或是在基于Alpha芯片的工作站上,PCI毫无疑问都是目前使用最广泛的一种总线接口标准。同旧式的ISA总线不同,PCI将计算机系统中的总线子系统与存储子系统完全地分开,CPU通过一块称为PCI桥(PCI-Bridge)的设备来完成同总线子系统的交互,如图13所示。

clip_image021

图13 PCI子系统的体系结构

图14是一个典型的基于PCI总线的计算机系统逻辑示意图,PCI总线体系结构是一种层次式的(Hierarchical)体系结构。在PCI总线体系结构中,有两个核心的概念存在:PCI总线和PCI设备(桥设备是一种特殊的PCI设备)。在这种层次式体系结构中,PCI桥设备占据着重要的地位,它将父总线与子总线连接在一起,从而使整个系统看起来像一颗倒置的树型结构。树的顶端是系统的CPU,它通过一个较为特殊的PCI桥设备——Host/PCI桥设备与根PCI总线(root pci bus)连接起来。系统的各个部分通过PCI总线和PCI-PCI桥连接在一起。从图中不难看出,CPU和RAM需要通过PCI桥连接到PCI总线0(即主PCI总线),而具有PCI接口的显卡则可以直接连接到主PCI总线上。PCI-PCI桥是一个特殊的PCI设备,它负责将PCI总线0和PCI总线1(即从PCI主线)连接在一起,通常PCI总线1称为PCI-PCI桥的下游(downstream),而PCI总线0则称为PCI-PCI桥的上游(upstream)。图中连接到从PCI总线上的是SCSI卡和以太网卡。为了兼容旧的ISA总线标准,PCI总线还可以通过PCI-ISA桥来连接ISA总线,从而能够支持以前的ISA设备。图中ISA总线上连接着一个多功能I/O控制器,用于控制键盘、鼠标和软驱。

CompactPCI是一种新的开放式工业计算机标准,正广泛地应用于工控和通信市场。它遵从PCI总线技术和成熟的Eurocard机械标准,非常适用于机架安装的工业应用。CompactPCI 也支持热插拔能力,具有更坚固、更可靠、使用维护简单的优点。PCI-PCI桥接器是用来扩展系统中PCI插槽的,它可以使Compact PCI系统更加满足工业标准的要求,从而成为高性能总线标准。简单地说,Compact PCI总线 = PCI总线的电气规范 + 标准针孔连接器(IEC-1076-4-101) + 欧洲卡规范(IEC297/IEEE 1011.1)。

clip_image022

图14 PCI系统示意图

如何在系统内扩展另一条PCI总线,唯一的方法是使用PCI-to-PCI(P2P)桥进行系统扩展。P2P桥是用来连接两条PCI总线。P2P桥设备既可嵌入在PCI总线上,也可以安装在PCI扩展连接器的一个插片上。P2P桥提供了一条PCI总线至另一条PCI总线之间的桥接,但它在其PCI总线上只相当于一个电气负载。这样,新的PCI总线就可以支持一定数量的额外设备或PCI扩展连接器。PCI规范中的电气负载限制是基于每条总线,而不是基于系统。当然,主系统内的电源必须能够向驻留在新总线上的附加设备提供足够的电源。系统设计者也可以安装多个P2P桥。

根据PCI桥的工作原理的不同,可以把P2P桥分为透明桥和非透明桥两种。

? 透明桥通常用于总线扩展。桥的二次侧的所有设备对一次侧的主系统是透明的。二次侧的所有设备只能由一次侧的主系统对其进行配置和控制。一次侧和二次侧的时钟必须同步,允许固定的相位差。许有固定的相位差。一次侧和二次侧的地址完全透明,在一次侧和二次侧之间的地址传递是直通模式,没有地址翻译;

? 非透明桥(non-transparent)通常用于嵌入式智能I/O板片。它连接两个独立的处理器域,二次侧的资源和地址对一次侧的主系统是不可见的。允许二次侧的主机处理器独立地配置和控制其子系统。一次侧和二次侧的时钟完全独立。一次侧和二次侧的地址完全独立,在一次侧和二次侧之间可以进行地址翻译。

P2P桥是为了扩充PCI总线的负载能力。具体的说,P2P桥的作用是协调两条PCI总线之间的交通,P2P桥必须具有如下的功能:

(1)监视桥两侧PCI总线上起动的所有交易(transaction),并决定是否将交易通过桥传送至另一条PCI总线。当桥确定一条总线上的交易需要传送到另一条总线上时,必须充当master的交易目的,以及充当目标总线的新交易的主设备。对于交易的master和target来说,驻留于交易的起始总线和目标总线之间的P2P桥是不可见的。

(2)桥监视第二总线上的SERR# (system error),如果采样有效,则将其传送至第一总线的SERR#。

(3)桥监视第一总线上的RST#,如果采样有效,则将其传送到第二总线上的RST#。

32bit PCI系统的管脚按功能来分有以下几类:

? 系统控制:CLK,PCI时钟,上升沿有效

? RST ,Reset信号

? 传输控制: FRAME#,标志传输开始与结束

? IRDY#,Master可以传输数据的标志

? DEVSEL#,当Slave发现自己被寻址时置低应答

? TRDY#,Slave可以转输数据的标志

? STOP#,Slave主动结束传输数据的信号

? IDSEL,在即插即用系统启动时用于选中板卡的信号

? 地址与数据总线: AD[31::0],地址/数据分时复用总线

? C/BE#[3::0],命令/字节使能信号

? PAR,奇偶校验信号

? 仲裁号: REQ#,Master用来请求总线使用权的信号

? GNT#,Arbiter允许Master得到总线使用权的信号

? 错误报告: PERR#,数据奇偶校验错

? SERR#,系统奇偶校验错

clip_image024

图15 PCI总线基本操作

如图15,当PCI总线进行操作时,发起者(Master)先置REQ#,当得到仲裁器(Arbiter)的许可时(GNT#),会将FRAME#置低,并在AD总线上放置Slave地址,同时C/BE#放置命令信号,说明接下来的传输类型。所有PCI总线上设备都需对此地址译码,被选中的设备要置DEVSEL#以声明自己被选中。然后当IRDY#与TRDY#都置低时,可以传输数据。当Master数据传输结束前,将FRAME#置高以标明只剩最后一组数据要传输,并在传完数据后放开IRDY#以释放总线控制权。

PCI总线设备驱动的开发过程如下:

1.利用供应商和设备标识确定设备得总线号、设备号和功能号,在系统中找到设备

pciFindDevice(......);

如图16,查找需要读取PCI配置空间。在PCI板卡中,有一组寄存器,叫“配置空间”(Configuration Space),用来存放基地址与内存地址,以及中断等信息。

以内存地址为例。当上电时,板卡从ROM里读取固定的值放到寄存器中,对应内存的地方放置的是需要分配的内存字节数等信息。操作系统要跟据这个信息分配内存,并在分配成功后把相应的寄存器中填入内存的起始地址。这样就不必手工设置开关来分配内存或基地址了。对于中断的分配也与此类似。

通常,系统的设计使得每一个PCI槽位的PCI配置头都有一个和它在板上的槽位相关的偏移量。所以,举例来说,板上的第一个槽位的PCI配置可能位于偏移0而第二个槽位的在偏移256(所有的头都一样长度,256字节),依此类推。定义了系统相关的硬件机制使得PCI配置代码可以尝试检查一个给定的PCI总线上的所有可能的PCI配置头,试图读取头中的一个域(通常是Vendor Identification 域)得到一些错误,从而知道哪些设备存在而哪些设备不存在。PCI Local Bus规范描述了一种可能的错误信息:试图读取一个空的PCI槽位的Verdor Identification和Device Indentification域时候返回0xFFFFFFFF。

2.配置设备的PCI基地址寄存器

pciConfigInLong(......);

3.确定映射到系统中的设备的基地址

通过基地址最后一位判断内存映射还是IO映射,通过内存空间和I/O空间两个空间可以访问PCI的本地总线,内存空间的访问就好像是对普通内存的访问,I/O空间的访问则好像是访问端口。

4.挂接中断

5.编写功能函数

clip_image025

图16 PCI配置空间

PCI配置空间中的Vendor Identification指唯一的数字,描述这个PCI设备的发明者。Digital的PCI Vendor Identification是0x1011而Intel是0x8086。

Device Identification 描述设备自身的唯一数字。例如Digital的21141快速以太网设备的设备标识符是0x0009。

Status 此域给除了设备的状态,它的位的含义由PCI Local Bus规范规定。

Command 系统通过写这个域控制这个设备。例如:允许设备访问PCI I/O内存。

Class Code 标识了设备的类型。对于每一种设备都有标准分类:显示、SCSI等等。对于SCSI的类型编码是0x0100。

Base Address Registers 这些寄存器用于确定和分配设备可以使用的PCI I/O和PCI内存的类型、大小和位置。

Interrupt Pin PCI卡的物理管脚中的4个用于向PCI总线传递中断。标准中把它们标记为A、B、C和D。Interrupt Pin域描述了这个PCI设备使用那个管脚。通常对于一个设备来说这时硬件决定的。就是说每一次系统启动的时候,这个设备都使用同一个中断管脚。这些信息允许中断处理子系统管理这些设备的中断。

PCI总线的一个突出优点是中断共享,而PCI中断共享由硬件与软件两部分组成。

硬件上,采用电平触发的办法:中断信号在系统一侧用电阻接高,而要产生中断的板卡上利用三极管的集电极将信号拉低。这样不管有几块板产生中断,中断信号都是低;而只有当所有板卡的中断都得到处理后,中断信号才会回复高电平。

软件上,采用中断链的方法:假设系统启动时,发现板卡A用了中断7,就会将中断7对应的内存区指向A卡对应的中断服务程序入口ISR_A;然后系统发现板卡B也用中断7,这时就会将中断7对应的内存区指向ISR_B,同时将ISR_B的结束指向ISR_A。以此类推,就会形成一个中断链。而当有中断发生时,系统跳转到中断7对应的内存,也就是ISR_B。ISR_B就要检查是不是B卡的中断,如果是,要处理,并将板卡上的拉低电路放开;如果不是,则呼叫ISR_A。这样就完成了中断的共享。

1.6 USB设备

USB体系结构格局为主从方式,USB中的协议栈特指主机端的软件设计。host端软件包括Client Driver,USBD和HCD三层。USBD和HCD两部分组成USB系统软件。图17显示了主驱动栈的各模块之间的功能联系。

clip_image027

图17 USB主机驱动协议栈结构及各模块之间功能关系

(1) Client Driver负责管理连接到USB上的不同设备,通过IRP(IO请求包)向USBD层发出数据接收或发送报文。通过对应用层提供API函数,屏蔽USB实现的细节,实现数据的透明传输。

(2) USBD通过IRP得到此设备的属性和本次数据通信的要求,将IRP转换成USB所能辨识的一系列事务处理,交给HCD层或者直接交给HCD。USBD还负责新设备的配置、被拔掉设备资源的释放和Client Driver的装载/卸载。

(3) HCD主要功能是对host控制器的管理、带宽分配、链表管理、根hub。将数据按传输类型组成不同的链表,然后定义不同类型传输在一帧中所占带宽的比例,交给host控制器处理,控制器根据规则从链表上摘下数据块,根据大小为他创建一个或多个事务处理,完成与设备的数据传输。当事务处理完成时,HCD将结果交给USBD层,由他通知给client层处理。

目前市场上有2大类USB主控制器,一种由Intel公司首先提出的通用主控制器接口UHCI(Universal host Controller Interface Specification),另一种是支持由微软、康柏和国家半导体公司联合设计提出的开放主控制器接口OHCI(Open Host Controller Interface specification)。就硬件性能而言,OHCI的硬件功能比UHCI稍强,其软件驱动相对简单些,适合嵌入式系统,而UHCI适合于计算机系统。

OHCI控制器驱动程序是在与控制器硬件的协同工作中实现主机控制器的全部功能,驱动程序的功能包括:

初始化和控制器硬件的管理、对链表的操作、带宽分配、中断处理、FrameInterval计数和对根集线器的操作等。

(1)初始化和管理控制器硬件

主要步骤:

安装驱动;对控制器检验、分配资源;配置控制器;启动控制器硬件;管理控制器状态。

(2)链表的操作

对非周期性ED链表的处理;对周期性ED链表的处理;对TD链表的处理;对TD的处理完成队列的处理。

(3)带宽分配

(4)中断处理

(5) FrameCounter计数

(6)对根集线器的操作

链表处理是USB主控制器驱动的关键部分。USB协议为USB设备提供了4种数据传输方式,即控制传输、中断传输、批量传输和实时传输。控制器驱动程序与控制器通过对TD和ED链表的共同操作完成数据传输的应用请求,实现了对4种不同类型的数据传输。

HCD的主要数据结构

ED(endpoint Descriptor,端点描述符)、TD(Transfer Descriptor,传输描述符)和HCCA(Host Controller Communication Area,控制器通信区)。HCD为总线上的所有设备的所有处于工作状态的接口中的每一个需要数据传输的端点(endpoint)指定一个ED,该ED包含了控制器与相关端点进行数据交换的所有必要信息,比如:最大数据包长度、该端点相关设备的地址、传输速度、数据传输的方向等,为host控制器提供对设备端点的理解。不同传输类型的ED用单向链表的方式连接在一起,以便控制器进行顺序操作。

每一个ED下都链接了一个与该ED相关的TD链。包含传输数据包的相关内容,如数据访问内存区的地址、数据传输状态字等。

HCCA是一个256字节的数据结构,他就是HCD和HC交互的第二个区域,包含了32个中断传输的ED链表的入口地址,通过对HCCA的操作,驱动程序可以实现中断、控制、批量、实时4种不同的数据传输。

链表处理的步骤

①USBD将数据请求的信息传送到HCD后,HCD首先根据请求的信息中所描述的endpoint情况创建一个ED,并根据不同传输类型按不同规则连接到不同的ED链表上。对控制传输和批量传输,仅连接到链表的尾部。对中断传输,将ED按传输周期插入到中断传输链中,控制器每毫秒处理一个叶子节点对应的一串ED链。对于实时传输,因为控制器每毫秒都要对他进行处理,因此所有的ED全部前插到根节点的中断ED后面。

②将ED插入到相应的链表上后,根据USBD发送来的数据请求信息中数据缓冲区的大小、数据传输的方向和传输类型创建连接到此ED上的TD。对于中断传输、批量传输和实时传输,将数据缓冲区按照4k大小分成多个部分,为每部分创建一个TD,按照OHCI的协议填充相应的值。对于控制传输,一般需创建3个TD,第一个TD对应控制传输的setup阶段;第二个TD对应控制传输所需的数据;第三个TD对应控制传输的握手阶段。

③当新创建的TD连接到ED上后,通过置位操作,使控制器能处理此ED中的数据传输。此后控制器经过SOF帧产生、组包、数据传输、YD处理完成等操作后,将处理完的TD前插入到处理完成队列中。每隔一定的时间,主控制器将处理完成队列的首地址拷贝到HCCA的hccaDoneHead中,产生TD完成链表处理中断。

④HCD的中断例程根据产生的中断信息判断是处理完成队列后,将此队列上TD的数据传输长度、返回状态等信息返回给上层,当上层对应的TD全部正确处理完成后,HCD通过回调函数通知数据请求发送方,数据处理完成。当数据传输有误时,主控制器在保证一定程度的数据重传后,若仍有错误,HCD会根据处理完成队列上TD的转台信息,取消与此次传输请求相关的所有TD操作,并把错误信息通知上层软件。

1.7 新设备添加

VxWorks BSP本身提供了一些通用驱动程序,如Timestamp Driver、SCSI设备驱动程序,以及串口驱动、内存驱动、定时器驱动等等。但是,在实际应用过程中,用户往往需要加入自己的驱动程序。VxWorks提供的驱动程序放在BSP目录%WIND_BASE%\target\h\drv和%WIND_BASE%\target\src\drv中,用户编写的驱动程序最好不要放在这两个目录中,这是因为不同的应用使用的驱动程序是不尽相同的。用户编写的驱动程序最好放在用户新建的目录bspname中。

VxWorks支持用GNU C编写的驱动程序。要把用户编写的驱动程序融入到VxWorks BSP中,可以利用makefile文件。

MACH_EXTRA

包含其它与机器硬件有关的内容的文件,它可以把.o文件链接到VxWorks输出目标中,如:

MACH_EXTRA += motFecEnd.o 29lv160bmtd.o bootControl.o dnlXmodem_n.o sysXmodem.o sysBootCfg.o mbFpga.o

LIB_EXTRA

可以在VxWorks中增加新的库存档,而无需改变标准的VxWorks存档或驱动库存档。如:

LIB_EXTRA +=lib/opcom3500_sdhm_hdlc.a

LIB_EXTRA +=lib/rotp_opcom3500e_sdhm.a

LIB_EXTRA +=lib/opcom3500e_sdhm_service.a

LIB_EXTRA +=lib/opcom3500e_sdhm_driver.a

EXTRA_INCLUDE

可以把用户目录下的头文件链接到VxWorks中。

ADDED_CFLAGS

可以用来控制VxWorks的配制,而无需修改任何源代码。

使用上述make宏MACH_EXTRA 、LIB_EXTRA、EXTRA_INCLUDE和ADDED_CFLAGS就可以把用户编写的文件方便地加入VxWorks中。

可以在MAKEFILE中重新指定BOOTINIT、BOOTCONFIG、USRCONFIG和DATASEGPAD,这样,当程序修改了这些文件后不用覆盖all目录中的内容:

BOOTINIT = bootInit.c

BOOTCONFIG = bootConfig.c

USRCONFIG = usrConfig.c

DATASEGPAD = dataSegPad.c

文件dataSegPad.s 仅被用于VxVMI text段的保护, 它保证text 段和data 段不共享一个MMU 页面。

1.8 标准输入输出

一般来说在使用基本I/O函数时,经常使用文件描述符来对文件进行操作。有三个基本的系统保留的描述符:

0 = standard input

1 = standard output

2 = standard error output

这三个描述符的值是在用open()和create()函数创建描述符时永远得不到的描述符,这就是我们分配的描述符永远都大于2的原因。注意VxWorks支持I/O重定向的功能,我们可以使用函数:ioGlobalStdSet(stdFd, newFd)来将系统保留的描述符stdFd(0,1,2)重定向到newFd上,这样做的好处是可以将标准输入/输出/错误输出重新定向到任何一个需要的I/O设备上,如:串口,socket,文件等等,便于跟踪调试。自然也可以使用ioGlobalStdGet(stdFd)读出现在系统的标准输入/输出/错误输出定向在哪个描述符上。更灵活的是我们可以使用ioTaskStdSet(tasked,stdFd,newFd)重定向某个指定任务的标准I/O。

    void logFdSet     (     int fd               /* file descriptor to use as logging device */ )

函数用于设置logMsg( )的首要输出位置。

logFdAdd( )、logFdDelete( )用于添加和删除logging的输出文件描述符。