Linux芯片级移植与底层驱动(基于3.7.4内核) –内核节拍推荐

宋宝华 Barry Song 21cnbao@gmail.com

新浪微博: @宋宝华Barry

1. SoC Linux底层驱动的组成和现状

为了让Linux在一个全新的ARM SoC上运行,需要提供大量的底层支撑,如定时器节拍、中断控制器、SMP启动、CPU hotplug以及底层的GPIO、clock、pinctrl和DMA硬件的封装等。定时器节拍、中断控制器、SMP启动和CPU hotplug这几部分相对来说没有像早期GPIO、clock、pinctrl和DMA的实现那么杂乱,基本上有个固定的套路。定时器节拍为Linux基于时间片的调度机制以及内核和用户空间的定时器提供支撑,中断控制器的驱动则使得Linux内核的工程师可以直接调用local_irq_disable()、disable_irq()等通用的中断API,而SMP启动支持则用于让SoC内部的多个CPU核都投入运行,CPU hotplug则运行运行时挂载或拔除CPU。这些工作,在Linux 3.7内核中,进行了良好的层次划分和架构设计。

在GPIO、clock、pinctrl和DMA驱动方面,Linux 2.6时代,内核已或多或少有GPIO、clock等底层驱动的架构,但是核心层的代码太薄弱,各SoC对这些基础设施实现方面存在巨大差异,而且每个SoC仍然需要实现大量的代码。pinctrl和DMA则最为混乱,几乎各家公司都定义了自己的独特的实现和API。

社区必须改变这种局面,于是内核社区在2011~2012年进行了如下工作,这些工作在目前的3.7内核中基本准备就绪:

ST-Ericsson的工程师Linus Walleij提供了新的pinctrl驱动架构,内核新增加一个drivers/pinctrl目录,支撑SoC上的引脚复用,各个SoC的实现代码统一放入该目录;

TI的工程师Mike Turquette提供了common clk框架,让具体SoC实现clk_ops成员函数并通过clk_register、clk_register_clkdev注册时钟源以及源与设备对应关系,具体的clock驱动都统一迁移到drivers/clk目录;

建议各SoC统一采用dmaengine架构实现DMA驱动,该架构提供了通用的DMA通道API如dmaengine_prep_slave_single()、dmaengine_submit()等,要求SoC实现dma_device的成员函数 ,实现代码统一放入drivers/dma目录;

在GPIO方面,drivers/gpio下的gpiolib已能与新的pinctrl完美共存,实现引脚的GPIO和其他功能之间的复用,具体的SoC只需实现通用的gpio_chip结构体的成员函数。

经过以上工作,基本上就把芯片底层的基础架构方面的驱动的架构统一了,实现方法也统一了。另外,目前GPIO、clock、pinmux等功能都能良好的进行Device Tree的映射处理,譬如我们可以方面的在.dts中定义一个设备要的时钟、pinmux引脚以及GPIO。

除了上述基础设施以外,在将Linux移植入新的SoC过程中,工程师常常强烈依赖于早期的printk功能,内核则提供了相关的DEBUG_LL和EARLY_PRINTK支持,只需要SoC提供商实现少量的callback或宏。

本文主要对上述各个组成部分进行架构上的剖析以及关键的实现部分的实例分析,以求完整归纳将Linux移植入新SoC的主要工作。本文基于3.7.4内核。

2. 用于操作系统节拍的timer驱动

Linux 2.6的早期(2.6.21之前)基于tick设计,一般SoC公司在将Linux移植到自己的芯片上的时候,会从芯片内部找一个定时器,并将该定时器配置会HZ的频率,在每个时钟节拍到来时,调用ARM Linux内核核心层的timer_tick()函数,从而引发系统里的一系列行为。如2.6.17中arch/arm/mach-s3c2410/time.c的做法是:

127/*

128 * IRQ handler for the timer

129 */

130static irqreturn_t

131s3c2410_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)

132{

133 write_seqlock( xtime_lock);

134 timer_tick(regs);

135 write_sequnlock( xtime_lock);

136 return IRQ_HANDLED;

137}

138

139static struct irqaction s3c2410_timer_irq = {

140 .name = S3C2410 Timer Tick ,

141 .flags = SA_INTERRUPT | SA_TIMER,

142 .handler = s3c2410_timer_interrupt,

143};

252static void __init s3c2410_timer_init (void)

253{

254 s3c2410_timer_setup();

255 setup_irq(IRQ_TIMER4, s3c2410_timer_irq);

256}

257

当前Linux多采用tickless方案,并支持高精度定时器,内核的配置一般会使能NO_HZ(即tickless,或者说动态tick)和HIGH_RES_TIMERS。要强调的是tickless并不是说系统中没有时钟节拍了,而是说这个节拍不再像以前那样,周期性地产生。Tickless意味着,根据系统的运行情况,以事件驱动的方式动态决定下一个tick在何时发生。如果画一个时间轴,周期节拍的系统tick中断发生的时序看起来如下:

而NO_HZ的Linux看起来则是,2次定时器中断发生的时间间隔可长可短:

在当前的Linux系统中,SoC底层的timer被实现为一个clock_event_device和clocksource形式的驱动。在clock_event_device结构体中,实现其set_mode()和set_next_event()成员函数;在clocksource结构体中,主要实现read()成员函数。而定时器中断服务程序中,不再调用timer_tick(),而是调用clock_event_device的event_handler()成员函数。一个典型的SoC的底层tick定时器驱动形如:

61static irqreturn_t xxx_timer_interrupt(int irq, void *dev_id)

62{

63 struct clock_event_device *ce = dev_id;

65

70 ce- event_handler(ce);

71

72 return IRQ_HANDLED;

73}

74

75/* read 64-bit timer counter */

76static cycle_t xxx_timer_read(struct clocksource *cs)

77{

78 u64 cycles;

79

80 /* read the 64-bit timer counter */

81 cycles = readl_relaxed(xxx_timer_base + XXX_TIMER_LATCHED_HI);

83 cycles = (cycles 32) | readl_relaxed(xxx_timer_base + XXX_TIMER_LATCHED_LO);

84

85 return cycles;

86}

87

88static int xxx_timer_set_next_event(unsigned long delta,

89 struct clock_event_device *ce)

90{

91 unsigned long now, next;

92

93 writel_relaxed(XXX_TIMER_LATCH_BIT, xxx_timer_base + XXX_TIMER_LATCH);

94 now = readl_relaxed(xxx_timer_base + XXX_TIMER_LATCHED_LO);

95 next = now + delta;

96 writel_relaxed(next, xxx_timer_base + SIRFSOC_TIMER_MATCH_0);

97 writel_relaxed(XXX_TIMER_LATCH_BIT, xxx_timer_base + XXX_TIMER_LATCH);

98 now = readl_relaxed(xxx_timer_base + XXX_TIMER_LATCHED_LO);

99

100 return next – now delta ? -ETIME : 0;

101}

102

103static void xxx_timer_set_mode(enum clock_event_mode mode,

104 struct clock_event_device *ce)

105{

107 switch (mode) {

108 case CLOCK_EVT_MODE_PERIODIC:

109

111 case CLOCK_EVT_MODE_ONESHOT:

112

114 case CLOCK_EVT_MODE_SHUTDOWN:

115

117 case CLOCK_EVT_MODE_UNUSED:

118 case CLOCK_EVT_MODE_RESUME:

119 break;

120 }

121}

144static struct clock_event_device xxx_clockevent = {

145 .name = xxx_clockevent ,

146 .rating = 200,

147 .features = CLOCK_EVT_FEAT_ONESHOT,

148 .set_mode = xxx_timer_set_mode,

149 .set_next_event = xxx_timer_set_next_event,

150};

151

152static struct clocksource xxx_clocksource = {

153 .name = xxx_clocksource ,

154 .rating = 200,

155 .mask = CLOCKSOURCE_MASK(64),

156 .flags = CLOCK_SOURCE_IS_CONTINUOUS,

157 .read = xxx_timer_read,

158 .suspend = xxx_clocksource_suspend,

159 .resume = xxx_clocksource_resume,

160};

161

162static struct irqaction xxx_timer_irq = {

163 .name = xxx_tick ,

164 .flags = IRQF_TIMER,

165 .irq = 0,

166 .handler = xxx_timer_interrupt,

167 .dev_id = xxx_clockevent,

168};

169

176static void __init xxx_clockevent_init(void)

177{

178 clockevents_calc_mult_shift( xxx_clockevent, CLOCK_TICK_RATE, 60);

179

180 xxx_clockevent.max_delta_ns =

181 clockevent_delta2ns(-2, xxx_clockevent);

182 xxx_clockevent.min_delta_ns =

183 clockevent_delta2ns(2, xxx_clockevent);

184

185 xxx_clockevent.cpumask = cpumask_of(0);

186 clockevents_register_device( xxx_clockevent);

187}

188

189/* initialize the kernel jiffy timer source */

190static void __init xxx_timer_init(void)

191{

192

214

215 BUG_ON(clocksource_register_hz( xxx_clocksource, CLOCK_TICK_RATE));

218

219 BUG_ON(setup_irq(xxx_timer_irq.irq, xxx_timer_irq));

220

221 xxx_clockevent_init();

222}

249struct sys_timer xxx_timer = {

250 .init = xxx_timer_init,

251};

上述代码中,我们特别关注其中的如下函数:

clock_event_device的set_next_event 成员函数xxx_timer_set_next_event()

该函数的delta参数是Linux内核传递给底层定时器的一个差值,它的含义是下一次tick中断产生的硬件定时器中计数器counter的值相对于当前counter的差值。我们在该函数中将硬件定时器设置为在 当前counter计数值 + delta的时刻产生下一次tick中断。xxx_clockevent_init()函数中设置了可接受的最小和最大delta值对应的纳秒数,即xxx_clockevent.min_delta_ns和xxx_clockevent.max_delta_ns。

clocksource 的read成员函数xxx_timer_read()

该函数可读取出从开机以来到当前时刻定时器计数器已经走过的值,无论有没有设置计数器达到某值的时候产生中断,硬件的计数总是在进行的。因此,该函数给Linux系统提供了一个底层的准确的参考时间。

定时器的中断服务程序xxx_timer_interrupt()

在该中断服务程序中,直接调用clock_event_device的event_handler()成员函数,event_handler()成员函数的具体工作也是Linux内核根据Linux内核配置和运行情况自行设置的。

clock_event_device的set_mode成员函数 xxx_timer_set_mode()

用于设置定时器的模式以及resume和shutdown等功能,目前一般采用ONESHOT模式,即一次一次产生中断。当然新版的Linux也可以使用老的周期性模式,如果内核编译的时候未选择NO_HZ,该底层的timer驱动依然可以为内核的运行提供支持。

这些函数的结合,使得ARM Linux内核底层所需要的时钟得以运行。下面举一个典型的场景,假定定时器的晶振时钟频率为1MHz(即计数器每加1等于1us),应用程序透过nanosleep() API睡眠100us,内核会据此换算出下一次定时器中断的delta值为100,并间接调用到xxx_timer_set_next_event()去设置硬件让其在100us后产生中断。100us后,中断产生,xxx_timer_interrupt()被调用,event_handler()会间接唤醒睡眠的进程导致nanosleep()函数返回,从而用户进程继续。

这里特别要强调的是,对于多核处理器来说,一般的做法是给每个核分配一个独立的定时器,各个核根据自身的运行情况动态设置自己时钟中断发生的时刻。看看我们说运行的电脑的local timer中断即知:

barry@barry-VirtualBox:~$ cat /proc/interrupts

CPU0 CPU1 CPU2 CPU3

20: 945 0 0 0 IO-APIC-fasteoi vboxguest

21: 4456 0 0 21592 IO-APIC-fasteoi ahci, Intel 82801AA-ICH

22: 26 0 0 0 IO-APIC-fasteoi ohci_hcd:usb2

NMI: 0 0 0 0 Non-maskable interrupts

LOC: 177279 177517 177146 177139 Local timer interrupts

SPU: 0 0 0 0 Spurious interrupts

PMI: 0 0 0 0 Performance monitoring

而比较低效率的方法则是只给CPU0提供定时器,由CPU0将定时器中断透过IPI(Inter Processor Interrupt,处理器间中断)广播到其他核。对于ARM来讲,1号IPIIPI_TIMER就是来负责这个广播的,从arch/arm/kernel/smp.c可以看出:

62 enum ipi_msg_type {

63 IPI_WAKEUP,

64 IPI_TIMER,

65 IPI_RESCHEDULE,

66 IPI_CALL_FUNC,

67 IPI_CALL_FUNC_SINGLE,

68 IPI_CPU_STOP,

69 };

人生才会更有意义。如果没有梦想,那就托做庸人。

Linux芯片级移植与底层驱动(基于3.7.4内核) –内核节拍推荐

相关文章:

你感兴趣的文章:

标签云: