Basic analysis of driver framework in Linux system

Hello everyone, today I will share an article about Linux driver software design ideas. Since the article is long, you can save it first and then read it slowly.

1. Linux-driven software architecture

1.1 Starting point

In order to adapt to hardware of various architectures, it enhances the reusability and cross-platform capabilities of the system.

1.2 Separation Thoughts

In order to achieve the goal of a driver that can be applied to any hardware platform without changing a single line, the driver is separated from the device. The driver only cares about the driver, the device only cares about the device, and the driver obtains board-level information in some common standard way. This reduces the coupling between the driver and the device.

picture

1.3 Hierarchical thinking

For similar devices, the basic framework is the same, so an intermediate layer is extracted. For example: for input devices (buttons, keyboards, touch screens, mice), although the file_operation and I/O models are indispensable, the basic framework They are all the same, so an Input  core layer can be extracted  , in which the Linux interface and the entire set  of input  event reporting mechanisms are implemented.

picture

2. Platform device driver

Platform: a virtual bus in Linux. A real Linux device and driver usually need to be connected to a bus (for ease of management), such as PCI, USB, I2C, SPI, etc., but for independent peripheral controllers integrated in the Soc system, they are connected to the Soc Peripherals in the memory space cannot be attached to the above-mentioned bus. At this time, Linux invented a virtual bus to manage this type of equipment (there is no physical hardware bus circuit).

     In the platform device driver model, it is divided into three entities:  device , driver and bus , which are called platform_device , paltform_driver  and  platform bus respectively . The bus is responsible for binding the device and driver. Every time the system registers a device, it will look for a matching driver; conversely, every time the system registers a driver, it will look for a matching device, and the matching process is completed by the bus.

2.1 platform device

Platform device: It consists of  the platform_device  structure and is responsible for managing peripheral resources, such as  I/O resources , memory resources , interrupt resources , etc.

Prototype: linux/platform_device.h

struct platform_device {
    const char    * name;
    int    id;
    struct device dev;
    u32    num_resources;
    struct resource    * resource;
    
    const struct platform_device_id    *id_entry;
    
    /* MFD cell pointer */
    struct mfd_cell *mfd_cell;
    
    /* arch specific additions */
    struct pdev_archdata archdata;
};

2.1.1 resource structure

The resource  structure describes  the resources of platform_device  :

struct resource {
    resource_size_t start;        //资源的开始
    resource_size_t end;        //资源的结束
    const char *name;
    unsigned long flags;        //资源的类型
    struct resource *parent, *sibling, *child;
};

 Common types of  parameter  flags are IORESOURCE_IO , IORESOURCE_MEM , IORESOURCE_IRQ , IORESOURCE_DMA , etc.  The meaning of  the parameters  start  and  end will  change depending on the flags .

1) flags  is  IORESOURCE_MEM , start and end  respectively represent the start and end addresses of the memory occupied by the platform_device ;

2) flags is  IORESOURCE_IRQ , start and end  respectively represent the start value and end value of the interrupt number used by the platform_device. If one interrupt number is used, the start and end values ​​are the same;

There can be multiple copies of the same type of resource. For example, if a device occupies multiple memory areas, multiple  IORESOURCE_MEM can be defined .

For example, the resource defined for the DM9000 network card in the arch/arm/mach-at91/board-sam9261ek.c board file:

static struct resource dm9000_resource[] = {
    [0] = {
        .start    = AT91_CHIPSELECT_2,
        .end    = AT91_CHIPSELECT_2 + 3,
        .flags    = IORESOURCE_MEM
    },
    [1] = {
        .start    = AT91_CHIPSELECT_2 + 0x44,
        .end    = AT91_CHIPSELECT_2 + 0xFF,
        .flags    = IORESOURCE_MEM
    },
    [2] = {
        .start    = AT91_PIN_PC11,
        .end    = AT91_PIN_PC11,
        .flags    = IORESOURCE_IRQ
        | IORESOURCE_IRQ_LOWEDGE | IORESOURCE_IRQ_HIGHEDGE,
    }
};

2.1.2 platform_data resource in device structure

In addition to defining resources in the BSP, the device can also add some data information, because in addition to standard resources such as interrupts and memory, the hardware description of the device may also have some configuration information, and these configuration information also depend on the board and do not depend on the board. It is suitable to place it directly on the device driver.

Therefore, platform_device provides a platform_data form  that can be customized by each device driver   to support adding some data information, that is, the Linux kernel does not require this piece of data.

device  structure:

/**
 * struct device - The basic device structure
......
 * @platform_data: Platform data specific to the device.
 *         Example: For devices on custom boards, as typical of embedded
 *         and SOC based hardware, Linux often uses platform_data to point
 *         to board-specific structures describing devices and how they
 *         are wired.  That can include what ports are available, chip
 *         variants, which GPIO pins act in what additional roles, and so
 *         on.  This shrinks the "Board Support Packages" (BSPs) and
 *         minimizes board-specific #ifdefs in drivers.
......
 */
struct device {
    ......
    void *platform_data;    /* Platform specific data, device
                           core doesn't touch it */
    ......
};

For example, in the arch/arm/mach-at91/board-sam9261ek.c board file,  platform_data  defines  the dm9000_plat_data  structure. After the definition is completed, put the MAC address, bus width, EEPROM information on the board, etc.:

static struct dm9000_plat_data dm9000_platdata = {
    .flags = DM9000_PLATF_16BITONLY | DM9000_PLATF_NO_EEPROM,
};

