Linux Porting Guide

Rajesh Palani

February 26, 2001

Rajesh PalaniFebruary 26, 2001

Why pay someone to port Linux for you? Here's one Linux how-to no embedded programmer should miss.

Linux is gaining in popularity in embedded systems. Many commercial vendors specialize in porting Linux to embedded systems. This article explains the work involved in porting Linux to a specific embedded system and how it was done for one embedded system in particular.

Until now, most embedded operating systems have been proprietary. If a new processor was designed and developed by a semiconductor company, they had to depend on an operating system company to port their operating system to the new processor. The other issue was with the development tools (compiler, debugger, simulator, and so on) for the given processor. Usually the operating system company also provided these tools. In addition, the peripherals around the processor required drivers that had to be developed for the specific operating system. With the introduction of Linux into the embedded sphere, it has become possible for the semiconductor company itself to port an operating system to a new processor, since the source code for the Linux kernel is available as open source. The GNU1 project provides a wealth of development tools that support Linux and are also open source. In addition, many device drivers are available for Linux, which could be used directly or as a starting point for your target devices.

Project specifics

In our example, the target platform (development board) to which we ported Linux consisted of an ASIC (application-specific integrated circuit) for Windows-based terminals (thin client) and Internet access terminals, a MIPS CPU, and a CPU interface (North Bridge). In addition, the development board also supported EDO DRAM, flash ROM, I2C RTC, I2C EEPROM, and I2C serial bus. The high-level system block diagram is shown in Figure 1. An architectural block diagram of the ASIC is shown in Figure 2. A ramdisk (explained later in the article) served as the root file system. A ramdisk was used initially in order to speed up the porting process. Though this article is based on Linux on MIPS, the overall approach is not very different for other processors (ARM, 386, and so on). In addition, the article deals only with minimum kernel functionality.

Cross-development tools were used for this project. The current development was done on a PC running Red Hat Linux. The Linux VR Web site (see References) is a good starting point for cross-development tools and sources for Linux on MIPS. The following cross-development tools need to be downloaded and installed on the PC running Linux: cross-binutils-as, ld, and so on; C cross compiler; and cross-development C libraries. The detailed steps for installation are available along with the tools.

The kernel sources

The Linux kernel sources for MIPS can be downloaded from the Linux-VR site. One of the most important steps in porting Linux to a new target platform is to have a very clear understanding of how the kernel sources are organized. The following directory structure is not complete and includes only parts that are of interest to this article. The $(TOPDIR)2 has the following sub-directories:

  • arch-This subdirectory contains all of the architecture-specific code. For each supported architecture (MIPS, ARM, 386, and so on), there is a subdirectory under "arch". Each supported architecture subdirectory has four major subdirectories:
  • kernel, which contains the architecture-specific kernel code
  • mm, which contains the architecture-specific memory management code
  • lib, which contains architecture specific library code (vsprintf and so on)
  • MY_PLATFORM (target platform directory), which contains platform-specific code.
Note that Linux ports to processors without memory management units (MMU) are also available
  • documentation-This subdirectory contains the documentation for the kernel
  • drivers-This subdirectory contains code for the device drivers. Each type of device has further subdirectories, such as char, block, net, and so on
  • fs-This directory contains the file system code. This has further sub-directories for each supported file system (ext2, proc, and so on)
  • include-The include subdirectory contains the include files for the kernel. It has further subdirectories for common include files (for all architectures), one for every architecture supported, and a couple of other subdirectories
  • init-This directory contains the initialization code for the kernel
  • kernel-This directory contains the main kernel code
  • lib-This directory contains the library code of the kernel
  • mm-This directory contains the memory management code
Building the kernel image

The Makefile in the $(TOPDIR) has to have ARCH (architecture) properly defined (MIPS in this case). The Makefile in $(TOPDIR)/arch/MY_ARCH/boot has to have CROSS_COMPILE (mipsel-linux, MIPS little-endian cross-compiler tool-set in this case) and LOADADDR (address at which the kernel image would be loaded) defined as per the configuration. If additional configuration options have to be added, the $(TOPDIR)/arch/MY_ARCH/config.in file has to be modified. You would need to have a config option for your platform (CONFIG_MYPLATFORM) to include code that is specific to your platform. The kernel has to be configured ("make config") to the barest minimum needs (serial driver, ramdisk driver, ext2 file system). Then do a "make dep" to set up the dependencies and finally a "make vmlinux" to produce the kernel image.

Ramdisk

