4. SPI驱动实战-轮询模式:ioremap映射寄存器、SPI寄存器读写封装、轮询发送/接收实现

好,咱们进入真正的实战环节了。前面几章我们把SPI协议和控制器架构理了一遍,现在该动手写代码了。这一章的目标很明确——用轮询模式把SPI数据发出去、收回来。不依赖中断,不依赖DMA,就是最纯粹的CPU轮询。

我个人习惯,学一个新外设驱动,先从轮询模式入手。为什么?因为逻辑简单,容易调试。你想想看,中断和DMA虽然效率高,但出了问题你根本分不清是硬件没响应还是软件配置错了。轮询模式就不一样,每一步都是你主动去查状态寄存器,心里有数。

4.1 ioremap:让CPU看见外设寄存器

ARM处理器上跑Linux,外设寄存器都在物理地址空间里。但应用程序和内核模块用的是虚拟地址,不能直接拿物理地址去读写。这时候就需要ioremap。

说白了,ioremap就是给物理地址配一个虚拟地址的“门牌号”。你拿着这个虚拟地址去读写,实际上操作的就是物理寄存器。

核心函数原型:

void __iomem *ioremap(phys_addr_t offset, size_t size);

参数1:物理基地址。参数2:映射长度(字节)。返回值:虚拟地址指针。

我在项目中遇到过一个问题:某款芯片的SPI寄存器基地址是0x01C19000,手册上写的是32位寄存器,每个偏移4字节。我一开始只映射了0x100字节,结果读写到偏移0x80之后的寄存器时,系统直接挂了。后来才发现,那个控制器的寄存器空间实际占了0x200字节。所以映射长度一定要给够,别省。

注意:ioremap返回的地址要用readl()/writel()来访问,不能直接用指针解引用。ARM架构下,外设寄存器对访问顺序和宽度有严格要求,用readl/writel能保证是32位原子操作,不会被编译器优化乱序。

4.2 SPI寄存器读写封装

直接在整个驱动里到处写writel(value, base + offset),代码会变得又臭又长。我建议把寄存器读写封装成内联函数或宏,这样可读性高,也方便后续维护。

先看看一个典型的SPI控制器寄存器布局(以某ARM SoC为例):

偏移地址 寄存器名称 功能描述
0x00 SPI_CR 控制寄存器(使能、模式、时钟极性/相位)
0x04 SPI_SR 状态寄存器(发送空、接收满、忙标志)
0x08 SPI_DR 数据寄存器(发送/接收共用)
0x0C SPI_CR2 时钟分频、数据长度配置

封装代码示例:

/* spi_regs.h */
#ifndef __SPI_REGS_H__
#define __SPI_REGS_H__

#include <linux/io.h>

/* 寄存器偏移宏 */
#define SPI_CR       0x00
#define SPI_SR       0x04
#define SPI_DR       0x08
#define SPI_CR2      0x0C

/* 状态寄存器位定义 */
#define SPI_SR_TX_EMPTY   (1 << 0)
#define SPI_SR_RX_FULL    (1 << 1)
#define SPI_SR_BUSY       (1 << 2)

/* 控制寄存器位定义 */
#define SPI_CR_ENABLE     (1 << 0)
#define SPI_CR_MASTER     (1 << 1)
#define SPI_CR_CPOL       (1 << 2)
#define SPI_CR_CPHA       (1 << 3)

/* 读写封装 */
static inline void spi_write_reg(void __iomem *base, u32 offset, u32 val)
{
    writel(val, base + offset);
}

static inline u32 spi_read_reg(void __iomem *base, u32 offset)
{
    return readl(base + offset);
}

/* 更细粒度的操作 */
static inline void spi_set_bit(void __iomem *base, u32 offset, u32 bit)
{
    u32 val = spi_read_reg(base, offset);
    val |= bit;
    spi_write_reg(base, offset, val);
}

static inline void spi_clear_bit(void __iomem *base, u32 offset, u32 bit)
{
    u32 val = spi_read_reg(base, offset);
    val &= ~bit;
    spi_write_reg(base, offset, val);
}

#endif /* __SPI_REGS_H__ */

你看,这样封装之后,驱动里写spi_set_bit(base, SPI_CR, SPI_CR_ENABLE),一眼就知道在干什么。我曾经接手过一个项目,前任工程师直接在代码里到处写writel(0x03, base + 0x00),我花了三天才搞清楚每个魔数代表什么。别学他。

4.3 轮询发送/接收实现

轮询模式的核心逻辑其实很简单:

  1. 把要发送的数据写到数据寄存器
  2. 轮询状态寄存器,等发送完成标志
  3. 读数据寄存器,拿到接收数据
  4. 重复直到所有字节发完

但这里有个坑——SPI是同步协议,你发一个字节的同时,会收到一个字节。所以发送和接收是成对出现的。你不能只发不收,也不能只收不发(除非你不在乎收到的垃圾数据)。

