srng 总结
本文中会混用 srng 和 ring 这两种描述, 这两个是等价的
驱动
https://github.com/PolarisZg/wireless_driver_simulation/tree/main/driver/driver_wireless
硬件
https://github.com/PolarisZg/qemu_simudevice/tree/master/hw/wireless_simu
两类方向的ring
enum hal_srng_dir
{
HAL_SRNG_DIR_SRC,
HAL_SRNG_DIR_DST
};
名称 | 方向 | 驱动更新 | 设备更新 |
---|---|---|---|
HAL_SRNG_DIR_SRC | 驱动 ---> 设备 | hp | tp |
HAL_SRNG_DIR_DST | 设备 ---> 驱动 | tp | hp |
ring 统一参数
不同的参数需求写在了 src 和 dst 小节中
-
ring_base_paddr
和ring_size
ring_base_paddr
一般为 40bit 或 32bit驱动侧为 ring 开辟的内存空间的起始物理地址, 设备需要这个参数来确定可以访问的内存空间, 一般来说是40bit的数据。
在 Linux 驱动中可以为某个设备(dev)设定一个 paddr 的位数上限, 如下述代码所示, 该内核提供的方法可以限制利用该设备申请的 dma 物理内存地址一定是仅 低32bit 有效, 其他 bit 为 0
当然在 qemu 中可以使用最长 64bit 的物理内存地址来读取内存的数据
dma_set_mask_and_coherent(&priv->pci_dev->dev, DMA_BIT_MASK(32));
超过 32 bit 的数据传输, 需要占用多于一个寄存器
因为这个地址对应的内存空间可能很大, 该地址是由使用ring的各个高级模块使用
dma_alloc_coherent
来申请的, 申请之后需要使用PTR_ALIGN
或ALIGN
来进行内存地址对齐, 在进行内存对齐的时候有两种方案, 高通驱动中在不同的地方使用了这两种:直接对齐 :
ring->base_addr_owner_space = PTR_ALIGN( ring->base_addr_owner_space_unaligned, SRNG_TEST_DESC_RING_ALIGN); ring->base_addr_test_space = ALIGN( ring->base_addr_test_space_unaligned, SRNG_TEST_DESC_RING_ALIGN);
对齐一个, 另一个使用偏移 :
ring->vaddr = PTR_ALIGN( ring->vaddr_unaligned, HAL_RING_BASE_ALIGN); ring->paddr = ring->paddr_unaligned + ((unsigned long)ring->vaddr - (unsigned long)ring->vaddr_unaligned);
ring_size
即每个 ring 的数据存储部分所占用的实际内存空间大小, 设备侧需要这个值来对自己更新的 tp 或 hp 来进行取模运算, 该值以 4 char 为单位 -
entry_size
和num_entries
32bit 足够大了
为了可以动态配置每个ring可传输数据的大小, 驱动会在初始化ring的时候配置ring中每个 entry 的大小和对应 ring 中 entry 的个数。
驱动在代码中保证
num_entries
的值是 2 的幂, 这样在取模的时候直接和(num_entries - 1)
即可实际上
((entry_size << 2 ) * num_entries)
在数值上应该等于上面提到的ring_size
, 不知道为什么这里要采用两种方式重复传递数据在高通驱动中
entry_size
参数的单位是 32bit (4 char)在 ring 的数据传输中使用的很多自定义的指针基本都是以 4char 为单位 (左移 2bit)
-
intr_timer_thres_us
和intr_batch_cntr_thres_entries
延迟中断机制, 为了减少中断的个数而引入的机制;一个是延迟时间, 一个是延迟个数; 默认设置为0, 不启用延迟中断。
延迟中断机制:指从ring中读取数据完毕, 或写入数据完毕后延后通知驱动的机制。一般情况下每个任务完成后都可以发一个中断来通知任务完成, 设置这两个参数后, 对ring的任务完成后达到一定数量或一定时间, 再发送中断向操作系统汇报。
-
flag
典型的标志位, 可能会根据 dst 和 src 的不同而含义不同。
HAL_SRNG_DIR_SRC
设定为该方向的 ring , 数据传输方向是操作系统驱动到硬件
特殊参数
-
low_threshold
为保证硬件总是能从 src ring 中读取到数据而设计的参数, 当 src ring 中的数据少于这个量时, 发出中断请求驱动进行填充
-
tp_paddr
硬件更新 tp 之后, 驱动需要知道这样的修改, 这样的修改实际上并不需要很强的实时性, 驱动只需要在用到 ring 的时候能读取到修改即可。因此硬件端拿到一个 tp 的 paddr , 当 tp 被修改后直接以 dma 的方式向这个区域写入 u32 的 tp 的值。
之后驱动侧只需要解引用一下指针就能拿到该值。
考虑到在硬件设计完毕后,实际可用的 ring 个数已经固定, 因此驱动侧可以统一申请一块 dma 一致性内存空间,然后将该内存空间切成小块分给每个 src ring 用来做 tp 使用
dma 一致性内存空间 (dma coherent), 指使用
dma_alloc_coherent()
申请到的内存空间, 这块内存空间不会被 load 进 cache , 每次对该内存空间的读取操作都是直接访问实际的内存, 借此保证读取到的一定是设备通过 dma 写入到内存的数据 -
寄存器-仅写 hp
驱动侧需要通过写这个寄存器, 来通知硬件 src_ring 中已经放入了数据
软件使用流程
对于使用流程可以参考本人qemu 假设备驱动的 wireless_simu_hal_srng_test_send
方法 和 wireless_simu_test_dst_enqueue_pipe
方法, 两个方法的流程是一样的, 只是写入 entry 的数据不一样
-
使用之前需要对所使用的 ring 加锁, 保证不发生多线程冲突
-
刷新 tp 的值, 实际就是解引用 tp 的 vaddr
-
向 ring 申请 entry 空间, 申请成功后 ring 会自动修改 hp 的值, 申请成功后存放数据
-
暂存发往设备的数据, 等设备处理完毕后再做释放处理
-
通过寄存器向硬件写入 hp 的值, 该操作会触发下面的硬件读取流程
-
解锁,结束
硬件读取流程
对于读取流程可以参考本人假设备代码中的 wireless_hal_src_ring_tp
代码和 ce_dst_ring_handler
代码。实际上ce_dst_ring_handler
是由wireless_hal_src_ring_tp
拉起的,在wireless_hal_src_ring_tp
中如果判定为该srng挂了一个对应的处理函数,则会转向到对应的处理函数进行处理。
-
由驱动写 hp 寄存器触发, 开始进行硬件对 ring 的读取
-
判断该 srng 是否挂了对应的处理函数, 如果挂了则转向, 这是拉起
ce_dst_ring_handler
等高级模块对ring进行处理的地方 -
对该 srng 加锁, 保证单线程读取, 主要是为了保证对 tp 的更新是单线程的
-
tp 不等于 hp 开始读取, 以 dma 的方式从 ring 中取出一个 entry , 取出后使用 dma 直接修改 tp 对应的 paddr 处的值, 并增加 tp 的值
取出一个 entry 的地址是
srng->ring_base_paddr + (srng->u.src_ring.tp << 2)
长度是srng->entry_size << 2
-
拉起某个方法对entry进行后处理
比如在
ce_dst_ring_handler
中 是将 entry 中存放的 paddr 放置在空闲区域中等待使用 -
解锁
HAL_SRNG_DIR_DST
在本段之中可能会出现一些描述上的混乱,注意本节中提到的 dst_ring 在方向判断上很有可能是 HAL_SRNG_DIR_SRC, 而某个被称作 status_ring 环形缓冲区在方向上一定是 HAL_SRNG_DIR_DST
特殊参数
-
max_buffer_length
并非是指每个 ring 中的 entry 大小限制, 因为向 dst_ring 中写入 entry 时有 entry_size 的限制; 该限制为了保证驱动端可以正确解析 ring 中的每个 entry , entry_size 的限制依靠硬件和驱动两端写入读取的 struct 具有相同的结构来实现的。
驱动对该数值的配置路径如下所示;
高层模块填充 params.max_buffer_len ---> 调用 ath11k_hal_srng_setup(params) ---> 从 params 读取 max_buffer_len 写入 max_buffer_length ---> max_buffer_length 写入硬件寄存器
对
params.max_buffer_len
的填充仅存在于 对ce的配置方法ath11k_ce_init_ring
中,填充的数据来自 ce.c 文件开头处的静态数组的src_sz_max
属性, 借此可以推测该字段的含义:描述该字段的含义之前, 需要先简单说明一下两种 ring (src 和 dst) 和两个方向 (tx 和 rx) 的数据传输之间的关系。如下图所示,tx 指数据从驱动发往硬件,仅使用一个填充有发送数据描述符的 src_ring; rx 指数据从硬件发往驱动, 需要使用一个填充 paddr 的 src_ring 来通知硬件数据在内存中的填充位置, 和一个填充有数据长度的字段 dst_ring 来通知驱动,某一区域中多长的数据是有效的。采取双 ring 的方式进行 rx 的时候, 由于驱动方向在内存中申请空闲区间用于接收数据时使用的是固定大小, 所以就需要使用
max_buffer_length
字段来告知硬件, 禁止发送超过该大小的数据至驱动(至于是丢弃掉还是切开, 这里暂时不清楚, 不过好像是丢弃掉) -
hp_paddr
和 src_ring 的 tp_addr 的作用相同, 硬件在更新 dst_ring 之后需要将 hp 直接写入到内存中, 这样驱动方面就可以通过对指针的解引用来拿到这个值
考虑到在硬件设计完毕后,实际可用的 ring 个数已经固定, 因此驱动侧可以统一申请一块 dma 一致性内存空间,然后将该内存空间切成小块分给每个 dst ring 用来做 hp 使用
-
寄存器-仅写 tp
驱动通过一个寄存器的写入操作来修改硬件中dst_ring的tp的值。
这里和 src_ring 具有不同之处在于,对 dst_ring 的 hp 的更新不会触发硬件的某些操作。在 src_ring 中,硬件修改 tp 之后触发中断的原因在于节省驱动占用的内存空间, 释放掉对应位置的 skb 内存。但对于更新 dst_ring 的 hp 的硬件来说, 将数据搬运至内存之中 这个操作是硬件控制的, 在搬运成功后就可以自行判断释放掉数据占用的设备空间,因此不会有 tp 被修改之后的处理
所以为了节省寄存器的话, 也可以将 tp 挂到一个驱动 dma 内存空间的某个位置, 当硬件使用 dst_ring 的时候去读取那个位置来得到 tp 信息
软件配置流程
对 dst_ring 的使用流程实际上就是 rx 的过程,单独一个 dst_ring 是无法工作的, 需要 src_ring 来辅助;整个驱动方面使用 dst_ring 的流程可以参考本人写的驱动中 wireless_simu_irq_hal_srng_dst_dma_test
( dma 地址配置和数据接收) 、wireless_simu_test_dst_post_pipe
(仅 dma 地址配置) 、高通驱动中的 ath11k_ce_recv_process_cb
( dma 地址配置和数据接收) 、ath11k_ce_rx_post_pipe
(仅 dma 地址配置)
下方的流程和上面提到的 src_ring 的驱动使用流程是一样的
-
对高级模块(如 ce)加锁, 保证对该模块管理的两个 ring 的配置不会出现问题
-
申请一段空间, 并映射得到到 dma_paddr, 在高通驱动中固定申请长度为 2048 字节的空间(来自静态配置数组
const struct ce_attr
中的src_sz_max
和上面提到的max_buffer_length
对应), 并将该空间的头地址指针放在该高级模块(如 ce)管理的一个数组中暂存, 放置的位置write_index
和 之后待使用的 src_ring 的 hp 具有一定的关系 -
对 src_ring 加锁
-
向 src_ring 中写入上方申请空间的 dma_paddr , 且仅需要写入该40bit数据即可, 更新 hp
-
将 hp 写入硬件寄存器, 该操作会触发硬件的 src_ring 接收操作
-
解ring锁,高级模块解锁
下图中是整体的软硬件配置流程
硬件的配置流程
硬件对 rx 模块的配置由驱动方面写 hp 寄存器触发, 可以参考本人假设备代码中的 ce_dst_ring_handler
。这个方法在假设备启动的时候被自动注册到一个特定 id 的 srng 之中, 当该 srng 的 hp 寄存器发生写入操作后, 可以自动的拉起 ce_dst_ring_handler
而非其他函数。
在硬件配置完毕后, 硬件方面就可以启动硬件向驱动的数据发送流程(rx)
-
由驱动写 hp 寄存器触发, 开始进行硬件对 ring 的读取
-
拉起
ce_dst_ring_handler
对ring进行处理 -
对该 srng 加锁, 保证单线程读取, 主要是为了保证对 tp 的更新是单线程的
-
tp 不等于 hp 开始读取, 以 dma 的方式从 ring 中取出一个 entry , 取出后使用 dma 直接修改 tp 对应的 paddr 处的值, 并增加 tp 的值
-
将 entry 中存放的 dma_paddr 放置在数组中等待使用, 对于放置的位置 write_index 和读取到该 entry 的位置 tp 具有一定的关系
-
解锁
硬件数据发送流程(发往驱动方向)
该操作启动之前需要保证硬件中已经存了一些 dma_paddr 数据
-
其他模块生成数据, 对于管理所有 ring 的 srng 来说, 仅需要一个
void *data
参数和size_t data_size
参数即可, 或许还需要一个type_id
来通知 srng 该数据必须使用那些 dst_ring 进行发送 -
遍历自己管理的所有
dest_ring - status_ring
组, 寻找含有空闲 dma_paddr 的 ring -
对找到的
dest_ring - status_ring
组中两个 ring 全部加锁, 保证数据的单线程发送 -
从 dma_paddr 数组中取出一个 dma_paddr 并将
void *data
拷贝至对应的内存位置(硬件调用 dma_write) -
从
status_ring
(typeHAL_SRNG_DIR_DST
) 中获取一个 entry, 写入size_t data_size
并更新 hp -
将上方更新的 hp 写入内存中特定位置(hp_paddr), 并发出中断, 通知驱动方面取数据
-
解锁
下图中是软硬件整体的 rx 流程
软件数据接收流程
该接收操作由硬件中断触发,可以参考本人写的驱动中 wireless_simu_irq_hal_srng_dst_dma_test
或 高通驱动中的 ath11k_ce_recv_process_cb
-
创建skb链表,用于存放从 status_ring(type
HAL_SRNG_DIR_DST
) 中读取数据 -
对 status_ring(type
HAL_SRNG_DIR_DST
) 加锁 -
从 status_ring(type
HAL_SRNG_DIR_DST
) 的 tp 处读取一个 entry, 该entry中含有接收到的数据长度, 从上方软件配置流程中暂存空间的数组中拿取 tp 对应的 write_index 位置处拿取一整块 skb 并将 skb->data 段的尾指针移动到数据长度对应的位置。对成功读取到的entry数量进行记录, 该记录用于之后向 dst_ring 中 dma_paddr 的重新填充 -
解除上一步拿到的skb的dma映射, 注意解除映射并不会释放空间
-
将 skb 加入链表
-
继续读取 status_ring(type
HAL_SRNG_DIR_DST
) 直到 tp 撞到 hp -
对 status_ring(type
HAL_SRNG_DIR_DST
) 解锁 -
对 skb 链表进行处理, 由于接收流程是中断的下半部, 所以这里可以进行较为耗时的操作
-
拉起 软件配置流程 对 dma_paddr 进行重新配置
ring 的初始化
对 srng 的使用分为以下几个步骤
初始化阶段
- srng 的初始化
- 使用 srng 的高层模块对 srng 初始化的配置
---
使用阶段
- 使用前配置(仅 rx 含有此步骤)
- 使用过程(上方已经写了)
---
删除阶段
硬件对srng 的初始化简单分为两部分
-
高层模块对srng的初始配置
-
被写寄存器
驱动: hal 前期初始化
驱动中 srng 初始化过程简单分为三部分:
-
srng 前期的初始化
-
高层模块对srng 初始配置
-
写硬件寄存器
hal 的前期初始化主要的任务有两项
-
初始化srng依赖的各种Linux Kernel提供的工具, 比如线程锁 mutex spin, 定时器 timer 等;
-
申请 dma 空间, 用于存放各个 src_ring 的 tp 或 dst_ring 的 hp, 这段空间会被驱动读取, 会被硬件写入。
下面详细描述对 dma 空间的申请和使用。如图所示, hal首先在内存中申请一块大小为 ring 数量 * 4char
大小的空间, 并保存虚拟内存地址 vaddr 和物理内存地址 paddr; 之后每个使用到的 ring 按照自己的 ring_id 作为偏移地址去获取自己的 tp 或 hp 可使用的一块 32 bit长的空间。在各个ring中只需要记录 虚拟内存地址即可, 物理内存地址 paddr 只需要借助写硬件寄存器的方式传递给硬件。
在驱动这一侧,对于 src 类型的 ring 的 tp 的虚拟地址使用如下的代码计算, 可以通过对该地址的解析指针拿到该位置存放的值
srng->u.src_ring.tp_addr = (void *)(hal->rdp.vaddr + ring_id);
对于 src 类型的 ring 的 tp 的物理内存地址可以使用如下代码计算, 计算完成后直接写入到硬件中对应 ring 的寄存器中
u64 tp_addr = hal->rdp.paddr +
((unsigned long)srng->u.src_ring.tp_addr -
(unsigned long)hal->rdp.vaddr);
驱动: 高层模块对 srng 的初始配置
由于不同的模块利用 ring 传输的描述符大小不同, 而且在同一模块中可能使用同一类型大量的 ring 应对高并发环境下的数据传输, 每个 ring 实际需要的, 含有大量 entry 的内存空间需要上级模块自己管理, 负责申请和释放, 这部分空间不应该让 srng 模块管理。
对于 srng 模块, 其内部只进行 tp 或 hp 的运算, 在高层模块传输数据之前需要获取到一个entry时, 对于 src_ring 其根据如下的公式计算得到空闲 entry 的起始地址:
驱动侧:
u32* desc = srng->ring_base_vaddr + srng->u.src_ring.hp
硬件侧:
dma_addr_t paddr = srng->ring_base_paddr + (srng->u.src_ring.tp << 2)
两侧的计算方式不同之处主要在于, 驱动侧使用了 u32 来对地址进行记录, 那么只需要相加即可, 但设备测并不知道实际的地址代表的数据单位, 因此需要右移两个bit(4 char, 32 bit)
删除阶段
删除阶段只讨论驱动的卸载, 不考虑硬件的删除。这样的考虑主要基于以下几点:
-
驱动的内存占用: 驱动是装入进Linux kernel进程运行运行,和普通运行在user space的进程不同,user space的进行被kill掉之后所占用的内存被完全的释放掉, 但运行于linux kernel的驱动即使被 rmmod, 也没有进程来回收内存
-
多任务并行方面: 驱动运行的过程中, 各个模块之间存在的一些并行性, 比如在同一时间可能有 src_ring 在发送数据, 中断系统在跑中断的上半部, dst_ring(中断下半部)在接收数据, 这时候强制卸载驱动释放某些位置的内存, 会导致某些模块访问到空指针, 使整个系统宕掉
-
硬件方面: 由于硬件是跑在qemu 中, kill 掉 qemu进程即可保证内存被回收, 因此泄露一些也没问题;
综上, 要正确考虑各个模块停止和占用内存的释放顺序, 保证不会出现空指针的访问。在设计的过程中, 可以使用依赖图的形式来记录各个模块之间的依赖顺序, 如下图所示, 其中每个结点表示一个模块, A 指向 B 表示 A 在运行过程中会拉起 B 模块。
因此一般来说,可以采用先删除/中止高级模块工作, 再删除/中止/释放低级模块的空间
标签:dma,硬件,tp,高通,ath11k,srng,驱动,ring From: https://www.cnblogs.com/polariszg/p/18616840