udelay详解

 1 loops_per_jiffy初始化

loops_per_jiffy主要用于udelay中延时计算,表示的是一个tick时间,需要loop多少次。

在系统初始化的时候,会对该值进行处理:

start_kernel

        ---------->calibrate_delay

 

void __cpuinit calibrate_delay(void)
{
	unsigned long lpj;
	static bool printed;
	int this_cpu = smp_processor_id();

	if (per_cpu(cpu_loops_per_jiffy, this_cpu)) {
		lpj = per_cpu(cpu_loops_per_jiffy, this_cpu);
		if (!printed)
			pr_info("Calibrating delay loop (skipped) "
				"already calibrated this CPU");
	} else if (preset_lpj) {
		lpj = preset_lpj;
		if (!printed)
			pr_info("Calibrating delay loop (skipped) "
				"preset value.. ");
	} else if ((!printed) && lpj_fine) {
		lpj = lpj_fine;
		pr_info("Calibrating delay loop (skipped), "
			"value calculated using timer frequency.. ");
	} else if ((lpj = calibrate_delay_is_known())) {
		;
	} else if ((lpj = calibrate_delay_direct()) != 0) {
		if (!printed)
			pr_info("Calibrating delay using timer "
				"specific routine.. ");
	} else {
		if (!printed)
			pr_info("Calibrating delay loop... ");
		lpj = calibrate_delay_converge();
	}
	per_cpu(cpu_loops_per_jiffy, this_cpu) = lpj;
	if (!printed)
		pr_cont("%lu.%02lu BogoMIPS (lpj=%lu)\n",
			lpj/(500000/HZ),
			(lpj/(5000/HZ)) % 100, lpj);

	loops_per_jiffy = lpj;
	printed = true;
}

 初次调用,核心函数是这个calibrate_delay_converge,返回值用于初始化loops_per_jiffy

static unsigned long __cpuinit calibrate_delay_converge(void)
{
	/* First stage - slowly accelerate to find initial bounds */
	unsigned long lpj, lpj_base, ticks, loopadd, loopadd_base, chop_limit;
	int trials = 0, band = 0, trial_in_band = 0;

	lpj = (1<<12);

	/* wait for "start of" clock tick */
	ticks = jiffies;  //第一次计算loops_per_jiffy的值,这次计算只是一个粗略的计算,为下面的计算打好基础
	while (ticks == jiffies)
		; /* nothing */
	/* Go .. */
	ticks = jiffies;
	do {
		if (++trial_in_band == (1<<band)) {
			++band;
			trial_in_band = 0;
		}
		__delay(lpj * band);
		trials += band;
	} while (ticks == jiffies);//在一个滴答的开始时, 立即重复执行一个极短的循环,当一个滴答结束时,这个循环执行了多少次就是我们要求的初步的loops_per_jiffy的值。 也就是当重复短循环结束后如果jiffs增加,则说明这个循环次数是一个jiffy短循环次数估值,如果jiffy没有增加,则loops_per_jiffy再翻倍测试。


	/*
	 * We overshot, so retreat to a clear underestimate. Then estimate
	 * the largest likely undershoot. This defines our chop bounds.
	 */
	trials -= band;
	loopadd_base = lpj * band;
	lpj_base = lpj * trials;

recalibrate:
	lpj = lpj_base;
	loopadd = loopadd_base;

	/*
	 * Do a binary approximation to get lpj set to
	 * equal one clock (up to LPS_PREC bits)
	 */
//这 个值误差太大,所以我们还要经过第二次计算
	chop_limit = lpj >> LPS_PREC;
	while (loopadd > chop_limit) {
		lpj += loopadd;
		ticks = jiffies;
		while (ticks == jiffies)
			; /* nothing */
		ticks = jiffies;
		__delay(lpj);
		if (jiffies != ticks)	/* longer than 1 tick */
			lpj -= loopadd;
		loopadd >>= 1;
	}
	/*
	 * If we incremented every single time possible, presume we've
	 * massively underestimated initially, and retry with a higher
	 * start, and larger range. (Only seen on x86_64, due to SMIs)
	 */
	if (lpj + loopadd * 2 == lpj_base + loopadd_base * 2) {
		lpj_base = lpj;
		loopadd_base <<= 2;
		goto recalibrate;
	}

	return lpj;
}

 上面的函数就是实现了一个算法,计算每个tick之间,需要多少个loop。loops_per_jiffy的单位即为__delay,也就是说一个loop就是一个__delay。__delay实现就是将参数一直subs递减,反复跳转。所以我的理解,一个loop就是一条arm递减指令+跳转指令。

更详细的说明,参考这篇文章:

https://blog.csdn.net/skyflying2012/article/details/16367983

 2 udelay实现

内核开发中经常用到延时函数,最熟悉的是mdelay msleep。这2个函数在实现上有着天壤之别。
msleep实现是基于调度,延时期间调用schedule_timeout产生调度,待时间到期后继续运行,该函数实现在kernel/timer.c中。

由于linux内核不是实时系统,因此涉及调度的msleep肯定不会精确。
今天不细说msleep,有时间再来分析它,今天重点来学习mdelay。
mdelay是使用最多的延时函数。它的实现是忙循环,利用了内核loop_peer_jiffy,延时相对于msleep更加准确。

mdelay ndelay都是基于udelay来实现的。在include/linux/delay.h中,如下:
 

#ifndef MAX_UDELAY_MS
#define MAX_UDELAY_MS   5
#endif
 
#ifndef mdelay
#define mdelay(n) (\
    (__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : \
    ({unsigned long __ms=(n); while (__ms--) udelay(1000);}))
