Linux晶片級移植與底層驅動(基於3.7.4核心)
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:
303structirq_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};
各個晶片公司會將晶片內部的中斷控制器實現為irq_chip驅動的形式。受限於中斷控制器硬體的能力,這些成員函式並不一定需要全部實現,有時候只需要實現其中的部分函式即可。譬如drivers/pinctrl/pinctrl-sirf.c驅動中的
1438staticstruct 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可以看出:
192voidirq_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}
在晶片內部,中斷控制器可能不止1個,多箇中斷控制器之間還很可能是級聯的。舉個例子,假設晶片內部有一箇中斷控制器,支援32箇中斷源,其中有4個來源於GPIO控制器外圍的4組GPIO,每組GPIO上又有32箇中斷(許多晶片的GPIO控制器也同時是一箇中斷控制器),其關係如下圖:
那麼,一般來講,在實際操作中,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()設定了第1級中斷髮生的時候,會呼叫鏈式IRQ處理函式sirfsoc_gpio_handle_irq():
1689 bank->domain =irq_domain_add_legacy(np, SIRFSOC_GPIO_BANK_SIZE,
1690 SIRFSOC_GPIO_IRQ_START+ i * SIRFSOC_GPIO_BANK_SIZE, 0,
1691 &sirfsoc_gpio_irq_simple_ops, bank);
1692
1693 if (!bank->domain) {
1694 pr_err("%s: Failedto create irqdomain\n", np->full_name);
1695 err = -ENOSYS;
1696 goto out;
1697 }
1698
1699 irq_set_chained_handler(bank->parent_irq, sirfsoc_gpio_handle_irq);
1700 irq_set_handler_data(bank->parent_irq, bank);
而在sirfsoc_gpio_handle_irq()函式的入口出呼叫chained_irq_enter()暗示自身進入鏈式IRQ處理,在函式體內判決具體的GPIO中斷,並透過generic_handle_irq()呼叫到最終的外設驅動中的中斷服務程式,最後呼叫chained_irq_exit()暗示自身退出鏈式IRQ處理:
1446staticvoid sirfsoc_gpio_handle_irq(unsigned int irq, struct irq_desc *desc)
1447{
1448 …
1454 chained_irq_enter(chip, desc);
1456 …
1477 generic_handle_irq(first_irq + idx);
1478 …
1484 chained_irq_exit(chip, desc);
1485}
很多中斷控制器的暫存器定義呈現出簡單的規律,如有一個mask暫存器,其中每1位可遮蔽1箇中斷等,這種情況下,我們無需實現1個完整的irq_chip驅動,可以使用核心提供的通用irq_chip驅動架構irq_chip_generic,這樣只需要實現極少量的程式碼,如arch/arm/mach-prima2/irq.c中,註冊CSRSiRFprimaII內部中斷控制器的程式碼僅為:
26static __init void
27sirfsoc_alloc_gc(void __iomem *base,unsigned int irq_start, unsigned int num)
28{
29 struct irq_chip_generic *gc;
30 struct irq_chip_type *ct;
31
32 gc = irq_alloc_generic_chip("SIRFINTC", 1, irq_start, base,handle_level_irq);
33 ct = gc->chip_types;
34
35 ct->chip.irq_mask = irq_gc_mask_clr_bit;
36 ct->chip.irq_unmask = irq_gc_mask_set_bit;
37 ct->regs.mask = SIRFSOC_INT_RISC_MASK0;
38
39 irq_setup_generic_chip(gc, IRQ_MSK(num), IRQ_GC_INIT_MASK_CACHE,IRQ_NOREQUEST, 0);
40}
特別值得一提的是,目前多數主流ARM晶片,內部的一級中斷控制器都使用了ARM公司的GIC,我們幾乎不需要實現任何程式碼,只需要在Device Tree中新增相關的結點並將gic_handle_irq()填入MACHINE的handle_irq成員。
如在arch/arm/boot/dts/exynos5250.dtsi即含有:
36 gic:[email protected] {
37 compatible = "arm,cortex-a9-gic";
38 #interrupt-cells = <3>;
39 interrupt-controller;
40 reg = <0x104810000x1000>, <0x10482000 0x2000>;
41 };
而在arch/arm/mach-exynos/mach-exynos5-dt.c中即含有:
95DT_MACHINE_START(EXYNOS5_DT, "SAMSUNGEXYNOS5 (Flattened Device Tree)")
96 /* Maintainer: Kukjin Kim <[email protected]> */
97 .init_irq =exynos5_init_irq,
98 .smp =smp_ops(exynos_smp_ops),
99 .map_io = exynos5250_dt_map_io,
100 .handle_irq = gic_handle_irq,
101 .init_machine =exynos5250_dt_machine_init,
102 .init_late =exynos_init_late,
103 .timer =&exynos4_timer,
104 .dt_compat =exynos5250_dt_compat,
105 .restart = exynos5_restart,
106MACHINE_END
4. SMP多核啟動以及CPU熱插拔驅動
在Linux系統中,對於多核的ARM晶片而言,Bootrom程式碼中,CPU0會率先起來,引導Bootloader和Linux核心執行,而其他的核則在上電時Bootrom一般將自身置於WFI或者WFE狀態,並等待CPU0給其發CPU核間中斷(IPI)或事件(一般透過SEV指令)喚醒之。一個典型的啟動過程如下圖:
被CPU0喚醒的CPUn可以在執行過程中進行熱插拔。譬如執行如下命令即可解除安裝CPU1並且將CPU1上的任務全部遷移到其他CPU:
# echo 0 >/sys/devices/system/cpu/cpu1/online
同樣地,執行如下命令可以再次啟動CPU1:
# echo 1 >/sys/devices/system/cpu/cpu1/online
之後CPU1會主動參與系統中各個CPU之間要執行任務的負載均衡工作。
CPU0喚醒其他 CPU的動作在核心中被封裝為一個smp_operations的結構體,該結構體的成員如下:
83struct smp_operations {
84#ifdef CONFIG_SMP
85 /*
86 * Setup the set of possible CPUs (via set_cpu_possible)
87 */
88 void (*smp_init_cpus)(void);
89 /*
90 * Initialize cpu_possible map, and enable coherency
91 */
92 void (*smp_prepare_cpus)(unsigned int max_cpus);
93
94 /*
95 * Perform platform specific initialisation of the specified CPU.
96 */
97 void (*smp_secondary_init)(unsigned int cpu);
98 /*
99 * Boot a secondary CPU, and assign it thespecified idle task.
100 * This also gives us the initial stack to use for this CPU.
101 */
102 int (*smp_boot_secondary)(unsigned int cpu, struct task_struct *idle);
103#ifdef CONFIG_HOTPLUG_CPU
104 int (*cpu_kill)(unsigned intcpu);
105 void (*cpu_die)(unsigned int cpu);
106 int (*cpu_disable)(unsigned intcpu);
107#endif
108#endif
109};
我們從arch/arm/mach-vexpress/v2m.c看到VEXPRESS電路板用到的smp_ops為vexpress_smp_ops:
666DT_MACHINE_START(VEXPRESS_DT,"ARM-Versatile Express")
667 .dt_compat = v2m_dt_match,
668 .smp =smp_ops(vexpress_smp_ops),
669 .map_io = v2m_dt_map_io,
670 .init_early =v2m_dt_init_early,
671 .init_irq = v2m_dt_init_irq,
672 .timer =&v2m_dt_timer,
673 .init_machine = v2m_dt_init,
674 .handle_irq = gic_handle_irq,
675 .restart = v2m_restart,
676MACHINE_END
透過arch/arm/mach-vexpress/platsmp.c的實現程式碼可以看出,smp_operations的成員函式smp_init_cpus() 即vexpress_smp_init_cpus()會探測SoC內CPU核的個數,並設定了核間通訊的方式為gic_raise_softirq()。可見於vexpress_smp_init_cpus()中呼叫的vexpress_dt_smp_init_cpus():
103staticvoid __init vexpress_dt_smp_init_cpus(void)
104{
…
128 for (i = 0; i < ncores; ++i)
129 set_cpu_possible(i, true);
130
131 set_smp_cross_call(gic_raise_softirq);
132}
而smp_operations的成員函式smp_prepare_cpus()即vexpress_smp_prepare_cpus()則會透過v2m_flags_set(virt_to_phys(versatile_secondary_startup))設定其他CPU的啟動地址為versatile_secondary_startup:
179staticvoid __init vexpress_smp_prepare_cpus(unsigned int max_cpus)
180{
181 …
189
190 /*
191 * Write the address of secondary startup into the
192 * system-wide flags register. The boot monitor waits
193 * until it receives a soft interrupt, and then the
194 * secondary CPU branches to this address.
195 */
196 v2m_flags_set(virt_to_phys(versatile_secondary_startup));
197}
注意這部分的具體實現方法是SoC相關的,由晶片的設計以及晶片內部的Bootrom決定。對於VEXPRESS來講,設定方法如下:
139void__init v2m_flags_set(u32 data)
140{
141 writel(~0, v2m_sysreg_base + V2M_SYS_FLAGSCLR);
142 writel(data, v2m_sysreg_base + V2M_SYS_FLAGSSET);
143}
即填充v2m_sysreg_base +V2M_SYS_FLAGSCLR地址為0xFFFFFFFF,將其他CPU初始啟動執行的指令地址填入v2m_sysreg_base +V2M_SYS_FLAGSSET。這2個地址屬於晶片實現時候設定的。填入的CPUn的起始地址都透過virt_to_phys()轉化為實體地址,因為此時CPUn的MMU尚未開啟。
比較關鍵的是smp_operations的成員函式smp_boot_secondary(),它完成最終的CPUn的喚醒工作:
27static void __cpuinit write_pen_release(intval)
28{
29 pen_release = val;
30 smp_wmb();
31 __cpuc_flush_dcache_area((void *)&pen_release, sizeof(pen_release));
32 outer_clean_range(__pa(&pen_release), __pa(&pen_release + 1));
33}
59int __cpuinitversatile_boot_secondary(unsigned int cpu, struct task_struct *idle)
60{
61 unsigned long timeout;
62
63 /*
64 * Set synchronisation state between this boot processor
65 * and the secondary one
66 */
67 spin_lock(&boot_lock);
68
69 /*
70 * This is really belt and braces; we hold unintended secondary
71 * CPUs in the holding pen until we're ready for them. However,
72 * since we haven't sent them a soft interrupt, they shouldn't
73 * be there.
74 */
75 write_pen_release(cpu_logical_map(cpu));
76
77 /*
78 * Send the secondary CPU a soft interrupt, thereby causing
79 * the boot monitor to read the system wide flags register,
80 * and branch to the address found there.
81 */
82 gic_raise_softirq(cpumask_of(cpu), 0);
83
84 timeout = jiffies + (1 * HZ);
85 while (time_before(jiffies, timeout)) {
86 smp_rmb();
87 if (pen_release == -1)
88 break;
89
90 udelay(10);
91 }
92
93 /*
94 * now the secondary core is starting up let it run its
95 * calibrations, then wait for it to finish
96 */
97 spin_unlock(&boot_lock);
98
99 return pen_release != -1 ? -ENOSYS : 0;
100}
上述程式碼中高亮的部分首先會將pen_release變數設定為要喚醒的CPU核的CPU號cpu_logical_map(cpu),而後透過gic_raise_softirq(cpumask_of(cpu), 0)給CPUcpu發起0號IPI,這個時候,CPUcpu核會從前面smp_operations中的smp_prepare_cpus()成員函式即vexpress_smp_prepare_cpus()透過v2m_flags_set()設定的其他CPU核的起始地址versatile_secondary_startup開始執行,如果順利的話,該CPU會將原先為正數的pen_release寫為-1,以便CPU0從等待pen_release成為-1的迴圈中跳出。
versatile_secondary_startup實現於arch/arm/plat-versatile/headsmp.S,是一段彙編:
21ENTRY(versatile_secondary_startup)
22 mrc p15, 0, r0, c0, c0, 5
23 and r0, r0, #15
24 adr r4, 1f
25 ldmia r4, {r5, r6}
26 sub r4, r4, r5
27 add r6, r6, r4
28pen: ldr r7, [r6]
29 cmp r7, r0
30 bne pen
31
32 /*
33 * we've been released from the holding pen: secondary_stack
34 * should now contain the SVC stack for this core
35 */
36 b secondary_startup
37
38 .align
391: .long .
40 .long pen_release
41ENDPROC(versatile_secondary_startup)
第1段高亮的部分實際上是等待pen_release成為CPU0設定的cpu_logical_map(cpu),一般直接就成立了。第2段高亮的部分則呼叫到核心通用的secondary_startup()函式,經過一系列的初始化如MMU等,最終新的被喚醒的CPU將呼叫到smp_operations的smp_secondary_init()成員函式,對於本例為versatile_secondary_init():
37void __cpuinitversatile_secondary_init(unsigned int cpu)
38{
39 /*
40 * if any interrupts are already enabled for the primary
41 * core (e.g. timer irq), then they will not have been enabled
42 * for us: do so
43 */
44 gic_secondary_init(0);
45
46 /*
47 * let the primary processor know we're out of the
48 * pen, then head off into the C entry point
49 */
50 write_pen_release(-1);
51
52 /*
53 * Synchronise with the boot thread.
54 */
55 spin_lock(&boot_lock);
56 spin_unlock(&boot_lock);
57}
上述程式碼中高亮的那1行會將pen_release寫為-1,於是CPU0還在執行的versatile_boot_secondary()函式中的如下迴圈就退出了:
85 while (time_before(jiffies, timeout)) {
86 smp_rmb();
87 if (pen_release == -1)
88 break;
89
90 udelay(10);
91 }
此後CPU0和新喚醒的其他CPU各自狂奔。整個系統在執行過程中會進行實時程序和正常程序的動態負載均衡。
CPU hotplug的實現也是晶片相關的,對於VEXPRESS而言,實現了smp_operations的cpu_die()成員函式即vexpress_cpu_die()。它會在進行CPUn的拔除操作時將CPUn投入低功耗的WFI狀態,相關程式碼位於arch/arm/mach-vexpress/hotplug.c:
90void __ref vexpress_cpu_die(unsigned intcpu)
91{
92 int spurious = 0;
93
94 /*
95 * we're ready for shutdown now, so do it
96 */
97 cpu_enter_lowpower();
98 platform_do_lowpower(cpu, &spurious);
99
100 /*
101 * bring this CPU back into the world of cache
102 * coherency, and then restore interrupts
103 */
104 cpu_leave_lowpower();
105
106 if (spurious)
107 pr_warn("CPU%u: %uspurious wakeup calls\n", cpu, spurious);
108}
57static inline void platform_do_lowpower(unsignedint cpu, int *spurious)
58{
59 /*
60 * there is no power-control hardware on this platform, so all
61 * we can do is put the core into WFI; this is safe as the calling
62 * code will have already disabled interrupts
63 */
64 for (;;) {
65 wfi();
66
67 if (pen_release ==cpu_logical_map(cpu)) {
68 /*
69 * OK, proper wakeup,we're done
70 */
71 break;
72 }
73
74 /*
75 * Getting here, means that wehave come out of WFI without
76 * having been woken up - thisshouldn't happen
77 *
78 * Just note it happening -when we're woken, we can report
79 * its occurrence.
80 */
81 (*spurious)++;
82 }
83}
CPUn睡眠於wfi(),之後再次online的時候,又會因為CPU0給它發出的IPI而從wfi()函式返回繼續執行,醒來時CPUn也判決了是否pen_release == cpu_logical_map(cpu)成立,以確定該次醒來確確實實是由CPU0喚醒的一次正常醒來。
5. DEBUG_LL和EARLY_PRINTK
在Linux啟動的早期,console驅動還沒有投入執行。當我們把Linux移植到一個新的SoC的時候,工程師一般非常需要早期就可以執行printk()功能以跟蹤除錯啟動過程。核心的DEBUG_LL和EARLY_PRINTK選項為我們提供了這樣的支援。而在Bootloader引導核心執行的bootargs中,則需要使能earlyprintk選項。
為了讓DEBUG_LL和EARLY_PRINTK可以執行,Linux核心中需實現早期解壓過程列印需要的putc()和後續的addruart、senduart和waituart等巨集。以CSR SiRFprimaII為例,putc()的實現位於arch/arm/mach-prima2/include/mach/uncompress.h:
22static __inline__ void putc(char c)
23{
24 /*
25 * during kernel decompression, all mappings are flat:
26 * virt_addr == phys_addr
27 */
28 while (__raw_readl((void __iomem *)SIRFSOC_UART1_PA_BASE +SIRFSOC_UART_TXFIFO_STATUS)
29 &SIRFSOC_UART1_TXFIFO_FULL)
30 barrier();
31
32 __raw_writel(c, (void __iomem *)SIRFSOC_UART1_PA_BASE +SIRFSOC_UART_TXFIFO_DATA);
33}
由於解壓過程中,MMU還沒有初始化,所以這個時候的列印是直接往UART埠FIFO對應的實體地址丟列印字元。
addruart、senduart和waituart等巨集的實現位於每個SoC對應的MACHINE程式碼目錄的include/mach/debug-macro.S,SiRFprimaII的實現mach-prima2/include/mach/debug-macro.S如下:
12 .macro addruart, rp, rv, tmp
13 ldr \rp,=SIRFSOC_UART1_PA_BASE @physical
14 ldr \rv,=SIRFSOC_UART1_VA_BASE @ virtual
15 .endm
16
17 .macro senduart,rd,rx
18 str \rd, [\rx,#SIRFSOC_UART_TXFIFO_DATA]
19 .endm
20
21 .macro busyuart,rd,rx
22 .endm
23
24 .macro waituart,rd,rx
251001: ldr \rd, [\rx,#SIRFSOC_UART_TXFIFO_STATUS]
26 tst \rd,#SIRFSOC_UART1_TXFIFO_EMPTY
27 beq 1001b
28 .endm
其中的senduart完成了往UART的FIFO丟列印字元的過程。waituart則相當於一個流量握手,等待FIFO為空。這些巨集最終會被核心的arch/arm/kernel/debug.S引用。
6. GPIO驅動
在drivers/gpio下實現了通用的基於gpiolib的GPIO驅動,其中定義了一個通用的用於描述底層GPIO控制器的gpio_chip結構體,並要求具體的SoC實現gpio_chip結構體的成員函式,最後透過gpiochip_add()註冊gpio_chip。
gpio_chip結構體封裝了底層的硬體的GPIO enable/disable等操作,它定義為:
94struct gpio_chip {
95 const char *label;
96 struct device *dev;
97 struct module *owner;
98
99 int (*request)(struct gpio_chip *chip,
100 unsigned offset);
101 void (*free)(struct gpio_chip *chip,
102 unsigned offset);
103
104 int (*direction_input)(struct gpio_chip *chip,
105 unsigned offset);
106 int (*get)(struct gpio_chip *chip,
107 unsigned offset);
108 int (*direction_output)(structgpio_chip *chip,
109 unsigned offset, int value);
110 int (*set_debounce)(struct gpio_chip *chip,
111 unsigned offset, unsigned debounce);
112
113 void (*set)(struct gpio_chip *chip,
114 unsigned offset, int value);
115
116 int (*to_irq)(struct gpio_chip *chip,
117 unsigned offset);
118
119 void (*dbg_show)(struct seq_file *s,
120 struct gpio_chip *chip);
121 int base;
122 u16 ngpio;
123 const char *const*names;
124 unsigned can_sleep:1;
125 unsigned exported:1;
126
127#if defined(CONFIG_OF_GPIO)
128 /*
129 * If CONFIG_OF is enabled, then all GPIOcontrollers described in the
130 * device tree automatically may have an OF translation
131 */
132 struct device_node *of_node;
133 int of_gpio_n_cells;
134 int (*of_xlate)(struct gpio_chip *gc,
135 const structof_phandle_args *gpiospec, u32 *flags);
136#endif
137};
透過這層封裝,每個具體的要用到GPIO的裝置驅動都使用通用的GPIO API來操作GPIO,這些API主要用於GPIO的申請、釋放和設定:
intgpio_request(unsigned gpio, const char *label);
voidgpio_free(unsigned gpio);
intgpio_direction_input(unsigned gpio);
intgpio_direction_output(unsigned gpio, int value);
intgpio_set_debounce(unsigned gpio, unsigned debounce);
intgpio_get_value_cansleep(unsigned gpio);
voidgpio_set_value_cansleep(unsigned gpio, int value);
intgpio_request_one(unsigned gpio, unsigned long flags, const char *label);
intgpio_request_array(const struct gpio *array, size_t num);
voidgpio_free_array(const struct gpio *array, size_t num);
intdevm_gpio_request(struct device *dev, unsigned gpio, const char *label);
intdevm_gpio_request_one(struct device *dev, unsigned gpio,
unsigned long flags,const char *label);
voiddevm_gpio_free(struct device *dev, unsigned int gpio);
注意,核心中針對記憶體、IRQ、時鐘、GPIO、pinctrl都有devm_開頭的API,使用這部分API的時候,核心會有類似於Java資源自動回收機制,因此在程式碼中做出錯處理時,無需釋放相關的資源。
對於GPIO而言,特別值得一提的是,核心會建立/sys結點 /sys/class/gpio/gpioN/,透過它我們可以echo值從而改變GPIO的方向、設定和獲取GPIO的值。
在擁有Device Tree支援的情況之下,我們可以透過Device Tree來描述某GPIO控制器提供的GPIO引腳被具體裝置使用的情況。在GPIO控制器對應的結點中,需定義#gpio-cells 和gpio-controller屬性,具體的裝置結點則透過xxx-gpios屬性來引用GPIO控制器結點及GPIO引腳。
如VEXPRESS電路板 DT檔案arch/arm/boot/dts/vexpress-v2m.dtsi中擁有如下GPIO控制器結點:
73 v2m_sysreg:[email protected] {
74 compatible ="arm,vexpress-sysreg";
75 reg = <0x000000x1000>;
76 gpio-controller;
77 #gpio-cells =<2>;
78 };
VEXPRESS電路板上的MMC控制器會使用該結點GPIO控制器提供的GPIO引腳,則具體的[email protected]裝置結點的會通過-gpios屬性引用GPIO:
111 [email protected] {
112 compatible ="arm,pl180", "arm,primecell";
113 reg =<0x05000 0x1000>;
114 interrupts =<9 10>;
115 cd-gpios = <&v2m_sysreg 0 0>;
116 wp-gpios =<&v2m_sysreg 1 0>;
117 …
121 };
其中的cd-gpios用於SD/MMC卡的detection,而wp-gpios用於防寫,MMC主機控制器驅動會透過如下方法獲取這2個GPIO,詳見於drivers/mmc/host/mmci.c:
1220static void mmci_dt_populate_generic_pdata(struct device_node *np,
1221 structmmci_platform_data *pdata)
1222{
1223 int bus_width = 0;
1224
1225 pdata->gpio_wp =of_get_named_gpio(np, "wp-gpios", 0);
1226 pdata->gpio_cd =of_get_named_gpio(np, "cd-gpios", 0);
…
}
7. pinctrl驅動
許多SoC內部都包含pin控制器,通過pin控制器的暫存器,我們可以配置一個或者一組引腳的功能和特性。在軟體上,Linux核心的pinctrl驅動可以操作pin控制器為我們完成如下工作:
§ 列舉並且命名pin控制器可控制的所有引腳;
§ 提供引腳複用的能力;
§ 提供配置引腳的能力,如驅動能力、上拉下拉、開漏(open drain)等。
pinctrl和引腳
在特定SoC的pinctrl驅動中,我們需要定義引腳。假設有一個PGA封裝的晶片的引腳排布如下:
A B C D E F G H
8 o o o o o o o o
7 o o o o o o o o
6 o o o o o o o o
5 o o o o o o o o
4 o o o o o o o o
3 o o o o o o o o
2 o o o o o o o o
1 o o o o o o o o
在pinctrl驅動初始化的時候,需要向pinctrl子系統註冊一個pinctrl_desc描述符,在該描述符中包含所有引腳的列表。可以通過如下程式碼來註冊這個pin控制器並命名其所有引腳:
59#include <linux/pinctrl/pinctrl.h>
60
61const struct pinctrl_pin_descfoo_pins[] = {
62 PINCTRL_PIN(0, "A8"),
63 PINCTRL_PIN(1, "B8"),
64 PINCTRL_PIN(2, "C8"),
65 ...
66 PINCTRL_PIN(61, "F1"),
67 PINCTRL_PIN(62, "G1"),
68 PINCTRL_PIN(63, "H1"),
69};
70
71static struct pinctrl_descfoo_desc = {
72 .name = "foo",
73 .pins = foo_pins,
74 .npins = ARRAY_SIZE(foo_pins),
75 .maxpin = 63,
76 .owner = THIS_MODULE,
77};
78
79int __init foo_probe(void)
80{
81 struct pinctrl_dev *pctl;
82
83 pctl = pinctrl_register(&foo_desc,<PARENT>, NULL);
84 if (IS_ERR(pctl))
85 pr_err("could not registerfoo pin driver\n");
86}
引腳組(pin group)
在pinctrl子系統中,支援將一組引腳繫結為同一功能。假設{ 0, 8, 16, 24 }這一組引腳承擔SPI的功能,而{ 24, 25 }這一組引腳承擔I2C介面功能。在驅動的程式碼中,需要體現這個分組關係,並且為這些分組實現pinctrl_ops的成員函式get_groups_count、get_groups_count和get_groups_count,將pinctrl_ops填充到前文pinctrl_desc的例項foo_desc中。
130#include <linux/pinctrl/pinctrl.h>
131
132struct foo_group {
133 const char *name;
134 const unsigned int *pins;
135 const unsigned num_pins;
136};
137
138static const unsigned int spi0_pins[] = { 0, 8, 16, 24 };
139static const unsigned int i2c0_pins[] = { 24, 25 };
140
141static const struct foo_group foo_groups[] = {
142 {
143 .name = "spi0_grp",
144 .pins = spi0_pins,
145 .num_pins =ARRAY_SIZE(spi0_pins),
146 },
147 {
148 .name = "i2c0_grp",
149 .pins = i2c0_pins,
150 .num_pins =ARRAY_SIZE(i2c0_pins),
151 },
152};
153
154
155static int foo_get_groups_count(struct pinctrl_dev *pctldev)
156{
157 return ARRAY_SIZE(foo_groups);
158}
159
160static const char *foo_get_group_name(struct pinctrl_dev *pctldev,
161 unsigned selector)
162{
163 return foo_groups[selector].name;
164}
165
166static int foo_get_group_pins(struct pinctrl_dev *pctldev, unsigned selector,
167 unsigned **const pins,
168 unsigned *const num_pins)
169{
170 *pins = (unsigned *) foo_groups[selector].pins;
171 *num_pins =foo_groups[selector].num_pins;
172 return 0;
173}
174
175static struct pinctrl_opsfoo_pctrl_ops = {
176 .get_groups_count =foo_get_groups_count,
177 .get_group_name = foo_get_group_name,
178 .get_group_pins = foo_get_group_pins,
179};
180
181
182static struct pinctrl_descfoo_desc = {
183 ...
184 .pctlops = &foo_pctrl_ops,
185};
get_groups_count()成員函式用於告知pinctrl子系統該SoC中合法的被選引腳組有多少個,而get_group_name()則提供引腳組的名字,get_group_pins()提供引腳組的引腳表。在裝置驅動呼叫pinctrl通用API使能某一組引腳的對應功能時,pinctrl子系統的核心層會呼叫上述callback函式。
引腳配置