To begin with, a ramdisk can be mounted as the root file system. Ramdisk images and objects are also available readily for MIPS (Linux-VR site). A ramdisk image is a file that contains an image of an ext2 filesystem, while a ramdisk object is an elf object that encapsulates a ramdisk image and can be linked into the kernel. The ramdisk image is usually stored in compressed form. CONFIG_BLK_DEV_RAM and CONFIG_BLK_DEV_INITRD need to be defined as Y in "make config." The ramdisk image can be modified to include your applications, if required. Tools (scripts) are available at the Linux VR site for creating a ramdisk object. The ramdisk.o file needs to be copied to $(TOPDIR)/arch/MY_ARCH/boot and linked into the kernel. There is a detailed document, $(TOPDIR)/Documenta-tion/ramdisk.txt, on how to use the RAM disk block device with Linux.

Processor-specific changes to the kernel code

If your processor is a standard (or popular) one, in most cases a Linux port to that processor would be available. If you are one of the unlucky few who has to deal with a specific implementation of a given processor core to which Linux has not yet been ported, you'll want to figure out which processor in the list of ported ones is closest to yours and use that port as a starting point for your specific processor implementation. For example, the MIPS core is licensed to many silicon vendors who have their own implementations. The number of TLB3 (translation lookaside buffers) entries may be different for different implementations. Add a config option (CONFIG_MYCPU) to include code that is specific to your processor. Directories $(TOPDIR)/arch/MY_ARCH/kernel and $(TOPDIR)/arch/MY_ARCH/mm contain the processor-specific code that require modifications if you are dealing with a new implementation.

Assembly file $(TOPDIR)/arch/MY_ARCH/kernel/head.S contains kernel_entry, the entry point for the kernel. This file also contains the exception handling code. Listing 1 shows the implementation of the kernel_entry routine in pseudocode.

Listing 1: Kernel entry pseudocode

  1. Set desired endian mode
  2. Clear the BEV bit
  3. Set the TLB bit
  4. GOTO cpu_probe and return
  5. Set up stack for kernel
  6. Clear bss
  7. GOTO prom_init and return
  8. GOTO loadmmu and return
  9. Disable coprocessors
  10. GOTO start_kernel
  11. GOTO 10
The configuration register has to be set up correctly. The first thing to be done is to make sure that we are running in the desired endian mode. In our case, we run the system in little-endian mode. The bootstrap exception vector bit needs to be cleared to make sure that the normal exception vectors are used henceforth. In addition, the TLB bit is set to make sure that TLB-based translation is used.

The next step is to probe for the cputype. Listing 2 is a very simple implementation of this function. $(TOPDIR)/include/asm/bootinfo.h contains entries for the cputype (MYCPU) and machine group (MY_MACH_GROUP). The mips_cputype variable has to be updated in the cpu_probe function. This value is used later to determine the exception handling and MMU routines that need to be loaded for the given CPU, as well as to get the CPU information in the /proc file system.

Listing 2: Code to probe cputype

LEAF(cpu_probe)
        la       t3, mips_cputype

        li       t2, MYCPU       /* include/asm-mips/bootinfo.h */
       b       probe_done
       sw       t2, (t3)
END(cpu_probe)

The initial stack for the kernel is set up next. Then the bss section of the kernel image is cleared. Control then transfers to the prom_init() function. Then the TLB and caches are flushed and the cache manipulation functions are set up inside loadmmu(). Disabling of the coprocessors other than coprocessor 0 is done next, followed by a jump to start_kernel(). $(TOPDIR)/arch/MY_ARCH/mm contains the TLB routines and cache handling routines.

Platform specific changes to the kernel code

$(TOPDIR)/arch/MY_ARCH has a sub-directory for each target development platform that is supported. Create a MY_PLATFORM directory by copying a platform closest to your configuration. This directory should contain the interrupt handling, timer, initialization, and setup routines for your specific platform. Create a MY_PLATFORM directory under $(TOPDIR)/include/asm. This directory is used to hold include files specific to your platform.

The prom_init() function, which is part of $(TOPDIR)/arch/MY_ARCH/MY_PLATFORM/prom.c (Listing 3), modifies the command line string to add parameters that need to be passed to the kernel from the bootloader. The machine group and upper bound of usable memory are set up here.

Listing 3: PROM initialization

int __init prom_init (int argc, char **argv, char **envp)
{
       unsigned int mem_limit;// set upper limit to maximum physical RAM (32MB)
       mem_limit = 32 * 1024 * 1024;

       // the bootloader usually passes us argc/argv[] .
       //In the present case, these arguments are not
       //passed from the bootloader. The kernel wants
       // one big string. put it in arcs_cmdline, which
        ater gets copied to command_line
       //(see arch/mips/kernel/setup.c)

       strcpy (arcs_cmdline, *root=/dev/ram*);

       mips_machgroup = MY_MACH_GROUP;

       // set the upper bound of usable memory
       mips_memory_upper = KSEG0 + mem_limit;

       printk(*Detected %dMB of memory\n*, mem_limit >> 20);

       return 0;
}

