DSMC 是什么
高速外部并行总线控制器
DSMC 硬件信号
原理图

DSMC 16位数据线对应表
| DSMC信号 | J9200针脚 | FLEXBUS复用 | GPIO PAD | 其他主要复用 |
| DSMC_DATA0 | 28 | FLEXBUS1_D1 | GPIO3_D4 | LCDC_DEN、EBC_SDLE |
| DSMC_DATA1 | 52 | FLEXBUS1_D3 | GPIO3_D0 | LCDC_D3、EBC_SDDO3 |
| DSMC_DATA2 | 48 | FLEXBUS1_D4 | GPIO3_C7 | LCDC_D4、EBC_SDDO4 |
| DSMC_DATA3 | 46 | FLEXBUS1_D5 | GPIO3_C6 | LCDC_D5、EBC_SDDO5 |
| DSMC_DATA4 | 44 | FLEXBUS1_D6 | GPIO3_C5 | LCDC_D6、EBC_SDDO6 |
| DSMC_DATA5 | 42 | FLEXBUS1_D7 | GPIO3_C4 | LCDC_D7、EBC_SDDO7 |
| DSMC_DATA6 | 53 | FLEXBUS1_D8 | GPIO3_C1 | LCDC_D10、EBC_SDDO10 |
| DSMC_DATA7 | 35 | FLEXBUS1_D9 | GPIO3_C0 | LCDC_D11、EBC_SDDO11 |
| DSMC_DATA8 | 25 | FLEXBUS0_D0 | GPIO3_B5 | LCDC_D14、ETH0_TXD0 |
| DSMC_DATA9 | 19 | FLEXBUS0_D1 | GPIO3_B4 | LCDC_D15、ETH0_TXD1 |
| DSMC_DATA10 | 31 | FLEXBUS0_D2 | GPIO3_B3 | LCDC_D16、ETH0_TXCTL |
| DSMC_DATA11 | 33 | FLEXBUS0_D3 | GPIO3_B2 | LCDC_D17、ETH0_RXD0 |
| DSMC_DATA12 | 29 | FLEXBUS0_D4 | GPIO3_B1 | LCDC_D18、ETH0_RXD1 |
| DSMC_DATA13 | 47 | FLEXBUS0_D5 | GPIO3_A7 | LCDC_D20、ETH0_RXCTL |
| DSMC_DATA14 | 49 | FLEXBUS0_D6 | GPIO3_A6 | LCDC_D21、ETH0_MDC |
| DSMC_DATA15 | 45 | FLEXBUS0_D7 | GPIO3_A5 | LCDC_D22、ETH0_MDIO |
设备树中的:
rockchip,io-width = <16>;
对应下面这16根 DSMC_DATA[15:0]:
时钟和数据选通信号
| DSMC信号 | J9200针脚 | FLEXBUS复用 | GPIO PAD | 作用 |
| DSMC_CLKP | 32 | FLEXBUS1_D0 | GPIO3_D5 | DSMC差分时钟正端 |
| DSMC_CLKN | 30 | FLEXBUS1_CLK | GPIO3_D6 | DSMC差分时钟负端 |
| DSMC_DQS0 | 37 | FLEXBUS1_D10 | GPIO3_B7 | 低字节数据采样选通 |
| DSMC_DQS1 | 41 | FLEXBUS0_CLK | GPIO3_B6 | 高字节数据采样选通 |
片选信号对应表
| DSMC信号 | J9200针脚 | FLEXBUS复用 | GPIO PAD | 设备树节点 |
| DSMC_CSN0 | 58 | FLEXBUS1_D2 | GPIO3_D3 | dsmc_lb_slave0 |
| DSMC_CSN1 | 17 | FLEXBUS0_D8 | GPIO3_B0 | dsmc_lb_slave1 |
| DSMC_CSN2 | 54 | FLEXBUS0_D11 | GPIO3_D1 | dsmc_lb_slave2 |
| DSMC_CSN3 | 56 | FLEXBUS0_D12 | GPIO3_D2 | dsmc_lb_slave3 |
复位和Ready信号
| DSMC信号 | J9200针脚 | FLEXBUS复用 | GPIO PAD | 作用 |
| DSMC_RESETN | 36 | FLEXBUS0_D15_M0 / FLEXBUS1_D12_M0 | GPIO3_D7 | DSMC从设备复位,低有效 |
| DSMC_RDYN | 21 | FLEXBUS1_D11 / FLEXBUS0_CSN_M0 | GPIO3_A4 | 从设备Ready/等待状态,低有效 |
DSMC外部中断信号
| DSMC信号 | J9200针脚 | FLEXBUS复用 | GPIO PAD |
|---|---|---|---|
| DSMC_INT0 | 18 | FLEXBUS0_D13_M0 / FLEXBUS1_D14_M0 | GPIO4_A0 |
| DSMC_INT1 | 51 | FLEXBUS0_D9 | GPIO3_C2 |
| DSMC_INT2 | 57 | FLEXBUS0_D14_M0 / FLEXBUS1_D13_M0 | GPIO4_A1 |
| DSMC_INT3 | 40 | FLEXBUS0_D10 | GPIO3_C3 |
总线图

