1. 深入解析IMX6ULL SPI控制器配置函数
作为一名嵌入式Linux驱动开发者,我经常需要深入研究各种外设控制器的底层实现。今天我想分享一个关于i.MX6ULL处理器中ECSPI控制器配置函数的详细分析。这个函数看似简单,但包含了大量硬件操作的精妙设计。
1.1 函数概述与寄存器操作基础
mx51_ecspi_config函数是Linux内核中i.MX6ULL SPI控制器的核心配置函数。它的主要职责是根据用户提供的SPI设备参数,设置硬件寄存器,使SPI控制器能够按照预期工作。
在开始分析前,我们需要明确几个关键概念:
- 寄存器映射:处理器通过内存映射方式访问外设寄存器
- 位操作:通过位掩码和移位操作设置寄存器特定位
- 时序要求:某些寄存器修改需要遵循特定的顺序
c复制static int mx51_ecspi_config(struct spi_device *spi,
struct spi_imx_config *config)
{
struct spi_imx_data *spi_imx = spi_master_get_devdata(spi->master);
u32 ctrl = MX51_ECSPI_CTRL_ENABLE;
u32 clk = config->speed_hz, delay, reg;
u32 cfg = readl(spi_imx->base + MX51_ECSPI_CONFIG);
int tx_wml = 0;
这段初始化代码做了以下几件事:
- 获取SPI控制器的私有数据结构
- 初始化控制寄存器值为使能状态
- 保存目标时钟频率
- 读取当前配置寄存器值
1.2 关键寄存器详解
1.2.1 控制寄存器(CTRL)
控制寄存器是整个SPI控制器的"大脑",它决定了控制器的基础工作模式。根据参考手册,我们需要关注几个关键位:
| 位域 | 名称 | 功能描述 |
|---|---|---|
| 0 | EN | 控制器使能位 |
| 4-7 | CHANNEL_MODE | 通道工作模式(主/从) |
| 8-11 | POST_DIVIDER | 后分频系数 |
| 12-15 | PRE_DIVIDER | 预分频系数 |
| 18-19 | CHANNEL_SELECT | 当前通道选择 |
在代码中,我们首先设置所有通道为主模式:
c复制ctrl |= MX51_ECSPI_CTRL_MODE_MASK;
这是因为i.MX6ULL的SPI控制器在模式切换时可能存在竞态条件,将所有通道固定为主模式可以避免这个问题。
1.2.2 配置寄存器(CONFIG)
配置寄存器控制SPI通信的细节参数,包括:
c复制cfg |= MX51_ECSPI_CONFIG_SBBCTRL(spi->chip_select);
这一行代码启用了"突发间保持片选"功能,确保在连续传输多个数据时片选信号保持有效。
1.3 时钟配置原理
SPI时钟配置是驱动中最复杂的部分之一。i.MX6ULL使用两级分频器来生成SPI时钟:
- 预分频器(PRE_DIVIDER):线性分频,分频系数=寄存器值+1
- 后分频器(POST_DIVIDER):指数分频,分频系数=2^寄存器值
时钟计算公式为:
code复制SCLK = f_ref / ((PRE_DIV + 1) × 2^POST_DIV)
在代码中,这个计算由mx51_ecspi_clkdiv函数完成:
c复制static unsigned int mx51_ecspi_clkdiv(struct spi_imx_data *spi_imx,
unsigned int fspi, unsigned int *fres)
{
unsigned int pre, post;
unsigned int fin = spi_imx->spi_clk;
post = fls(fin) - fls(fspi);
if (fin > fspi << post)
post++;
post = max(4U, post) - 4;
pre = DIV_ROUND_UP(fin, fspi << post) - 1;
*fres = (fin / (pre + 1)) >> post;
return (pre << MX51_ECSPI_CTRL_PREDIV_OFFSET) |
(post << MX51_ECSPI_CTRL_POSTDIV_OFFSET);
}
这个函数的核心算法是:
- 通过fls函数估算需要的分频系数
- 确保实际频率不超过目标频率
- 将分频压力尽量分配给线性分频器以提高精度
- 返回配置好的分频器寄存器值
1.4 SPI模式配置
SPI有四种工作模式,由CPOL和CPHA两个参数决定:
| 模式 | CPOL | CPHA | 时钟极性 | 采样边沿 |
|---|---|---|---|---|
| 0 | 0 | 0 | 空闲低电平 | 第一个上升沿 |
| 1 | 0 | 1 | 空闲低电平 | 第二个下降沿 |
| 2 | 1 | 0 | 空闲高电平 | 第一个下降沿 |
| 3 | 1 | 1 | 空闲高电平 | 第二个上升沿 |
在代码中这样实现:
c复制if (spi->mode & SPI_CPHA)
cfg |= MX51_ECSPI_CONFIG_SCLKPHA(spi->chip_select);
else
cfg &= ~MX51_ECSPI_CONFIG_SCLKPHA(spi->chip_select);
if (spi->mode & SPI_CPOL) {
cfg |= MX51_ECSPI_CONFIG_SCLKPOL(spi->chip_select);
cfg |= MX51_ECSPI_CONFIG_SCLKCTL(spi->chip_select);
}
这里需要注意SCLKPOL和SCLKCTL需要同时设置,以确保时钟信号的空闲状态与极性一致。
1.5 DMA配置与性能优化
对于高速SPI传输,使用DMA可以显著降低CPU负载:
c复制if (spi_imx->usedma)
ctrl |= MX51_ECSPI_CTRL_SMC;
writel(MX51_ECSPI_DMA_RX_WML(spi_imx->wml) |
MX51_ECSPI_DMA_TX_WML(tx_wml) |
MX51_ECSPI_DMA_RXT_WML(spi_imx->wml) |
MX51_ECSPI_DMA_TEDEN | MX51_ECSPI_DMA_RXDEN |
MX51_ECSPI_DMA_RXTDEN, spi_imx->base + MX51_ECSPI_DMA);
关键配置参数:
- RX_WML:接收水位线,决定DMA请求触发时机
- TX_WML:发送水位线,对于i.MX6UL需要特殊处理
- 使能位:控制DMA通道的启用
2. 关键问题与解决方案
2.1 时钟配置中的精度问题
在实际使用中,我发现时钟配置函数有时无法精确产生所需的SPI时钟频率。经过分析,这是因为:
- 两级分频器的限制导致某些频率无法精确实现
- 参考时钟本身可能有波动
解决方案是:
- 选择最接近的可用频率
- 必要时调整参考时钟源
2.2 模式切换的竞态条件
如注释所述,硬件在切换模式时存在竞态条件。我的解决方法是:
- 固定所有通道为主模式
- 在模式切换时增加适当延迟
- 使用硬件提供的测试寄存器验证状态
2.3 DMA配置的注意事项
在使用DMA时需要注意:
- 确保DMA缓冲区是cache对齐的
- 对于短传输,DMA可能反而会降低性能
- 需要正确配置水位线以避免欠载或过载
3. 实际调试经验
3.1 示波器是必备工具
在调试SPI驱动时,示波器是不可或缺的。通过观察实际的波形,可以验证:
- 时钟频率是否正确
- 数据对齐是否符合预期
- 片选信号的行为
3.2 利用内核调试功能
Linux内核提供了多种调试SPI控制器的方法:
- 通过sysfs查看控制器状态
- 使用tracepoints跟踪SPI传输
- 通过debugfs访问内部寄存器
3.3 性能优化技巧
对于高性能应用,可以考虑:
- 使用更大的DMA缓冲区
- 调整SPI时钟分频器
- 优化中断处理流程
4. 总结与建议
通过深入分析mx51_ecspi_config函数,我们不仅理解了i.MX6ULL SPI控制器的工作原理,还掌握了许多实用的调试和优化技巧。对于嵌入式Linux开发者,我的建议是:
- 仔细阅读参考手册,理解硬件行为
- 善用内核提供的调试工具
- 在实际硬件上验证理论分析
- 记录和分享遇到的问题和解决方案
希望这篇分析能帮助其他开发者更好地理解和运用i.MX6ULL的SPI控制器。在实际项目中,这种深入的理解往往能帮助我们快速定位和解决复杂的问题。