static struct platform_device dm9000_device = {
    .name = "dm9000",
    .id    = 0,
    .num_resources = ARRAY_SIZE(dm9000_resource),
    .resource = dm9000_resource,
    .dev = {
        .platform_data    = &dm9000_platdata,
    }
};

2.1.3 Registration of platform_device

For the Linux 2.6 ARM platform,  the definition of platform_device  is usually implemented in the BSP board file. In the board file,  platform_device  is summarized into an array. As the board file is loaded, it is finally   registered uniformly through the platform_add_devices() function.

The platform_add_devices()  function can add platform devices to the system. The prototype of this function is:

int platform_add_devices(struct platform_device **devs, int num)

The first parameter is a pointer to the platform device array, and the second parameter is the number of platform devices. Inside the function, the  platform_device_register()  function is called to register the platform devices one by one.

If the registration is successful, you can see the subdirectory with the corresponding name in the sys/devices/platform directory.

After Linux 3.x, ARM Linux does not fill in platform_device  and register it in the form of coding  , but prefers to automatically expand platform_device based on the content in the device tree .

2.2 platform driver

Platform driver: It consists of  the platform_driver  structure and is responsible for the implementation of driver operations, such as loading, unloading, closing, suspending, restoring, etc. The prototype is located in linux/platform_driver.h:

struct platform_driver {
    int (*probe)(struct platform_device *);
    int (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
};

 /* @probe:    Called to query the existence of a specific device,
 *        whether this driver can work with it, and bind the driver
 *        to a specific device.
 * @remove:    Called when the device is removed from the system to
 *        unbind a device from this driver.
 * @shutdown:    Called at shut-down time to quiesce the device.
 * @suspend:    Called to put the device to sleep mode. Usually to a
 *        low power state.
 * @resume:    Called to bring a device from sleep mode.
 */

probe()  and  remove()  respectively correspond to the operations performed by the driver when loading and unloading.

The method of directly filling in  platform_driver  's  suspend() and resume()  for power management callbacks is currently outdated. A better approach is to implement  the dev_pm_ops  structure members  in  platform_driver  's  device_driver .

2.2.1 device_driver structure

struct device_driver {
    const char *name;
    struct bus_type *bus;
    
    struct module *owner;
    const char *mod_name;    /* used for built-in modules */
    
    bool suppress_bind_attrs;    /* disables bind/unbind via sysfs */
    
    const struct of_device_id    *of_match_table;
    
    int (*probe) (struct device *dev);
    int (*remove) (struct device *dev);
    void (*shutdown) (struct device *dev);
    int (*suspend) (struct device *dev, pm_message_t state);
    int (*resume) (struct device *dev);
    const struct attribute_group **groups;
    
    const struct dev_pm_ops *pm;
    
    struct driver_private *p;
};

The i2c_driver , spi_driver , usb_driver , and pci_driver  , which are equal to  the  platform_driver , all contain  device_driver structure instance members. It actually describes some commonalities in the driver sense of various xxx_drivers (xxx is the bus name).

2.2.2 Obtain board resources from the driver

Obtain resource resources in the device  : dm9000_probe() function  in drivers/net/dm9000.c 

    db->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    db->data_res = platform_get_resource(pdev, IORESOURCE_MEM, 1);
    db->irq_res  = platform_get_resource(pdev, IORESOURCE_IRQ, 0);

or:

db->irq_wake = platform_get_irq(pdev, 1);

In fact, platform_get_resource(dev, IORESOURCE_IRQ, num) is called  ;

Get the platform_data  resource in the device  : dm9000_probe() function in drivers/net/dm9000.c 

struct dm9000_plat_data *pdata = pdev->dev.platform_data;

2.2.3 Registration of platform_driver

Platform_driver  is   registered and unregistered through  platform_driver_register() and platform_driver_unregister() .

static int __init
dm9000_init(void)
{
    printk(KERN_INFO "%s Ethernet Driver, V%s\n", CARDNAME, DRV_VERSION);
    
    return platform_driver_register(&dm9000_driver);
}

static void __exit
dm9000_cleanup(void)
{
    platform_driver_unregister(&dm9000_driver);
}

module_init(dm9000_init);
module_exit(dm9000_cleanup);

The registration and deregistration of the original character device (or other device) is transferred to  the probe()  and  remove()  member functions  of  platform_driver . Registering the character device driver in this form is just a  shell of platform_driver  and does not change the nature of the character device.

For example, in drivers/net/dm9000.c, it is still defined as a network device, but the registration process of the network device driver is placed in  probe()  :

static const struct net_device_ops dm9000_netdev_ops = {
    .ndo_open = dm9000_open,
    .ndo_stop = dm9000_stop,
    .ndo_start_xmit = dm9000_start_xmit,
    .ndo_tx_timeout = dm9000_timeout,
    .ndo_set_multicast_list = dm9000_hash_table,
    .ndo_do_ioctl = dm9000_ioctl,
    .ndo_change_mtu = eth_change_mtu,
    .ndo_set_features = dm9000_set_features,
    .ndo_validate_addr = eth_validate_addr,
    .ndo_set_mac_address = eth_mac_addr,
#ifdef CONFIG_NET_POLL_CONTROLLER
    .ndo_poll_controller = dm9000_poll_controller,
#endif
};

2.3 platform bus

Platform bus : Responsible for managing the matching between peripherals and drivers.

The system defines an   instance of  bus_type platform_bus_type for the platform bus , and its definition is located under drivers/base/platform.c:

struct bus_type platform_bus_type = {
    .name = "platform",
    .dev_attrs = platform_dev_attrs,
    .match = platform_match,
    .uevent = platform_uevent,
    .pm    = &platform_dev_pm_ops,
};

2.3.1 .match member function

Focus on its  match()  member function, which determines  how platform_device  and  platform_driver  are matched.

static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* Attempt an OF style match first */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* Then try to match against the id table */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* fall-back to driver name match */
    return (strcmp(pdev->name, drv->name) == 0);
}

It can be seen that   there are three possibilities for matching between platform_device  and  platform_driver :

  1. Matching based on device tree style;

  2. Match the ID table (that is, whether the device name of platform_device appears in the ID table of platform_driver);

  3. Match platform_device device name and driver name.

2.3.2 Platform bus registration

start_kernel()
    rest_init()
        kernel_init()
            do_basic_setup()
                driver_init()
                    platform_bus_init()
    
int __init platform_bus_init(void)
{
    int error;

    early_platform_cleanup();                //早期的平台清理

    error = device_register(&platform_bus);  //注册设备 (在/sys/devices/目录下建立 platform目录对应的设备对象  /sys/devices/platform/) 
    if (error)
        return error;
    error =  bus_register(&platform_bus_type);//总线注册
    if (error)
        device_unregister(&platform_bus);
    return error;
}

2.3.3 platform bus automatic matching

platform_device_register()
    platform_device_add()
        device_add()
            bus_probe_device()
                device_attach()
                    bus_for_each_drv()
                        ----------

platform_driver_register()
    driver_register()
        bus_add_driver()
            driver_attach()
                bus_for_each_dev()       
                    ---------

Regardless of whether the device is registered first or the device driver is registered first, a matching process between the device and the device driver will be performed. After the matching is successful, it will be bound. The principle of matching is to traverse the linked list of devices or device drivers under the bus.

2.4 Advantages of platform

  1. Allows the device to be mounted on a bus, conforming to the device model of the Linux 2.6 and later kernels. The result is that supporting sysfs node and device power management are possible.

  2. Isolate the BSP from the driver. Define the platform device, the resources used by the device, and the specific configuration information of the device in the BSP. In the driver, you only need to obtain resources and data through the general API, thus achieving the separation of board-related code and driver code, making the driver better. scalability and cross-platform capabilities.

  3. Let a driver support multiple device instances. For example, there is only one copy of the DM9000 driver, but we can add multiple copies of the DM9000 platform_device at the board level, and they can all match the unique driver.

  4. In Linux kernels after 3.x, the DM9000 driver can be enumerated through the device tree method, and the added action only requires simple modification of the dts file. (Detailed links will be posted later)

3. Layered thinking of device drivers

In object-oriented programming, a base class can be defined for a certain type of similar things, and specific things can inherit the functions in this base class. If the implementation of a member function is consistent with that of the base class for the inherited thing, then it can directly inherit the function of the base class; conversely, it can also override (Overriding) and redefine the function of the parent class .

​ If a method in the subclass has the same method name, return type, and parameter list as a method in the parent class, the new method will overwrite the original method. This can greatly improve the reusability of code.

Although the Linux kernel is entirely written in C and Assembly, object-oriented design ideas are frequently used. In terms of device drivers, a framework is often designed for similar devices, and the core layer in the framework implements some common functions of the device. Similarly, if a specific device does not want to use the core layer functions, it can also be rewritten.

  • Example 1:

return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
    if (bottom_dev->funca)
    return bottom_dev->funca(param1, param2);
    /* 核心层通用的funca代码 */
    ...
}

In the implementation of the core_funca() function, it is checked whether the underlying device overloads core_funca(). If it is overloaded, the underlying code will be called, otherwise, the general layer code will be used directly.

The advantage of this is that the core layer code can handle the functions corresponding to core_funca() for most devices of this type, and only a few special devices need to reimplement core_funca() .

  • Example 2:

return_type core_funca(xxx_device * bottom_dev, param1_type param1, param1_type param2)
{
    /* 通用的步骤代码A */
    typea_dev_commonA();
    ...
    
    /* 底层操作 ops1 */
    bottom_dev->funca_ops1();
    
    /* 通用的步骤代码B */
    typea_dev_commonB();
    ...
    /* 底层操作 ops2 */    
    bottom_dev->funca_ops2();
    
    /* 通用的步骤代码C */
    typea_dev_commonC();
    ...
    
    /* 底层操作 ops3 */
    bottom_dev->funca_ops3();
}

The above code assumes that in order to implement funca(), for similar devices, the operation process is the same, and they must go through the steps of "general code A, bottom ops1, general code B, bottom ops2, general code C, bottom ops3", layered The obvious benefit of the design is that for the general codes A, B, and C, the specific underlying driver does not need to be implemented (extracted and implemented in the core layer), but only the underlying operations ops1, ops2, and ops3 are concerned. .

The figure below clearly reflects the relationship between the core layer of the device driver and the specific device driver. In fact, this layering may only have two layers, or it may be multiple layers.

picture

This kind of layered design exists in many types of device drivers such as Linux's Input, RTC, MTD, I2C, SPI, tty, USB, etc.

3.1 Input device driver

Input devices (such as buttons, keyboards, touch screens, mice, etc.) are typical character devices. Their general working mechanism is that the bottom layer generates an interrupt (or the driver queries periodically through Timer) when sending actions such as key presses and touches, and then the CPU passes SPI , I2C or external memory bus reads key values, coordinates and other data and puts it into a buffer. The character device driver manages the buffer, and the driver's read() interface allows users to read key values, coordinates and other data.

