3. SPI字符设备驱动模型:VxWorks字符设备驱动注册流程与回调函数实现

在VxWorks中,SPI设备通常以字符设备的形式暴露给应用层。理解字符设备驱动的注册流程以及核心回调函数(open、close、ioctl、read、write)的实现,是开发高效SPI驱动的基础。本节将详细拆解这一模型,并结合SPI场景给出具体实现要点。

3.1 字符设备驱动注册流程

VxWorks字符设备驱动的注册遵循“设备号分配 → 驱动表初始化 → 设备创建”的标准化流程。核心数据结构为DEV_HDR和DRV_ENTRY,驱动通过iosDrvInstall()和iosDevAdd()完成注册。

关键步骤:
  1. 分配驱动号:调用iosDrvInstall(),传入7个回调函数指针(create、remove、open、close、read、write、ioctl),返回驱动号drvNum。
  2. 创建设备描述符:分配并初始化DEV_HDR结构体,设置设备名(如/dev/spi0)、驱动号、设备私有数据指针。
  3. 添加设备到I/O系统:调用iosDevAdd()将设备描述符挂载到系统设备链表,使应用层可通过open()访问。
字符设备驱动注册流程 iosDrvInstall() 分配驱动号 drvNum 初始化 DEV_HDR iosDevAdd() 设备就绪 /dev/spi0 注册完成后,应用层 open("/dev/spi0", ...) 即可触发驱动回调

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逐字节传输。推荐实现双路径策略:

SPI read/write 双路径实现架构 read/write 回调入口 DMA使能? DMA路径 cacheDmaMalloc + 双通道启动 PIO路径 逐字节轮询SPI状态寄存器 数据拷贝到用户空间 DMA路径适用于大块传输(>64字节),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传输优化和吞吐量提升奠定了驱动层基础。