返回所有文章
模块 / TSENGCHEN BLOG

DSMC

04_Module · DSMC

04_Module · 第 3 / 4 篇

保留原笔记正文与技术内容,仅脱敏敏感信息。配图按作者要求保留原样。

DSMC 是什么

高速外部并行总线控制器

DSMC 硬件信号

原理图

Pasted image 20260715105252

DSMC 16位数据线对应表

DSMC信号J9200针脚FLEXBUS复用GPIO PAD其他主要复用
DSMC_DATA028FLEXBUS1_D1GPIO3_D4LCDC_DEN、EBC_SDLE
DSMC_DATA152FLEXBUS1_D3GPIO3_D0LCDC_D3、EBC_SDDO3
DSMC_DATA248FLEXBUS1_D4GPIO3_C7LCDC_D4、EBC_SDDO4
DSMC_DATA346FLEXBUS1_D5GPIO3_C6LCDC_D5、EBC_SDDO5
DSMC_DATA444FLEXBUS1_D6GPIO3_C5LCDC_D6、EBC_SDDO6
DSMC_DATA542FLEXBUS1_D7GPIO3_C4LCDC_D7、EBC_SDDO7
DSMC_DATA653FLEXBUS1_D8GPIO3_C1LCDC_D10、EBC_SDDO10
DSMC_DATA735FLEXBUS1_D9GPIO3_C0LCDC_D11、EBC_SDDO11
DSMC_DATA825FLEXBUS0_D0GPIO3_B5LCDC_D14、ETH0_TXD0
DSMC_DATA919FLEXBUS0_D1GPIO3_B4LCDC_D15、ETH0_TXD1
DSMC_DATA1031FLEXBUS0_D2GPIO3_B3LCDC_D16、ETH0_TXCTL
DSMC_DATA1133FLEXBUS0_D3GPIO3_B2LCDC_D17、ETH0_RXD0
DSMC_DATA1229FLEXBUS0_D4GPIO3_B1LCDC_D18、ETH0_RXD1
DSMC_DATA1347FLEXBUS0_D5GPIO3_A7LCDC_D20、ETH0_RXCTL
DSMC_DATA1449FLEXBUS0_D6GPIO3_A6LCDC_D21、ETH0_MDC
DSMC_DATA1545FLEXBUS0_D7GPIO3_A5LCDC_D22、ETH0_MDIO

设备树中的:

rockchip,io-width = <16>;

对应下面这16根 DSMC_DATA[15:0]:

时钟和数据选通信号

DSMC信号J9200针脚FLEXBUS复用GPIO PAD作用
DSMC_CLKP32FLEXBUS1_D0GPIO3_D5DSMC差分时钟正端
DSMC_CLKN30FLEXBUS1_CLKGPIO3_D6DSMC差分时钟负端
DSMC_DQS037FLEXBUS1_D10GPIO3_B7低字节数据采样选通
DSMC_DQS141FLEXBUS0_CLKGPIO3_B6高字节数据采样选通

片选信号对应表

DSMC信号J9200针脚FLEXBUS复用GPIO PAD设备树节点
DSMC_CSN058FLEXBUS1_D2GPIO3_D3dsmc_lb_slave0
DSMC_CSN117FLEXBUS0_D8GPIO3_B0dsmc_lb_slave1
DSMC_CSN254FLEXBUS0_D11GPIO3_D1dsmc_lb_slave2
DSMC_CSN356FLEXBUS0_D12GPIO3_D2dsmc_lb_slave3

复位和Ready信号

DSMC信号J9200针脚FLEXBUS复用GPIO PAD作用
DSMC_RESETN36FLEXBUS0_D15_M0 / FLEXBUS1_D12_M0GPIO3_D7DSMC从设备复位,低有效
DSMC_RDYN21FLEXBUS1_D11 / FLEXBUS0_CSN_M0GPIO3_A4从设备Ready/等待状态,低有效

DSMC外部中断信号

DSMC信号J9200针脚FLEXBUS复用GPIO PAD
DSMC_INT018FLEXBUS0_D13_M0 / FLEXBUS1_D14_M0GPIO4_A0
DSMC_INT151FLEXBUS0_D9GPIO3_C2
DSMC_INT257FLEXBUS0_D14_M0 / FLEXBUS1_D13_M0GPIO4_A1
DSMC_INT340FLEXBUS0_D10GPIO3_C3

总线图

Pasted image 20260715093411

引脚方向电平说明
CLKP CLKPO-通信时钟,差分时钟用于1.8V设备,3.0V以上设备只使用CLKP 主机发送数据和时钟交叉沿中心对齐(从机在时钟跳变沿采样)
CSn0~CSn3O-片选,支持4个从设备
DQ0~DQ15I/O-双向数据总线
DQS0 DQS1I/O-RWDS读写选通信号,CA命令期间用来向主机表示是否需要额外延时 主机读数据期间和数据信号沿对齐用于主机获取数据 主机写数据期间用于数据掩码(localbus模式下无效)
可选接口
RESETNO上拉从设备复位信号
RDYNI开漏从机准备好信号,LocalBus写反压
INTn0~INTn3I开漏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>

对应:

索引字段当前值作用
0rcshi1读操作CS高电平间隔
1wcshi0写操作CS高电平间隔
2rcss0读操作CS建立时间
3wcss0写操作CS建立时间
4rcsh0读操作CS保持时间
5wcsh0写操作CS保持时间
6rd_latency2读延迟
7wr_latency2写延迟

它控制的是一次访问中:

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

Pasted image 20260715110925

流程

Pasted image 20260715113852

阶段主要行为
开始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

Pasted image 20260715141522

流程

类似于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 位名称含义
47R/W#0:写,1:读
46Target选择目标设备/配置
45Burst Type0:WRAP,1:INCR
44RFU保留位,固定为 0
43:16Page Address地址的 Address[31:4]
15RFU保留位
14:13RFU保留位
12:3RFU保留位
2:0Column 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 设备内部空间可以划分多个区域,不同区域用于不同场景,设置与外设不同的访问方式。 Pasted image 20260715141055

  • 区域大小由 DSMC_DEV_SIZE 和 DSMC_SLV_RGN_DIVn 配置。
  • 每个区域可设置为:
    • DPRA
    • REG
    • FIFO(merged/un-merged)
类型中文作用
DPRA双口 RAM 区域用于普通内存式读写,可配置合并或不合并
REG寄存器区域用于访问 FPGA/从设备寄存器,可配置合并或不合并
FIFOFIFO 区域用于连续数据流传输,可配置合并或不合并

C/A Ecoder

LocalBus 模式下,一次传输同样使用 48-bit C/A 信息,用于描述读写类型、区域类型、Burst 长度和访问地址。

C/A BitName(中文)Assignment(定义)
47R/W#(读写控制)0:Read;1:Write
46Target(目标配置)由 DSMC_MCRn 中的 CRT 配置
45Burst Type(突发类型)0:WRAP;1:INCR
44Space Type(空间类型)0:FIFO;1:DPRA
43FIFO Merge(FIFO 合并)0:不合并;1:合并
42RFU(保留位)固定为 0
41:40Region(区域编号)选择 Region 0~3
39:32Length(突发长度)本次 Burst 的数据长度
31:0Address(访问地址)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_DIVnRegion边界
DSMC_RGNx_ATTRnRegion属性
DSMC_VDMCn设置为LocalBus协议
DSMC_MCRnIO宽度、访问属性
DSMC_MTRnCS与时钟时序调整
DSMC_MRGTCRn合并访问
DSMC_BDRTCRn跨边界处理

信号线

信号方向作用
CLKP/CLKNOutput(输出)差分传输时钟,为 C/A 和数据传输提供时序基准。
CS[3:0]#Output(输出)Chip Select(片选),选择对应的 LocalBus 从设备;低有效。
RESETNOutput(输出)Reset(复位),复位外部从设备;低有效。
DQ[15:0]I/O(双向)C/A 和数据复用总线。C/A、写数据由 DSMC 输出,读数据由从设备输出。
DQS[1:0]I/O(双向)读时作为 Data Strobe(数据选通);写时作为 Byte Mask(字节屏蔽)。
RDYNInput(输入)Readiness/Ready(就绪/反压)信号。写入时从设备 FIFO 满,可使 DSMC 暂停发送数据和时钟。
INT[3:0]Input(输入)Interrupt(中断)输入,由 LocalBus 从设备向 RK3576 发出中断请求。

驱动分析

RK3576-Linux-DSMC-驱动关系图谱

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值驱动枚举
16MCR_IOWIDTH_X16
8MCR_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;  
}

结果表:

PSRAMLOCALBUS结果
00失败:没有启用任何外设
10成功:进入PSRAM模式
01成功:进入LocalBus模式
11失败:两种模式不能共存

这里的标志表示“是否存在至少一个启用节点”,不是设备数量。

所以可以同时开启多个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);

参数

参数内容
dsmcDSMC Host控制器的完整私有结构
dsmc->cfg设备树解析结果及运行时配置
dsmc->regsDSMC控制器寄存器虚拟基址
dsmc->cs_mapPSRAM或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字段配置来源作用
ACScfg->acs地址/CS控制相关模式
DEVTYPE固定HyperRAM编码选择控制器设备接口类型
EXCLUSIVE_DQScfg->exclusive_dqsDQS工作方式
WRAPSIZEcfg->wrap_sizeburst wrap长度
MAXENcfg->max_length_en是否限制最大连续长度
MAXLENcfg->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 clockslv_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;
};
成员含义谁写入
physDSMC窗口物理起始地址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;
}

参数

参数含义
startDSMC窗口物理起始地址
len映射长度
mem_attr驱动定义的缓存属性

选择关系

驱动属性内核API典型语义
DSMC_MEM_ATTRIBUTE_CACHEioremap_cache()CPU可缓存访问
DSMC_MEM_ATTRIBUTE_WR_COMioremap_wc()写合并,适合连续写
其他/NO_CACHEioremap()设备型或非缓存访问

底层逻辑

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].statusregion的DTS status
slv_rgn[i].attributeMerged FIFO等region类型
region_map->phys/sizedsmc_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 region00
CS0 region33
CS1 region04
CS2 region210
CS3 region315

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_PFNMAPVMA映射的是指定PFN,不是普通匿名页
VM_DONTDUMPcore 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
x80byte0
x161byte0、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_devDSMC设备对象和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方向实现方式
readDSMC → CPU寄存器PIO
writeCPU寄存器 → DSMCPIO
copy_fromDSMC → DDRDMA
copy_toDDR → DSMCDMA
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负责:

  1. 填写源地址、目标地址和长度。
  2. 准备DMA描述符。
  3. 打开DSMC硬件请求。
  4. 通知外部Slave。
  5. 在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/regionDSMC目标窗口
src_physDDR源物理地址,不是普通CPU虚拟地址
toregion内目标偏移
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清理

PIO链路图

DSMC数据链路

DMA链路图

DSMC_DMA数据链路

04_Module · 文档目录
上一篇DMA下一篇EtherCAT