Linux設備驅動中的軟件架構思想


更新記錄

version status description date author
V1.0 C Create Document 2019.3.31 John Wan

status:
C―― Create,
A—— Add,
M—— Modify,
D—— Delete。

注:內核版本 3.0.15,迅為iTop4412開發板

一、Linux驅動的軟件架構

1.1 出發點

  為適應多種體系架構的硬件,增強系統的可重用和跨平台能力。

1.2 分離思想

  為達到一個驅動最好一行都不改就可以適用任何硬件平台的目的,將驅動與設備分離開來,驅動只管驅動,設備只管設備,而驅動以某種通用的標准途徑去拿板級信息,從而降低驅動與設備的耦合程度。

1.3 分層思想

  對於同類設備,其基本框架都是一樣的,那么提煉出一個中間層,例如:對於 Input 設備(按鍵、鍵盤、觸摸屏、鼠標)來說,盡管 file_operation、I/O模型不可或缺,但基本框架都是一樣的,因此可提煉出一個 Input 核心層,把跟 Linux 接口以及整個一套 input 事件的匯報機制都在這里面實現。

二、platform設備驅動

  platform:linux中的一種虛擬總線。一個現實的linux設備和驅動通常都需要掛接在一種總線上(方便管理),例如PCI、USB、I2C、SPI等,但是對於在Soc系統中集成的獨立外設控制器、掛接在Soc內存空間的外設等卻不能依附於上述總線,這時候linux就發明了一種虛擬總線,來管理這一類的設備(沒有實體的硬件總線電路)。

  platform設備驅動模型中,分為設備、驅動、總線3個實體,分別稱為 platform_devicepaltform_driverplatform總線,總線負責將設備和驅動進行綁定。在系統每注冊一個設備時,會尋找與之匹配的驅動;相反的,在系統每注冊一個驅動時,會尋找與之匹配的設備,而匹配的過程則由總線完成。

2.1 platform設備

  platform設備:由 platform_device 結構體構成,負責管理外設的資源,例如 I/O資源、內存資源、中斷資源等等。

  原型: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 結構體

   resource 結構體,描述了 platform_device 的資源:

struct resource {
	resource_size_t start;		//資源的開始
	resource_size_t end;		//資源的結束
	const char *name;
	unsigned long flags;		//資源的類型
	struct resource *parent, *sibling, *child;
};

  參數 flags 常用類型 IORESOURCE_IOIORESOURCE_MEMIORESOURCE_IRQIORESOURCE_DMA等。參數 startend 的含義會隨着 flags 的不同有所變化。

1)flagsIORESOURCE_MEMstartend 分別表示該platform_device占據的內存的開始與結束地址;

