这段代码是 Qualcomm 平台上的 EPSS (Embedded Power and System Services) L3 缓存频率控制驱动程序,用于实现 L3 缓存的动态频率调整。下面是对代码的解释
init_epss_data
/*** init_epss_data - 初始化EPSS数据结构* @dev: 设备结构指针** 初始化EPSS设备数据,映射L3寄存器基地址。*/
static int init_epss_data(struct device *dev)
{int idx, ret = 0;struct resource res;/* 分配EPSS数据结构内存 */epss_data = devm_kzalloc(dev, sizeof(*epss_data), GFP_KERNEL);if (!epss_data)return -ENOMEM;/* 在父设备的设备树中查找"l3-base"资源 */idx = of_property_match_string(dev->parent->of_node, "reg-names","l3-base");if (idx < 0) {dev_err(dev, "%s: Unable to find l3-base: %d\n", __func__, idx);return -EINVAL;}/* 获取资源地址和大小 */ret = of_address_to_resource(dev->parent->of_node, idx, &res);if (ret < 0) {dev_err(dev, "Unable to get resource from address: %d\n", ret);return ret;}/* 映射L3寄存器到内存空间 */epss_data->l3_base = devm_ioremap(dev->parent, res.start,resource_size(&res));if (!epss_data->l3_base) {dev_err(dev, "Unable to map l3-base!\n");return -ENOMEM;}return ret;
}
init_epss_data
函数是 EPSS (Embedded Power and System Services) L3 缓存控制驱动的关键初始化函数,主要完成以下任务:
- 全局数据结构初始化:
- 为
epss_data
全局指针分配内存 - 确保系统中只有一个 EPSS 数据实例
- 使用
devm_kzalloc
分配设备管理的内存,避免内存泄漏
- 为
- 硬件资源定位:
- 在父设备的设备树中查找 L3 寄存器基地址资源
- 通过
reg-names
属性定位名为 "l3-base" 的资源 - 这反映了 L3 缓存通常属于整个 SoC 或 CPU 集群,而非单个设备
- 内存映射:
- 将 L3 控制寄存器的物理地址映射到内核虚拟地址空间
- 使驱动程序能够通过指针直接读写硬件寄存器
- 这是实现硬件控制的基础步骤
系统意义
- 硬件抽象层建立:
- 为上层 DCVS 框架提供统一的 L3 控制接口
- 隐藏硬件细节,使频率控制算法与具体硬件解耦
- 单例模式实现:
- 确保系统中只有一个 EPSS 数据实例
- 避免多个驱动同时修改 L3 寄存器导致的冲突
- 系统初始化基础:
- 为后续的 L3 频率控制提供必要的硬件访问能力
- 是
setup_epss_l3_device
函数能够正确工作的前提
setup_epss_l3_sp_device
int setup_epss_l3_sp_device(struct device *dev, struct dcvs_hw *hw,struct dcvs_path *path)
{return setup_epss_l3_device(dev, hw, path, true);
}
populate_shared_offset
qcom,shared-offset = <0x0090>;
static int populate_shared_offset(struct device *dev, u32 *offset)
{int ret;ret = of_property_read_u32(dev->of_node, "qcom,shared-offset", offset);if (ret < 0) {dev_err(dev, "Error reading shared offset: %d\n", ret);return ret;}return ret;
}
读取一个设备树,如果有就return 0;并赋值与l3_shared_offset;
setup_epss_l3_percpu_device
int setup_epss_l3_percpu_device(struct device *dev, struct dcvs_hw *hw,struct dcvs_path *path)
{return setup_epss_l3_device(dev, hw, path, false);
}
populate_percpu_offsets
#define PERCPU_OFFSETS "qcom,percpu-offsets"
/*** populate_percpu_offsets - 从设备树读取每个CPU的L3缓存寄存器偏移量* @dev: 设备结构指针* @cpu_offsets: CPU偏移量数组输出参数(将被分配并填充)** 该函数负责从设备树中获取每个CPU对应的L3缓存控制寄存器偏移量。* 在支持每个CPU独立L3缓存的系统中,不同CPU可能需要写入不同的寄存器* 偏移量来控制各自的L3缓存频率。此函数确保系统正确识别这些偏移量。*/
static int populate_percpu_offsets(struct device *dev, u32 **cpu_offsets)
{int ret, len;struct device_node *of_node = dev->of_node; // 初始化为当前设备节点/* * 1. 尝试通过phandle获取包含偏移量的设备节点* 设备树中可能使用phandle指向另一个包含偏移量表的节点* of_parse_phandle: 解析指定属性的phandle,返回指向目标节点的指针* 如果解析成功,of_node将指向新的设备节点;否则保持原值*/of_node = of_parse_phandle(of_node, PERCPU_OFFSETS, 0);if (!of_node)of_node = dev->of_node; // 如果phandle不存在,继续使用当前设备节点/* * 2. 检查是否存在偏移量属性* of_find_property: 在设备节点中查找指定属性* 如果找到,len将包含属性值的字节长度*/if (!of_find_property(of_node, PERCPU_OFFSETS, &len)) {dev_err(dev, "Unable to find percpu offsets prop!\n");ret = -EINVAL;goto out; // 属性不存在,跳转到清理部分}/* * 3. 计算偏移量数量* len: 属性值的总字节长度* sizeof(**cpu_offsets): 每个偏移量的大小(4字节,u32类型)* len /= sizeof(**cpu_offsets): 计算偏移量的数量*/len /= sizeof(**cpu_offsets);/* * 4. 验证偏移量数量是否与系统CPU数量匹配* num_possible_cpus(): 获取系统支持的最大CPU数量* 如果偏移量数量与CPU数量不匹配,说明设备树配置错误*/if (len != num_possible_cpus()) {dev_err(dev, "Invalid percpu offsets table len=%d\n", len);ret = -EINVAL;goto out; // 验证失败,跳转到清理部分}/* * 5. 分配内存存储偏移量* devm_kzalloc: 设备管理的内存分配(设备移除时自动释放)* 分配大小: len * sizeof(**cpu_offsets) (每个偏移量4字节)* GFP_KERNEL: 内存分配标志(常规内核内存分配)*/*cpu_offsets = devm_kzalloc(dev, len * sizeof(**cpu_offsets),GFP_KERNEL);if (!*cpu_offsets) {ret = -ENOMEM;goto out; // 内存分配失败,跳转到清理部分}/* * 6. 从设备树读取偏移量数组* of_property_read_u32_array: 读取设备树中的u32数组* 参数:* - of_node: 设备节点* - PERCPU_OFFSETS: 属性名称* - *cpu_offsets: 目标数组指针* - len: 要读取的元素数量*/ret = of_property_read_u32_array(of_node, PERCPU_OFFSETS, *cpu_offsets,len);if (ret < 0) {dev_err(dev, "Error reading percpu offsets from DT: %d\n", ret);goto out; // 读取失败,跳转到清理部分}out:/* * 7. 清理设备节点引用* 如果之前通过phandle获取了新的设备节点,需要释放其引用* of_node_put: 减少设备节点的引用计数,可能释放节点*/if (of_node != dev->of_node)of_node_put(of_node);/* 返回结果: 0成功,负值表示错误 */return ret;
}
1. 设备树集成
- 该函数实现了与设备树的深度集成,从设备树中提取硬件配置信息
- 支持两种设备树结构:
- 直接在当前设备节点中定义
qcom,percpu-offsets
属性 - 通过 phandle 引用另一个包含该属性的设备节点
- 直接在当前设备节点中定义
2. CPU偏移量验证
- 数量验证:确保偏移量数量与系统CPU数量精确匹配
if (len != num_possible_cpus()) {dev_err(dev, "Invalid percpu offsets table len=%d\n", len);ret = -EINVAL;goto out;
}
- 防止因设备树配置错误导致的系统不稳定
- 确保每个CPU都有对应的L3控制寄存器偏移量
setup_epss_l3_device
该函数在dcvs.c 的probe函数中被调用到,这个是慢速路径:
/*** setup_epss_l3_device - 设置EPSS L3缓存控制设备* @dev: 设备结构指针,代表当前设备* @hw: DCVS硬件描述符,包含L3硬件特性* @path: DCVS路径结构,用于频率控制* @shared: 布尔标志,true=共享L3模式,false=每个CPU的L3模式** 该函数负责初始化EPSS L3缓存控制机制,设置适当的寄存器偏移量* 和提交函数,使系统能够动态调整L3缓存频率。*/
static int setup_epss_l3_device(struct device *dev, struct dcvs_hw *hw,struct dcvs_path *path, bool shared)
{int ret = 0; // 初始化返回值为0(成功)/* 获取互斥锁,确保多线程环境下的安全初始化 */mutex_lock(&epss_lock);/* 检查全局epss_data是否已初始化 */if (!epss_data)/* 如果未初始化,调用init_epss_data进行初始化 */ret = init_epss_data(dev);/* 释放互斥锁 */mutex_unlock(&epss_lock);/* 检查初始化是否成功 */if (ret < 0)return ret; // 如果失败,返回错误码/* 根据shared参数选择不同的配置方式 */if (shared) {/* 共享L3模式配置:* - 所有CPU共享同一个L3缓存* - 从设备树读取共享偏移量*/ret = populate_shared_offset(dev, &epss_data->l3_shared_offset);/* 设置共享L3的频率提交函数 */path->commit_dcvs_freqs = commit_epss_l3_shared;} else {/* 每个CPU L3模式配置:* - 每个CPU有自己独立的L3缓存* - 从设备树读取每个CPU的偏移量数组*/ret = populate_percpu_offsets(dev, &epss_data->l3_percpu_offsets);/* 设置每个CPU L3的频率提交函数 */path->commit_dcvs_freqs = commit_epss_l3_percpu;}/* 检查配置是否成功 */if (ret < 0)return ret; // 如果失败,返回错误码/* 将EPSS设备数据关联到DCVS路径结构 */path->data = epss_data;/* 返回成功状态 */return ret;
}
commit_epss_l3
/*** commit_epss_l3 - 提交L3频率请求* @path: DCVS路径结构* @freqs: 频率请求* @update_mask: 更新掩码* @shared: 是否为共享L3** 将软件请求的频率转换为频率表中的索引,并写入硬件寄存器。* 这是EPSS L3控制的核心函数。*/
static int commit_epss_l3(struct dcvs_path *path, struct dcvs_freq *freqs,u32 update_mask, bool shared)
{struct dcvs_hw *hw = path->hw;struct epss_dev_data *d = path->data;int cpu;u32 idx, offset;/* 在频率表中查找最接近的频率索引 */for (idx = 0; idx < hw->table_len; idx++)if (freqs->ib <= hw->freq_table[idx])break;/* 处理L3类型 */if (hw->type == DCVS_L3) {if (shared)offset = d->l3_shared_offset; // 共享L3使用共享偏移量else {cpu = smp_processor_id(); // 获取当前CPU IDoffset = d->l3_percpu_offsets[cpu]; // 使用CPU特定偏移量}/* 将频率索引写入L3寄存器 */writel_relaxed(idx, d->l3_base + offset);}/* 更新当前频率记录 */path->cur_freq.ib = freqs->ib;return 0;
}
函数核心作用
commit_epss_l3
函数是 EPSS L3 缓存控制机制的核心,主要完成以下任务:
1. 频率映射转换
- 将软件请求的带宽值(
freqs->ib
)转换为频率表中的索引 - 采用"向上取整"策略:找到第一个大于或等于请求值的频率
- 这确保系统获得不低于请求的性能水平
2. 硬件寄存器写入
- 根据系统配置(共享L3或每个CPU L3)确定正确的寄存器偏移量
- 将频率索引写入L3控制寄存器,实际改变L3缓存的工作频率
- 使用
writel_relaxed
实现高效写入(不强制内存屏障,提高性能)
3. 状态同步
- 更新
path->cur_freq.ib
记录当前生效的带宽值 - 保持软件状态与硬件状态一致,为后续操作提供准确参考
技术细节
频率表查找机制
- 频率表按升序排列(从低到高)
- 使用简单的线性搜索(因为表很小,通常不超过40项)
- 例如:如果频率表为[300, 400, 600, 800]MHz,请求500MHz,则返回索引2(600MHz)
两种L3架构支持
- 共享L3架构:
- 所有CPU共享同一个L3缓存
- 使用单一偏移量
d->l3_shared_offset
- 适用于较老的或简单的SoC设计
- 每个CPU L3架构:
- 每个CPU有自己独立的L3缓存
- 使用
smp_processor_id()
获取当前CPU ID - 从数组
d->l3_percpu_offsets
中查找对应偏移量 - 适用于现代多集群CPU设计(如big.LITTLE架构)
populate_l3_table
该函数在qcom_dcvs_hw_probe中使用:
/*** populate_l3_table - 从硬件中读取并解析L3缓存频率表* @dev: 设备结构指针* @freq_table: 频率表输出参数(单位kHz),函数将分配内存并填充** 该函数负责从硬件寄存器中读取L3缓存频率表,解析硬件编码的频率值,* 并构建软件可用的频率表。频率表用于L3缓存的动态频率调整,* 是DCVS(Dynamic Clock and Voltage Scaling)系统的关键数据。*/
int populate_l3_table(struct device *dev, u32 **freq_table)
{int idx, ret, len;u32 data, src, mult;unsigned long freq, prev_freq = 0;struct resource res;void __iomem *ftbl_base; // 频率表基地址(内存映射后)unsigned int ftbl_row_size; // 频率表每行大小(字节)u32 *tmp_l3_table; // 临时存储解析的频率表/* * 1. 在设备树中查找"l3tbl-base"资源* of_property_match_string: 在"reg-names"属性中查找"l3tbl-base"的索引* "l3tbl-base"是设备树中定义的L3频率表基地址的名称*/idx = of_property_match_string(dev->of_node, "reg-names", "l3tbl-base");if (idx < 0) {dev_err(dev, "Unable to find l3tbl-base: %d\n", idx);return -EINVAL; // 未找到资源返回错误}/* * 2. 获取资源地址和大小* of_address_to_resource: 将设备树中的地址转换为资源结构* 从设备树节点获取指定索引(idx)的资源信息*/ret = of_address_to_resource(dev->of_node, idx, &res);if (ret < 0) {dev_err(dev, "Unable to get resource from address: %d\n", ret);return -EINVAL;}/* * 3. 映射频率表到内存空间* ioremap: 将物理地址(res.start)映射到内核虚拟地址空间* resource_size(&res): 获取资源区域的大小* ftbl_base: 映射后的虚拟地址,用于访问硬件频率表*/ftbl_base = ioremap(res.start, resource_size(&res));if (!ftbl_base) {dev_err(dev, "Unable to map l3tbl-base!\n");return -ENOMEM; // 内存映射失败}/* * 4. 从设备树读取频率表行大小(可选)* 默认使用FTBL_ROW_SIZE(4字节)* 某些平台可能有不同的表结构*/ret = of_property_read_u32(dev->of_node, "qcom,ftbl-row-size",&ftbl_row_size);if (ret < 0)ftbl_row_size = FTBL_ROW_SIZE; // 使用默认值/* * 5. 临时分配内存存储解析的频率表* kcalloc: 分配并清零内存* MAX_L3_ENTRIES: 频率表最大条目数(40)* GFP_KERNEL: 内核内存分配标志*/tmp_l3_table = kcalloc(MAX_L3_ENTRIES, sizeof(*tmp_l3_table), GFP_KERNEL);if (!tmp_l3_table) {iounmap(ftbl_base); // 清理已映射的资源return -ENOMEM; // 内存分配失败}/* * 6. 解析硬件频率表* 遍历频率表的每个条目,直到检测到表结束*/for (idx = 0; idx < MAX_L3_ENTRIES; idx++) {/* 6.1 读取频率表行 */data = readl_relaxed(ftbl_base + idx * ftbl_row_size);/* 6.2 提取源时钟信息(31-30位) */src = ((data & SRC_MASK) >> SRC_SHIFT);/* 6.3 提取倍频因子(7-0位) */mult = (data & MULT_MASK);/* 6.4 计算实际频率 */freq = src ? XO_HZ * mult : INIT_HZ;/* * 解释:* - 如果src != 0: 使用XO时钟(19.2MHz) * 倍频因子* - 如果src == 0: 使用初始频率(300MHz)*//* 6.5 检测表结束条件 *//* 两个连续相同的频率表示表结束 */if (idx > 0 && prev_freq == freq)break;/* 6.6 转换为kHz并存储 */tmp_l3_table[idx] = freq / 1000UL; // 转换为kHzprev_freq = freq; // 保存当前频率用于下一次比较}len = idx; // 记录实际频率表长度/* * 7. 分配设备管理的内存存储最终频率表* devm_kzalloc: 设备管理的内存分配(设备移除时自动释放)*/*freq_table = devm_kzalloc(dev, len * sizeof(**freq_table), GFP_KERNEL);if (!*freq_table) {iounmap(ftbl_base); // 清理资源return -ENOMEM;}/* 8. 复制频率表到目标内存 */for (idx = 0; idx < len; idx++)(*freq_table)[idx] = tmp_l3_table[idx];/* 9. 清理临时资源 */iounmap(ftbl_base); // 取消内存映射kfree(tmp_l3_table); // 释放临时内存/* 10. 返回频率表长度 */return len;
}
1. 硬件频率表结构
- 频率表存储在特定硬件区域("l3tbl-base")
- 每个表项包含:
- 源时钟选择(src): 31-30位,决定使用哪种时钟源
- 倍频因子(mult): 7-0位,决定频率倍数
- 频率计算公式:
freq = (src == 0) ? INIT_HZ : (XO_HZ * MULT)
2. 频率表解析过程
- 定位硬件资源:
- 通过设备树查找"l3tbl-base"资源
- 将物理地址映射到内核虚拟地址空间
- 读取表结构:
- 获取每行大小(通常为4字节)
- 逐行读取表项数据
- 解析表项:
- 提取源时钟信息和倍频因子
- 计算实际频率值
- 转换为kHz单位
- 表结束检测:
- 当连续出现两个相同频率时,认为表已结束
- 这是硬件设计的表结束标记
3. 内存管理
-
临时内存: 使用 kcalloc
分配临时缓冲区
- 用于解析过程中的中间存储
- 函数返回前释放
-
永久内存: 使用 devm_kzalloc
分配设备管理内存
- 用于存储最终的频率表
- 设备移除时自动释放,避免内存泄漏
4. 错误处理
- 每个关键步骤都有错误检查
- 资源清理路径统一(通过
iounmap
和kfree
) - 适当的错误日志记录
- 错误时返回标准Linux错误码