Obviously, in these tasks, only interrupts and read values ​​are device-related, while the buffer management of input events and the file_operations interface of the character device driver are common to input devices. Based on this, the kernel designs an input subsystem, and the core layer handles common work.

picture

3.1.1 The input core provides the APIs required by low-level input device drivers

Such as allocating /freeing an input device:

struct input_dev *input_allocate_device(void);    //返回的结构体用于表征1个输入设备。

void input_free_device(struct input_dev *dev);

The following interfaces are used to register/unregister input devices:

int __must_check input_register_device(struct input_dev *);

void input_unregister_device(struct input_dev *);

The following interfaces are used to report input events:

/* 报告指定type、code的输入事件 */
void input_event(struct input_dev *dev, unsigned int type, unsigned int code, int value);

/* 报告键值 */
void input_report_key(struct input_dev *dev, unsigned int code, int value);

/* 报告相对坐标 */
void input_report_rel(struct input_dev *dev, unsigned int code, int value);

/* 报告绝对坐标 */
void input_report_abs(struct input_dev *dev, unsigned int code, int value);

/* 报告同步事件 */
void input_sync(struct input_dev *dev);

All input events are described by the kernel with a unified data structure. This data structure is input_event :

struct input_event {
    struct timeval time;
    __u16 type;
    __u16 code;
    __s32 value;
};

3.1.2 Case: gpio button driver

drivers/input/keyboard/gpio_keys.c is a general GPIO button driver based on the input architecture. The driver is based on the platform_driver architecture and is named "gpio-keys". It shields hardware-related information (such as the used GPIO number, level, etc.) in the platform_data of the board file platform_device, so the driver can be applied to various processors and has good cross-platform performance.

The driver's  probe()  function:

static int __devinit gpio_keys_probe(struct platform_device *pdev)
{
    ......
    input = input_allocate_device();    //分配一个输入设备
    ......                                    
    input->name = pdata->name ? : pdev->name;    //初始化该 input_dev 的一些属性
    input->phys = "gpio-keys/input0";
    input->dev.parent = &pdev->dev;
    input->open = gpio_keys_open;
    input->close = gpio_keys_close;
    
    input->id.bustype = BUS_HOST;
    input->id.vendor = 0x0001;
    input->id.product = 0x0001;
    input->id.version = 0x0100;                
    ......

    for (i = 0; i < pdata->nbuttons; i++) {        //初始化所用到的 GPIO
        struct gpio_keys_button *button = &pdata->buttons[i];
        struct gpio_button_data *bdata = &ddata->data[i];
        unsigned int type = button->type ?: EV_KEY;
        
        bdata->input = input;
        bdata->button = button;
        
        error = gpio_keys_setup_key(pdev, bdata, button);
        if (error)
            goto fail2;
        
        if (button->wakeup)
            wakeup = 1;
        
        input_set_capability(input, type, button->code);
    }
    ......
    error = input_register_device(input);        //注册输入设备
    ......
}

After registering the input device, the core work of the underlying input device driver is only to report events when human actions such as keystrokes and touches occur. In the interrupt service function, the GPIO button driver   reports button events and synchronization events through functions such as input_event() and input_sync() .

It can be seen from the underlying GPIO button driver that there is no file_operation action in the driver, and there are no various I/O models. Registration into the system also uses   input-related APIs such as input_register_device() .

This is because this part of the code that interfaces with Linux VFS is all implemented in drivers/input/evdev.c:

The file_operations and read() functions of the input  core layer:

static ssize_t evdev_read(struct file *file, char __user *buffer,
              size_t count, loff_t *ppos)
{
    struct evdev_client *client = file->private_data;
    struct evdev *evdev = client->evdev;
    struct input_event event;
    int retval;

    if (count < input_event_size())
        return -EINVAL;

    if (!(file->f_flags & O_NONBLOCK)) {                //检查是否是非阻塞访问
            retval = wait_event_interruptible(evdev->wait,
                client->packet_head != client->tail || !evdev->exist);
        if (retval)
            return retval;
    }
    
    if (!evdev->exist)
        return -ENODEV;

    while (retval + input_event_size() <= count &&        //处理了阻塞的睡眠情况
            evdev_fetch_next_event(client, &event)) {
    
        if (input_event_to_user(buffer + retval, &event))
            return -EFAULT;
    
        retval += input_event_size();
    }

    if (retval == 0 && file->f_flags & O_NONBLOCK)
        retval = -EAGAIN;
    return retval;
}

3.2 RTC device driver

The RTC (real-time clock) is powered by a battery and can still keep time normally even when the system is powered off. It usually also has the ability to generate periodic interrupts and alarm clock interrupts, and is a typical character device.

As a character device driver, RTC needs to implement the interface functions in file_operations, such as open(), read(), etc. The typical RTC IOCTL includes  RTC_SET_TIME , RTC_ALM_READ , RTC_ALM_SET , RTC_IRQP_SET , RTC_IRQP_READ , etc. These are common to RTC, so these common ones are placed in the core layer of RTC, while the specific implementation related to the device is placed at the bottom layer.

与 RTC 核心有关的文件有:
/drivers/rtc/class.c        //该文件向linux设备模型核心注册了一个类RTC,然后向驱动程序提供了注册/注销接口
/drivers/rtc/rtc-dev.c      //该文件定义了基本的设备文件操作函数,如:open,read等
/drivers/rtc/interface.c    //该文件主要提供用户程序与RTC驱动的接口函数,用户程序一般通过ioctl与RTC                              //驱动交互,这里定义了每个ioctl命令需要调用的函数
/drivers/rtc/rtc-sysfs.c    //与sysfs有关
/drivers/rtc/rtc-proc.c     //与proc文件系统有关
/include/linux/rtc.h        //定义了与RTC有关的数据结构

The RTC driver model is as shown below:

picture

Below we mainly understand the following points of the RTC core:

  1. Implement the member functions of file_operations and some common RTC control codes;

  2. Export  rtc_device_register() and  rtc_device_unregister() to the bottom layer to register and unregister RTC;

  3. Export the rtc_class_ops structure to describe the underlying RTC hardware operations.

Under such a driver model, the underlying RTC driver no longer needs to care about the specific implementation of RTC as a character device driver, nor does it need to care about some general RTC control logic. The relationship is as follows:

picture

Take the RTC driver of S3C6410 as an example:

RTC core:

1. In the file drivers/rtc/rtc-dev.c: implement file_operations related member functions

static const struct file_operations rtc_dev_fops = {
    .owner        = THIS_MODULE,
    .llseek        = no_llseek,
    .read        = rtc_dev_read,
    .poll        = rtc_dev_poll,
    .unlocked_ioctl    = rtc_dev_ioctl,
    .open        = rtc_dev_open,
    .release            = rtc_dev_release,
    .fasync        = rtc_dev_fasync,
};

2. In the file drivers/rtc/class.c: Provide registration/unregistration interface to the bottom layer

struct rtc_device *rtc_device_register(const char *name, struct device *dev,
                    const struct rtc_class_ops *ops,
                    struct module *owner)

void rtc_device_unregister(struct rtc_device *rtc)

3. In the file drivers/rtc/class.h: export the rtc_class_ops structure

struct rtc_class_ops {
    int (*open)(struct device *);
    void (*release)(struct device *);
    int (*ioctl)(struct device *, unsigned int, unsigned long);
    int (*read_time)(struct device *, struct rtc_time *);
    int (*set_time)(struct device *, struct rtc_time *);
    int (*read_alarm)(struct device *, struct rtc_wkalrm *);
    int (*set_alarm)(struct device *, struct rtc_wkalrm *);
    int (*proc)(struct device *, struct seq_file *);
    int (*set_mmss)(struct device *, unsigned long secs);
    int (*read_callback)(struct device *, int data);
    int (*alarm_irq_enable)(struct device *, unsigned int enabled);
};

S3C6410 bottom layer: in the drivers/rtc/rtc-s3c.c file

It registers RTC and binds rtc_class_ops:

static const struct rtc_class_ops s3c_rtcops = {
    .read_time    = s3c_rtc_gettime,
    .set_time    = s3c_rtc_settime,
    .read_alarm    = s3c_rtc_getalarm,
    .set_alarm    = s3c_rtc_setalarm,
    .alarm_irq_enable = s3c_rtc_setaie,
};
static int __devinit s3c_rtc_probe(struct platform_device *pdev)
{
    ......
    /* register RTC and exit */
    rtc = rtc_device_register("s3c", &pdev->dev, &s3c_rtcops,
                  THIS_MODULE);
    ......
}

drivers/rtc/rtc-dev.c and the RTC core layers called drivers/rtc/interface.c are equivalent to indirectly "forwarding" open(), release(), read and set time, etc. in file_operations An underlying instance is given. The following is an excerpt of part of the process in which the RTC core layer calls specific underlying driver callbacks:

1)open

/* 文件 drivers/rtc/rtc-dev.c 中: */

static int rtc_dev_open(struct inode *inode, struct file *file)
{
    const struct rtc_class_ops *ops = rtc->ops;
    ......
    err = ops->open ? ops->open(rtc->dev.parent) : 0;
    ......
}

2) IOCTL command :

/* 文件 drivers/rtc/rtc-dev.c 中 */
static long rtc_dev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    ......
    switch (cmd) {
    case RTC_ALM_READ:
        ......    
        err = rtc_read_alarm(rtc, &alarm);
        ......
    case RTC_ALM_SET:
        ......
    case RTC_SET_TIME:
        ......        
        return rtc_set_time(rtc, &tm);
    ......
    }
    ......
}

/* 文件 drivers/rtc/interface.c 中 */
static int __rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
{
    int err;
    if (!rtc->ops)
        err = -ENODEV;
    else if (!rtc->ops->read_time)        //回调
        err = -EINVAL;
    ......
}
    
int rtc_read_time(struct rtc_device *rtc, struct rtc_time *tm)
{
    ......
    err = __rtc_read_time(rtc, tm);
    ......
}

3.3 Framebuffer device driver

Without going in-depth, refer to "Detailed Explanation of Linux Device Driver Development: Based on the Latest Linux 4.0 Kernel"

3.4 Terminal device driver

In the Linux system, the terminal is a character-type device, which has many types. Usually, tty (Teletype) is used to refer to various types of terminal devices. In embedded systems, the most commonly used is the UART serial port.

3.4.1 Hierarchy of tty in the kernel

picture

The diagram contains three levels:

  1. tty_io.c : tty core;

  2. n_tty.c : tty line discipline;

  3. xxx_tty.c : tty driver instance.

3.4.1.1 tty_io.c

tty_io.c  itself is a standard character device driver. Therefore, it has the responsibility of character devices and needs to implement the  file_operations  structure member function.