2)flagsIORESOURCE_IRQstartend 分別表示該`platform_device 使用的中斷號的開始值與結束值,如果使用 1個中斷號,開始與結束值相同;

  同類型的資源可以有多份,例如某設備占據了多個內存區域,則可以定義多個 IORESOURCE_MEM

  例如在 arch/arm/mach-at91/board-sam9261ek.c 板文件中為 DM9000 網卡定義的 resource:

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 device 結構體中的 platform_data 資源

  設備除了可在 BSP 中定義資源以外,還可以附加一些數據信息,因為對設備的硬件描述除了中斷、內存等標准資源以外,可能還會有一些配置信息,而這些配置信息也依賴於板,不適宜直接放在設備驅動上。

  因此,platform_device 提供可供每個設備驅動自定義的 platform_data 形式以支持添加一些數據信息,即 Linux 內核不對這塊的數據做要求。

  device 結構體:

/**
 * 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 */
......
};

  例如在 arch/arm/mach-at91/board-sam9261ek.c 板文件中,將 platform_data 定義了 dm9000_plat_data 結構體,完成定義后,將MAC地址、總線寬度、板上有無EEPROM信息等放入:

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 platform_device 的注冊

  對於Linux 2.6 ARM 平台而言,對 platform_device 的定義通常在 BSP 的板文件中實現,在板文件中,將 platform_device 歸納為一個數組,隨着板文件的加載,最終通過 platform_add_devices() 函數統一注冊。

  platform_add_devices() 函數可以將平台設備添加到系統中,這個函數的原型為:

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

  第一個參數為平台設備數組的指針,第二個參數為平台設備的數量,函數的內部是調用 platform_device_register() 函數逐一注冊平台設備。

  如果注冊順利,可在 sys/devices/platform 目錄下看到相應名字的子目錄。

  Linux 3.x 之后,ARM Linux 不太以編碼的形式去填寫 platform_device 和注冊,更傾向於根據設備樹中的內容自動展開platform_device

2.2 platform驅動

  platform驅動:由 platform_driver 結構體構成,負責驅動的操作實現,例如加載、卸載、關閉、懸掛、恢復等。原型位於 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()remove() 分別對應驅動在加載、卸載時執行的操作。

  而直接填充 platform_driversuspend()resume() 做電源管理回調的方法目前已經過時,較好的做法是實現 platfrom_driverdevice_driverdev_pm_ops 結構體成員(詳細的參考電源管理章節)。

2.2.1 device_driver 結構體

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;
};

  與 platform_driver 地位對等的 i2c_driverspi_driverusb_driverpci_driver中都包含了 device_driver結構體實例成員。它其實描述了各種 xxx_driver(xxx是總線名)在驅動意義上的一些共性。

2.2.2 驅動中獲取板的資源

  獲取設備中 resource 資源: drivers/net/dm9000.c 中的 dm9000_probe()函數

	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);

  或者:

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

  實際上是調用了 platform_get_resource(dev, IORESOURCE_IRQ, num);

  獲取設備中 platform_data 資源: drivers/net/dm9000.c 中的 dm9000_probe()函數

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

2.2.3 platform_driver 的注冊

  通過 platform_driver_register()platform_driver_unregister() 進行 platform_driver 的注冊於注銷。

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);

  而原本的字符設備(或其它設備)的注冊和注銷工作移交到 platform_driverprobe()remove() 成員函數中。以這樣的形式對字符設備驅動進行注冊,只是套了一層 platform_driver 的外殼,並沒有改變是字符設備的本質。

  例如在 drivers/net/dm9000.c 中,還是將其定義為網絡設備,只是將網絡設備驅動的注冊流程放在 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總線

  platform總線:負責管理外設與驅動之間的匹配。

  系統為 platfrom總線 定義了一個 bus_type 的實例 platform_bus_type,其定義位於 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 成員函數

  重點關注其 match() 成員函數,此成員函數確定了 platform_deviceplatform_driver 之間是如何進行匹配的。

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);
}

  可以看出 platform_deviceplatform_driver 之間匹配有 3 種可能性:

1)基於設備樹風格的匹配;

2)匹配 ID 表(即 platform_device 設備名是否出現在 platform_driver 的 ID 表內);

3)匹配 platform_device 設備名和驅動的名字。

2.3.2 platform總線的注冊

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總線自動匹配

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()		---------

  無論是先注冊設備還是先注冊設備驅動,都會進行一次設備與設備驅動的匹配過程,匹配成功之后就會將其進行綁定,匹配的原理就是去遍歷總線下設備或者設備驅動的鏈表。

2.4 platform 的優點

1)使得設備被掛接在一個總線上,符合 Linux 2.6 以后內核的設備模型。其結果是使配套的 sysfs 節點、設備電源管理都成為可能。

2)將 BSP 和 驅動隔離。在 BSP 中定義 platform 設備和設備使用的資源、設備的具體配置信息,而在驅動中,只需要通過通用 API 去獲取資源和數據,做到了板相關代碼和驅動代碼的分離,使得驅動具有更好的可擴展性和跨平台性。

3)讓一個驅動支持多個設備實例。譬如 DM9000 的驅動只有一份,但是我們可以在板級添加多份 DM9000 的 platform_device,他們都可以與唯一的驅動匹配。

4)在 Linux 3.x之后的內核中,DM9000 驅動可通過設備樹的方法被枚舉,添加的動作只需要簡單的修改 dts 文件。(詳細的后續再貼鏈接)

三、設備驅動的分層思想

  在面向對象的程序設計中,可以為某一類相似的事物定義一個基類,而具體的事物可以繼承這個基類中的函數。如果對於繼承的這個事物而言,某成員函數的實現與基類一致,那它就可以直接繼承基類的函數;相反,它也可以重寫(Overriding),對父類的函數進行重新定義。若子類中的方法與父類中的某方法具有相同的方法名、返回類型和參數表,則新方法將覆蓋原有的方法。這樣可以極大的提高代碼的可重用能力。

  雖然 Linux 內核完全是由 C 和 匯編寫的,但卻頻繁用到了面向對象的設計思想。在設備驅動方面,往往為同類的設備設計一個框架,而框架中的核心層則實現了該設備通用的一些功能。同樣的,如果具體的設備不想使用核心層的函數,也可以重寫。

  • 例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代碼 */
	...
}

  在 core_funca() 函數的實現中,會檢查底層設備是否重載了 core_funca()。如果重載了,就調用底層的代碼,否則,直接使用通用層的。這樣做的好處是,核心層的代碼可以處理絕大多數該類設備的 core_funca() 對應的功能,只有少數特殊設備需要重新實現 core_funca()

  • 例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();
}

  上述代碼假定為了實現funca(),對於同類設備而言,操作流程一致,都要經過“通用代碼A、底層ops1、通用代碼B、底層ops2、通用代碼C、底層ops3”這幾步,分層設計明顯帶來的好處是,對於通用代碼A、B、C,具體的底層驅動不需要再實現(抽離出來,放到核心層實現),而僅僅只關心其底層的操作ops1、ops2、ops3。下圖明確反映了設備驅動的核心層與具體設備驅動的關系,實際上,這種分層可能只有2層,也可能是多層。

  這樣的分層設計在 Linux 的 Input、RTC、MTD、I2C、SPI、tty、USB等諸多類型設備驅動中都存在。

3.1 輸入設備驅動

  輸入設備(如按鍵、鍵盤、觸摸屏、鼠標等)是典型的字符設備,其一般的工作機理是底層在按鍵、觸摸等動作發送時產生一個中斷(或驅動通過 Timer 定時查詢),然后CPU通過SPI、I2C 或外部存儲器總線讀取鍵值、坐標等數據,放入1個緩沖區,字符設備驅動管理該緩沖區,而驅動的 read() 接口讓用戶可以讀取鍵值、坐標等數據。

  顯然,在這些工作中,只有中斷、讀值是設備相關的,而輸入事件的緩沖區管理以及字符設備驅動的 file_operations 接口則對輸入設備是通用的。基於此,內核設計了輸入子系統,由核心層處理公共的工作。

3.1.1 輸入核心提供了底層輸入設備驅動程序所需的API

如分配/釋放一個輸入設備:

struct input_dev *input_allocate_device(void);	//返回的結構體用於表征1個輸入設備。

void input_free_device(struct input_dev *dev);

注冊/注銷輸入設備用的如下接口:

int __must_check input_register_device(struct input_dev *);

void input_unregister_device(struct input_dev *);

報告輸入事件用的如下接口:

/* 報告指定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);

  而所有的輸入事件,內核都用統一的數據結構來描述,這個數據結構是input_event:

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

3.1.2 案例:gpio按鍵驅動

  drivers/input/keyboard/gpio_keys.c 是基於 input 架構實現的一個通用的 GPIO 按鍵驅動。該驅動基於 platform_driver架構,名為 “gpio-keys”。它將硬件相關的信息(如使用的GPIO號,電平等)屏蔽在板文件 platform_device 的 platform_data 中,因此該驅動可應用於各個處理器,具有良好的跨平台性。

  該驅動的 probe() 函數:

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);		//注冊輸入設備
......
}

  在注冊輸入設備后,底層輸入設備驅動的核心工作只剩下在按鍵、觸摸等人為動作發生時報告事件。在中斷服務函數中,GPIO 按鍵驅動通過 input_event()input_sync() 這樣的函數來匯報按鍵事件以及同步事件。

  從底層的 GPIO 按鍵驅動可以看出,該驅動中沒有任何 file_operation 的動作,也沒有各種 I/O 模型,注冊進入系統也用的是 input_register_device() 這樣與 input 相關的 API。

  這是由於與 Linux VFS 接口的這一部分代碼全部都在 drivers/input/evdev.c 中實現了:

  input 核心層的 file_operations 和 read() 函數:

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 設備驅動

  RTC (實時時鍾)借助電池供電,在系統掉電的情況下依然可以正常計時。通常還具有產生周期性中斷以及鬧鍾中斷的能力,是一種典型的字符設備。

  作為一種字符設備驅動,RTC 需要實現 file_operations 中的接口函數,例如 open()、read()等等。而 RTC 典型的 IOCTL 包括 RTC_SET_TIMERTC_ALM_READRTC_ALM_SETRTC_IRQP_SETRTC_IRQP_READ等,這些對於 RTC 來說是通用的,那么這些通用的就放在 RTC 的核心層,而與設備相關的具體實現則放在底層。

與 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有關的數據結構

RTC 驅動模型如下圖:

下面主要了解 RTC 核心 的以下幾點:

1)實現 file_operations 的成員函數以及一些通用的關於 RTC 的控制代碼;

2)向底層導出 rtc_device_register()rtc_device_unregister()以注冊和注銷 RTC;

3)導出 rtc_class_ops 結構體以描述底層的 RTC 硬件操作。

  在這樣的驅動模型下,底層的 RTC 驅動不再需要關心 RTC 作為字符設備驅動的具體實現,也無需關心一些通用的 RTC 控制邏輯。關系如下:

以S3C6410 的 RTC驅動為例:

RTC 核心:

   1) 在文件 drivers/rtc/rtc-dev.c 中:實現 file_operations 相關成員函數

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)在文件 drivers/rtc/class.c中:向底層提供注冊/注銷接口

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)在文件 drivers/rtc/class.h中:導出 rtc_class_ops 結構體

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底層:在drivers/rtc/rtc-s3c.c 文件中

   其注冊 RTC 以及綁定 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 以及其調用的drivers/rtc/interface.c 等 RTC 核心層相當於把 file_operations 中的 open()、release()、讀取和設置時間等,都間接 “轉發” 給了底層的實例。如下摘取部分 RTC 核心層調用具體底層驅動 callback 的過程:

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的 命令:

/* 文件 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 設備驅動

  未深入,參考《Linux設備驅動開發詳解:基於最新的Linux 4.0內核》

3.4 終端設備驅動

  在 Linux 系統中,終端是一種字符型設備,它有多種類型,通常使用 tty (Teletype)來簡稱各種類型的終端設備。在嵌入式系統中,最常用的是 UART 串行端口。

3.4.1 內核中 tty 的層次結構

  圖中包含三個層次:

1)tty_io.c:tty 核心;

2)n_tty.c:tty 線路規程;

3)xxx_tty.c:tty 驅動實例。

3.4.1.1 tty_io.c

  tty_io.c 本身是一個標准的字符設備驅動,因此,它對上有字符設備的職責,需實現 file_operations 結構體成員函數。

  但 tty 核心層對下又定義了 tty_driver 的架構,因此 tty 設備驅動的主體工作就變成了填充 tty_driver 結構體中的成員,實現其成員 tty_operations結構體的成員函數,而不再是去實現file_operations 結構體成員函數這一級的工作。

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:tty 線路規程的工作是以特殊的方式格式化從一個用戶或者硬件收到的數據,這種格式化常常采用一個協議轉換的形式。

3.4.2 tty 設備的發送/接收流程

  發送流程: tty 核心從一個用戶獲取將要發送給一個 tty 設備的數據,tty 核心將數據傳遞給 tty 線路規程驅動,接着數據被傳遞到 tty 驅動,tty 驅動將數據轉換為可以發送給硬件的格式。

  從 tty_driver 操作集 tty_operations 的成員函數 write() 函數接收3個參數: tty_struct、發送數據指針和發送的字節數。該函數是被 file_operations 的 write() 成員函數間接觸發調用的。

  接收流程:從 tty 硬件接收到的數據向上交給 tty 驅動,接着進入 tty 線路規程驅動,再進入 tty 核心,在這里它被一個用戶獲取。

  tty 驅動一般收到字符后會通過 tty_flip_buffer_push() 將接收緩沖區推到線路規程。

3.4.3 串口核心層

  盡管一個特定的底層 UART 設備驅動完全可以遵循上述 tty_driver 的方法來設計,即定義tty_driver 並實現 tty_operations 中的成員函數,但是鑒於串口之間的共性,Linux 考慮在文件 drivers/tty/serial/serial_core.c 中實現 UART 設備的通用 tty 驅動層(稱為串口核心層)。這樣,UART 驅動的主要任務就進一步演變成了實現 文件 serial_core.c中定義的一組 uart_xxx 接口,而不是 tty_xxx 接口。

  按照面向對象的思想,可認為 tty_driver 是字符設備的泛化、serial_core 是 tty_driver 的泛化,而具體的串口驅動又是 serial_core 的泛化。

  在串口核心層又定義新的 uart_driver 結構體和其操作集 uart_ops。一個底層的 UART 驅動需要創建和通過 uart_register_driver() 注冊一個 uart_driver 而不是 tty_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);

  uart_driver 結構體在本質上是派生自 tty_driver 結構體,因此,uart_driver 結構體中包含 tty_dirver 結構體成員。

  tty_operations 在UART 這個層面上也被進一步泛化為 uart_ops

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
};

  由於 driver/tty/serial/serial_core.c 是一個 tty_driver ,因此在 serial_core.c 中,存在一個 tty_operations 的實例,這個實例的成員函數會進一步調用 struct uart_ops 的成員函數,這樣就把 file_operaions 里的成員函數、tty_operations 的成員函數和 uart_ops 的成員函數串起來。

3.5 misc 設備驅動

3.6 驅動核心層

  核心層的 3 大職責:

  1)對上提供接口。file_operations 的讀、寫、ioctl 都被中間層搞定,各種 I/O 模型也被處理掉了。

  2)中間層實現通用邏輯。可以被底層各種實例共享的代碼都被中間層搞定,避免底層重復實現。

  3)對下定義框架。底層的驅動不再需要關心 Linux 內核 VFS 的接口和各種可能的 I/O 模型,而只需處理與具體硬件相關的訪問。

  這種分層有時候還不是兩層,可以有更多層,在軟件上呈現為面向對象里類繼承和多態的狀態。

四、主機驅動與外設驅動分離的設計思想

4.1 主機驅動與外設驅動分離

  Linux 中的 SPI、I2C、USB 等子系統都是典型的利用主機驅動和外設驅動分離的思想。

  讓主機端只負責產生總線上的傳輸波形,而外設端只是通過標准的 API 來讓主機端以適當的波形訪問自身。涉及 4 個軟件模塊:

  1)主機端的驅動。根據具體的 SPI、I2C、USB 等控制器的硬件手冊,操作具體的控制器,產生總線的各種波形。

  2)連接主機和外設的紐帶。外設不直接調用主機端的驅動來產生波形,而是調用一個標准的 API。由這個標准的 API 把這個波形的傳輸請求間接 “轉發” 給具體的主機端驅動。最好在這里把關於波形的描述也以某種數據結構標准化。

  3)外設端的驅動。外設接在 SPI、I2C、USB 這樣的總線上,但是它們本身可以是觸摸屏、網卡、聲卡或任意一種類型的設備。當這些外設要求 SPI 、I2C、USB等去訪問它的時候,它調用 “連接主機和外設的紐帶” 模塊的標准 API。

  4)板級邏輯。用來描述主機和外設是如何互聯的,它相當於一個 “路由表”。假設板子上有多個 SPI 控制器和多個 SPI 外設,那究竟誰接在誰上面?管理互聯關系,既不是主機端的責任,也不是外設端的責任,這屬於板級邏輯的責任。

  linux 通過上述設計方法,划分為 4 個輕量級的小模塊,各個模塊各司其職。

4.2 Linux SPI 主機和設備驅動

4.2.1 SPI 主機驅動

  在 Linux 中,通過 spi_master 結構體來描述一個 SPI 主動控制器驅動其主要成員由主機控制器的序號、片選數量、SPI 模式、時鍾設置相關函數 和 數據傳輸相關函數。

  文件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);
};

  分配、注冊和注銷 SPI 主機的 API 由 SPI 核心提供:文件 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);

  SPI 主機控制器驅動主體是實現了 spi_master 的 transfer()、setup() 這樣的成員函數。也可能實現 spi_bitbang 的 txrx_buf()、setup_transfer()、chipselect() 這樣的成員函數。

  例如在文件 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 紐帶

4.2.3 SPI 外設驅動

  在 Linux 中,通過 spi_driver 結構體來描述一個 SPI 外設驅動,這個外設驅動可以認為是 spi_mater 的客戶端驅動。SPI 只是一種總線,spi_driver 的作用只是將 SPI 外設掛接在該總線上,因此在 spi_driver 的 probe() 成員函數中,將注冊 SPI 外設本身所屬設備驅動的類型。

  文件 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);
}

  可看出,spi_driver 結構體 和 platform_driver 結構體有極大的相似性,都有 prob()、remove()、suspend()、resume()這樣的接口和 device_driver 的實例。(這幾乎是一切客戶端驅動的常用模板)

在SPI 外設驅動中(文件 spi/spi.hdriver/spi/spi.c ):

  1)spi_tansfer 結構體:通過 SPI 總線進行數據傳輸的接口。

  2)spi_message 結構體:組織一個或多個spi_transfer,從而完成一次完整的 SPI 傳輸流程。

  3)初始化 spi_message

static inline void spi_message_init(struct spi_message *m)

  4)將 spi_transfer 添加到 spi_message 隊列:

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

  5)spi_message 的同步傳輸 API,阻塞等待這個消息被處理完:

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

  6)spi_message 的異步傳輸 API,不會阻塞等待這個消息被處理完,但可在 spi_messagecomplete 字段掛接一個回調函數,當消息被處理完成后,該函數會被調用:

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

  7)初始化 spi_transferspi_message 並進行 SPI 數據傳輸的例子,同時 spi_write()spi_read() 也是SPI 核心層的兩個通用API,在外設驅動中可直接調用進行簡單的純寫、純讀操作:

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 板級邏輯

  通 platform_driver 對應着一個platform_device一樣,spi_driver 也對應着一個 spi_device;platform_device 需要在 BSP 的板文件中添加板信息數據,同樣的 spi_device 也需要。

  spi_device 的板信息用 spi_board_info 結構體描述,該結構體記錄着 SPI 外設使用的主機控制器序號、片選序號、數據比特率、SPI 傳輸模式等。

  兩種方式添加板級信息:

  1)與 platfrom_add_devices 添加 platform_device 類似,通過 spi_register_board_info() 在 Linux 啟動過程中的 機器 init_machine() 函數中進行注冊:

  在文件 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)在 ARM Linux 3.x 之后的內核在改為設備樹后,不再需要正在 arch/arm/mach-xxx 中編碼 SPI 的板級信息了,而傾向於在 SPI 控制器節點下填寫子節點。

參考

  1. 《Linux設備驅動開發詳解:基於最新的Linux 4.0內核》第12章 - 宋寶華
  2. linux RTC 驅動模型分析
  3. 迅為iTop4412資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM