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 轮询发送/接收实现
轮询模式的核心逻辑其实很简单:
- 把要发送的数据写到数据寄存器
- 轮询状态寄存器,等发送完成标志
- 读数据寄存器,拿到接收数据
- 重复直到所有字节发完
但这里有个坑——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驱动的初始化到收发流程是这样的:
这个流程图把整个轮询驱动的骨架画出来了。你写驱动的时候,就按这个顺序来,每一步都检查返回值,基本不会出大问题。
4.5 避坑指南
我曾经踩过的坑:
- 寄存器映射长度不够:只映射了0x100,结果访问0x120的寄存器时触发SError。后来我统一映射0x1000,反正虚拟地址空间够用。
- 忘了加内存屏障:ARM是弱一致性内存模型,
readl/writel自带屏障,但如果你用__raw_readl,就必须手动加mb()。我有一版代码用了__raw_writel,结果寄存器写入顺序乱掉,SPI时序全错。 - 轮询超时时间太短:SPI时钟频率低的时候,一个字节传输可能花几十微秒。超时循环次数设得太少,高速CPU一下就超时了。我一般根据SPI时钟频率算一下,留3倍余量。
嗯,轮询模式就这些内容。代码量不大,但每一步都是基础。你把这个模式吃透了,后面学中断模式和DMA模式会轻松很多。毕竟,不管用什么方式传输,最终都是操作那几个寄存器。