经验之谈:轮询模式最怕的就是死等。一定要加超时机制。我见过一个驱动,轮询发送时没加超时,结果SPI从机没响应,CPU就在那死循环,整系统卡死。加个timeout变量,超时了就报错退出,这是基本功。

完整实现代码:

/* spi_poll.c */
#include <linux/delay.h>
#include "spi_regs.h"

#define POLL_TIMEOUT_LOOPS    100000

/**
 * spi_poll_transfer - 轮询模式SPI收发
 * @base: 寄存器基地址(ioremap后的虚拟地址)
 * @tx_buf: 发送数据缓冲区
 * @rx_buf: 接收数据缓冲区
 * @len: 数据长度(字节)
 *
 * 返回值:0成功,-ETIMEDOUT超时
 */
int spi_poll_transfer(void __iomem *base, 
                      const u8 *tx_buf, 
                      u8 *rx_buf, 
                      int len)
{
    int i;
    int timeout;
    u32 status;

    if (!base || !tx_buf || !rx_buf || len <= 0)
        return -EINVAL;

    /* 确保SPI控制器已使能 */
    spi_set_bit(base, SPI_CR, SPI_CR_ENABLE);

    for (i = 0; i < len; i++) {
        /* 步骤1:写数据到发送寄存器 */
        spi_write_reg(base, SPI_DR, (u32)tx_buf[i]);

        /* 步骤2:轮询等待发送完成(TX_EMPTY置位) */
        timeout = POLL_TIMEOUT_LOOPS;
        while (timeout--) {
            status = spi_read_reg(base, SPI_SR);
            if (status & SPI_SR_TX_EMPTY)
                break;
            cpu_relax();  /* 给CPU一个喘息的机会 */
        }

        if (timeout < 0) {
            pr_err("SPI: TX timeout at byte %d\n", i);
            return -ETIMEDOUT;
        }

        /* 步骤3:轮询等待接收数据就绪(RX_FULL置位) */
        timeout = POLL_TIMEOUT_LOOPS;
        while (timeout--) {
            status = spi_read_reg(base, SPI_SR);
            if (status & SPI_SR_RX_FULL)
                break;
            cpu_relax();
        }

        if (timeout < 0) {
            pr_err("SPI: RX timeout at byte %d\n", i);
            return -ETIMEDOUT;
        }

        /* 步骤4:读取接收数据 */
        rx_buf[i] = (u8)spi_read_reg(base, SPI_DR);
    }

    /* 等待最后一个字节传输完成(BUSY标志清除) */
    timeout = POLL_TIMEOUT_LOOPS;
    while (timeout--) {
        status = spi_read_reg(base, SPI_SR);
        if (!(status & SPI_SR_BUSY))
            break;
        cpu_relax();
    }

    return 0;
}
EXPORT_SYMBOL_GPL(spi_poll_transfer);

这段代码有几个细节值得注意:

  • cpu_relax():在轮询循环里调用,告诉CPU“我在忙等,你可以做点别的优化”。ARM上这个宏会执行一条yield指令,降低功耗。
  • 超时计数器递减:用递减方式,最后判断timeout < 0就知道是否超时,省一个变量。
  • 最后等BUSY清除:这是很多人容易漏的。最后一个字节写完后,控制器可能还在传输中,不等BUSY清除就返回,下次操作可能冲突。

4.4 整体流程梳理

把上面几块拼起来,轮询模式SPI驱动的初始化到收发流程是这样的:

轮询模式SPI驱动整体流程 1. 获取SPI控制器物理基地址 2. ioremap映射到虚拟地址 3. 配置模式/时钟/使能控制器 4. 轮询收发循环(每字节) 写DR → 等TX_EMPTY → 等RX_FULL → 读DR 超时检查 + cpu_relax() 5. 等待BUSY清除后返回 关键点:每字节都做超时检测,最后必须等BUSY清除,否则下次传输可能出错

这个流程图把整个轮询驱动的骨架画出来了。你写驱动的时候,就按这个顺序来,每一步都检查返回值,基本不会出大问题。

4.5 避坑指南

我曾经踩过的坑:

  • 寄存器映射长度不够:只映射了0x100,结果访问0x120的寄存器时触发SError。后来我统一映射0x1000,反正虚拟地址空间够用。
  • 忘了加内存屏障:ARM是弱一致性内存模型,readl/writel自带屏障,但如果你用__raw_readl,就必须手动加mb()。我有一版代码用了__raw_writel,结果寄存器写入顺序乱掉,SPI时序全错。
  • 轮询超时时间太短:SPI时钟频率低的时候,一个字节传输可能花几十微秒。超时循环次数设得太少,高速CPU一下就超时了。我一般根据SPI时钟频率算一下,留3倍余量。

嗯,轮询模式就这些内容。代码量不大,但每一步都是基础。你把这个模式吃透了,后面学中断模式和DMA模式会轻松很多。毕竟,不管用什么方式传输,最终都是操作那几个寄存器。