#endif
 
#ifndef ndelay
static inline void ndelay(unsigned long x)
{
    udelay(DIV_ROUND_UP(x, 1000));
}
#define ndelay(x) ndelay(x)
#endif
 
#define DIV_ROUND_UP(n,d) (((n) + (d) - 1) / (d))

gcc的内建函数__builtin_constant_p用于判断n是否为编译时常数,如果n是常数,返回 1,否则返回 0。
mdelay实现,如果参数为常数,且小于5,则直接调用udelay,说明udelay最大支持5000us延时。否则则循环调用udelay达到延时目的。

ndelay实现可以看出非常不精确,经过计算调用udelay。因此ndelay最少也是延时1us。


所以接下来来看udelay实现。这里讨论基于ARM处理器架构的实现,udelay实现在arch/arm/include/asm/delay.h中。
 

#define MAX_UDELAY_MS 2
 
#define udelay(n)                           \
    (__builtin_constant_p(n) ?                  \
      ((n) > (MAX_UDELAY_MS * 1000) ? __bad_udelay() :      \
            __const_udelay((n) * ((2199023U*HZ)>>11))) :    \
      __udelay(n))

最终会调用__const_udelay或者__udelay,2者实现在arch/arm/lib/delay.s中,如下:

.LC0:       .word   loops_per_jiffy
.LC1:       .word   (2199023*HZ)>>11
 
 
/*
 * r0  <= 2000
 * lpj <= 0x01ffffff (max. 3355 bogomips)
 * HZ  <= 1000
 */
 
 
ENTRY(__udelay)
        ldr r2, .LC1
        mul r0, r2, r0
ENTRY(__const_udelay)               @ 0 <= r0 <= 0x7fffff06
        mov r1, #-1
        ldr r2, .LC0
        ldr r2, [r2]        @ max = 0x01ffffff
        add r0, r0, r1, lsr #32-14
        mov r0, r0, lsr #14     @ max = 0x0001ffff
        add r2, r2, r1, lsr #32-10
        mov r2, r2, lsr #10     @ max = 0x00007fff
        mul r0, r2, r0      @ max = 2^32-1
        add r0, r0, r1, lsr #32-6
        movs    r0, r0, lsr #6
        moveq   pc, lr
 
 
 
 
上面这段汇编运算规则可以总结为下面这个计算公式,n为传入参数:
loops = ( ( (n *((2199023*HZ)>>11)) >> 14 ) * (loops_per_jiffy >> 10) ) >> 6 
 
 
/*
 * loops = r0 * HZ * loops_per_jiffy / 1000000
 *
 * Oh, if only we had a cycle counter...
 */
 
 
@ Delay routine
ENTRY(__delay)
        subs    r0, r0, #1
        bhi __delay
        mov pc, lr
ENDPROC(__udelay)
ENDPROC(__const_udelay)
ENDPROC(__delay)

 

__udelay的实现利用了loop_per_jiffy,该变量是内核全局变量,在内核启动时调用calibrate_delay计算得出,表示处理器在一个jiffy中loop数。
calibrate-delay实现在下面一节介绍。

loop_per_jiffy内核下转换为bogoMIPS反馈给用户,我们执行命令cat /proc/cpuinfo,可以看到bogoMIPS,表征处理器每秒执行百万指令数,是一个cpu性能测试数。

根据上面汇编实现可以看出,先计算出延时us所需的loop数,最后调用__delay循环递减完成延时,很明显,udelay实现最终就是一个处理器忙循环。

但是对于__udelay实现最大的疑问在于有一个奇怪的数字(2199023*HZ)>>11是什么意思,并且汇编中实现的计算规则各种移位又是什么意思呢。

首先最常规的方式,借助loop_per_jiffy根据延时us计算loop数,计算公式应该是汇编注释中那样:
 loops = n * HZ * loops_per_jiffy / 1000000
 HZ表征内核每秒jiffy个数,则HZ*loops_per_jiffy/1000000代表了1us中的loop数
查找各种资料找到原因,对于处理器这个公式有一个极大的缺陷,如果处理器没有浮点处理单元,即非浮点处理器(整型处理器),运行时,这个公式计算很容易变为0。
因为除数1000000极大,loops_per_jiffy * HZ / 1000000=0。无能你想要延迟多少微秒,总为0。
内核的解决方法是,除1000000变为乘1/1000000,为保持精度,1/1000000要先左移30位, 变为
(1/1000000)<<30  =  2^30 / 1000000 = 2199023U>>11

这就明白了(2199023*HZ)>>11来源啦

汇编中出现的反复移位则是为了把2199023U>>11实现中向左移的30位移回来。考虑到溢出,所以分成了>>14 , >>10, >>6,最后等同于 >>30

到此处就彻底明白汇编实现的loops计算公式的巧妙之处了,也就明白了arm的udelay实现方法。

可以看出内核在处理大数据除法运算时不直接除,而是运用了移位运算,我理解原因可能有两点:
(1)如上面遇到的问题,精度问题,除数很大,计算结果可能出现0.
(2)之前驱动开发中遇到的一种情况,内核编译时编译器对于除法会替换为gcc.so库的数学运算函数__aeabi_ldivmod,但是内核编译不依赖任何库,所以会出现编译错误。倒是可以使用内核提供的do_div替换。


udelay分析就到这里,2点小启发:
(1)内核的delay函数实现的确就是个忙循环。不同于sleep函数。
(2)内核开发中使用除法运算时要考虑清楚哦。
 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值