However, the tty core layer defines  the architecture of tty_driver  , so the main work of the tty device driver becomes filling  in the members of the tty_driver  structure and implementing the member functions of its member  tty_operations  structure, instead of implementing  the file_operations  structure . Body member functions work at this level.

struct tty_driver {
    ......
    /*
    * Driver methods
    */
    
    const struct tty_operations *ops;
    struct list_head tty_drivers;
};

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
    struct inode *inode, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
        const unsigned char *buf, int count);
    ......
    const struct file_operations *proc_fops;
};
3.4.1.2 n_tty.c

n_tty.c : The job of the tty line discipline is to format data received from a user or hardware in a special way. This formatting often takes the form of a protocol conversion.

3.4.2 Send/receive process of tty device

picture

Sending process : The tty core obtains data from a user to be sent to a tty device. The tty core passes the data to the tty line discipline driver. The data is then passed to the tty driver. The tty driver converts the data into a format that can be sent to the hardware.

The member function write() of  the tty_driver  operation set  tty_operations  receives 3 parameters: tty_struct, pointer to send data and the number of bytes to send. This function is   invoked indirectly by the write() member function of file_operations .

Receiving process : The data received from the tty hardware is handed up to the tty driver, then enters the tty line discipline driver, and then enters the tty core, where it is obtained by a user.

 The tty driver generally pushes the receive buffer to the line discipline through tty_flip_buffer_push() after receiving characters  .

3.4.3 Serial port core layer

Although a specific underlying UART device driver can be designed according to the above  tty_driver  method, that is, defining tty_driver and implementing   the member functions in tty_operations , in view of the commonality between serial ports, Linux considers in the file drivers/tty/serial/serial_core.c A common tty driver layer (called the serial port core layer) is implemented for UART devices. In this way, the main task of the UART driver has further evolved into implementing a set of uart_xxx interfaces defined in the file serial_core.c, rather than the tty_xxx interface.

According to the object-oriented idea, it can be considered that tty_driver is a generalization of character devices, serial_core is a generalization of tty_driver, and the specific serial port driver is a generalization of serial_core.

picture

In the serial port core layer, a new  uart_driver  structure and its operation set  uart_ops are defined . A low-level UART driver needs to create and   register a  uart_driver  instead of  a tty_driver via uart_register_driver() .

struct uart_driver {
    struct module       *owner;
    const char        *driver_name;
    const char        *dev_name;
    int             major;
    int             minor;
    int             nr;
    struct console     *cons;
    
    /*
    * these are private; the low level driver should not
    * touch these; they should be initialised to NULL
    */
    struct uart_state    *state;
    struct tty_driver    *tty_driver;
};


int uart_register_driver(struct uart_driver *drv);
void uart_unregister_driver(struct uart_driver *drv);

The uart_driver structure is essentially derived from the tty_driver structure, so the uart_driver structure contains the tty_dirver structure members.

tty_operations is further generalized to uart_ops  at the UART level  :

struct uart_ops {
    unsigned int    (*tx_empty)(struct uart_port *);
    void    (*set_mctrl)(struct uart_port *, unsigned int mctrl);
    unsigned int    (*get_mctrl)(struct uart_port *);
    void    (*stop_tx)(struct uart_port *);
    void    (*start_tx)(struct uart_port *);
    void    (*send_xchar)(struct uart_port *, char ch);
    void    (*stop_rx)(struct uart_port *);
    void    (*enable_ms)(struct uart_port *);
    void    (*break_ctl)(struct uart_port *, int ctl);
    int     (*startup)(struct uart_port *);
    void    (*shutdown)(struct uart_port *);
    void    (*flush_buffer)(struct uart_port *);
    void    (*set_termios)(struct uart_port *, struct ktermios *new,
           struct ktermios *old);
    void    (*set_ldisc)(struct uart_port *, int new);
    void    (*pm)(struct uart_port *, unsigned int state,
         unsigned int oldstate);
    int     (*set_wake)(struct uart_port *, unsigned int state);
    void    (*wake_peer)(struct uart_port *);
    
    /*
    * Return a string describing the type of the port
    */
    const char *(*type)(struct uart_port *);
    
    /*
    * Release IO and memory resources used by the port.
    * This includes iounmap if necessary.
    */
    void    (*release_port)(struct uart_port *);
    
    /*
    * Request IO and memory resources used by the port.
    * This includes iomapping the port if necessary.
    */
    int     (*request_port)(struct uart_port *);
    void    (*config_port)(struct uart_port *, int);
    int     (*verify_port)(struct uart_port *, struct serial_struct *);
    int     (*ioctl)(struct uart_port *, unsigned int, unsigned long);
#ifdef CONFIG_CONSOLE_POLL
    void    (*poll_put_char)(struct uart_port *, unsigned char);
    int     (*poll_get_char)(struct uart_port *);
#endif
};

Since driver/tty/serial/serial_core.c is a  tty_driver  , there is an instance of tty_operations in serial_core.c. The member function of this instance will further call the member function of struct uart_ops, so that the member function in file_operaions, The member functions of tty_operations and the member functions of uart_ops are strung together.

3.5 misc device driver

......

3.6 Driver core layer

The three major responsibilities of the core layer:

  1. Provides an interface to the above. The reading, writing, and ioctl of file_operations are all handled by the middle layer, and various I/O models are also handled.

  2. The middle layer implements common logic. Code that can be shared by various instances of the bottom layer is handled by the middle layer to avoid repeated implementation of the bottom layer.

  3. Define the framework. The underlying driver no longer needs to care about the Linux kernel VFS interface and various possible I/O models, but only needs to handle access related to specific hardware.

