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 = "S3C2410Timer Tick", 141 .flags = SA_INTERRUPT | SA_TIMER, 142 .handler =s3c2410_timer_interrupt, 143}; 252staticvoid __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(intirq, 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(structclocksource *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(unsignedlongdelta,
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(enumclock_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中斷即知:
[email protected]:~$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(InterProcessor Interrupt,處理器間中斷)廣播到其他核。對於ARM來講,1號IPIIPI_TIMER就是來負責這個廣播的,從arch/arm/kernel/smp.c可以看出:
62enum 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 };
3. 中斷控制器驅動
在Linux內核中,各個設備驅動可以簡單地調用request_irq()、enable_irq()、disable_irq()、local_irq_disable()、local_irq_enable()等通用API完成中斷申請、使能、禁止等功能。在將Linux移植到新的SoC時,芯片供應商需要提供該部分API的底層支持。
local_irq_disable()、local_irq_enable()的實現與具體中斷控制器無關,對於ARMv6以上的體系架構而言,是直接調用CPSID/CPSIE指令進行,而對於ARMv6以前的體系結構,則是透過MRS、MSR指令來讀取和設置ARM的CPSR寄存器。由此可見,local_irq_disable()、local_irq_enable()針對的並不是外部的中斷控制器,而是直接讓CPU本身不響應中斷請求。相關的實現位於arch/arm/include/asm/irqflags.h:
- 11#if __LINUX_ARM_ARCH__ >= 6
- 12
- 13static inline unsigned longarch_local_irq_save(void)
- 14{
- 15 unsigned long flags;
- 16
- 17 asm volatile(
- 18 " mrs %0, cpsr @ arch_local_irq_save\n"
- 19 " cpsid i"
- 20 : "=r" (flags) : :"memory", "cc");
- 21 return flags;
- 22}
- 23
- 24static inline voidarch_local_irq_enable(void)
- 25{
- 26 asm volatile(
- 27 " cpsie i @ arch_local_irq_enable"
- 28 :
- 29 :
- 30 : "memory","cc");
- 31}
- 32
- 33static inline voidarch_local_irq_disable(void)
- 34{
- 35 asm volatile(
- 36 " cpsid i @ arch_local_irq_disable"
- 37 :
- 38 :
- 39 : "memory","cc");
- 40}
- 44#else
- 45
- 46/*
- 47 * Save the current interrupt enable state& disable IRQs
- 48 */
- 49static inline unsigned longarch_local_irq_save(void)
- 50{
- 51 unsigned long flags, temp;
- 52
- 53 asm volatile(
- 54 " mrs %0, cpsr @ arch_local_irq_save\n"
- 55 " orr %1, %0, #128\n"
- 56 " msr cpsr_c, %1"
- 57 : "=r" (flags),"=r" (temp)
- 58 :
- 59 : "memory","cc");
- 60 return flags;
- 61}
- 62
- 63/*
- 64 * Enable IRQs
- 65 */
- 66static inline voidarch_local_irq_enable(void)
- 67{
- 68 unsigned long temp;
- 69 asm volatile(
- 70 " mrs %0, cpsr @ arch_local_irq_enable\n"
- 71 " bic %0, %0, #128\n"
- 72 " msr cpsr_c, %0"
- 73 : "=r" (temp)
- 74 :
- 75 : "memory","cc");
- 76}
- 77
- 78/*
- 79 * Disable IRQs
- 80 */
- 81static inline voidarch_local_irq_disable(void)
- 82{
- 83 unsigned long temp;
- 84 asm volatile(
- 85 " mrs %0, cpsr @arch_local_irq_disable\n"
- 86 " orr %0, %0, #128\n"
- 87 " msr cpsr_c, %0"
- 88 : "=r" (temp)
- 89 :
- 90 : "memory","cc");
- 91}
- 92 #endif
與local_irq_disable()和local_irq_enable()不同,disable_irq()、enable_irq()針對的則是外部的中斷控制器。在內核中,透過irq_chip結構體來描述中斷控制器。該結構體內部封裝了中斷mask、unmask、ack等成員函數,其定義於include/linux/irq.h:
- 303struct irq_chip {
- 304 const char *name;
- 305 unsigned int (*irq_startup)(structirq_data *data);
- 306 void (*irq_shutdown)(struct irq_data *data);
- 307 void (*irq_enable)(struct irq_data *data);
- 308 void (*irq_disable)(struct irq_data *data);
- 309
- 310 void (*irq_ack)(struct irq_data *data);
- 311 void (*irq_mask)(structirq_data *data);
- 312 void (*irq_mask_ack)(struct irq_data *data);
- 313 void (*irq_unmask)(struct irq_data *data);
- 314 void (*irq_eoi)(struct irq_data *data);
- 315
- 316 int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest,bool force);
- 317 int (*irq_retrigger)(struct irq_data *data);
- 318 int (*irq_set_type)(struct irq_data *data,unsigned int flow_type);
- 319 int (*irq_set_wake)(struct irq_data *data, unsigned int on);
- 334};
- 1438static struct irq_chip sirfsoc_irq_chip = {
- 1439 .name = "sirf-gpio-irq",
- 1440 .irq_ack = sirfsoc_gpio_irq_ack,
- 1441 .irq_mask = sirfsoc_gpio_irq_mask,
- 1442 .irq_unmask = sirfsoc_gpio_irq_unmask,
- 1443 .irq_set_type = sirfsoc_gpio_irq_type,
- 1444};
我們只實現了其中的ack、mask、unmask和set_type成員函數,ack函數用於清中斷,mask、unmask用於中斷屏蔽和取消中斷屏蔽、set_type則用於配置中斷的觸發方式,如高電平、低電平、上升沿、下降沿等。至於enable_irq()的時候,雖然沒有實現irq_enable成員函數,但是內核會間接調用到irq_unmask成員函數,這點從kernel/irq/chip.c可以看出:
- 192void irq_enable(struct irq_desc *desc)
- 193{
- 194 irq_state_clr_disabled(desc);
- 195 if (desc->irq_data.chip->irq_enable)
- 196 desc->irq_data.chip->irq_enable(&desc->irq_data);
- 197 else
- 198 desc->irq_data.chip->irq_unmask(&desc->irq_data);
- 199 irq_state_clr_masked(desc);
- 200}
那麼,一般來講,在實際操作中,gpio0_0——gpio0_31這些引腳本身在第1級會使用中斷號28,而這些引腳本身的中斷號在實現GPIO控制器對應的irq_chip驅動時,我們又會把它映射到Linux系統的32——63號中斷。同理,gpio1_0——gpio1_31這些引腳本身在第1級會使用中斷號29,而這些引腳本身的中斷號在實現GPIO控制器對應的irq_chip驅動時,我們又會把它映射到Linux系統的64——95號中斷,以此類推。對於中斷號的使用者而言,無需看到這種2級映射關係。如果某設備想申請gpio1_0這個引腳對應的中斷,它只需要申請64號中斷即可。這個關係圖看起來如下:
還是以drivers/pinctrl/pinctrl-sirf.c的irq_chip部分爲例,我們對於每組GPIO都透過irq_domain_add_legacy()添加了相應的irq_domain,每組GPIO的中斷號開始於SIRFSOC_GPIO_IRQ_START + i *SIRFSOC_GPIO_BANK_SIZE,而每組GPIO本身佔用的第1級中斷控制器的中斷號則爲bank->parent_irq,我們透過irq_set_chained_handler" target="_blank">?