23.5 Linux内核的移植
Linux 内核的移植主要含义是将 Linux 内核运行于一块新的 SoC 芯片或一块新的电路板之上,其实质含义就是建立 Linux 的板级支持包(BSP)。BSP 的本质作用有二:为内核的运行提供底层支撑;屏蔽与板相关的硬件细节。对于 ARM ,BSP 代码位于 arch/arm/的各个 plat 和 mach目录下,结构如下:
plat-xxx
linux-2.6/arch/arm/
plat-omap/
plat-pxa/
plat-s3c/
plat-s3c24xx/
plat-s3c64xx/
plat-stmp3xxx/
mach-xxx
linux-2.6/arch/arm/
mach-s3c2400/
mach-s3c2410/
mach-s3c2412/
mach-s3c2440/
mach-s3c2442/
mach-s3c2443/
mach-s3c24a0/
mach-s3c6400/
mach-s3c6410/
所有 S3C6410 板的板文件都位于 arch/arm/mach-s3c6410/,如 LDD6410 的即为 arch/arm/
mach-s3c6410/mach-ldd6410.c,而所有 S3C 系列芯片 BSP 公用的部分又被提炼到 arch/arm/plat-s3c/。
这些代码完成的主要工作如下。
1.时钟 tick(Hz)的产生
系统节拍是 Linux 操作系统得以运行的基本条件之一,为 Linux 建立节拍只需要在硬件上指定一个定时器,并以 sys_timer 的形式对其进行封装,根据 Hz 调整定时器硬件计数器,并在定时器中断的处理函数中调用 timer_tick()。代码清单 23.15 列出了 S3C6410 处理器的系统定时器。
代码清单 23.15 S3C6410 处理器的系统定时器
struct sys_timer s3c24xx_timer = {
.init = s3c2410_timer_init,
.offset = s3c2410_gettimeoffset,
.resume = s3c2410_timer_setup
};
static void __init s3c2410_timer_init (void)
{
s3c2410_timer_setup();
setup_irq(IRQ_TIMER4, &s3c2410_timer_irq);
}
/*
* IRQ handler for the timer
*/
static irqreturn_t
s3c2410_timer_interrupt(int irq, void *dev_id)
{
write_seqlock(&xtime_lock);
timer_tick();
write_sequnlock(&xtime_lock);
return IRQ_HANDLED;
}
static struct irqaction s3c2410_timer_irq = {
.name = "S3C2410 Timer Tick",
.flags = IRQF_DISABLED | IRQF_TIMER | IRQF_IRQPOLL,
.handler = s3c2410_timer_interrupt,
};
分析:
sys_timer 结构体的 init()成员函数用于定时器的初始化(设置硬件计数器以产生 Hz、中断)。由于系统以 Hz 为单位更新墙上时间,因此 Linux 的 gettimeofday() API 如果没有 offset()的帮助是无法达到微秒级精度的,offset()函数实际是计算当前硬件计数值与节拍之前的差异。
2.系统中断控制的方法
BSP 中需要将系统的所有中断以 irq_chip 结构体的形式进行组织,并实现各中断的 mask()、unmask()、ack()、mask_ack()方法,代码清单 23.16 给出 S3C6410 处理中断的部分代码片段。
代码清单 23.16 S3C6410 BSP 中断处理
static struct irq_chip s3c_irq_uart = {
.name = "s3c-uart",
.mask = s3c_irq_uart_mask,
.unmask = s3c_irq_uart_unmask,
.mask_ack = s3c_irq_uart_maskack,
.ack = s3c_irq_uart_ack,
};
static void __init s3c64xx_uart_irq(struct uart_irq *uirq)
{
for (offs = 0; offs < 3; offs++) {
irq = uirq->base_irq + offs;
set_irq_chip(irq, &s3c_irq_uart);
set_irq_chip_data(irq, uirq);
set_irq_handler(irq, handle_level_irq);
set_irq_flags(irq, IRQF_VALID);
}
set_irq_chained_handler(uirq->parent_irq, s3c_irq_demux_uart);
}
void __init s3c64xx_init_irq(u32 vic0_valid, u32 vic1_valid)
{
set_irq_chip(irq, &s3c_irq_timer);
...
for (uart = 0; uart < ARRAY_SIZE(uart_irqs); uart++)
s3c64xx_uart_irq(&uart_irqs[uart]);
}
3.GPIO、DMA、时钟资源的统一管理
在 BSP 中,通常需要将所有 GPIO 以 gpio_chip 结构体的形式进行组织,这个结构体中的成员函数用于设置 GPIO 的方向、读取和设置 GPIO 的电平,如代码清单 23.17 所示。
代码清单 23.17 gpio_chip 结构体
include/linux/gpio/driver.h
struct gpio_chip {
const char *label;
struct device *dev;
struct module *owner;
struct list_head list;
int (*request)(struct gpio_chip *chip,unsigned offset);
void (*free)(struct gpio_chip *chip,unsigned offset);
int (*get_direction)(struct gpio_chip *chip,unsigned offset);
int (*direction_input)(struct gpio_chip *chip,unsigned offset);
int (*direction_output)(struct gpio_chip *chip,unsigned offset, int value);
int (*get)(struct gpio_chip *chip, unsigned offset);
void (*set)(struct gpio_chip *chip, unsigned offset, int value);
int (*set_debounce)(struct gpio_chip *chip,unsigned offset, unsigned debounce);
int (*to_irq)(struct gpio_chip *chip, unsigned offset);
void (*dbg_show)(struct seq_file *s, struct gpio_chip *chip);
int base;
u16 ngpio;
struct gpio_desc *desc;
const char *const *names;
bool can_sleep;
bool irq_not_threaded;
bool exported;
#ifdef CONFIG_GPIOLIB_IRQCHIP
/*
* With CONFIG_GPIOLIB_IRQCHIP we get an irqchip inside the gpiolib
* to handle IRQs for most practical cases.
*/
struct irq_chip *irqchip;
struct irq_domain *irqdomain;
unsigned int irq_base;
irq_flow_handler_t irq_handler;
unsigned int irq_default_type;
#endif
#if defined(CONFIG_OF_GPIO)
/*
* If CONFIG_OF is enabled, then all GPIO controllers described in the
* device tree automatically may have an OF translation
*/
struct device_node *of_node;
int of_gpio_n_cells;
int (*of_xlate)(struct gpio_chip *gc, const struct of_phandle_args *gpiospec, u32 *flags);
#endif
#ifdef CONFIG_PINCTRL
/*
* If CONFIG_PINCTRL is enabled, then gpio controllers can optionally
* describe the actual pin range which they serve in an SoC. This
* information would be used by pinctrl subsystem to configure
* corresponding pins for gpio usage.
*/
struct list_head pin_ranges;
#endif
};
Linux 会提供如下一组通用关于 GPIO 的 API:
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value_cansleep(unsigned gpio);
这样做的好处是驱动的代码完全可以以与平台无关的方式申请和使用 GPIO,通过读写寄存器来访问 GPIO,提高驱动的可移植性。
同样的,BSP 也要实现针对 DMA 通用 API,如:
int request_dma(unsigned int chan, const char * device_id);
void free_dma(unsigned int chan);
void enable_dma(unsigned int chan);
void disable_dma(unsigned int chan);
void set_dma_mode (unsigned int chan, unsigned int mode);
void set_dma_sg (unsigned int chan, struct scatterlist *sg, int nr_sg);
同样的,BSP 也要实现针对时钟的通用API,包括clk_get()、clk_put()、clk_enable()、clk_disable()、
clk_get_rate()、clk_round_rate()、clk_set_rate()、clk_get_parent()、clk_set_parent()。
4.静态映射的 I/O 内存
在 BSP 中,可以通过 map_desc 结构体和 iotable_init()函数提前建立某段物理地址和虚拟地址之间的静态映射。
5.设备的 I/O、中断、DMA 等资源封装平台数据
主要是 platform 信息、SPI board 信息和 I 2 C board 信息。最后,对于一块 ARM 电路板,会将它的中断初始化、静态内存映射、系统定时器等板级信息透过 MACHINE_START 和 MACHINE_END 之间的宏绑定在一起。代码清单 23.18 给出 LDD6410 的例子。
代码清单 23.18 LDD6410 开发板的 MACHINE_START 和 MACHINE_END
MACHINE_START(SMDK6410, "LDD6410")
.phys_io = S3C_PA_UART & 0xfff00000,
.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C64XX_PA_SDRAM + 0x100,
.init_irq = s3c6410_init_irq,
.map_io = ldd6410_map_io,
.init_machine = ldd6410_machine_init,
.timer = &s3c64xx_timer,
MACHINE_END
分析:
从“MACHINE_START(SMDK6410, "LDD6410")”看出,LDD6410 仍然使用了 SMDK6410 的 mach ID(机器码),实际上可以通过 Linux ARM 的邮件列表获得一个新的 mach ID。
大多数工程师就职于设备提供商,因此不涉及 SoC 级的移植,也就是说芯片公司已经将系统定时器、GPIO、DMA、时钟等都封装好了,工程师只需要进行板相关的移植。这里给出 Linux板级移植的原则:切忌直接修改现有电路板的代码作为自身电路板的代码,例如,如果电路板与 SMDK6410 有差异,就直接修改 arch/arm/mach-s3c6410/mach-smdk6410.c,这是完全错误的,正确的方法是新建自己的板级文件,将本身的设备和资源填写在新的板文件里面。
除了 SoC 芯片级和板级 Linux 移植外,移植工作量最大的是体系结构相关的 Linux 移植,例如将 Linux 移植到一个全新的 CPU 体系架构,如 TI 的 DSP 芯片。则工作量还涉及内存管理、进程调度、异常和陷阱等。
总结:
1、编写 Linux 设备驱动程序时,特别注意代码的可移植性,留意数据类型的长度、结构体的对齐、CPU 大小端模式以及内存页面的大小。
2、为了加速驱动的开发过程,在拿到一个驱动开发任务的时候,务必搜集足够的“情报”,找到可模拟的芯片或可利用的代码,这样可以事半功倍。一般而言,demo 板的驱动、类似芯片的驱动以及无操作系统时的硬件操作代码都是可以参考的代码。
3、Linux 2.4 到 Linux 2.6 的改进导致驱动中发生了一些细微的变化,了解这些变化后可进行驱动的更新。除了不同版本的 Linux 以外,其他操作系统中的驱动源代码经过适当的修改也可被移植到 Linux 中。