ARM64架构下PMCCNTR频率与CPU频率不匹配的原因及解决方法
树莓派3B+ PMCCNTR频率与CPU频率不匹配的问题解决
问题场景
在树莓派3B+上尝试测量单条指令的CPU周期,已通过以下内核态代码启用用户态对PMCCNTR的访问:
static inline u32 armv8pmu_pmcr_read(void) { u64 val=0; asm volatile("mrs %0, pmcr_el0" : "=r" (val)); return (u32)val; } static inline void armv8pmu_pmcr_write(u32 val) { val &= 0x3f; isb(); asm volatile("msr pmcr_el0, %0" : : "r" ((u64)val)); } static inline void enable_cpu_counters(void* data) { asm volatile("msr pmuserenr_el0, %0" : : "r"(0xf)); armv8pmu_pmcr_write((1 << 6) | (1 << 0)); asm volatile("msr PMCNTENSET_EL0, %0" :: "r" ((u32)(1 << 31))); armv8pmu_pmcr_write(armv8pmu_pmcr_read() | (1 << 0) | (1 << 6)); asm volatile("isb"); asm volatile("msr pmccfiltr_el0, %0" :: "r" (1 << 27)); printk("\nCPU:%d ", smp_processor_id()); } static inline void enable_pmu_pmccntr(void) { on_each_cpu(enable_cpu_counters, NULL, 1); printk(KERN_INFO "Enable Access PMU Initialized"); }
同时通过config.txt固定CPU频率为1GHz:
arm_freq=1000 over_voltage=4 core_freq=500 force_turbo=1
使用以下测试代码验证时,发现计数器增长值与cntfeq(系统计数器频率)一致,而非CPU的1GHz:
int main() { u64 begin,end; begin = arch_counter_get_cntpct(); sleep(1); end= arch_counter_get_cntpct(); printf("The count is %llu.\n",end-begin); return 0; }
原因分析
测试代码混淆了计数器类型
arch_counter_get_cntpct()读取的是系统计数器(CNTPCT_EL0),这个计数器的频率由cntfeq定义,是固定的系统时钟频率,和CPU核心频率无关。需要读取的是PMU的周期计数器PMCCNTR_EL0,而非系统计数器。PMCCNTR默认时钟源为系统时钟
树莓派3B+搭载的Cortex-A53架构中,PMCCNTR的时钟源由PMCR_EL0寄存器的D位(第3位)控制:D=0(默认):计数器使用系统时钟(与cntfeq一致)D=1:计数器使用CPU核心时钟(与arm_freq一致)
当前代码未配置D位,因此PMCCNTR默认使用系统时钟,即使正确读取,计数频率也不会匹配CPU频率。
解决方法
1. 修正测试代码:读取PMCCNTR而非系统计数器
在用户态实现读取PMCCNTR_EL0的函数:
static inline u64 read_pmccntr(void) { u64 val; // 读取PMCCNTR_EL0寄存器 asm volatile("mrs %0, pmccntr_el0" : "=r"(val)); return val; }
同时替换测试代码中的计数器读取函数,并且避免使用sleep()(sleep会让CPU进入休眠状态,若使用CPU核心时钟,计数器会停止计数),改用忙等测试:
int main() { u64 begin, end; begin = read_pmccntr(); // 忙等一段时间,确保CPU保持运行 for (volatile int i = 0; i < 100000000; i++); end = read_pmccntr(); printf("The count is %llu.\n", end - begin); return 0; }
2. 修改PMU配置,让PMCCNTR使用CPU核心时钟
修改enable_cpu_counters函数中armv8pmu_pmcr_write的参数,添加D位(1<<3)的配置:
static inline void enable_cpu_counters(void* data) { asm volatile("msr pmuserenr_el0, %0" : : "r"(0xf)); // 配置PMCR_EL0:设置D位(使用CPU核心时钟)、E位(启用计数器)、C位(复位计数器) armv8pmu_pmcr_write((1 << 6) | (1 << 3) | (1 << 0)); asm volatile("msr PMCNTENSET_EL0, %0" :: "r" ((u32)(1 << 31))); // 再次写入时保留D位 armv8pmu_pmcr_write(armv8pmu_pmcr_read() | (1 << 3) | (1 << 0) | (1 << 6)); asm volatile("isb"); asm volatile("msr pmccfiltr_el0, %0" :: "r" (1 << 27)); printk("\nCPU:%d ", smp_processor_id()); }
这里(1<<3)就是设置D位,让PMCCNTR使用CPU核心时钟。
3. 验证配置生效
重新编译内核模块并加载,运行测试代码,此时计数器增长值应接近CPU频率(1GHz)乘以忙等时间,比如100000000次循环大约对应0.1秒,计数器增长约100M左右。
内容的提问来源于stack exchange,提问作者Lqs66
相关产品推荐
相关产品推荐