Starting the kernel

Listing 4 contains the first few interesting lines of the start_kernel() function, located in $(TOPDIR)/init/main.c.

Listing 4: The beginning of the start_kernel function

asmlinkage void __init start_kernel(void)
{
       char * command_line;

        /*
         * Interrupts are still disabled. Do necessary setups, then
         * enable them
         */
        lock_kernel();
       printk(linux_banner);
       setup_arch(&command_line, &memory_start, &memory_end);
       memory_start = paging_init(memory_start, memory_end);
       trap_init();
       init_IRQ();
       sched_init();
       time_init();
       parse_options(command_line);
       .
       .
       .

Listing 5 shows the setup_arch() function in $(TOPDIR)/arch/MY_ARCH/kernel/setup.c. The board-specific setup function is called from here. The command line string and memory start and memory end are passed over to the caller of this function. The start and end addresses for the linked-in ramdisk image are also updated here.

Listing 5: Architecture setup function

__initfunc(void setup_arch(char **cmdline_p,
              unsigned long * memory_start_p, unsigned long * memory_end_p))
{ #ifdef CONFIG_BLK_DEV_INITRD
#if CONFIG_BLK_DEV_INITRD_OFILE
       extern void *__rd_start, *__rd_end;
#endif
#endif

       myplatform_setup();        strncpy(command_line, arcs_cmdline, CL_SIZE);
       *cmdline_p = command_line;

       *memory_start_p = (unsigned_long) &_end;
       *memory_end_p = mips_memory_upper;

#ifdef CONFIG_BLK_DEV_INITRD
#if CONFIG_BLK_DEV_INITRD_OFILE
       // Use the linked-in ramdisk
       // image located at __rd_start.
       initrd_start = (unsigned long)&__rd_start;
       initrd_end = (unsigned long)&__rd_end;
       initrd_below_start_ok = 1;
       if (initrd_end > memory_end)
       {
          printk(*initrd extends beyond end of memory *
          *(0x%08lx > 0x%08lx)\ndisabling initrd\n*,
          initrd_end, memory_end);
      initrd_start = 0;
    }
#endif
#endif
}

Listing 6: Platform-specific initialization code

__initfunc(void myplatform_setup(void)) {

     irq_setup = myplatform_irq_setup;     /*
     * mips_io_port_base is the beginning
     *of the address space to which x86
     * style I/O ports are mapped.
     */
     mips_io_port_base = 0xa0000000;

    /*
     * platform_io_mem_base is the beginning of I/O bus memory space as
     * seen from the physical address bus. This may or may not be ident-
     * ical to mips_io_port_base, e.g. the former could point to the beginning of PCI
     *memory space while the latter might indicate PCI I/O
     * space. The two values are used in different sets of macros. This
     * must be set to a correct value by the platform setup code.
     */
     platform_io_mem_base=0x10000000;

    /*
     * platform_mem_iobus_base is the beginning of main memory as seen
     * from the I/O bus, and must be set by the platform setup code.
     */
platform_mem_iobus_base=0x0; #ifdef CONFIG_REMOTE_DEBUG
    /*
     * Do the minimum necessary to set up debugging
     */
    myplatform_kgdb_hook(0);
    remote_debug = 1;
#endif

#ifdef CONFIG_BLK_DEV_IDE
    ide_ops = &std_ide_ops;
#endif

#ifdef CONFIG_VT
#if defined(CONFIG_DUMMY_CONSOLE)
    conswitchp = &dummy_con;
#endif
#endif

/*
 * just set rtc_ops && pci_ops; forget the rest
 */
rtc_ops = &myplatform_rtc_ops;
pci_ops = &myplatform_pci_ops;
}

$(TOPDIR)/arch/MY_ARCH/MY_PLATFORM/setup.c contains the platform-specific initialization code (Listing 6). Here, the various base addresses and the platform-specific RTC and PCI operations are set up. For PCI, the following seven functions need to be implemented for the given platform:

  • myplatform_pcibios_fixup()
  • myplatform_pcibios_read_config_byte()
  • myplatform_pcibios_read_config_word()
  • myplatform_pcibios_read_config_dword()
  • myplatform_pcibios_write_config_byte()
  • myplatform_pcibios_write_config_word()
  • myplatform_pcibios_write_config_dword()

< Previous
Page 1 of 3
Next >

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER