作为一名嵌入式Linux开发者,我经常需要与各种外设总线打交道,其中SPI(Serial Peripheral Interface)因其简单高效的特性,成为连接传感器、存储芯片等外设的首选方案。今天我将结合i.MX51处理器的ECSPI控制器,带大家深入剖析Linux内核中SPI子系统的实现细节,特别是PIO(Programmed I/O)模式下的数据传输机制。
在Linux SPI子系统中,一次完整的传输涉及三个关键角色:
静态配置层(mx51_ecspi_config):负责SPI总线参数的初始化,包括时钟极性(CPOL)、相位(CPHA)、频率和片选模式等。这些参数决定了SPI波形的基本形态。
动态执行层(spi_imx_pio_transfer):作为时序的实际执行者,它不直接配置时钟,而是通过控制数据流来驱动时序运行。主要关注FIFO填充、交换触发(XCH)和数据读取等实时操作。
硬件抽象层:通过寄存器直接操作ECSPI控制器,实现物理层面的数据收发。
这三层的关系可以用一个简单的类比理解:静态配置就像设定好乐谱的音调和节奏,动态执行则是乐手按照乐谱实际演奏,而硬件抽象层就是乐器本身。
在深入代码之前,我们需要了解几个核心数据结构:
c复制struct spi_transfer {
const void *tx_buf; // 发送缓冲区指针
void *rx_buf; // 接收缓冲区指针
unsigned len; // 传输长度(字节)
u32 speed_hz; // 传输速率
u8 bits_per_word; // 每个字的位数
u16 delay_usecs; // 传输后的延迟
// 其他DMA相关字段省略...
};
struct spi_imx_data {
void __iomem *base; // 寄存器基地址
const void *tx_buf; // 当前传输的发送缓冲区
void *rx_buf; // 当前传输的接收缓冲区
int count; // 剩余传输字节数
unsigned txfifo; // FIFO中待发送数据计数
struct completion xfer_done; // 传输完成通知
// 其他字段省略...
};
这些结构体构成了SPI驱动数据传输的基础框架,贯穿整个传输生命周期。
传输始于spi_imx_pio_transfer函数:
c复制static int spi_imx_pio_transfer(struct spi_device *spi,
struct spi_transfer *transfer)
{
struct spi_imx_data *spi_imx = spi_master_get_devdata(spi->master);
// 初始化传输参数
spi_imx->tx_buf = transfer->tx_buf;
spi_imx->rx_buf = transfer->rx_buf;
spi_imx->count = transfer->len;
spi_imx->txfifo = 0;
reinit_completion(&spi_imx->xfer_done);
spi_imx_push(spi_imx);
// 后续代码省略...
}
这个阶段主要完成三项工作:
关键点:
reinit_completion用于重置完成量,这是Linux内核中一种常见的同步机制,相当于初始化一个"信号灯",后续将通过它来通知传输完成。
spi_imx_push函数是PIO模式的核心:
c复制static void spi_imx_push(struct spi_imx_data *spi_imx)
{
while (spi_imx->txfifo < spi_imx_get_fifosize(spi_imx)) {
if (!spi_imx->count)
break;
spi_imx->tx(spi_imx); // 实际写入操作
spi_imx->txfifo++;
}
spi_imx->devtype_data->trigger(spi_imx);
}
这个函数执行了两个关键操作:
FIFO填充:循环将数据写入TX FIFO,直到FIFO满或所有数据写完。i.MX51的ECSPI控制器具有64x32位的FIFO深度,这意味着最多可以预装64个字(256字节)的数据。
传输触发:通过trigger函数启动实际传输。触发方式取决于SMC(Start Mode Control)位的设置:
c复制static void mx51_ecspi_trigger(struct spi_imx_data *spi_imx)
{
u32 reg = readl(spi_imx->base + MX51_ECSPI_CTRL);
if (!spi_imx->usedma)
reg |= MX51_ECSPI_CTRL_XCH; // PIO模式使用XCH触发
else if (spi_imx->devtype_data->devtype == IMX6UL_ECSPI)
reg |= MX51_ECSPI_CTRL_SMC; // i.MX6UL DMA模式可用SMC
else
reg &= ~MX51_ECSPI_CTRL_SMC; // 其他DMA模式需禁用SMC
writel(reg, spi_imx->base + MX51_ECSPI_CTRL);
}
经验之谈:在老款i.MX处理器(如i.MX51)上使用DMA时,必须禁用SMC模式而改用XCH手动触发,这是为了规避芯片勘误ERR008517描述的问题。这个坑我在实际项目中踩过,表现为DMA传输随机失败。
启动传输后,驱动通过中断机制管理数据传输过程:
c复制spi_imx->devtype_data->intctrl(spi_imx, MXC_INT_TE);
这行代码使能了TX FIFO空中断(TEEN),当FIFO完全排空时会触发中断。中断处理函数大致流程如下:
这种"填充-等待-再填充"的机制确保了大数据量传输时的连续性,同时避免了轮询带来的CPU资源浪费。
理解SPI控制器需要掌握几个核心寄存器:
这个寄存器控制着SPI传输的基础行为:
| 位域 | 名称 | 功能描述 |
|---|---|---|
| 31-20 | BURST_LENGTH | 定义单次突发传输的比特数(0=1bit,0xFFF=4096bit) |
| 19-18 | CHANNEL_SELECT | 选择片选信号(SS0-SS3) |
| 15-12 | PRE_DIVIDER | 前级分频系数(1-16分频) |
| 11-8 | POST_DIVIDER | 后级分频系数(2^n分频) |
| 3 | SMC | 启动模式控制(0=XCH触发,1=自动启动) |
| 2 | XCH | 交换位(手动触发传输) |
| 0 | EN | 模块使能控制 |
时钟计算公式为:
code复制SCLK = 系统时钟 / (PRE_DIVIDER * (2^POST_DIVIDER))
这个寄存器反映了SPI控制器的实时状态:
| 位 | 名称 | 描述 |
|---|---|---|
| 7 | TC | 传输完成标志(1=完成) |
| 6 | RO | RX FIFO溢出(1=溢出) |
| 5 | RF | RX FIFO满(1=满) |
| 3 | RR | RX FIFO就绪(1=有数据) |
| 2 | TF | TX FIFO满(1=满) |
| 0 | TE | TX FIFO空(1=空) |
避坑指南:在读取RXDATA前必须检查RR位,否则可能读到无效数据;同样,在写入TXDATA前要检查TF位,避免数据丢失。
特别注意:无论BURST_LENGTH设置多少,每次读写都必须以32位为单位,多余的高位会被忽略。
SPI传输需要合理的超时机制:
c复制transfer_timeout = spi_imx_calculate_timeout(spi_imx, transfer->len);
timeout = wait_for_completion_timeout(&spi_imx->xfer_done, transfer_timeout);
超时时间计算考虑了:
当发生超时时,驱动会:
实战经验:在实际项目中,我曾遇到SPI传输偶尔超时的问题,最终发现是片选信号线受到干扰。通过在驱动中增加重试机制(最多3次)和更详细的错误日志,显著提高了系统稳定性。
根据项目经验,分享几个SPI性能优化要点:
FIFO阈值设置:合理配置TX/RX阈值可以减少中断次数。例如设置TX_THRESHOLD=16,可以在FIFO剩余16个空位时就触发中断预填充数据。
双缓冲技术:对于高速持续传输,可以实现双缓冲机制,在一个缓冲区传输时准备下一个缓冲区的数据。
DMA使用:对于大数据量传输,应优先使用DMA模式。i.MX6UL之后的芯片修复了DMA相关勘误,可以安全使用SMC自动触发模式。
时钟分频优化:PRE_DIVIDER和POST_DIVIDER的组合会影响最终时钟精度,应选择能产生最接近目标频率的分频组合。
中断合并:同时使能TE和RR中断,在一次中断中处理收发,减少上下文切换开销。
通过深入理解SPI控制器的寄存器级操作和Linux驱动框架的实现细节,开发者可以更高效地利用SPI总线,并能够诊断和解决实际项目中遇到的各种问题。记住,好的驱动不仅要功能正确,还需要考虑性能、稳定性和错误恢复等工程实践问题。