3. SPI字符设备驱动模型:VxWorks字符设备驱动注册流程与回调函数实现
在VxWorks中,SPI设备通常以字符设备的形式暴露给应用层。理解字符设备驱动的注册流程以及核心回调函数(open、close、ioctl、read、write)的实现,是开发高效SPI驱动的基础。本节将详细拆解这一模型,并结合SPI场景给出具体实现要点。
3.1 字符设备驱动注册流程
VxWorks字符设备驱动的注册遵循“设备号分配 → 驱动表初始化 → 设备创建”的标准化流程。核心数据结构为DEV_HDR和DRV_ENTRY,驱动通过iosDrvInstall()和iosDevAdd()完成注册。
关键步骤:
- 分配驱动号:调用
iosDrvInstall(),传入7个回调函数指针(create、remove、open、close、read、write、ioctl),返回驱动号drvNum。 - 创建设备描述符:分配并初始化
DEV_HDR结构体,设置设备名(如/dev/spi0)、驱动号、设备私有数据指针。 - 添加设备到I/O系统:调用
iosDevAdd()将设备描述符挂载到系统设备链表,使应用层可通过open()访问。
3.2 核心回调函数实现
SPI字符设备驱动需要实现7个标准回调函数,其中open、close、ioctl、read、write最为关键。下表总结了每个回调在SPI场景下的职责与实现要点。
| 回调函数 | SPI场景职责 | 实现要点 |
|---|---|---|
| open | 初始化SPI控制器、配置默认参数、分配DMA缓冲区 | 检查设备是否已被占用;初始化SPI时钟、模式、位宽;注册中断 |
| close | 释放资源、关闭SPI控制器、释放DMA缓冲区 | 确保未完成的DMA传输已终止;释放中断;清空环形缓冲区 |
| ioctl | 动态配置SPI参数(频率、模式、片选、DMA使能等) | 定义自定义命令码(如SPI_SET_FREQ、SPI_SET_MODE);参数校验 |
| read | 从SPI从设备读取数据(全双工模式下需同时发送空数据) | 支持阻塞/非阻塞模式;DMA传输时等待完成回调;数据拷贝到用户空间 |
| write | 向SPI从设备写入数据 | DMA方式下需保证缓冲区物理连续;处理片选信号时序 |
3.3 open/close 回调实现详解
open 回调:当应用层调用open("/dev/spi0", O_RDWR)时触发。典型实现包括:
- 检查设备引用计数,防止重复打开。
- 初始化SPI控制器硬件(时钟使能、GPIO复用配置)。
- 分配DMA描述符和缓冲区(若使用DMA模式)。
- 注册中断服务函数(ISR)。
STATUS spiDevOpen(DEV_HDR *pDevHdr, char *name, int mode)
{
SPI_DEV *pSpi = (SPI_DEV *)pDevHdr;
if (pSpi->refCount > 0) return ERROR; /* 设备忙 */
/* 初始化SPI控制器寄存器 */
sysSpiclkEnable(pSpi->baseAddr);
spiRegSetMode(pSpi->baseAddr, SPI_MODE_0);
spiRegSetFreq(pSpi->baseAddr, 1000000); /* 1MHz */
/* 分配DMA缓冲区(物理连续) */
pSpi->dmaTxBuf = cacheDmaMalloc(SPI_DMA_BUF_SIZE);
pSpi->dmaRxBuf = cacheDmaMalloc(SPI_DMA_BUF_SIZE);
/* 注册中断 */
intConnect(INUM_TO_IVEC(pSpi->intVec), spiIsr, (int)pSpi);
sysIntEnable(pSpi->intLevel);
pSpi->refCount = 1;
return OK;
}
close 回调:释放open阶段分配的所有资源,确保DMA传输已停止。
STATUS spiDevClose(DEV_HDR *pDevHdr)
{
SPI_DEV *pSpi = (SPI_DEV *)pDevHdr;
if (pSpi->refCount == 0) return OK;
/* 等待DMA传输完成 */
semTake(pSpi->dmaDoneSem, WAIT_FOREVER);
/* 释放中断 */
sysIntDisable(pSpi->intLevel);
/* 释放DMA缓冲区 */
cacheDmaFree(pSpi->dmaTxBuf);
cacheDmaFree(pSpi->dmaRxBuf);
/* 关闭SPI控制器时钟 */
sysSpiclkDisable(pSpi->baseAddr);
pSpi->refCount = 0;
return OK;
}
3.4 ioctl 回调:SPI参数动态配置
ioctl是SPI驱动中最灵活的回调,用于运行时调整通信参数。建议定义如下命令码:
| 命令码 | 参数类型 | 功能描述 |
|---|---|---|
SPI_SET_FREQ |
int | 设置SPI时钟频率(Hz) |
SPI_SET_MODE |
int | 设置SPI模式(0/1/2/3) |
SPI_SET_BITS |
int | 设置数据位宽(8/16/32) |
SPI_SET_CS |
int | 选择片选信号线 |
SPI_ENABLE_DMA |
BOOL | 使能/禁用DMA传输模式 |
STATUS spiDevIoctl(DEV_HDR *pDevHdr, int function, int arg)
{
SPI_DEV *pSpi = (SPI_DEV *)pDevHdr;
switch (function) {
case SPI_SET_FREQ:
if (arg < SPI_MIN_FREQ || arg > SPI_MAX_FREQ)
return ERROR;
spiRegSetFreq(pSpi->baseAddr, arg);
pSpi->frequency = arg;
break;
case SPI_SET_MODE:
if (arg < 0 || arg > 3) return ERROR;
spiRegSetMode(pSpi->baseAddr, arg);
pSpi->mode = arg;
break;
case SPI_ENABLE_DMA:
pSpi->dmaEnabled = (BOOL)arg;
break;
default:
return ENOSYS;
}
return OK;
}
3.5 read/write 回调:DMA与非DMA双路径
SPI的read和write回调需要处理全双工特性。在DMA模式下,需同时启动TX和DMA通道;在非DMA模式下,使用PIO逐字节传输。推荐实现双路径策略:
write 回调实现示例(DMA路径):
int spiDevWrite(DEV_HDR *pDevHdr, char *buf, int nBytes)
{
SPI_DEV *pSpi = (SPI_DEV *)pDevHdr;
int ret = ERROR;
if (pSpi->dmaEnabled && nBytes > SPI_DMA_THRESHOLD) {
/* DMA路径 */
bcopy(buf, pSpi->dmaTxBuf, nBytes);
cacheFlush(DATA_CACHE, pSpi->dmaTxBuf, nBytes);
/* 启动DMA传输(TX + RX同时启动) */
dmaStart(pSpi->dmaTxChan, pSpi->dmaTxBuf, SPI_TX_REG, nBytes);
dmaStart(pSpi->dmaRxChan, SPI_RX_REG, pSpi->dmaRxBuf, nBytes);
/* 等待DMA完成信号量 */
semTake(pSpi->dmaDoneSem, WAIT_FOREVER);
ret = nBytes;
} else {
/* PIO路径:逐字节发送 */
for (int i = 0; i < nBytes; i++) {
while (!(spiRegRead(pSpi->baseAddr, SPI_SR) & SPI_TX_EMPTY));
spiRegWrite(pSpi->baseAddr, SPI_DR, buf[i]);
while (!(spiRegRead(pSpi->baseAddr, SPI_SR) & SPI_RX_FULL));
(void)spiRegRead(pSpi->baseAddr, SPI_DR); /* 清除RX */
}
ret = nBytes;
}
return ret;
}
性能提示:在DMA路径中,务必在启动DMA前调用
cacheFlush()(TX方向)和DMA完成后调用cacheInvalidate()(RX方向),否则CPU缓存与DMA之间会出现数据不一致问题。
3.6 设备注册完整代码骨架
以下代码展示了SPI字符设备驱动的完整注册流程,整合了上述所有回调函数:
/* SPI设备结构体 */
typedef struct {
DEV_HDR devHdr; /* 设备头,必须放在第一个字段 */
UINT32 baseAddr; /* SPI控制器基地址 */
int intVec; /* 中断向量号 */
int intLevel; /* 中断优先级 */
BOOL dmaEnabled; /* DMA使能标志 */
char *dmaTxBuf; /* DMA发送缓冲区 */
char *dmaRxBuf; /* DMA接收缓冲区 */
SEM_ID dmaDoneSem; /* DMA完成信号量 */
int refCount; /* 打开计数 */
int frequency; /* 当前频率 */
int mode; /* 当前模式 */
} SPI_DEV;
/* 驱动注册函数 */
STATUS spiDrvInit(void)
{
int drvNum;
SPI_DEV *pSpiDev;
/* 1. 安装驱动 */
drvNum = iosDrvInstall(spiDevCreate, spiDevRemove,
spiDevOpen, spiDevClose,
spiDevRead, spiDevWrite, spiDevIoctl);
if (drvNum == ERROR) return ERROR;
/* 2. 分配并初始化设备结构体 */
pSpiDev = (SPI_DEV *)malloc(sizeof(SPI_DEV));
if (pSpiDev == NULL) return ERROR;
bzero(pSpiDev, sizeof(SPI_DEV));
pSpiDev->devHdr.drvNum = drvNum;
pSpiDev->baseAddr = SPI0_BASE_ADDR;
pSpiDev->intVec = SPI0_INT_VEC;
pSpiDev->intLevel = SPI0_INT_LEVEL;
pSpiDev->dmaDoneSem = semBCreate(SEM_Q_FIFO, SEM_EMPTY);
/* 3. 添加设备到I/O系统 */
return iosDevAdd(&pSpiDev->devHdr, "/dev/spi0", drvNum);
}
常见错误:DEV_HDR必须作为设备结构体的第一个成员,否则
iosDevAdd()和回调函数中的类型转换会出错。此外,iosDrvInstall()的7个回调函数指针必须全部提供,不可为NULL,否则系统会崩溃。
通过以上实现,SPI字符设备驱动具备了完整的文件操作接口,应用层可以像操作普通文件一样使用open/read/write/ioctl/close来控制SPI外设,为后续DMA传输优化和吞吐量提升奠定了驱动层基础。