| 引脚 | 方向 | 电平 | 说明 |
|---|---|---|---|
| CLKP CLKP | O | - | 通信时钟,差分时钟用于1.8V设备,3.0V以上设备只使用CLKP 主机发送数据和时钟交叉沿中心对齐(从机在时钟跳变沿采样) |
| CSn0~CSn3 | O | - | 片选,支持4个从设备 |
| DQ0~DQ15 | I/O | - | 双向数据总线 |
| DQS0 DQS1 | I/O | - | RWDS读写选通信号,CA命令期间用来向主机表示是否需要额外延时 主机读数据期间和数据信号沿对齐用于主机获取数据 主机写数据期间用于数据掩码(localbus模式下无效) |
| 可选接口 | |||
| RESETN | O | 上拉 | 从设备复位信号 |
| RDYN | I | 开漏 | 从机准备好信号,LocalBus写反压 |
| INTn0~INTn3 | I | 开漏 | Localbus中断通知 |
设备树:
dsmc 控制器
├── slave:外部访问窗口、DQS参数 │ └── dsmc_slave:外部从设备类型 ├── psram │ ├── psram0 │ ├── psram1 │ ├── psram2 │ └── psram3 │ └── lb-slave ├── lb-slave0 │ └── region0~3 ├── lb-slave1 │ └── region0~3 ├── lb-slave2 │ └── region0~3 └── lb-slave3 └── region0~3
DSMC控制器硬件资源
dsmc: dsmc@2a280000 {
compatible = "rockchip,rk3576-dsmc";
reg = <0x0 0x2a280000 0x0 0x10000>;
#address-cells = <2>;
#size-cells = <2>;
rockchip,grf = <&ioc_grf>;
interrupts = <GIC_SPI 360 IRQ_TYPE_LEVEL_HIGH>;
power-domains = <&power RK3576_PD_SDGMAC>;
resets = <&cru SRST_A_DSMC>, <&cru SRST_P_DSMC>;
reset-names = "dsmc", "apb";
clocks = <&cru CLK_DSMC_SYS>,
<&cru ACLK_DSMC>,
<&cru PCLK_DSMC>,
<&cru ACLK_DSMC_ROOT>;
clock-names = "clk_sys", "aclk_dsmc", "pclk", "aclk_root";
clock-frequency = <100000000>;
dmas = <&dmac0 30>, <&dmac0 31>;
dma-names = "req0", "req1";
status = "disabled";
外部访问窗口和DQS采样
slave {
rockchip,dqs-dll = <0x40 0x40
0x40 0x40
0x40 0x40
0x40 0x40>;
rockchip,ranges = <0x0 0x10000000 0x0 0x2000000>;
rockchip,slave-dev = <&dsmc_slave>;
};
DSMC控制器如何访问外部从设备。
-
rockchip,dqs-ddl对应DSMC_DSQ0,DSMC_DSQ1.
-
DSQ DLL用于调整读取数据时的采样点
-
这里的
0x40是模板初始值,不应直接认为是所有PCB的最终值。频率提高后通常需要根据训练结果或读写稳定窗口调整。
-
rockchip,ranges
rockchip,ranges =
<0x0 0x10000000 0x0 0x2000000>;
作用是给 DSMC 外部访问提供 CPU 物理地址窗口。 CPU访问这段地址时,不是访问DDR,而是由 DSMC 转换成外部总线事务:
设备总入口
dsmc_slave: dsmc-slave {
compatible = "rockchip,dsmc-slave";
rockchip,clk-mode = <0>;
status = "disabled";
psram {
...
};
lb-slave {
...
};
};
DSMC外部从设备的配置容器 分为:psram和local bus
rockchip,clk-mode
rockchip,clk-mode = <0>;
控制 DSMC 外部时钟工作模式。
它最终影响原理图上的:
DSMC_CLKP
DSMC_CLKN
但数值 0 的完整寄存器定义需要查看当前 RK3576 TRM 或驱动中的枚举,不能只根据字段名猜测。
这里保持 0,表示采用模板默认时钟模式。
PSRAM设备配置
psram {
dsmc_psram0: psram0 {
status = "disabled";
};
dsmc_psram1: psram1 {
status = "disabled";
};
dsmc_psram2: psram2 {
status = "disabled";
};
dsmc_psram3: psram3 {
status = "disabled";
};
};
Local Bus公共配置
lb-slave {
rockchip,io-width = <16>;
dsmc_lb_slave0: lb-slave0 {
...
};
dsmc_lb_slave1: lb-slave1 {
...
};
dsmc_lb_slave2: lb-slave2 {
...
};
dsmc_lb_slave3: lb-slave3 {
...
};
};
rockchip,io-width = <16>
对应原理图中的:
DSMC_DATA0
DSMC_DATA1
...
DSMC_DATA15
表示采用16位数据宽度。
低字节:DATA[7:0] ↔ DQS0
高字节:DATA[15:8] ↔ DQS1
一次基础数据传输可以携带16位数据:
0x1234
DATA[15:8] = 0x12
DATA[7:0] = 0x34
lb-slave0~3
每一个 lb-slaveX 表示一个 Local Bus从设备配置实例。
可以先按下面模型理解:
lb-slave0 → 从设备端口/片选索引0
lb-slave1 → 从设备端口/片选索引1
lb-slave2 → 从设备端口/片选索引2
lb-slave3 → 从设备端口/片选索引3
在硬件上与:
DSMC_CSN0
DSMC_CSN1
DSMC_CSN2
DSMC_CSN3
相关联,但具体对应关系应在当前 SDK 驱动的 CS索引计算中确认。
每个从设备的总线时序
dsmc_lb_slave0: lb-slave0 {
rockchip,mtr-timing = <1 0 0 0 0 0 2 2>;
status = "disabled";
region {
...
};
};
rockchip,mtr-timing
rockchip,mtr-timing = <1 0 0 0 0 0 2 2>;
这是 Memory Timing Register 对应的一组时序参数。
按当前常见驱动解析顺序,可以拆成:
<rcshi wcshi rcss wcss rcsh wcsh rd_latency wr_latency>
对应:
| 索引 | 字段 | 当前值 | 作用 |
|---|---|---|---|
| 0 | rcshi | 1 | 读操作CS高电平间隔 |
| 1 | wcshi | 0 | 写操作CS高电平间隔 |
| 2 | rcss | 0 | 读操作CS建立时间 |
| 3 | wcss | 0 | 写操作CS建立时间 |
| 4 | rcsh | 0 | 读操作CS保持时间 |
| 5 | wcsh | 0 | 写操作CS保持时间 |
| 6 | rd_latency | 2 | 读延迟 |
| 7 | wr_latency | 2 | 写延迟 |
它控制的是一次访问中:
CS什么时候拉低
│
▼
时钟什么时候开始
│
▼
数据什么时候有效
│
▼
CS什么时候释放
模板值:
<1 0 0 0 0 0 2 2>
更适合作为初始值。最终是否合理,需要结合外部设备时序规格和示波器波形确认。
不能简单把数字直接换算成纳秒,因为寄存器字段可能存在:
N
N+1
特殊编码
半周期
控制器内部转换
Region配置
Region的公共参数
四个Region都包含:
rockchip,ca-addr-width = <0>;
rockchip,dummy-clk-num = <1>;
rockchip,cs0-be-ctrled = <0>;
rockchip,cs0-ctrl = <0>;
ca-addr-width
rockchip,ca-addr-width = <0>;
控制命令/地址阶段的地址编码宽度。
它影响一次事务开始时,Host如何把访问地址发送给外部从设备。
数值 0 对应哪一种具体宽度,需要查看当前SDK中的枚举或寄存器定义,不能仅凭字段名断言。
dummy-clk-num
rockchip,dummy-clk-num = <1>;
用于配置命令地址阶段和数据阶段之间的等待时钟。
目的:
Host发出命令/地址
│
▼
从设备内部地址译码、准备数据
│
▼
进入读写数据阶段
需要注意:DTS值可能会在驱动里转换后再写入寄存器,所以不能直接认为 <1> 就一定表示波形上额外增加一个完整周期。
cs0-be-ctrled
rockchip,cs0-be-ctrled = <0>;
与 Region 的字节使能、CS控制方式相关。
设置为0表示不启用相应特殊控制模式。
完整位义应查看 TRM 或驱动中的寄存器宏。
cs0-ctrl
rockchip,cs0-ctrl = <0>;
用于配置 Region下与 CS0有关的控制属性。
模板设置为0,表示使用默认控制方式。
同样不建议脱离驱动和寄存器定义直接修改。
Region的不同协议
region {
region0 { ... };
region1 { ... };
region2 { ... };
region3 { ... };
};
Region的本质是:
在同一个从设备/片选下面,定义不同访问属性的地址窗口。
它不是原理图上额外的物理引脚。
Region0:Merged FIFO
region0 {
rockchip,attribute = "Merged FIFO";
rockchip,ca-addr-width = <0>;
rockchip,dummy-clk-num = <1>;
rockchip,cs0-be-ctrled = <0>;
rockchip,cs0-ctrl = <0>;
status = "disabled";
};
该Region被控制器配置成 Merged FIFO 属性。
用于适合连续、流式访问的FIFO类资源。
“Merge”具体是如何合并主机侧事务,需要结合当前 DSMC 控制器实现理解,不应简单等同于“自动保持数据包边界”。
Region1:No-Merge FIFO
region1 {
rockchip,attribute = "No-Merge FIFO";
...
};
仍然是 FIFO 属性,但不采用合并机制。
适合需要每次访问相对独立的FIFO资源。
Region2:DPRA
region2 {
rockchip,attribute = "DPRA";
...
};
表示一种 RAM 区域访问属性。
它通常用于共享缓冲区或可随机访问的数据区域。但 DPRA 的完整硬件定义,应以 RK3576 DSMC TRM 中的 Region Attribute 定义为准。
Region3:Register
region3 {
rockchip,attribute = "Register";
...
};
表示寄存器型访问窗口。
适合:
控制寄存器
状态寄存器
版本寄存器
中断状态
命令寄存器
例如外部设备内部设计为:
Offset 0x00:CONTROL
Offset 0x04:STATUS
Offset 0x08:VERSION
Offset 0x0C:IRQ_STATUS
Host访问这个Region时,控制器使用 Register 属性产生外部事务。
时序分析
HyperRAM
Read

流程

| 阶段 | 主要行为 |
|---|---|
| 开始 | CS# 拉低,选中从设备 |
| C/A 阶段 | DSMC 在 DQ 上发送 48-bit C/A,R/W# = 0 表示读操作 |
| 等待阶段 | 从设备解析地址并准备数据 |
| 数据阶段 | 从设备通过 DQ 返回数据,并输出 DQS 作为数据选通信号 |
| 读反压 | FIFO 为空时,从设备停止输出 DQS 和新数据,保持当前 DQ |
| 恢复 | FIFO 有数据后,继续输出 DQS 和 DQ |
| 结束 | 数据读取完成,CS# 拉高 |
时序分析
| 总线 / 阶段 | 开始阶段 | C/A 阶段 | Latency 阶段 | 数据阶段 | 结束阶段 |
|---|---|---|---|---|---|
CS# | 拉低,选中 HyperRAM | 保持低电平 | 保持低电平 | 保持低电平 | 拉高,结束事务 |
CK/CK# | 开始输出时钟 | 提供命令和地址传输时钟 | 继续计数等待 | 提供 DDR 数据传输时钟 | 停止本次时钟 |
RWDS | 准备响应 | 指示 1 倍或 2 倍 Latency | 等待数据准备 | 作为读数据选通信号 | 释放总线 |
DQ[7:0] | Host 准备驱动 | Host 发送 48-bit 命令和地址 | 无有效数据 | HyperRAM 输出 Dn A、Dn B... | 释放总线 |
Write

流程
类似于Read
| 阶段 | 主要行为 |
|---|---|
| 开始 | CS# 拉低,选中从设备 |
| C/A 阶段 | DSMC 在 DQ 上发送 48-bit C/A,R/W# = 1 表示写操作 |
| 数据阶段 | DSMC 在 DQ 上发送写数据 |
| 写反压 | 从设备 FIFO 满时拉起 RDYN,DSMC暂停发送 DQ 和 CLK |
| 恢复 | RDYN 释放后继续发送数据 |
| 结束 | 数据发送完成,CS# 拉高 |
时序分析
| 总线 / 阶段 | 开始阶段 | C/A 阶段 | Latency 阶段 | 数据阶段 | 结束阶段 | |
|---|---|---|---|---|---|---|
CS# | 拉低,选中 HyperRAM | 保持低电平 | 保持低电平 | 保持低电平 | 拉高,结束事务 | |
CK/CK# | 开始输出时钟 | 提供命令和地址传输时钟 | 继续计数等待 | 提供 DDR 数据传输时钟 | 停止本次时钟 | |
RWDS | 准备响应 | 指示 1 倍或 2 倍 Latency | 等待数据准备 | 作为读数据选通信号 | 释放总线 | |
DQ[7:0] | Host 准备驱动 | Host 发送 48-bit 命令和地址 | 无有效数据 | HyperRAM 输出 Dn A、Dn B... | 释放总线 | 像这样的写操作 |
FIFO
| 名称 | 中文 | 作用 |
|---|---|---|
| ADDR FIFO | 地址 FIFO | 保存地址、长度、Burst 类型和读写标志,并将 AXI 时钟域请求同步到 DSMC 时钟域。 |
| W DAT FIFO | 写数据 FIFO | 保存 AXI 侧的写数据、有效信息和字节选通信号,等待 DSMC 发出写时序。 |
| R DAT FIFO | 读数据 FIFO | 保存从 DSMC 接收到的读数据和错误信息,再按 AXI 时钟输出给主机。 |
写:AXI → W DAT FIFO → DSMC设备 读:DSMC设备 → R DAT FIFO → AXI 地址:AXI → ADDR FIFO → DSMC控制器
Byte Mask
字节屏蔽:
写操作时,DSMC 将 DQS 作为字节屏蔽信号输出:
DQS = HIGH:对应字节被屏蔽,不写入。DQS = LOW:对应字节有效,正常写入。- HyperFlash 只支持 16-bit 写入,因此被屏蔽的字节会填充为全
1。 在 x16 模式下一次传输两个字节,Byte Mask 可以只写高字节或低字节,避免另一个字节被误修改。x8 模式则控制当前这一个字节是否写入。
TX/RX Controller
发送/接收控制器:
TX/RX Controller 通过状态机控制 DSMC 的读写操作,并负责 AXI 总线与 DSMC 之间的数据流控制。 当 AXI 接收到多个 Outstanding 请求时,如果后续访问地址与当前地址连续,可以将其合并到当前事务中,以提高带宽。
- 控制读写流程;
- 协调 AXI 和 DSMC 两侧的数据传输;
- 合并连续访问,提高传输效率。
C/A Encoder
命令/地址编码器:
DSMC 设备使用 6 Byte(48 bit)命令/地址信息描述一次传输,包括:
- 读或写;
- 目标设备;
- Burst 类型;
- 访问地址。
把 AXI 侧的读写请求和地址,转换成 DSMC 设备能识别的 C/A 时序数据,并在数据传输前通过
DQ发送出去。
C/A位:
| C/A 位 | 名称 | 含义 |
|---|---|---|
47 | R/W# | 0:写,1:读 |
46 | Target | 选择目标设备/配置 |
45 | Burst Type | 0:WRAP,1:INCR |
44 | RFU | 保留位,固定为 0 |
43:16 | Page Address | 地址的 Address[31:4] |
15 | RFU | 保留位 |
14:13 | RFU | 保留位 |
12:3 | RFU | 保留位 |
2:0 | Column Address | 地址的 Address[3:1] |
解释:
| 英文名称 | 中文名称 | 含义 |
|---|---|---|
| C/A Bit | 命令/地址位 | 表示该字段位于 48-bit C/A 数据中的哪一位 |
| Name | 名称 | 该字段的功能名称 |
| Assignment | 位定义 | 该字段不同取值对应的含义 |
| R/W# (Read/Write) | 读/写控制 | 0 表示写,1 表示读 |
| Target | 目标配置 | 选择对应片选设备的目标类型,由 DSMC_MCRn 配置 |
| Burst Type | 突发类型 | WRAP 回绕突发,INCR 地址递增突发 |
| RFU (Reserved for Future Use) | 保留位 | 暂未使用,按 TRM 要求填写固定值 |
| Page Address | 页地址 | 地址的高位部分 Address[31:4] |
| Column Address | 列地址 | 地址的低位部分 Address[3:1] |
Localbus
Read
CS# 拉低 → DSMC发送C/A → FPGA解析地址 → FPGA输出DQ和DQS → CS#拉高
Write
CS# 拉低 → DSMC发送C/A → DSMC输出DQ写数据 → FPGA接收数据 → CS#拉高
Region
Localbus 设备内部空间可以划分多个区域,不同区域用于不同场景,设置与外设不同的访问方式。

- 区域大小由
DSMC_DEV_SIZE和DSMC_SLV_RGN_DIVn配置。 - 每个区域可设置为:
DPRAREGFIFO(merged/un-merged)
| 类型 | 中文 | 作用 |
|---|---|---|
| DPRA | 双口 RAM 区域 | 用于普通内存式读写,可配置合并或不合并 |
| REG | 寄存器区域 | 用于访问 FPGA/从设备寄存器,可配置合并或不合并 |
| FIFO | FIFO 区域 | 用于连续数据流传输,可配置合并或不合并 |
C/A Ecoder
LocalBus 模式下,一次传输同样使用 48-bit C/A 信息,用于描述读写类型、区域类型、Burst 长度和访问地址。
| C/A Bit | Name(中文) | Assignment(定义) |
|---|---|---|
47 | R/W#(读写控制) | 0:Read;1:Write |
46 | Target(目标配置) | 由 DSMC_MCRn 中的 CRT 配置 |
45 | Burst Type(突发类型) | 0:WRAP;1:INCR |
44 | Space Type(空间类型) | 0:FIFO;1:DPRA |
43 | FIFO Merge(FIFO 合并) | 0:不合并;1:合并 |
42 | RFU(保留位) | 固定为 0 |
41:40 | Region(区域编号) | 选择 Region 0~3 |
39:32 | Length(突发长度) | 本次 Burst 的数据长度 |
31:0 | Address(访问地址) | LocalBus 32-bit 地址 |
Back Pressure
| 方向 | 条件 | 处理 |
|---|---|---|
| 读 | FPGA FIFO 空 | FPGA停止发送 DQS,保持当前 DQ |
| 写 | FPGA FIFO 满 | FPGA拉起 RDYN,DSMC暂停发送 DQ 和 CLK |
| 恢复 | FIFO状态恢复 | 继续原事务 |
Register
| 寄存器 | 作用 |
|---|---|
DSMC_DEV_SIZE | 设备空间大小 |
DSMC_SLV_RGN_DIVn | Region边界 |
DSMC_RGNx_ATTRn | Region属性 |
DSMC_VDMCn | 设置为LocalBus协议 |
DSMC_MCRn | IO宽度、访问属性 |
DSMC_MTRn | CS与时钟时序调整 |
DSMC_MRGTCRn | 合并访问 |
DSMC_BDRTCRn | 跨边界处理 |
信号线
| 信号 | 方向 | 作用 |
|---|---|---|
CLKP/CLKN | Output(输出) | 差分传输时钟,为 C/A 和数据传输提供时序基准。 |
CS[3:0]# | Output(输出) | Chip Select(片选),选择对应的 LocalBus 从设备;低有效。 |
RESETN | Output(输出) | Reset(复位),复位外部从设备;低有效。 |
DQ[15:0] | I/O(双向) | C/A 和数据复用总线。C/A、写数据由 DSMC 输出,读数据由从设备输出。 |
DQS[1:0] | I/O(双向) | 读时作为 Data Strobe(数据选通);写时作为 Byte Mask(字节屏蔽)。 |
RDYN | Input(输入) | Readiness/Ready(就绪/反压)信号。写入时从设备 FIFO 满,可使 DSMC 暂停发送数据和时钟。 |
INT[3:0] | Input(输入) | Interrupt(中断)输入,由 LocalBus 从设备向 RK3576 发出中断请求。 |
驱动分析
probe
probe整体链路
设备树 status = "okay"
|
v
compatible 匹配 dsmc_of_match
|
v
创建 platform_device
|
v
调用 rk_dsmc_probe()
Probe 组成部分
- 分配并绑定私有对象
- 映射寄存器,获取GRF、读取频率
- 解析设备树、完成芯片级的IOMUX初始化
- 获取复位与时钟资源
- 启用始终并设置频率
- 申请DMA通道、安装操作接口
- 初始化控制器、映射外部空间、训练并检查硬件
- 失败时反向释放资源
分配并绑定私有对象
struct device *dev = &pdev->dev;
struct device_node *np = dev->of_node;
struct rockchip_dsmc *dsmc;
struct rockchip_dsmc_device *priv;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
platform_set_drvdata(pdev, priv);
dsmc = &priv->dsmc;
字段分析:
- pdev:设备树DSMC节点对应的平台设备
- dev:通用设备对象,用于日志、资源管理和DMA
- np:设备树节点
- priv:驱动对外保存的完整DSMC设备对象
- dsmc:控制器的内部状态、硬件资源和配置
devm_kzalloc() 分配并且清零对象,资源生命周期由设备管理。probe失败或设备移除后,内核会自动是否该内存。
platform_set_drvdata()把priv绑定到pdev,使remove()函数以后可以取回它
结果:驱动以及有了一个表示当前DSMC控制器的软件对象
映射寄存器、获取GRF、读取频率
映射DSMC寄存器
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
dsmc->regs = devm_ioremap_resource(dev, mem);
if(IS_ERR(dsmc->regs))
return PTR_ERR(dsmc->regs);
对应设备树
reg = <0x0 0x2a280000 0x0 0x10000>;
转换关系 DSMC 物理寄存器地址 0x2a280000 | v devm_ioremap_resource() | v dsmc->regs 虚拟地址 | v readl()/writel() 访问寄存器
获取GRF
dsmc->grf = syscon_regmap_lookup_by_phandle(np, "rockchip,grf");
对应设备树:
rockchip,grf = <&ioc_grf>;
读取工作频率
ret = device_property_read_u32(dev, "clock-frequency",
&dsmc->cfg.freq_hz);
dsmc->cfg.ctrl_freq_hz = dsmc->cfg.freq_hz * 2;
CPU 可以访问 DSMC 寄存器,驱动可以访问 GRF,并且知道目标时钟频率。
解析设备树与芯片级初始化
if (dsmc_parse_dt(pdev, dsmc)) {
dev_err(dev, "The dts parameters get fail!\n");
return -ENODEV;
}
ret = rockchip_dsmc_platform_init(pdev);
if (ret)
return ret;
dsmc_parse_dt()
该函数把设备树转换为 dsmc->cfg。解析主线:
&dsmc/slave
|- rockchip,dqs-dll
|- rockchip,ranges
`- rockchip,slave-dev -> &dsmc_slave
|- rockchip,clk-mode
|- psram/psram0..3
`- lb-slave/lb-slave0..3
`- region0..3
它首先读取:
rockchip,dqs-dll:每个 CS 的 DQS DLL 初始值。rockchip,ranges:外部设备的物理映射起点和窗口大小。rockchip,slave-dev:指向dsmc_slave配置节点。rockchip,clk-mode:DSMC 时钟模式。
然后扫描 psram0..3:
psram0 -> CS0
psram1 -> CS1
psram2 -> CS2
psram3 -> CS3
启用的 PSRAM 会令对应:
cfg->cs_cfg[cs].device_type = OPI_XCCELA_PSRAM;
接着扫描 lb-slave0..3:
lb-slave0 -> CS0
lb-slave1 -> CS1
lb-slave2 -> CS2
lb-slave3 -> CS3
启用的 Local Bus 从设备会令对应:
cfg->cs_cfg[cs].device_type = DSMC_LB_DEVICE;
同时解析:
rockchip,mtr-timing:CS 读写时序。rockchip,int-en:该 CS 使用的中断通道。rockchip,io-width:x8 或 x16。region0..3的状态和属性。
region 属性转换:
"Register" -> RGNX_ATTR_REG
"DPRA" -> RGNX_ATTR_DPRA
"No-Merge FIFO" -> RGNX_ATTR_NO_MERGE_FIFO
"Merged FIFO" -> RGNX_ATTR_MERGE_FIFO
驱动要求 PSRAM 和 Local Bus 互斥:
if (psram && lb_slave)
return -ENODEV;
else if (!(psram || lb_slave))
return -ENODEV;
也就是说:两种都开会失败,两种都不开也会失败。
rockchip_dsmc_platform_init()
该函数根据complatible取得of_match_table中的芯片问题
rockchip,rk3576-dsmc
|
v
rk3576_dsmc_platform_init()
RK3576回调通过GRF把对应GPIO切换到DSMC复用功能。不同芯片初始化通过complitable区分
结果:驱动知道使用哪个模式、CS、region、何种位宽和时序并且完成SOC侧引脚复用
获取复位和时钟资源
复位
dsmc->areset = devm_reset_control_get(dev, "dsmc");
dsmc->preset = devm_reset_control_get(dev, "apb");
对应设备树:
resets = <&cru SRST_A_DSMC>, <&cru SRST_P_DSMC>;
reset-names = "dsmc", "apb";
areset 用于 DSMC 主体,preset 用于 APB 寄存器接口。
获取时钟
dsmc->clk_sys = devm_clk_get(dev, "clk_sys");
dsmc->aclk = devm_clk_get(dev, "aclk_dsmc");
dsmc->pclk = devm_clk_get(dev, "pclk");
dsmc->aclk_root = devm_clk_get(dev, "aclk_root");
对应设备树中的clock-names:
clock-names = "clk_sys", "aclk_dsmc", "pclk", "aclk_root";
名称必须完全匹配,否则 devm_clk_get() 返回错误,probe 终止。
这一部分只是取得资源句柄,还没有打开时钟。
启用时钟并设置频率
驱动依次启用:
clk_prepare_enable(dsmc->aclk_root);
clk_prepare_enable(dsmc->aclk);
clk_prepare_enable(dsmc->pclk);
clk_prepare_enable(dsmc->clk_sys);
然后设置:
clk_set_rate(dsmc->aclk_root, dsmc->cfg.freq_hz);
clk_set_rate(dsmc->clk_sys, dsmc->cfg.ctrl_freq_hz);
当前目标:
aclk_root = 25 MHz
clk_sys = 50 MHz
每次启用失败都会跳转到对应标签,按相反顺序关闭已经打开的时钟。
这一部分的结果:DSMC 相关总线和控制器时钟已经运行,寄存器访问和硬件初始化具备时钟条件。
申请DMA并且安装操作接口
申请DMA
ret = rockchip_dsmc_dma_request(dev, dsmc);
if (ret)
goto err_dis_all_clk;
对应设备树
dmas = <&dmac0 30>, <&dmac0 31>;
dma-names = "req0", "req1";
驱动通过 DMA Engine API 获取 DSMC 的请求通道。DMA 用于在 DDR 与 DSMC Local Bus/PSRAM 映射空间之间搬运数据。
保存设备和操作接口
dsmc->dev = dev;
priv->ops = &rockchip_dsmc_ops;
rockchip_dsmc_ops 提供:
read
write
copy_from
copy_from_state
copy_to
copy_to_state
上层 Local Bus client 或测试代码不需要直接操作 DSMC 寄存器,可以通过这些接口访问。
CPU 与 DMA 的地址关系:
CPU 使用虚拟地址访问缓冲区
DMA 使用 dma_addr_t/总线地址搬运
DSMC 把传输转换为外部总线时序
这一部分的结果:DSMC 拥有 DMA 通道,并向上层提供统一读写接口。
硬件初始化、映射、DLL训练与检查
dsmc_init()
if(dsmc_init(dsmc)){
dev_err(dev, "DSMC init fail!\n");
goto err_release_dma;
}
主要流程:
dsmc_data_init()
-> 设置默认 burst、DMA 和 CS 参数
若为 PSRAM
-> 检测器件类型、协议、位宽和容量
复位 DSMC
遍历 CS0..CS3
|- PSRAM -> rockchip_dsmc_psram_reinit()
`- LB -> rockchip_dsmc_lb_init()
真正的 DSMC 寄存器配置主要在 dsmc-controller.c 中完成。
dsmc_mem_remap()
if (dsmc_mem_remap(dev, dsmc)) {
dev_err(dev, "DSMC memory remap fail!\n");
goto err_release_dma;
}
它根据设备树 rockchip,ranges 和已经启用的 CS/region,建立 CPU 可访问的映射,并把结果保存在:
dsmc->cs_map[cs].region_map[region]
|- phys
|- virt
`- size
因此后续可以按以下方式定位目标:
CS + region + offset
|
v
region_map[region].virt + offset
DLL训练
if (rockchip_dsmc_dll_training(priv)) {
dev_err(dev, "DSMC dll training fail!\n");
goto err_release_dma;
}
DLL 训练用于校准 DQS 数据采样时序。如果频率、走线、DQS DLL 初值或外部设备不匹配,训练可能失败。
状态检查
if (rockchip_dsmc_status_check(priv)) {
dev_err(dev, "DSMC status error, please check hardware matched(io, slave etc.)\n");
goto err_release_dma;
}
这是 probe 最后的硬件一致性检查,重点确认控制器状态以及 IO、外部从设备是否匹配。
全部成功后:
return 0;
这表示驱动 probe 成功。
第八部分:错误回滚
错误处理标签:
err_release_dma:
dma_release_channel(...);
err_dis_all_clk:
clk_disable_unprepare(dsmc->clk_sys);
err_dis_pclk:
clk_disable_unprepare(dsmc->pclk);
err_dis_aclk:
clk_disable_unprepare(dsmc->aclk);
err_dis_aclk_root:
clk_disable_unprepare(dsmc->aclk_root);
out:
return ret;
原则是:初始化按正序申请,失败按反序释放。
申请顺序:aclk_root -> aclk -> pclk -> clk_sys -> DMA
释放顺序:DMA -> clk_sys -> pclk -> aclk -> aclk_root
这样可以避免时钟引用泄漏、DMA 通道泄漏,以及半初始化状态继续运行。
使用 devm_* 取得的内存、MMIO、reset 和 clock 句柄由设备管理;但已经执行的 clk_prepare_enable() 和普通 DMA channel 申请仍需驱动显式回滚。
probe成功条件
设备树节点 status = "okay"
compatible 能匹配当前 SoC
reg 和 GRF 正确
clock-frequency 存在
PSRAM/Local Bus 恰好选择一种模式
至少启用一个有效 CS
Local Bus 至少启用有效 region
时钟和复位资源名称正确
DMA 通道可申请
外部硬件连接、位宽和时序匹配
DLL 训练和状态检查通过
rk_dsmc_probe() 获取设备树资源
总览
找到控制器的slave子节点
↓
读取每个CS的DQS DLL初值
↓
读取DSMC数据窗口基址和单窗口大小
↓
通过phandle找到slave设备模板
↓
读取时钟模式
↓
扫描psram0~3
↓
扫描lb-slave0~3
├── 读取LocalBus时序
├── 读取中断编号
├── 解析region0~3
└── 读取总线宽度
↓
检查PSRAM和LocalBus是否互斥
↓
dsmc_reg_remap()计算地址窗口
↓
释放device_node引用并返回
局部变量
uint32_t io_width_val;
uint32_t cs;
uint32_t psram = 0, lb_slave = 0;
uint64_t mem_ranges[2];
uint32_t dqs_dll[2 * DSMC_MAX_SLAVE_NUM];
uint32_t mtr_timing[8];
| 变量 | 含义 |
|---|---|
cs | 当前扫描的CS编号,范围0~3 |
psram | 是否至少启用了一个PSRAM节点 |
lb_slave | 是否至少启用了一个LocalBus节点 |
mem_ranges[0] | DSMC数据窗口物理基地址 |
mem_ranges[1] | 单个分配窗口的大小 |
dqs_dll[] | 每个CS的两个DQS byte lane DLL初值 |
mtr_timing[8] | LocalBus读写时序临时数组 |
| DLL数组对应关系: |
dqs_dll[0], dqs_dll[1] -> CS0 byte0/byte1
dqs_dll[2], dqs_dll[3] -> CS1 byte0/byte1
dqs_dll[4], dqs_dll[5] -> CS2 byte0/byte1
dqs_dll[6], dqs_dll[7] -> CS3 byte0/byte1
寻找slave节点
slave_np = of_get_child_by_name(np, "slave");
if (!slave_np) {
dev_err(dev, "Failed to find slave node\n");
return -ENODEV;
}
寻找
slave {
rockchip,dqs-dll = <...>;
rockchip,ranges = <...>;
rockchip,slave-dev = <&dsmc_slave>;
};
解析DQS DLL
DLL: LL用于调整DSMC读取数据时的采样时刻。
外部PSRAM或FPGA把数据发送给RK3576后,DSMC不能随便找一个时间读取,必须在数据稳定的时间窗 口内采样。
数据变化 数据稳定区域 数据变化 ──────┤██████████████████████████├────── 太早 太晚 ↑ 合适采样点
DLL通过增加不同程度的延迟,把采样点向前或向后移动:
DLL值较小 → 较早采样 DLL值增大 → 采样点逐渐后移
ret = of_property_read_u32_array(slave_np, "rockchip,dqs-dll", dqs_dll,ARRAY_SIZE(dqs_dll));
驱动要求读8个32位值
2 * DSMC_MAX_SLAVE_NUM = 2 * 4 = 8
接着按CS保存
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
cfg->cs_cfg[cs].dll_num[0] = dqs_dll[2 * cs];
cfg->cs_cfg[cs].dll_num[1] = dqs_dll[2 * cs + 1];
}
转换结果:
DTS rockchip,dqs-dll
↓
临时数组dqs_dll[8]
↓
cfg->cs_cfg[cs].dll_num[byte]
↓
后续rockchip_dsmc_ctrller_init()
↓
DSMC_RDS_DLL寄存器
这些值是初始采样延迟。后续DLL训练还可能扫描并更新它们。
读取地址窗口
ret = of_property_read_u64_array(slave_np, "rockchip,ranges",
mem_ranges, ARRAY_SIZE(mem_ranges));
解析得到
mem_ranges[0] = 数据窗口物理基地址
mem_ranges[1] = 每个分配单元的长度
这两个值暂时保存在栈数组中,函数末尾传给:
dsmc_reg_remap(dev, cfg, dsmc, mem_ranges, dqs_dll);
注意:这里的“remap”不仅是 ioremap,还负责计算每个CS和region的物理地址。
phandle找到设备配置
驱动通过:
dsmc_slave_np = of_parse_phandle(slave_np, "rockchip,slave-dev", 0);
获得dsmc_slave对应的device_node,然后读取它的属性。 用完需要释放引用:
of_node_put(dsmc_slave_np);
phandle类似设备树节点之间的“指针”。 • phandle用于从当前设备树节点跳转到任意被引用的节点,然后读取该节点及其子节点的属性。
读取时钟
if (of_property_read_u32(dsmc_slave_np, "rockchip,clk-mode", &cfg->clk_mode)) {
dev_err(dev, "Failed to get rockchip,clk-mode\n");
ret = -ENODEV;
goto release_dsmc_slave_node;
}
扫描PSRAM CS
psram_np = of_get_child_by_name(dsmc_slave_np, "psram");
if (psram_np) {
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
snprintf(slave_name, sizeof(slave_name), "psram%d", cs);
child_node = of_get_child_by_name(psram_np, slave_name);
if (!child_node)
continue;
if (!of_device_is_available(child_node)) {
of_node_put(child_node);
continue;
}
cfg->cs_cfg[cs].device_type = OPI_XCCELA_PSRAM;
psram = 1;
of_node_put(child_node);
}
}
扫描LocalBus CS
lb_slave_np = of_get_child_by_name(dsmc_slave_np, "lb-slave");
循环生成节点名:
lb-slave0 -> CS0
lb-slave1 -> CS1
lb-slave2 -> CS2
lb-slave3 -> CS3
节点存在且可用时:
cfg->cs_cfg[cs].device_type = DSMC_LB_DEVICE;
lb_slave = 1;
这行决定后续 dsmc_init()进入LocalBus分支:
if (cs_cfg->device_type == OPI_XCCELA_PSRAM)
rockchip_dsmc_psram_reinit(dsmc, cs);
else
rockchip_dsmc_lb_init(dsmc, cs);
读取LocalBus时序
ret = of_property_read_u32_array(child_node, "rockchip, mtrtiming",mtr_timing, 8);
数组与结构体成员严格按位置对应:
| 数组下标 | 结构体成员 | 含义 |
|---|---|---|
[0] | rcshi | 读CS高电平间隔相关时序 |
[1] | wcshi | 写CS高电平间隔相关时序 |
[2] | rcss | 读CS建立相关时序 |
[3] | wcss | 写CS建立相关时序 |
[4] | rcsh | 读CS保持相关时序 |
[5] | wcsh | 写CS保持相关时序 |
[6] | rd_latency | 读延迟周期 |
[7] | wr_latency | 写延迟周期 |
| 数据流: |
rockchip,mtr-timing
↓
mtr_timing[8]
↓
cs_cfg[cs]的8个时序成员
↓
dsmc_ctrller_cfg_for_lb()
↓
MTR_CFG(...)
↓
DSMC_MTR(cs)寄存器
这里数组顺序就是设备树ABI的一部分,顺序写错不会由编译器发现,却会导致硬件时序错误。
读取中断选择
ret = of_property_read_u32(child_node,
"rockchip,int-en",
&cfg->cs_cfg[cs].int_en);
if (ret) {
cfg->cs_cfg[cs].int_en = cs;
}
属性缺失不会使probe失败,而是使用当前CS编号作为默认值。
后续用途:
cs_cfg[cs].int_en
├── DSMC_INT_STATUS位选择
├── DSMC_INT_EN位选择
└── DMA_REQ_MUX请求映射
它不是Linux IRQ号,而是DSMC控制器内部的中断/请求选择值。
解析region
dsmc_parse_dt_regions(pdev, child_node, &cfg->cs_cfg[cs]);
传入参数:
| 参数 | 当前含义 |
|---|---|
pdev | 用于获取dev并输出设备日志 |
child_node | 当前启用的lb-slaveX节点 |
&cfg->cs_cfg[cs] | 当前CS配置的保存位置 |
结果写入:
cfg->cs_cfg[cs].slv_rgn[0..3]
该函数将在单元单独拆解。
读取LocalBus位宽
ret = of_property_read_u32(lb_slave_np,
"rockchip,io-width",
&io_width_val);
注意属性是从公共父节点 lb_slave_np读取,不是从某个 lb-slaveX读取。
转换关系:
| DTS值 | 驱动枚举 |
|---|---|
16 | MCR_IOWIDTH_X16 |
8 | MCR_IOWIDTH_X8 |
| 缺失 | 默认MCR_IOWIDTH_X16 |
| 其他值 | 警告并默认MCR_IOWIDTH_X16 |
因此当前实现要求所有LocalBus CS共享公共位宽配置。
后续:
cs_cfg[cs].io_width
↓
rockchip_dsmc_ctrller_init()
↓
DSMC_MCR的IOWIDTH字段
PSRAM和LocalBus互斥检查
if (psram && lb_slave) {
ret = -ENODEV;
} else if (!(psram || lb_slave)) {
ret = -ENODEV;
}
结果表:
| PSRAM | LOCALBUS | 结果 |
|---|---|---|
| 0 | 0 | 失败:没有启用任何外设 |
| 1 | 0 | 成功:进入PSRAM模式 |
| 0 | 1 | 成功:进入LocalBus模式 |
| 1 | 1 | 失败:两种模式不能共存 |
这里的标志表示“是否存在至少一个启用节点”,不是设备数量。
所以可以同时开启多个PSRAM CS,也可以同时开启多个LocalBus CS,但不能让任意PSRAM CS与任意LocalBus CS混合启用。
dsmc_parse_dt_regions()
找到region容器
region_node = of_get_child_by_name(lb_np, "region");
设备树层级:
lb-slaveX ← lb_np
└── region ← region_node
├── region0
├── region1
├── region2
└── region3
遍历region0~3
for (i = 0; i < DSMC_LB_MAX_RGN; i++) {
rgn = &cfg->slv_rgn[i];
snprintf(region_name, sizeof(region_name), "region%d", i);
child_node = of_get_child_by_name(region_node, region_name);
...
}
计算最大region槽位数
rgn_num_max = 1;
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
if (cfg->cs_cfg[cs].device_type == DSMC_UNKNOWN_DEVICE)
continue;
if (cfg->cs_cfg[cs].rgn_num == 3)
cfg->cs_cfg[cs].rgn_num++;
rgn_num_max = max_t(uint32_t, rgn_num_max,
cfg->cs_cfg[cs].rgn_num);
}
特殊处理:启用region数量为3时改成4。
这是地址分区的硬件布局要求:3个有效region仍按4个槽位计算,避免不同CS窗口布局出现不符合硬件分区规则的跨度。
PSRAM地址公式
phys = mem_ranges[0] + cs * mem_ranges[1];
size = mem_ranges[1];
即:
PSRAM CS0 = base + 0 * unit
PSRAM CS1 = base + 1 * unit
PSRAM CS2 = base + 2 * unit
PSRAM CS3 = base + 3 * unit
PSRAM只使用:
cs_map[cs].region_map[0]
LocalBus地址公式
phys = mem_ranges[0]
+ rgn_num_max * mem_ranges[1] * cs
+ num * mem_ranges[1];
拆开理解:
base
- 当前CS之前的全部预留空间
- 当前CS中第num个已启用region的偏移
其中 num只在遇到启用region时增加。
例如只启用region0和region3:
region0 -> num=0 -> base + CS偏移 + 0*unit
region3 -> num=1 -> base + CS偏移 + 1*unit
因此:
region_map[3]保存的是region3的逻辑身份,但它的物理窗口可能紧跟region0,并不一定是base + 3 * unit。
这是本函数最需要注意的地方。
建立临时映射
region_map->virt = rk_dsmc_map_kernel(
region_map->phys,
region_map->size,
DSMC_MEM_ATTRIBUTE_NO_CACHE);
解析结束时先建立无缓存映射,供后续控制器初始化访问外部设备公共配置区、PSRAM寄存器或进行检测。
probe()后面执行 dsmc_mem_remap()时,还会根据region类型重新选择缓存属性:
Register region -> 无缓存
其他LB region -> 可缓存
PSRAM -> 可缓存
所以 dsmc_reg_remap()是初始映射,dsmc_mem_remap()是硬件初始化后的最终映射。
更新容量
cfg->cap = max_t(uint32_t, cfg->cap, region_map->size);
...
cfg->cap *= rgn_num_max;
这里的 cap用于描述控制器地址布局所需的最大容量/跨度,并在后续写入设备容量相关配置。它不是简单统计所有启用region实际容量之和。
错误清理路径
函数末尾:
release_dsmc_slave_node:
of_node_put(psram_np);
of_node_put(lb_slave_np);
of_node_put(dsmc_slave_np);
release_slave_node:
of_node_put(slave_np);
return ret;
goto的意义是让不同失败位置进入共同清理路径:
较晚阶段失败
↓
释放psram/lb-slave/dsmc_slave引用
↓
释放slave引用
↓
返回错误
这里释放的是 `device_node`引用,不是删除设备树节点,也不是关闭硬件。
dsmc_init()
static int dsmc_init(struct rockchip_dsmc *dsmc);
参数
| 参数 | 内容 |
|---|---|
dsmc | DSMC Host控制器的完整私有结构 |
dsmc->cfg | 设备树解析结果及运行时配置 |
dsmc->regs | DSMC控制器寄存器虚拟基址 |
dsmc->cs_map | PSRAM或LocalBus窗口映射 |
dsmc->areset/preset | 控制器和APB复位句柄 |
dsmc_data_init(dsmc);
dsmc_data_init(dsmc);
遍历有效CS
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
if (cfg->cs_cfg[cs].device_type == DSMC_UNKNOWN_DEVICE)
continue;
if (cfg->cs_cfg[cs].device_type == OPI_XCCELA_PSRAM) {
ret = rockchip_dsmc_device_dectect(dsmc, cs);
if (ret)
return ret;
}
}
只有设备树中启用的PSRAM或LocalBus CS会被初始化。
PSRAM设备检测
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
if (cfg->cs_cfg[cs].device_type == OPI_XCCELA_PSRAM) {
ret = rockchip_dsmc_device_dectect(dsmc, cs);
if (ret)
return ret;
}
}
检查多CS位宽
ret = dsmc_check_mult_psram_cap(dsmc);
if (io_width == 0xffffffff)
io_width = cfg->cs_cfg[cs].io_width;
else if (io_width != cfg->cs_cfg[cs].io_width)
return -1;
作用:保证所有启用CS具有相同I/O位宽
rockchip_dsmc_ctrller_init() (dsmc_controller.c)
公共控制器初始化:
- 硬件属性寄存器写入初始化
开启读写合并
writel(MRGTCR_READ_WRITE_MERGE_EN, dsmc->regs + DSMC_MRGTCR(CS));
向当前CS的Merge Control Register写入合并并使能
控制侧的事务合并能力与Merge FIFO无光
配置DQS DLL初始值
writel(sample_select | cfg->dll_num[0], DSMC_RDS_DLL0_CTL(cs));
writel(sample_select | cfg->dll_num[1], DSMC_RDS_DLL1_CTL(cs));
设备树属性:
DTS rockchip,dqs-dll
-> dsmc_parse_dt()
-> cs_cfg[cs].dll_num[0/1]
-> RDS_DLL0_CTL / RDS_DLL1_CTL
配置I/O带宽
dsmc_lb_cmn_config():
for (i = 0; i < DSMC_MAX_SLAVE_NUM; i++)
REG_CLRSETBITS(dsmc, DSMC_MCR(cs),
MCR_IOWIDTH_MASK << MCR_IOWIDTH_SHIFT,
cfg->io_width << MCR_IOWIDTH_SHIFT);
保证不同CS使用相同带宽
配置MCR Memory Control Register(存储器控制寄存器)
REG_CLRSETBITS(dsmc, DSMC_MCR(cs),
(MCR_ACS_MASK << MCR_ACS_SHIFT) |
(MCR_DEVTYPE_MASK << MCR_DEVTYPE_SHIFT) |
(MCR_EXCLUSIVE_DQS_MASK << MCR_EXCLUSIVE_DQS_SHIFT) |
(MCR_WRAPSIZE_MASK << MCR_WRAPSIZE_SHIFT) |
(MCR_MAXEN_MASK << MCR_MAXEN_SHIFT) |
(MCR_MAXLEN_MASK << MCR_MAXLEN_SHIFT),
(cfg->acs << MCR_ACS_SHIFT) |
(MCR_DEVTYPE_HYPERRAM << MCR_DEVTYPE_SHIFT) |
(cfg->exclusive_dqs << MCR_EXCLUSIVE_DQS_SHIFT) |
(cfg->wrap_size << MCR_WRAPSIZE_SHIFT) |
(cfg->max_length_en << MCR_MAXEN_SHIFT) |
(cfg->max_length << MCR_MAXLEN_SHIFT));
| MCR字段 | 配置来源 | 作用 |
|---|---|---|
ACS | cfg->acs | 地址/CS控制相关模式 |
DEVTYPE | 固定HyperRAM编码 | 选择控制器设备接口类型 |
EXCLUSIVE_DQS | cfg->exclusive_dqs | DQS工作方式 |
WRAPSIZE | cfg->wrap_size | burst wrap长度 |
MAXEN | cfg->max_length_en | 是否限制最大连续长度 |
MAXLEN | cfg->max_length | 最大连续传输长度 |
配置WRAP2INCR
writel(cfg->wrap2incr_en, dsmc->regs + DSMC_WRAP2INCR(cs));
LocalBus默认开启wrap到increment转换,PSRAM默认关闭。
- Burst 一次请求连续传输多个数据
- Increment Burst 每次传输后地址继续递增
- Wrap Burst 地址到达边界后回绕到块起始地址
- WRAP2INCR 将 Wrap Burst 转换为 Increment Burst
- WAP2INCR 通常是 WRAP2INCR 的缩写或拼写遗漏
配置VDMC
Variable-latency Device Mode Control Register: 可变延迟设备模式控制寄存器
REG_CLRSETBITS(dsmc, DSMC_VDMC(cs),
(VDMC_LATENCY_FIXED_MASK << VDMC_LATENCY_FIXED_SHIFT) |
(VDMC_PROTOCOL_MASK << VDMC_PROTOCOL_SHIFT),
(VDMC_LATENCY_VARIABLE << VDMC_LATENCY_FIXED_SHIFT) |
(cfg->device_type << VDMC_PROTOCOL_SHIFT));
作用:设置可变延迟模式和当前设备协议/类型编码。
这里使用的是驱动内部的 device_type枚举,必须确保枚举值与VDMC寄存器字段定义匹配。
rockchip_dsmc_lb_init()
LocalBus 初始化总入口
int rockchip_dsmc_lb_init(struct rockchip_dsmc *dsmc, uint32_t cs)
{
dsmc_ctrller_cfg_for_lb(dsmc, cs);
ret = dsmc_lb_cmn_config(dsmc, cs);
if (ret)
return ret;
ret = dsmc_lb_csr_config(dsmc, cs);
return ret;
}
三个函数分别配置三个层次
| 函数 | 配置对象 |
|---|---|
dsmc_ctrller_cfg_for_lb() | RK3576内部DSMC Host控制器寄存器 |
dsmc_lb_cmn_config() | 通过DSMC窗口访问外部LocalBus Slave公共配置寄存器 |
dsmc_lb_csr_config() | 外部Slave的中断/CSR寄存器 |
| 内外部寄存器空间 |
dsmc->regs + offset
-> RK3576内部DSMC控制器寄存器
region_map->virt + offset
-> 经过DSMC总线访问外部Slave地址空间
dsmc_ctrller_cfg_for_lb()
RK3576内部DSMC Host控制器寄存器
时钟模式
writel(dsmc->cfg.clk_mode,
dsmc->regs + DSMC_CLK_MD);
设备树属性:
DTS rockchip,clk-mode
-> cfg->clk_mode
-> DSMC_CLK_MD
LocalBus 时序
writel(MTR_CFG(cfg->rcshi, cfg->wcshi,
cfg->rcss, cfg->wcss,
cfg->rcsh, cfg->wcsh,
calc_ltcy_value(cfg->rd_latency),
calc_ltcy_value(cfg->wr_latency)),
dsmc->regs + DSMC_MTR(cs));
MTR_CFG把8个独立配置压入一个MTR寄存器值。
calc_ltcy_value()把人类使用的延迟周期转换为硬件字段编码:
if (latency >= 5 && latency <= 10)
return latency - 5;
else
return latency + 0xb;
所以DTS值不一定等于最终寄存器字段值。
region分区
writel(cfg->rgn_num / 2, dsmc->regs + DSMC_SLV_RGN_DIV(cs));
控制器根据启用region数量配置Slave地址窗口分区。前一阶段中3个region会修正为4,也是为了配合该硬件分区规则。
AXI读错策略
REG_CLRSETBITS(dsmc, DSMC_AXICTL,
AXICTL_RD_NO_ERR_MASK,
AXICTL_RD_NO_ERR_ENABLE);
驱动设置AXI读取时不向上游响应错误。这可能避免外部设备探测过程触发系统级总线异常,但也会减弱错误可见性,调试时需要同时查看DSMC状态寄存器。
写region属性寄存器
for (i = 0; i < DSMC_LB_MAX_RGN; i++) {
slv_rgn = &cfg->slv_rgn[i];
if (!slv_rgn->status)
continue;
if (slv_rgn->dummy_clk_num == 1)
value = (0x1 << RGNX_ATTR_DUM_CLK_EN_SHIFT) |
(0x1 << RGNX_ATTR_DUM_CLK_NUM_SHIFT);
else if (slv_rgn->dummy_clk_num == 0)
value = (0x1 << RGNX_ATTR_DUM_CLK_EN_SHIFT) |
(0x0 << RGNX_ATTR_DUM_CLK_NUM_SHIFT);
else
value = 0x0 << RGNX_ATTR_DUM_CLK_EN_SHIFT;
writel((slv_rgn->attribute << RGNX_ATTR_SHIFT) |
(slv_rgn->cs0_ctrl << RGNX_ATTR_CTRL_SHIFT) |
(slv_rgn->cs0_be_ctrled <<
RGNX_ATTR_BE_CTRLED_SHIFT) | value |
(RGNX_ATTR_32BIT_ADDR_WIDTH <<
RGNX_ATTR_ADDR_WIDTH_SHIFT),
dsmc->regs + DSMC_RGN0_ATTR(cs) + 4 * i);
}
每个启用region写入:
| 字段 | 来源 |
|---|---|
| region属性 | slv_rgn->attribute |
| 控制方式 | slv_rgn->cs0_ctrl |
| BE控制 | slv_rgn->cs0_be_ctrled |
| dummy clock | slv_rgn->dummy_clk_num |
| 地址宽度 | 此阶段先写32-bit编码 |
中断和DMA请求映射
REG_CLRSETBITS(dsmc, DSMC_INT_STATUS,
INT_STATUS_MASK(cfg->int_en),
INT_STATUS(cfg->int_en));
REG_CLRSETBITS(dsmc, DSMC_INT_EN,
INT_EN_MASK(cfg->int_en),
INT_EN(cfg->int_en));
先清除对应状态,再使能对应中断。
if (dsmc->cfg.dma_req_mux_offset && cs < 2)
REG_CLRSETBITS(dsmc, dma_req_mux_offset,
DMA_REQ_MUX_MASK(cs),
DMA_REQ_MUX(cs, cfg->int_en));
只有CS0和CS1进入该DMA请求复用配置。CS2和CS3是否支持同样的硬件DMA请求,需要结合芯片手册确认,不能仅靠设备树启用推断。
DSMC发出DMA请求 → DMA控制器搬运数据 → DMA完成并产生中断 → DMAengine调用回调函数 → 回调清除忙状态、关闭DSMC DMA、恢复DSMC中断
dsmc_lb_cmn_config()配置外部Slave
这个函数不是只写Host控制器。先临时切换到外部Slave的配置空间,再通过region_map_virt发起总线访问
保存所有MCR
tmp[i] = readl(dsmc->regs + DSMC_MCR(i));
保存原状态是为了配置完成后恢复正常memory space访问模式。
临时切换到CR space和x8
for (i = 0; i < DSMC_MAX_SLAVE_NUM; i++) {
tmp[i] = readl(dsmc->regs + DSMC_MCR(i));
/* config to CR space */
REG_CLRSETBITS(dsmc, DSMC_MCR(i),
(MCR_IOWIDTH_MASK << MCR_IOWIDTH_SHIFT) |
(MCR_CRT_MASK << MCR_CRT_SHIFT),
(MCR_IOWIDTH_X8 << MCR_IOWIDTH_SHIFT) |
(MCR_CRT_CR_SPACE << MCR_CRT_SHIFT));
}
配置阶段使用外部Slave定义的控制寄存器访问方式。这里循环使用 DSMC_MCR(i),确实逐个切换所有CS。
遍历启用region
for (i = 0; i < DSMC_LB_MAX_RGN; i++) {
slv_rgn = &cfg->slv_rgn[i];
if (!slv_rgn->status)
continue;
ret = dsmc_slv_cmn_rgn_config(dsmc, slv_rgn, i, cs);
if (ret)
break;
ret = dsmc_slv_cmn_config(dsmc, slv_rgn, i, cs);
if (ret)
break;
}
遍历Region0~3 → 检查是否启用 → 配置Region属性 → 配置Region通信参数 → 失败则停止
恢复正常 memory space
for (i = 0; i < DSMC_MAX_SLAVE_NUM; i++)
/* config to memory space */
writel(tmp[i], dsmc->regs + DSMC_MCR(i));
更新最终CA地址宽度
恢复后再次修改每个启用region的Host属性寄存器,使其地址宽度与DTS解析结果一致。
› for (i = 0; i < DSMC_LB_MAX_RGN; i++) {
slv_rgn = &cfg->slv_rgn[i];
if (!slv_rgn->status)
continue;
REG_CLRSETBITS(dsmc, DSMC_RGN0_ATTR(cs) + 4 * i,
RGNX_ATTR_CA_ADDR_MASK << RGNX_ATTR_ADDR_WIDTH_SHIFT,
slv_rgn->ca_addr_width << RGNX_ATTR_ADDR_WIDTH_SHIFT);
}
dsmc_slv_cmn_rgn_config()
配置 LocalBus Slave端某个 Region 的公共时序寄存器。
region_map = &dsmc->cs_map[cs].region_map[rgn];
tmp = lb_read_cmn(region_map, RGN_CMN_CON(rgn, 0));
读写
static inline void lb_write_cmn(struct dsmc_map *map,
uint32_t cmn_reg, uint32_t val)
{
writel(val, map->virt + cmn_reg);
}
static inline uint32_t lb_read_cmn(struct dsmc_map *map, uint32_t cmn_reg)
{
return readl(map->virt + cmn_reg);
}
该函数配置:
| 外部字段 | 来源 |
|---|---|
| 写数据扩展周期 | dummy_clk_num |
| 读延迟周期 | cfg->rd_latency |
| 写延迟周期 | cfg->wr_latency |
| CA周期/地址宽度 | slv_rgn->ca_addr_width |
代码只接受 rd_latency和 wr_latency为1或2;其他值直接返回失败。这是LocalBus当前实现的实际限制。
dsmc_slv_cmn_config()
该函数配置外部Slave公共寄存器,而不是单独某个region的全部属性。
数据位宽
tmp = lb_read_cmn(region_map, CMN_CON(3));
tmp |= 0x1 << RDYN_GEN_CTRL_SHIFT;
tmp &= ~(DATA_WIDTH_MASK << DATA_WIDTH_SHIFT);
tmp |= cfg->io_width << DATA_WIDTH_SHIFT;
lb_write_cmn(region_map, CMN_CON(3), tmp);
把Host侧选择的x8/x16位宽同步到外部Slave公共配置。
dummy clock和CA地址周期
lb_write_cmn(region_map, CMN_CON(0), tmp);
通过 CMN_CON(0)配置写数据扩展周期和CA地址周期。
这说明LocalBus能正常通信要求两端参数配对:
Host DSMC region属性/时序
必须匹配
外部Slave CMN/RGN配置
只在Host设备树中启用模式,但外部FPGA逻辑没有对应实现或参数不一致,仍然无法通信。
dsmc_lb_csr_config()中断配置CSR
找到第一个启用的region
for (i = 0; i < DSMC_LB_MAX_RGN; i++) {
if (slv_rgn->status) {
region_map = &dsmc->cs_map[cs].region_map[i];
break;
}
}
如果一个region都未启用,返回 -ENODEV。
临时把访问解释称Register
函数保存当前MCR和region属性,然后临时配置:
把 Region0 临时切换成 Register 类型访问窗口:
REG_CLRSETBITS(dsmc, DSMC_MCR(cs),
MCR_CRT_MASK << MCR_CRT_SHIFT,
MCR_CRT_MEM_SPACE << MCR_CRT_SHIFT);
REG_CLRSETBITS(dsmc, DSMC_RGN0_ATTR(cs),
RGNX_ATTR_MASK << RGNX_ATTR_SHIFT,
RGNX_ATTR_REG << RGNX_ATTR_SHIFT);
切换
mcr_tmp = readl(dsmc->regs + DSMC_MCR(cs));
rgn_attr_tmp = readl(dsmc->regs + DSMC_RGN0_ATTR(cs));
目的是通过选中的窗口访问外部Slave CSR寄存器。
配置S2H中断
/* enable all s2h interrupt */
writel(0xffffffff, region_map->virt + LBC_S2H_INT_STA_EN);
writel(0xffffffff, region_map->virt + LBC_S2H_INT_STA_SIG_EN);
/* clear all s2h interrupt */
writel(LBC_S2H_INT_STA_MASK << LBC_S2H_INT_STA_SHIFT,
region_map->virt + LBC_S2H_INT_STA);
含义:
使能Slave-to-Host中断状态
-> 使能中断信号输出
-> 清除历史状态
恢复原配置
writel(mcr_tmp, dsmc->regs + DSMC_MCR(cs));
writel(rgn_attr_tmp, dsmc->regs + DSMC_RGN0_ATTR(cs));
临时访问完成后恢复正常region模式。
值得检查:函数选择的是“第一个启用region”的 region_map,但保存和修改的是 DSMC_RGN0_ATTR(cs)。当region0未启用、首个启用region不是0时,需要结合硬件CSR寻址规则确认该做法是否仍然正确。
映射关系
| 阶段 | 函数 | 建立的关系 |
|---|---|---|
| 地址计算和初始映射 | dsmc_reg_remap() | DSMC物理窗口 → 内核临时虚拟地址 |
| 最终内核映射 | dsmc_mem_remap() | DSMC物理窗口 → 指定缓存属性的内核虚拟地址 |
| 用户空间映射 | dsmc_mmap() | DSMC物理页 → 当前进程虚拟地址 |
dsmc_map结构
dsmc_host.h:
struct dsmc_map {
void *virt;
phys_addr_t phys;
size_t size;
};
| 成员 | 含义 | 谁写入 |
|---|---|---|
phys | DSMC窗口物理起始地址 | dsmc_reg_remap() |
size | 窗口长度 | dsmc_reg_remap() |
virt | 内核映射后的虚拟地址 | dsmc_reg_remap()、dsmc_mem_remap() |
| 每个CS有4个逻辑region槽: |
struct dsmc_cs_map {
struct dsmc_map region_map[DSMC_LB_MAX_RGN];
};
访问方式:
dsmc->cs_map[cs].region_map[region]
例如:
dsmc->cs_map[0].region_map[3]
表示CS0的逻辑region3,并不保证它位于物理地址的第4个连续窗口;物理布局在第06章已经由 dsmc_reg_remap()计算完成。
rk_dsmc_map_kernel()
static void *rk_dsmc_map_kernel(phys_addr_t start,
size_t len,
uint32_t mem_attr)
{
void *vaddr;
if (mem_attr == DSMC_MEM_ATTRIBUTE_CACHE)
vaddr = ioremap_cache(start, len);
else if (mem_attr == DSMC_MEM_ATTRIBUTE_WR_COM)
vaddr = ioremap_wc(start, len);
else
vaddr = ioremap(start, len);
return vaddr;
}
参数
| 参数 | 含义 |
|---|---|
start | DSMC窗口物理起始地址 |
len | 映射长度 |
mem_attr | 驱动定义的缓存属性 |
选择关系
| 驱动属性 | 内核API | 典型语义 |
|---|---|---|
DSMC_MEM_ATTRIBUTE_CACHE | ioremap_cache() | CPU可缓存访问 |
DSMC_MEM_ATTRIBUTE_WR_COM | ioremap_wc() | 写合并,适合连续写 |
| 其他/NO_CACHE | ioremap() | 设备型或非缓存访问 |
底层逻辑
ioremap*()建立页表映射,并给页表项设置不同ARM64内存属性。缓存属性影响:
- CPU是否可以把读数据保存在cache中。
- 连续写是否可以合并。
- 访问顺序和外部设备可见时机。
- 是否需要显式cache clean/invalidate。
它不会改变CS和region硬件配置,只改变CPU访问该物理窗口的方式。
rk_dsmc_unmap_kernel()
static void rk_dsmc_unmap_kernel(void *vaddr)
{
if (vaddr != NULL)
iounmap(vaddr);
}
iounmap()删除内核虚拟地址对应的页表映射。它不会:
- 释放外部PSRAM。
- 删除FPGA中的region。
- 改变DSMC物理窗口。
- 关闭DSMC控制器。
取消映射后,旧 vaddr不能继续解引用。
dsmc_mem_remap()
static int dsmc_mem_remap(struct device *dev,
struct rockchip_dsmc *dsmc)
输入:
| 数据 | 来源 |
|---|---|
cfg->cs_cfg[cs].device_type | 设备树解析 |
slv_rgn[i].status | region的DTS status |
slv_rgn[i].attribute | Merged FIFO等region类型 |
region_map->phys/size | dsmc_reg_remap() |
region_map->virt | 初始化阶段的临时映射 |
| 输出: |
- 为有效region建立最终内核映射。
- LocalBus模式创建字符设备。
- 失败时返回非0,
probe()进入DMA和时钟回滚。
遍历有效CS
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
if (cfg->cs_cfg[cs].device_type == DSMC_UNKNOWN_DEVICE)
continue;
if (cfg->cs_cfg[cs].rgn_num == 3)
cfg->cs_cfg[cs].rgn_num++;
rgn_num_max = max_t(uint32_t, rgn_num_max,cfg>cs_cfg[cs].rgn_num);
}
只处理设备树启用的CS。
取消初始映射
region_map = &dsmc->cs_map[cs].region_map[0];
if (region_map->virt) {
rk_dsmc_unmap_kernel(region_map->virt);
region_map->virt = NULL;
}
代码只取消 region_map[0]的初始映射。
对于PSRAM没有问题,因为PSRAM只使用region0。对于LocalBus,如果同时启用了region0和region3,dsmc_reg_remap()此前为两者都建立过临时映射;这里却只取消region0。
随后region3的 virt会被新映射覆盖,原映射地址丢失,形成内核虚拟映射泄漏。这是当前实现值得修正的点:LocalBus应逐个取消所有已存在的region临时映射。
LocalBus 最终映射
for (i = 0; i < DSMC_LB_MAX_RGN; i++) {
region_map = &dsmc->cs_map[cs].region_map[i];
slv_rgn = &cfg->cs_cfg[cs].slv_rgn[i];
if (!slv_rgn->status)
continue;
if (slv_rgn->attribute == RGNX_ATTR_REG)
mem_attr = DSMC_MEM_ATTRIBUTE_NO_CACHE;
else
mem_attr = DSMC_MEM_ATTRIBUTE_CACHE;
region_map->virt = rk_dsmc_map_kernel(...);
}
PSRAM最终映射
region_map = &dsmc->cs_map[cs].region_map[0];
region_map->virt = rk_dsmc_map_kernel(
region_map->phys,
region_map->size,
DSMC_MEM_ATTRIBUTE_CACHE);
PSRAM作为数据内存使用可缓存映射。后续CPU测试和DLL训练可能通过该地址读写。
缓存映射意味着DMA与CPU共享数据时必须正确维护cache一致性;第10章DMA数据通路会继续分析。
rockchip_dsmc_register_lb_device()
分配设备号
ret = alloc_chrdev_region(&dsmc_devt, 0,
DSMC_LB_MAX_RGN, "dsmc");
内核动态分配:
一个major号
- 从minor 0开始的4个minor号
设备号由:
dev_t = major + minor
共同唯一标识字符设备。
为4个region创建设备对象
for (j = 0; j < DSMC_LB_MAX_RGN; j++) {
device_create(...,
MKDEV(MAJOR(dsmc_devt), cs * 4 + j),
NULL, "dsmc/cs%d/region%d", cs, j);
}
minor编码规则: minor = cs * 4 + region
| 路径 | MINOR |
|---|---|
| CS0 region0 | 0 |
| CS0 region3 | 3 |
| CS1 region0 | 4 |
| CS2 region2 | 10 |
| CS3 region3 | 15 |
device_create()创建内核设备对象和uevent。用户空间的devtmpfs/udev据此创建 /dev/dsmc/csX/regionX节点。
绑定file_operations
cdev_init(&cs_info[cs].cdev[j], &dsmc_fops);
cdev_add(&cs_info[cs].cdev[j], devno, 1);
cdev_init()把字符设备与以下操作表绑定:
static const struct file_operations dsmc_fops = {
.owner = THIS_MODULE,
.open = dsmc_open,
.release = dsmc_release,
.mmap = dsmc_mmap,
};
cdev_add()把设备号加入内核字符设备映射表。用户打开对应设备号时,VFS就能找到 dsmc_fops。
禁用region也会创建设备节点
当前循环无条件为region0~3创建节点,没有检查 slv_rgn[j].status。
因此禁用region的 /dev节点也可能存在,但打开时 dsmc_open()会返回 -EINVAL。这在功能上可阻止访问,但用户看到的设备节点与实际可用region不完全一致。
从设备号反推CS和region
static inline int get_cs_index(struct inode *inode)
{
return iminor(inode) / DSMC_LB_MAX_RGN;
}
static inline int get_mem_region_index(struct inode *inode)
{
return iminor(inode) % DSMC_LB_MAX_RGN;
}
公式:
cs = minor / 4
region = minor % 4
例如minor 7:
cs = 7 / 4 = 1
region = 7 % 4 = 3
对应CS1 region3。
dsmc_open()
调用链:
用户open(path)
-> VFS查设备节点的dev_t
-> 字符设备表找到cdev
-> dsmc_fops.open
-> dsmc_open()
找到DSMC Host设备
dsmc_dev = rockchip_dsmc_find_device_by_compat( rockchip_dsmc_get_compat(0));
内部通过Platform总线按compatible查找DSMC设备,再用:
platform_get_drvdata(pdev)
取回 probe()保存的 struct rockchip_dsmc_device。
校验CS和region
if (cs_index >= DSMC_MAX_SLAVE_NUM)
return -EINVAL;
if (cfg->device_type == DSMC_UNKNOWN_DEVICE ||
!cfg->slv_rgn[mem_region_index].status)
return -EINVAL;
它确保:
- CS编号有效。
- 该CS确实配置了设备。
- 该region在设备树中启用。
保存当前文件对应的region
pfile->private_data =
&dsmc->cs_map[cs_index].region_map[mem_region_index];
后续 mmap()不再重新查找CS,而是直接使用这个 private_data。
inode minor
-> cs/region
-> region_map
-> file->private_data
每次 open()都会产生独立的 struct file,因此不同进程或不同设备节点可以保存各自的region指针。
dsmc_mmap()
参数
| 参数 | 含义 |
|---|---|
pfile | 当前打开的字符设备文件对象 |
vma | 用户进程要求创建的虚拟内存区 |
取回region
struct dsmc_map *region = pfile->private_data;
这个指针由 dsmc_open()提前保存。
设置VMA标志
Virtual Memory Area,虚拟内存区域。
vm_flags_set(vma, VM_PFNMAP | VM_DONTDUMP);
| 标志 | 作用 |
|---|---|
VM_PFNMAP | VMA映射的是指定PFN,不是普通匿名页 |
VM_DONTDUMP | core dump时不导出该硬件窗口 |
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
根据VMA标志生成页保护属性。
计算映射大小和PFN
vm_size = vma->vm_end - vma->vm_start;
pfn = __phys_to_pfn(region->phys);
PFN是物理页号:
pfn = region->phys / PAGE_SIZE
建立用户页表映射
remap_pfn_range(vma,
vma->vm_start,
pfn,
vm_size,
vma->vm_page_prot);
底层结果:
用户虚拟地址 [vm_start, vm_end)
-> 进程页表
-> DSMC物理页region->phys
-> DSMC控制器
-> 外部设备
此后用户程序读写映射地址,不再逐次进入驱动系统调用。CPU直接通过页表访问硬件窗口。
remove时的释放链
rk_dsmc_remove()调用释放逻辑,核心方向为:
遍历有效CS和region
-> iounmap(region_map->virt)
-> LocalBus调用rockchip_dsmc_unregister_lb_device()
-> 释放DMA通道
-> 关闭时钟
字符设备注销函数负责:
device_destroy()
-> 删除class设备和用户设备节点
cdev_del()
-> 从字符设备号映射表删除cdev
unregister_chrdev_region()
-> 释放设备号范围
DLL训练
DSMC接收外部设备返回的数据时,需要在合适的时间点采样。
外部设备输出数据
┌──────── 有效数据窗口 ────────┐
──────┤ ├──────
↑ ↑
太早采样 太晚采样
↑
理想采样点
板级走线、时钟频率、负载、PVT条件和外部器件响应都会改变有效窗口位置。DLL即Delay-Locked Loop相关延迟控制,驱动通过改变延迟tap来移动读采样点。
当前驱动扫描:
0x00, 0x01, 0x02, ... , 0xfe, 0xff
每个tap都进行写入和读回校验:
写入正确 + 读回正确 -> PASS
数据不匹配 -> FAIL
最终从最长PASS区间中选择中点。
调用链
rockchip_dsmc_dll_training(priv)
└── 遍历有效CS
└── 遍历byte lane
└── dsmc_dll_full_range_training(priv, cs, byte)
├── 扫描dll=0x00~0xff
│ └── dsmc_dll_training_method(..., dll)
│ ├── 选择测试region
│ ├── 设置当前DLL tap
│ ├── 写测试pattern
│ ├── cache clean/invalidate
│ └── 读回比较
├── 找最长连续PASS窗口
├── 计算窗口中点best_dll
├── 写入best_dll
├── 再次校验best_dll
└── 保存cfg->dll_num[byte]
rockchip_dsmc_dll_training
int rockchip_dsmc_dll_training(struct rockchip_dsmc_device *priv);
priv是 probe()分配的完整DSMC设备对象:
rockchip_dsmc_device
├── ops
│ ├── read
│ └── write
└── dsmc
├── cfg
├── cs_map
├── regs
└── dev
训练同时需要:
cfg判断设备类型和位宽。cs_map找到测试窗口。regs修改DLL寄存器。ops执行统一的PIO读写。
返回值:
| 返回值 | 含义 | |
|---|---|---|
0 | 所有有效CS和lane训练成功 | |
-ENODEV | 找不到可用region等设备配置错误 | |
-EINVAL | 没有有效DLL窗口或数据比较失败 | |
-EIO | 选出最佳值后复验失败 |
任意一个CS或lane失败都会终止整个 probe()。
遍历CS
for (cs = 0; cs < DSMC_MAX_SLAVE_NUM; cs++) {
cfg = &priv->dsmc.cfg.cs_cfg[cs];
if (cfg->device_type == DSMC_UNKNOWN_DEVICE)
continue;
...
}
只训练设备树启用的CS
UNKNOWN -> 跳过
LocalBus -> 训练
Xccela PSRAM -> 训练
Hyper PSRAM -> 训练
遍历byte lane
for (byte = MCR_IOWIDTH_X8; byte <= cfg->io_width; byte++) {
ret = dsmc_dll_full_range_training(priv, cs, byte);
if (ret) {
dev_err(dev, "DSMC: cs%d byte%d dll training failed\n", cs, byte);
return ret;
}
}
驱动枚举值
#define MCR_IOWIDTH_X8 0x0
#define MCR_IOWIDTH_X16 0x1
循环行为是:
| 总线位宽 | CFG->IO_WIDTH | 训练LANE |
|---|---|---|
| x8 | 0 | byte0 |
| x16 | 1 | byte0、byte1 |
| x16有两个8-bit byte lane,每条lane有独立的读采样DLL寄存器: |
byte0 -> DSMC_RDS_DLL_CTL(cs, 0)
byte1 -> DSMC_RDS_DLL_CTL(cs, 1)
dsmc_dll_training_method()
stati[[09-RK3576-DSMC-DLL训练完整带读]]c int dsmc_dll_training_method(
struct rockchip_dsmc_device *dsmc_dev,
uint32_t cs,
uint32_t byte,
uint32_t dll_num);
| 参数 | 含义 |
|---|---|
dsmc_dev | DSMC设备对象和ops |
cs | 当前训练的CS编号 |
byte | 当前训练的byte lane,0或1 |
dll_num | 本次测试的DLL tap,0x00~0xff |
| 返回: |
- 0 : 当前tap 通过
- -EINVAL : 数据不匹配
- -ENODEV: 没有可用测试region
单点测试的局部数据
uint32_t size = 0x100;
uint32_t pattern[] = {0x5aa5f00f, 0xffff0000};
测试范围
offset 0x000 ~ 0x0fc
共0x100字节
每4字节写一个u32
每种pattern写64个word
两种基础pattern用于覆盖不同的为翻转组合
(pattern[j] + i) & mask
选择训练region
- LocalBus
if (cfg->device_type == DSMC_LB_DEVICE) {
for (rgn = 0; rgn < DSMC_LB_MAX_RGN; rgn++) {
if (cfg->slv_rgn[rgn].status) {
map = &dsmc->cs_map[cs].region_map[rgn];
break;
}
}
}
LocalBus选择编号最小的已启用region:
region0启用 -> 使用region0
region0禁用、region1启用 -> 使用region1
只启用region3 -> 使用region3
它没有按照region属性筛选,也不要求必须是FIFO或DPRA。
- PSRAM
rgn = 0;
map = &dsmc->cs_map[cs].region_map[0];
PSRAM固定使用region0
找不到region
if(map == NULL)
return -ENODEV;
训练会破坏测试区数据
如果只启用Register region:
第一个启用region = Register
-> 训练向offset 0x00~0xfc写测试数据
-> 可能修改控制寄存器、触发中断或设备动作
如果FIFO写入会触发外部逻辑消费数据,读回也未必能得到相同内容。
通用驱动更稳妥的策略应是:
- 明确选择可安全读写的DPRA/内存测试region。
- 通过设备树指定训练region。
- 或在训练前备份、训练后恢复数据。
- 禁止在Register region执行破坏性训练。
x8和x16的mask
if (cfg->io_width == MCR_IOWIDTH_X8) {
mask = 0xffffffff;
} else {
if (byte == 0)
mask = 0x0000ffff;
else
mask = 0xffff0000;
}
x16模式
每次只验证当前byte lane对应的数据半区
训练byte0 -> mask 0x0000ffff
训练byte1 -> mask 0xffff0000
这样可以分别寻找两条采样路径的稳定窗口
x8模式
代码使用 0xffffffff。在具体硬件协议下,x8总线可能通过多个传输周期完成32位访问,因此最终仍比较完整32位word。
mask的实际lane映射还受到DSMC I/O字节排列影响,不能只按CPU内存中的字节顺序理解。
data_cpu_to_dsmc_io()
static void data_cpu_to_dsmc_io(uint32_t *data)
{
uint32_t byte0, byte3;
byte0 = (*data >> 0) & 0xff;
byte3 = (*data >> 24) & 0xff;
*data &= 0x00ffff00;
*data |= byte3 << 0;
*data |= byte0 << 24;
}
它交换32位数据中的最低字节和最高字节
输入字节:[B3 B2 B1 B0]
输出字节:[B0 B2 B1 B3]
例如
0x5aa5f00f -> 0x0fa5f05a
0xffff0000 -> 0x00ff00ff
目的是让CPU构造的pattern符合DSMC I/O路径中的字节排列。
函数直接修改局部 pattern[]。每次调用 dsmc_dll_training_method()都会重新初始化该局部数组,因此不同DLL tap之间不会持续重复交换。
写入当前的DLL tap
REG_CLRSETBITS(dsmc,
DSMC_RDS_DLL_CTL(cs, byte),
RDS_DLL0_CTL_RDS_0_CLK_DELAY_NUM_MASK,
dll_num << RDS_DLL0_CTL_RDS_0_CLK_DELAY_NUM_SHIFT);
寄存器地址公式
#define DSMC_RDS_DLL_CTL(cs, byte) \
(0x1000 * ((cs) + 1) + 0x30 + (byte) * 0x4)
关系:
CS编号决定寄存器组基址
byte编号决定DLL0或DLL1寄存器
dll_num写入低8位delay字段
REG_CLRSETBITS只替换delay字段,保留采样选择等其他位。
写入测试数据
for (j = 0; j < ARRAY_SIZE(pattern); j++) {
data_cpu_to_dsmc_io(&pattern[j]);
for (i = 0; i < size; i += 4)
ops->write(dsmc_dev, cs, rgn, i,
(pattern[j] + i) & mask);
...
}
ops在 probe()中设置:
priv->ops = &rockchip_dsmc_ops;
对应:
```c
static struct dsmc_ops rockchip_dsmc_ops = {
.read = dsmc_read,
.write = dsmc_write,
...
};
最终写路径:
ops->write
-> dsmc_write()
-> writel_relaxed(val, map->virt + offset)
-> DSMC物理窗口
-> 当前CS/region
-> 外部设备
这里是PIO访问,不使用DMA。
读回比较
for (i = 0; i < size; i += 4) {
ops->read(dsmc_dev, cs, rgn, i, &data);
if (data != ((pattern[j] + i) & mask))
return -EINVAL;
}
读路径:
ops->read
-> dsmc_read()
-> readl_relaxed(map->virt + offset)
-> 外部设备返回数据
任何一个word不匹配,当前DLL tap立即判为FAIL,不再检查剩余word和pattern。
两个pattern全部通过才返回0。
维护cache
#ifdef CONFIG_ARM64
dcache_clean_inval_poc((unsigned long)map->virt,
(unsigned long)(map->virt + size));
#else
dmac_flush_range(map->virt, map->virt + size);
#endif
非Register LocalBus region和PSRAM被重新映射为cache属性。写入测试数据后,如果数据仍停留在CPU cache,外部总线和后续读回可能观察不到预期时序。
clean + invalidate的目的:
clean -> 把脏cache line写到PoC
invalidate -> 丢弃本地cache副本
后续读取 -> 重新从DSMC窗口获取数据
这样训练测试的是DSMC物理数据路径,而不是“刚写入的cache副本”。
需要注意,驱动把 ioremap_cache()得到的地址用于 readl_relaxed/writel_relaxed和显式cache维护,这属于平台相关用法,移植到其他体系结构时必须重新确认内存属性和API组合是否有效。
单个tap的测试量
每个pattern:
0x100 / 4 = 64次写
0x100 / 4 = 64次读
两个pattern:
128次写 + 128次读 = 256次MMIO操作/tap
完整扫描256个tap:
32768次写 + 32768次读
= 65536次MMIO操作/byte lane
x16训练两个lane,理论完整通过时约:
131072次MMIO操作/CS
最后还会对最佳tap额外复验一次。因此训练可能明显增加probe耗时,这是全范围扫描带来的成本。
dsmc_dll_full_range_training()
static int dsmc_dll_full_range_training(
struct rockchip_dsmc_device *dsmc_dev,
uint32_t cs,
uint32_t byte);
该函数固定扫描当前CS、当前lane的所有256个DLL值。
状态变量
int start = -1;
int best_start = -1;
int best_end = -1;
int max_length = 0;
int current_length = 0;
| 变量 | 含义 |
|---|---|
start | 当前连续PASS窗口起点,-1表示当前不在窗口中 |
current_length | 当前PASS窗口长度 |
best_start | 已发现最长窗口起点 |
best_end | 已发现最长窗口终点 |
max_length | 已发现最长窗口长度 |
扫描0x00~0xff
for (dll = 0; dll <= 0xff; dll++) {
result = dsmc_dll_training_method(..., dll);
...
}
- 当前tap通过
if (result == 0) {
if (start == -1)
start = dll;
current_length++;
}
第一次PASS记录窗口起点;连续PASS只增加长度。
- 当前tap失败
if (start != -1) {
if (current_length > max_length) {
max_length = current_length;
best_start = start;
best_end = dll - 1;
}
start = -1;
current_length = 0;
}
FAIL表示当前连续窗口结束。若它比已知最长窗口更长,就替换最佳窗口。
- 设备错误
if (result == -ENODEV)
return -ENODEV;
没有测试region属于拓扑错误,继续扫描其他tap没有意义,因此立即终止。
扫描结果示例
假设测试结果:
0x00-0x1f FAIL
0x20-0x4f PASS 长度48
0x50-0x7f FAIL
0x80-0xbf PASS 长度64
0xc0-0xff FAIL
最终:
best_start = 0x80
best_end = 0xbf
max_length = 0x40
最佳值:
best_dll = (best_start + best_end) / 2;
即:
(0x80 + 0xbf) / 2 = 0x9f
整数除法向下取整。
处理结尾窗口
如果PASS一直持续到0xff,循环中没有后续FAIL触发窗口结算,因此额外处理:
if (start != -1 && current_length > max_length) {
max_length = current_length;
best_start = start;
best_end = 0xff;
}
例如:
0xd0~0xff全部PASS
扫描结束后仍能记录该窗口。
长度相同窗口选择
代码比较条件是:
current_length > max_length
而不是:
current_length >= max_length
所以两个窗口长度相同时,保留先出现的低tap窗口。
0x20~0x3f PASS,长度32
0x80~0x9f PASS,长度32
最终选择0x20~0x3f
驱动没有根据初始DTS值、窗口中心位置或离边界距离进一步做平局选择。
算法不处理环形窗口
DLL tap在部分硬件上可能具有周期性。如果:
0x00~0x10 PASS
0x11~0xef FAIL
0xf0~0xff PASS
从环形角度,0xf0~0xff和 0x00~0x10可能属于跨越边界的同一窗口。
当前算法会把它们视为两个独立窗口,不会合并。这是否影响实际结果取决于RK3576 DLL寄存器的tap物理语义,需要结合硬件手册确认。
没有有效窗口
if (best_start == -1) {
/* No passing window found */
dev_err(dev, "DSMC: cs%d byte%d no valid DLL window found\n", cs, byte);
return -EINVAL;
}
可能原因包括:
- 引脚/IOMUX错误。
- 时钟未输出或频率过高。
- Host和Slave位宽、时序不匹配。
- 选择了无法普通读回的FIFO/Register region。
- 外部FPGA没有响应。
- cache属性或数据一致性处理错误。
- 采样窗口确实不在0x00~0xff范围内。
因此“没有窗口”不只表示DLL参数不对,也可能是整个数据通路不通。
写入最佳值
best_dll = (best_start + best_end) / 2;
REG_CLRSETBITS(dsmc,
DSMC_RDS_DLL_CTL(cs, byte),
DELAY_MASK,
best_dll << DELAY_SHIFT);
选择中点是为了让采样位置尽量远离窗口两侧的不稳定边界,为温度、电压和时钟抖动留出裕量。
日志示例格式:
cs0 byte0: DLL window [0x20-0x70] length=0x51, current=0x48
字段:
| 字段 | 含义 |
|---|---|
cs0 | 当前CS |
byte0 | 当前lane |
[start-end] | 最长PASS窗口 |
length | 窗口tap数量 |
current | 最终选择的tap |
Verify the selected value
/* Verify the selected value */
if (dsmc_dll_training_method(dsmc_dev, cs, byte, best_dll) != 0) {
dev_err(dev, "DSMC: cs%d byte%d current DLL 0x%x verification failed\n",cs, byte, best_dll);
return -EIO;
}
扫描期间best tap曾经通过,但再次复验可以发现:
- 数据路径不稳定。
- 外部设备状态已改变。
- cache维护或访问顺序存在问题。
- 训练结果偶然通过。
复验失败返回 -EIO,整个probe失败,不会带着不可信的采样值继续运行。
保存训练结果
cfg->dll_num[byte] = best_dll;
此时结果同时存在两处:
硬件寄存器
-> DSMC_RDS_DLL_CTL(cs, byte)
软件结构
-> cfg->cs_cfg[cs].dll_num[byte]
注意:驱动只是修改运行时内存,不会自动回写设备树或DTB。下次重启仍从DTS初值开始重新训练。
如果要把训练结果固化为新的初始值,需要人工读取日志并修改 rockchip,dqs-dll,随后重新编译DTB。但是否跳过训练由驱动逻辑决定,单纯修改初值并不会取消全范围训练。
数据链路
对外ops接口
static struct dsmc_ops rockchip_dsmc_ops = {
.read = dsmc_read,
.write = dsmc_write,
.copy_from = dsmc_copy_from,
.copy_from_state = dsmc_copy_from_state,
.copy_to = dsmc_copy_to,
.copy_to_state = dsmc_copy_to_state,
};
| ops | 方向 | 实现方式 |
|---|---|---|
read | DSMC → CPU寄存器 | PIO |
write | CPU寄存器 → DSMC | PIO |
copy_from | DSMC → DDR | DMA |
copy_to | DDR → DSMC | DMA |
copy_*_state | 查询异步DMA状态 | 原子状态位 |
PIO读取
static int dsmc_read(struct rockchip_dsmc_device *dsmc_dev,
uint32_t cs, uint32_t region,
unsigned long addr_offset,
uint32_t *data)
{
struct dsmc_map *map =
&dsmc_dev->dsmc.cs_map[cs].region_map[region];
*data = readl_relaxed(map->virt + addr_offset);
return 0;
}
地址形成:
map->virt
+ addr_offset
= 内核访问地址
硬件路径:
CPU load
-> 内核页表
-> region物理窗口
-> DSMC地址译码
-> CS选择
-> region选择
-> 外部设备返回32位数据
PIO写入
static int dsmc_write(..., uint32_t val)
{
struct dsmc_map *map =
&dsmc_dev->dsmc.cs_map[cs].region_map[region];
writel_relaxed(val, map->virt + addr_offset);
return 0;
}
硬件路径与读取相反:
CPU store
-> DSMC物理窗口
-> 指定CS/region
-> FPGA、LocalBus Slave或PSRAM
readl_relaxed/writel_relaxed保留MMIO访问本身,但比非relaxed版本提供更弱的全局顺序保证。若业务依赖与普通内存、doorbell或DMA描述符之间的严格先后关系,需要额外屏障或使用更强的访问API。
当前边界
dsmc_read/write()没有检查:
cs < 4。region < 4。- region是否启用。
addr_offset + 4 <= map->size。- 地址是否4字节对齐。
这些函数依赖调用者传入正确参数。写业务驱动时应在上层验证。
DMA传输地址
DMA传输的两端是物理地址:
系统DDR物理地址
↕ Rockchip DMA
DSMC窗口物理地址
↕ DSMC控制器
外部设备
CPU负责:
- 填写源地址、目标地址和长度。
- 准备DMA描述符。
- 打开DSMC硬件请求。
- 通知外部Slave。
- 在DMA完成回调中清理状态。
数据搬运期间CPU不逐字节复制。DMA完成后,CPU再使用DDR虚拟地址处理结果。
DMA资源申请
static int rockchip_dsmc_dma_request(struct device *dev,
struct rockchip_dsmc *dsmc)
{
atomic_set(&dsmc->xfer.state, 0);
dsmc->dma_req[0] = dma_request_chan(dev, "req0");
dsmc->dma_req[1] = dma_request_chan(dev, "req1");
}
设备树链路:
dma-names = "req0", "req1"
↓
dma中的对应项
↓
DMA provider和请求线
↓
struct dma_chan
当前只申请两条通道:
dma_req[0] -> CS0/REQ0
dma_req[1] -> CS1/REQ1
dma_req[2]、dma_req[3]未申请
而 copy_to/from()按 dma_req[cs]取通道。因此CS2/CS3不能直接使用当前DMA实现。
错误指针问题
dma_request_chan()失败返回 ERR_PTR(...),当前代码使用 if (!chan)判断不正确。应使用:
if (IS_ERR(chan))
return PTR_ERR(chan);
传输共享
struct dsmc_transfer {
uint32_t ops_cs;
struct dma_chan *dma_chan;
dma_addr_t src_addr;
dma_addr_t dst_addr;
size_t transfer_size;
u8 brst_size;
u8 brst_len;
atomic_t state;
};
该控制器只有一个共享 xfer,不是每个CS一份。
状态位:
RXDMA -> DSMC向DDR搬运正在进行
TXDMA -> DDR向DSMC搬运正在进行
因此驱动设计上一次只允许一个方向、一个CS执行DMA。
copy_to: DDR到DSMC
static int dsmc_copy_to(...,
dma_addr_t src_phys,
uint32_t to,
size_t size)
参数:
| 参数 | 含义 |
|---|---|
cs/region | DSMC目标窗口 |
src_phys | DDR源物理地址,不是普通CPU虚拟地址 |
to | region内目标偏移 |
size | 搬运长度 |
地址设置:
xfer.src_addr = src_phys;
xfer.dst_addr = map->phys + to;
完整流程:
检查RX/TX是否忙
-> 保存源、目标、长度和CS
-> mask DSMC中断
-> prepare TX DMA
-> 使能DSMC硬件DMA request
-> 通知外部Slave
-> 立即返回0
这里返回0只表示“启动流程已提交”,不表示数据已经搬运完成。
copy_from:DSMC到DDR
static int dsmc_copy_from(...,
uint32_t from,
dma_addr_t dst_phys,
size_t size)
地址设置:
xfer.src_addr = map->phys + from;
xfer.dst_addr = dst_phys;
方向:
DSMC region物理窗口
-> Rockchip DMA
-> DDR物理缓冲区
启动步骤与 copy_to()基本相同,区别是DMA方向和状态位为RXDMA。
busy检查与状态查询
启动前:
if (atomic_read(&xfer.state) & (RXDMA | TXDMA))
return -EBUSY;
查询接口:
copy_to_state() -> TXDMA仍置位则返回-EBUSY
copy_from_state() -> RXDMA仍置位则返回-EBUSY
上层典型用法:
ret = ops->copy_to(...);
if (!ret) {
while (ops->copy_to_state(dsmc_dev))
mdelay(1);
}
当前busy检查和后续 atomic_or()不是一个原子事务。两个线程同时启动时都可能先看到空闲,再覆盖共享 xfer字段。可靠业务驱动应使用mutex或atomic compare-exchange串行化整个提交过程。
DMA slave配置
TX:
struct dma_slave_config txconf = {
.direction = DMA_MEM_TO_DEV,
.dst_addr = xfer->dst_addr,
.dst_addr_width = xfer->brst_size,
.dst_maxburst = xfer->brst_len,
};
RX:
struct dma_slave_config rxconf = {
.direction = DMA_DEV_TO_MEM,
.src_addr = xfer->src_addr,
.src_addr_width = xfer->brst_size,
.src_maxburst = xfer->brst_len,
};
当前默认:
brst_size = 8 bytes
brst_len = 16
一个burst = 128 bytes
dmaengine_slave_config()把这些通道参数交给Rockchip DMA provider,provider再转换成DMAC寄存器配置。
当前代码没有检查该函数返回值。
flexible array补丁
struct dma_interleaved_template *xt;
xt = kzalloc(struct_size(xt, sgl, 1), GFP_KERNEL);
dma_interleaved_template末尾包含柔性数组 sgl[]。需要额外分配一个chunk空间:
sizeof(struct dma_interleaved_template)
+ sizeof(struct data_chunk) * 1
旧代码只在栈上声明结构体,却访问 xt.sgl[0],会越过栈对象边界。补丁改用 struct_size()动态分配后,sgl[0]才具有真实存储空间。
准备描述符后立即 kfree(xt),前提是DMA provider在 dmaengine_prep_interleaved_dma()中已经复制或消费模板,DMA Engine API允许调用者在prep返回后释放模板。
interleaved模板
xt->frame_size = 1;
xt->sgl[0].size = brst_size * brst_len; // 128 bytes
xt->numf = transfer_size / 128;
xt->dir = dir;
可以理解为:
每frame只有1个chunk
每chunk 128字节
共有 transfer_size / 128 个frame
TX设置DDR源起点,RX设置DDR目标起点;设备端地址由 dma_slave_config提供。
准备和提交DMA描述符
desc = dmaengine_prep_interleaved_dma(chan, xt,
DMA_CTRL_ACK | DMA_PREP_INTERRUPT);
标志:
| 标志 | 作用 |
|---|---|
DMA_CTRL_ACK | 描述符允许DMA Engine管理确认 |
DMA_PREP_INTERRUPT | 完成时请求callback |
绑定回调:
desc->callback = rockchip_dsmc_lb_dma_txcb; // 或rxcb
desc->callback_param = dsmc;
提交:
atomic_or(TXDMA/RXDMA, &xfer.state);
dmaengine_submit(desc);
dma_async_issue_pending(chan);
dmaengine_submit()把描述符加入通道队列;dma_async_issue_pending()通知provider真正启动待处理描述符。
当前代码没有检查 dmaengine_submit()返回的cookie是否为错误。
DSMC硬件请求数
dma_req_num = DIV_ROUND_UP(size, 128); // 代码手工实现
writel(dma_req_num, DSMC_DMA_REQ_NUM(cs));
writel(DMA_REQ_EN(cs), DSMC_DMA_EN);
这一步配置的是DSMC控制器,不是DMAC:
DMAC描述符已经准备
+
DSMC DMA request发生器开启
↓
DSMC按外部握手产生请求
↓
DMAC响应请求并搬运burst
DMA通道和DSMC硬件请求必须同时准备好。
通知外部Slave
rockchip_dsmc_lb_dma_trigger_by_host(dsmc, cs);
驱动优先寻找Register region:
找到Register region -> 通过该region访问LBC_CON(15)
没有Register region -> 临时把region0解释为Register
随后:
flag = readl(map->virt + LBC_CON(15));
writel(flag + 1, map->virt + LBC_CON(15));
外部Slave看到标志变化后触发S2H中断,Host DSMC收到信号后开始硬件DMA请求流程。
当前TEST1启用了region3 Register,因此通过region3通知外部FPGA。
中断mask的作用
启动前:
rockchip_dsmc_interrupt_mask(dsmc);
完成后:
rockchip_dsmc_interrupt_unmask(dsmc);
DMA硬件模式使用DSMC内部中断/请求信号进行握手,驱动在传输期间mask普通中断处理,避免同一信号同时按CPU中断路径处理。
这里的mask是DSMC控制器寄存器操作,不是Linux通用 disable_irq()。
18. DMA完成回调
TX:
atomic_fetch_andnot(TXDMA, &xfer.state);
rockchip_dsmc_lb_dma_hw_mode_dis(dsmc);
rockchip_dsmc_interrupt_unmask(dsmc);
RX同理清除RXDMA。
rockchip_dsmc_lb_dma_hw_mode_dis():
清DSMC中断状态
-> 禁用DSMC DMA request
-> 清外部Slave的S2H DMA中断状态
完成后 copy_*_state()返回0。
状态位在硬件清理前先被清除,另一个CPU可能看到“空闲”并提交下一次传输,而callback尚未完成寄存器清理。更严格的顺序应在硬件收尾完成后再发布空闲状态,或用锁保护。
cfopy_to/rom错误传播问题
当前调用:
rockchip_dsmc_lb_prepare_tx_dma(...);
dsmc_lb_dma_hw_mode_en(...);
rockchip_dsmc_lb_dma_trigger_by_host(...);
return 0;
没有接收prepare函数返回值。若内存分配、slave config或描述符准备失败,代码仍可能打开DSMC请求、通知Slave并向上层返回成功。
正确方向应是每一步检查:
prepare DMA失败 -> 解除mask并返回错误
打开硬件失败 -> 终止DMA描述符并回滚
触发Slave失败 -> 终止DMA并关闭硬件请求
当前TEST1 DMA完整链路
DDR写入FPGA LocalBus
ops->copy_to(CS0, region0, src_phys, offset, size)
-> DMA_MEM_TO_DEV描述符
-> 目标 = region0.phys + offset
-> DSMC_REQ0
-> 通过region3 LBC_CON(15)通知FPGA
-> FPGA发S2H握手
-> DSMC产生DMA request
-> DMAC从DDR搬到region0
-> TX callback清理
FPGA LocalBus读入DDR
ops->copy_from(CS0, region0, offset, dst_phys, size)
-> DMA_DEV_TO_MEM描述符
-> 源 = region0.phys + offset
-> 相同握手链路
-> DMAC从region0搬到DDR
-> RX callback清理