Sometimes this layering is not just two layers, but can have more layers, which are presented in software as class inheritance and polymorphism in object-oriented software.

picture

4. Design idea of ​​separation of host driver and peripheral driver

4.1 Separation of host driver and peripheral driver

Subsystems such as SPI, I2C, and USB in Linux are all typical ideas that use the separation of host drivers and peripheral drivers.

Let the host side only be responsible for generating the transmission waveform on the bus, while the peripheral side only allows the host side to access itself with appropriate waveforms through standard APIs. 4 software modules are involved:

1. Host-side driver. According to the hardware manual of the specific SPI, I2C, USB and other controllers, operate the specific controller to generate various waveforms of the bus.

2. The link between the host and peripherals. The peripheral does not directly call the driver on the host side to generate the waveform, but calls a standard API. The standard API indirectly "forwards" the waveform transmission request to the specific host driver. It would be better here to standardize the description of the waveform in some data structure as well.

3. Peripheral side driver. Peripherals are connected to buses such as SPI, I2C, and USB, but they themselves can be touch screens, network cards, sound cards, or any type of device. When these peripherals require SPI, I2C, USB, etc. to access it, it calls the standard API of the "Link between Host and Peripheral" module.

4. Board level logic. Used to describe how hosts and peripherals are interconnected, it is equivalent to a "routing table". Assuming that there are multiple SPI controllers and multiple SPI peripherals on the board, who is connected to whom? It is neither the responsibility of the host side nor the responsibility of the peripheral side to manage the interconnection relationship, which is the responsibility of the board-level logic.

Through the above design method, Linux is divided into 4 lightweight small modules, each module performs its own duties.

4.2 Linux SPI host and device driver

4.2.1 SPI host driver

In Linux,  the spi_master  structure is used to describe an SPI active controller driver. Its main members include the serial number of the host controller, the number of chip selects, the SPI mode, clock setting related functions and data transmission related functions.

In the file spi/spi.h

struct spi_master {
    struct device dev;
    
    struct list_head list;
    
    /* other than negative (== assign one dynamically), bus_num is fully
    * board-specific.  usually that simplifies to being SOC-specific.
    * example:  one SOC has three SPI controllers, numbered 0..2,
    * and one board's schematics might show it using SPI-2.  software
    * would normally use bus_num=2 for that controller.
    */
    s16            bus_num;
    
    /* chipselects will be integral to many controllers; some others
    * might use board-specific GPIOs.
    */
    u16            num_chipselect;
    
    /* some SPI controllers pose alignment requirements on DMAable
    * buffers; let protocol drivers know about these requirements.
    */
    u16            dma_alignment;
    
    /* spi_device.mode flags understood by this controller driver */
    u16            mode_bits;
    
    /* other constraints relevant to this driver */
    u16            flags;
    #define SPI_MASTER_HALF_DUPLEX    BIT(0)        /* can't do full duplex */
    #define SPI_MASTER_NO_RX    BIT(1)        /* can't do buffer read */
    #define SPI_MASTER_NO_TX    BIT(2)        /* can't do buffer write */
    
    /* lock and mutex for SPI bus locking */
    spinlock_t bus_lock_spinlock;
    struct mutex bus_lock_mutex;
    
    /* flag indicating that the SPI bus is locked for exclusive use */
    bool bus_lock_flag;
    
    /* Setup mode and clock, etc (spi driver may call many times).
    *
    * IMPORTANT:  this may be called when transfers to another
    * device are active.  DO NOT UPDATE SHARED REGISTERS in ways
    * which could break those transfers.
    */
    int (*setup)(struct spi_device *spi);
    
    /* bidirectional bulk transfers
    *
    * + The transfer() method may not sleep; its main role is
    *   just to add the message to the queue.
    * + For now there's no remove-from-queue operation, or
    *   any other request management
    * + To a given spi_device, message queueing is pure fifo
    *
    * + The master's main job is to process its message queue,
    *   selecting a chip then transferring data
    * + If there are multiple spi_device children, the i/o queue
    *   arbitration algorithm is unspecified (round robin, fifo,
    *   priority, reservations, preemption, etc)
    *
    * + Chipselect stays active during the entire message
    *   (unless modified by spi_transfer.cs_change != 0).
    * + The message transfers use clock and SPI mode parameters
    *   previously established by setup() for this device
    */
    int (*transfer)(struct spi_device *spi,
            struct spi_message *mesg);
    
    /* called on release() to free memory provided by spi_master */
    void (*cleanup)(struct spi_device *spi);
};

APIs for allocating, registering and unregistering SPI hosts are provided by the SPI core: file drivers/spi/spi.c

struct spi_master *spi_alloc_master(struct device *dev, unsigned size);
int spi_register_master(struct spi_master *master);
void spi_unregister_master(struct spi_master *master);

The main body of the SPI host controller driver implements   member functions such as transfer() and setup() of spi_master . It is also possible to implement   member functions such as txrx_buf(), setup_transfer(), and chipselect() of spi_bitbang .

For example in the file driver/spi/spi_s3c24xx.c:

static int __init s3c24xx_spi_probe(struct platform_device *pdev)
{
    struct s3c2410_spi_info *pdata;
    struct s3c24xx_spi *hw;
    struct spi_master *master;
    struct resource *res;
    ......
    /* initialise fiq handler */
    
    s3c24xx_spi_initfiq(hw);
    
    /* setup the master state. */
    
    /* the spi->mode bits understood by this driver: */
    master->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH;    //设置模式
    
    master->num_chipselect = hw->pdata->num_cs;               //设置片选序号
    master->bus_num = pdata->bus_num;                      //主机控制器的序号
    
    /* setup the state for the bitbang driver */
    
    hw->bitbang.master         = hw->master;
    hw->bitbang.setup_transfer = s3c24xx_spi_setupxfer;
    hw->bitbang.chipselect     = s3c24xx_spi_chipsel;
    hw->bitbang.txrx_bufs      = s3c24xx_spi_txrx;
    
    hw->master->setup  = s3c24xx_spi_setup;
    hw->master->cleanup = s3c24xx_spi_cleanup;
    ......
}

4.2.2 Ties

......

4.2.3 SPI peripheral driver

In Linux, an SPI peripheral driver is described through the spi_driver structure. This peripheral driver can be considered as the client driver of spi_mater. SPI is just a bus, and the role of spi_driver is only to attach SPI peripherals to the bus. Therefore, in the probe() member function of spi_driver, the type of device driver to which the SPI peripheral itself belongs will be registered.

In the file spi/spi.h:

struct spi_driver {
    const struct spi_device_id *id_table;
    int    (*probe)(struct spi_device *spi);
    int    (*remove)(struct spi_device *spi);
    void (*shutdown)(struct spi_device *spi);
    int    (*suspend)(struct spi_device *spi, pm_message_t mesg);
    int    (*resume)(struct spi_device *spi);
    struct device_driver driver;
};

static int spi_drv_probe(struct device *dev)
{
    const struct spi_driver *sdrv = to_spi_driver(dev->driver);

    return sdrv->probe(to_spi_device(dev));
}

int spi_register_driver(struct spi_driver *sdrv)
{
    sdrv->driver.bus = &spi_bus_type;
    if (sdrv->probe)
        sdrv->driver.probe = spi_drv_probe;
    if (sdrv->remove)
        sdrv->driver.remove = spi_drv_remove;
    if (sdrv->shutdown)
        sdrv->driver.shutdown = spi_drv_shutdown;
    return driver_register(&sdrv->driver);
}

It can be seen that the spi_driver structure and the platform_driver structure are very similar. They have interfaces such as prob(), remove(), suspend(), resume() and instances of device_driver. (This is a common template for almost all client drivers)

In the SPI peripheral driver (files spi/spi.h and driver/spi/spi.c ):

1.  spi_tansfer  structure: an interface for data transmission through the SPI bus.

2.  spi_message  structure: organize one or more spi_transfer to complete a complete SPI transfer process.

3. Initialize  spi_message :

static inline void spi_message_init(struct spi_message *m);

4. Add  spi_transfer  to  the spi_message  queue:

spi_message_add_tail(struct spi_transfer *t, struct spi_message *m);

5.  The synchronous transmission API of spi_message  blocks and waits for the message to be processed:

spi_sync(struct spi_device *spi, struct spi_message *message);

6.  The asynchronous transmission API of spi_message  will not block and wait for the message to be processed, but a callback function can be attached to the  complete  field of spi_message. When the message is processed, the function will be called:

spi_async(struct spi_device *spi, struct spi_message *message);

7. Example of initializing  spi_transfer and spi_message  and performing SPI data transmission. At the same time,  spi_write()  and  spi_read()  are also two common APIs of the SPI core layer. They can be directly called in the peripheral driver for simple pure write and pure read operations:

static inline int
spi_write(struct spi_device *spi, const void *buf, size_t len)
{
    struct spi_transfer t = {
            .tx_buf = buf,
            .len    = len,
        };
    struct spi_message m;
    
    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    return spi_sync(spi, &m);
}

static inline int
spi_read(struct spi_device *spi, void *buf, size_t len)
{
    struct spi_transfer    t = {
        .rx_buf    = buf,
        .len    = len,
    };
    struct spi_message    m;
    
    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    return spi_sync(spi, &m);
}

4.2.4 SPI board level logic

Just like platform_driver corresponds to a platform_device, spi_driver also corresponds to a spi_device; platform_device needs to add board information data in the BSP board file, and the same spi_device is also required.

The board information of spi_device is described by  the spi_board_info  structure, which records the host controller serial number, chip select serial number, data bit rate, SPI transmission mode, etc. used by the SPI peripheral.  

There are two ways to add board-level information:

1. Similar to adding platform_device to platform_add_devices,   register it in the machine init_machine( ) function  during the Linux startup process  through spi_register_board_info() :

In the file arch/arm/mach-exynos/mach-itop4412.c:

static struct spi_board_info spi_board_info[] __initdata = {
    {
        .modalias    = "lms501kf03",
        .platform_data    = NULL,
        .max_speed_hz    = 1200000,
        .bus_num    = LCD_BUS_NUM,
        .chip_select    = 0,
        .mode        = SPI_MODE_3,
        .controller_data = (void *)DISPLAY_CS,
    }
};


spi_register_board_info(spi_board_info, ARRAY_SIZE(spi_board_info));

2. After the kernel after ARM Linux 3.x is changed to the device tree, it is no longer necessary to encode the SPI board-level information in arch/arm/mach-xxx, but prefers to fill in the sub-nodes under the SPI controller node. .

Guess you like

Origin blog.csdn.net/weixin_41114301/article/details/132773031