Linux x86架构下ACPI PNP Hardware ID的识别机制
參考博客:
http://blog.csdn.net/jiangwei0512
參考文章:
http://blog.csdn.net/morixinguan/article/details/79138325
http://blog.chinaunix.net/uid-27717694-id-3624294.html
http://blog.csdn.net/wh_19910525/article/details/16370863
https://www.ibm.com/developerworks/cn/linux/l-acpi/part1/
http://www.latelee.org/embedded-linux/kernel-note-7%EF%BC%8Dintel-lpc_ich-driver.html
http://www.latelee.org/embedded-linux/kernel-note-10-intel-gpio-driver.html ? ?
關于Hardware ID的用途,在前面已經大致的解釋了它的用途,以及它和ACPI以及PNP之間的關系:
http://blog.csdn.net/morixinguan/article/details/79092440
? ? 接下來主要來看看在Linux內核中,內核是怎么去通過BIOS傳遞的參數表,傳遞對應的字串,然后內核又是如何來解析它,最終為Linux驅動統一模型所用。其實ARM和X86的驅動本質并沒有太大的區別,都是有了一個基地址,然后依靠偏移來獲取定位寄存器,寫值驅動設備。ARM也會去解析uboot傳遞的參數,然而并沒有那么的復雜,而X86對設備驅動進行了統一的管理,這點與ARM軟件架構的實現是有很大區別的,比如,讓GPIO的基地址在BIOS中進行統一分配,使用BIOS來統一管理電源等等。。。而ARM就相對來說簡單很多,沒有這么多的步驟,使用標準的Linux驅動模型+類ARM裸機操作(操作的地址需要進行映射,將物理地址轉換成虛擬地址,這點和單片機是不太一樣的),也就是說,如果掌握了ARM的驅動模型,同樣的,只要我們擁有X86架構的CPU數據手冊,我們同樣也可以使用ARM的思想來完成對X86架構的CPU的各類驅動BSP的編寫。
以下是較為重要的結構體:
在這個結構體里發現,_HID是以內核鏈表成員的形式加載進Linux內核的 (內核源碼/include/acpi/Acpi_bus.h) struct acpi_hardware_id {struct list_head list; char *id; };//ACPI的對象類型結構體 typedef u32 acpi_object_type; //ACPI對象 union acpi_object {acpi_object_type type; /* See definition of acpi_ns_type for values */struct {acpi_object_type type; /* ACPI_TYPE_INTEGER */u64 value; /* The actual number */} integer;struct {acpi_object_type type; /* ACPI_TYPE_STRING */u32 length; /* # of bytes in string, excluding trailing null */char *pointer; /* points to the string value */} string;struct {acpi_object_type type; /* ACPI_TYPE_BUFFER */u32 length; /* # of bytes in buffer */u8 *pointer; /* points to the buffer */} buffer;struct {acpi_object_type type; /* ACPI_TYPE_PACKAGE */u32 count; /* # of elements in package */union acpi_object *elements; /* Pointer to an array of ACPI_OBJECTs */} package;struct {acpi_object_type type; /* ACPI_TYPE_LOCAL_REFERENCE */acpi_object_type actual_type; /* Type associated with the Handle */acpi_handle handle; /* object reference */} reference;struct {acpi_object_type type; /* ACPI_TYPE_PROCESSOR */u32 proc_id;acpi_io_address pblk_address;u32 pblk_length;} processor;struct {acpi_object_type type; /* ACPI_TYPE_POWER */u32 system_level;u32 resource_order;} power_resource; };typedef char acpi_bus_id[8]; typedef unsigned long acpi_bus_address; typedef char acpi_device_name[40]; typedef char acpi_device_class[20];//這是一個位段,用來描述pnp中的類型 struct acpi_pnp_type {u32 hardware_id:1;u32 bus_address:1;u32 platform_id:1;u32 reserved:29; };//acpi的pnp設備,包括對象名稱、ID類型、以及各種ID,具體參考ACPI spec struct acpi_device_pnp {acpi_bus_id bus_id; /* Object name */struct acpi_pnp_type type; /* ID type */acpi_bus_address bus_address; /* _ADR */char *unique_id; /* _UID */struct list_head ids; /* _HID and _CIDs */acpi_device_name device_name; /* Driver-determined */acpi_device_class device_class; /* " */union acpi_object *str_obj; /* unicode string for _STR method */ };????那X86架構的CPU在啟動內核的時候又是如何知道BIOS傳遞過來的HID參數?我們可以來看看X86架構在Linux下的啟動流程: ? ?
? ? 不管是在ARM還是X86平臺,本質都是將一系列代碼拷貝到對應的存儲器對應的區域中,這個存儲器一般是NOR FLASH或者NAND FLASH,當然現在還有EMMC等其它的存儲設備,然后在執行Uboot(ARM的叫法,也叫bootloader,用來引導內核,而X86用的是BIOS,也差不多)中,通過地址跳轉的形式去啟動內核,如果是我們自己實現的Bootloader,一般會在作為uboot的第一、第二階段以后,通過如下的代碼跳轉到操作系統啟動的模式:
/* 0. 幫內核設置串口: 內核啟動的開始部分會從串口打印一些信息,但是內核一開始沒有初始化串口 */uart0_init();/* 1. 從NAND FLASH里把內核讀入內存 */puts("Copy kernel from nand\n\r");nand_read(0x60000+64, (unsigned char *)0x30008000, 0x200000);puthex(0x1234ABCD);puts("\n\r");puthex(*p);puts("\n\r");/* 2. 設置參數 */puts("Set boot params\n\r");setup_start_tag();setup_memory_tags();setup_commandline_tag("noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0");setup_end_tag();/* 3. 跳轉執行 */puts("Boot kernel\n\r");theKernel = (void (*)(int, int, unsigned int))0x30008000;theKernel(0, 362, 0x30000100);? ?如上代碼段,Linux內核在啟動的過程中會去解析ARM傳遞過去的參數:noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0。
? ? ?ARM的啟動相對來說比較簡單: uboot----->內核------>文件系統------>app,在uboot之前一般還會有IC廠商的固件驅動代碼。
? ? 而X86架構的CPU與ARM的啟動形式就不太一樣,顯然比這里要復雜得多,由于BIOS的源代碼并不開放,所以我們也并不知道BIOS的內幕具體是怎么實現的,但我們可以從以下這張圖可以得知X86架構從BIOS到kernel的整個流程,在這里我們能夠得知,X86的OS和BIOS之間銜接的橋梁是ACPI,使用ACPI來對一些資源進行統一管理,我們要獲取的這個Hardware ID其實就是ACPI Tables中的其中一個參數。
? ?到這里我們就明白了,不懂BIOS是怎么實現的也沒有什么關系,我們只要去百度下載一個ACPI的Spec,不就可以知道BIOS中具體的工作是做什么了嗎?只要了解了BIOS和內核之間是要完成什么樣的事情,對于我們驅動工程師來說就已經足夠了。
? ?接下來我們來看看在X86 Linux內核的啟動過程中,是如何去識別BIOS傳遞過來的Hardware ID的?
? ?不管是ARM架構的還是X86架構的CPU,在啟動Linux內核的時候一定要進入start_kernel函數,這個函數位于:
? ?內核源碼/init/main.c
? ?在這個函數中,會做操作系統的設備等一系列初始化,與ACPI最關鍵的地方在這個函數:acpi_early_init,這里完成的工作主要有如下:
acpi_early_init acpi_reallocate_root_table acpi_initialize_subsystem (drivers/acpi/acpica/Tbxfload.c) 1.acpi_load_tables: --->acpi_status __init acpi_load_tables(void) 2.acpi_tb_load_namespace: --->static acpi_status acpi_tb_load_namespace(void) 3.acpi_ns_load_table 在table中會得到一系列參數,包括Hardware ID,需要根據不同的參數表來解析 ---> (1)acpi_ut_acquire_mutex (2)acpi_tb_is_table_loaded (3)acpi_tb_allocate_owner_id (4)acpi_ns_parse_table? ? ?原來,內核就是這樣來獲取BIOS傳遞過來的table的,這個table中就會包括Hardware ID,當然還會有其它的ID,具體請參考ACPI的Spec,根據Linux實現的驅動模型,那么有設備,自然就要有驅動,驅動和設備要相輔相成,在:內核源碼/drivers/acpi/bus.c中就實現了acpi的驅動,在這個文件中,我們看到:
static int __init acpi_init(void) {int result;if (acpi_disabled) {printk(KERN_INFO PREFIX "Interpreter disabled.\n");return -ENODEV;}acpi_kobj = kobject_create_and_add("acpi", firmware_kobj);if (!acpi_kobj) {printk(KERN_WARNING "%s: kset create error\n", __func__);acpi_kobj = NULL;}init_acpi_device_notify();result = acpi_bus_init();if (result) {disable_acpi();return result;}pci_mmcfg_late_init();acpi_scan_init();acpi_ec_init();acpi_debugfs_init();acpi_sleep_proc_init();acpi_wakeup_device_init();return 0; }? ? 那么acpi_init函數又是怎么被內核調用的呢?通過subsys_initcall(acpi_init)這個宏來調用,我們將subsys_initcall展開看看,在內核源碼/include/init.h
#define subsys_initcall(fn) __define_initcall(fn, 4)將__define_initcall(fn,4)這個宏展開
#define __define_initcall(fn, id) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(".initcall" #id ".init"))) = fn; \LTO_REFERENCE_INITCALL(__initcall_##fn##id)? ? 其中initcall_t是函數指針,原型:typedef int (*initcall_t)(void);
? ?屬性 __attribute__((__section__())) 則表示把對象放在一個這個由括號中的名稱所指代的section中,這個對象就是我們的acpi_init函數。由此可見__define_initcall主要是完成以下幾個功能:
(1)聲明一個名稱為__initcall_##fn的函數指針;
(2) 將這個函數指針初始化為fn;
(3) 編譯的時候需要把這個函數指針變量放置到名稱為 ".initcall" level ".init"的section中。
? ? 而對應的這些.include,level,.init定義在Vmlinux.lds.h中,這個文件在內核源碼/include/asm-generic/Vmlinux.lds.h中:
在Linux4.0的內核實現如下:
#define INIT_CALLS_LEVEL(level) \VMLINUX_SYMBOL(__initcall##level##_start) = .; \*(.initcall##level##.init) \*(.initcall##level##s.init) \#define INIT_CALLS \VMLINUX_SYMBOL(__initcall_start) = .; \*(.initcallearly.init) \INIT_CALLS_LEVEL(0) \INIT_CALLS_LEVEL(1) \INIT_CALLS_LEVEL(2) \INIT_CALLS_LEVEL(3) \INIT_CALLS_LEVEL(4) \INIT_CALLS_LEVEL(5) \INIT_CALLS_LEVEL(rootfs) \INIT_CALLS_LEVEL(6) \INIT_CALLS_LEVEL(7) \VMLINUX_SYMBOL(__initcall_end) = .;__initcall_start和__initcall_end以及INITCALLS中定義的SECTION都是在arch/x86/kernel/vmlinux.lds.S中放在.init.begin段中的,如下,這是linux4.0內核中實現的。
SECTIONS{...... /* Init code and data - will be freed after init */. = ALIGN(PAGE_SIZE);.init.begin : AT(ADDR(.init.begin) - LOAD_OFFSET) {__init_begin = .; /* paired with __init_end */}....... = ALIGN(PAGE_SIZE);/* freed after init ends here */.init.end : AT(ADDR(.init.end) - LOAD_OFFSET) {__init_end = .;}...... }????而這些SECTION里的函數在初始化時被順序執行,具體的調用流程是這樣的:
rest_init ====> ?kernel_thread(kernel_init, NULL, CLONE_FS) ====>?kernel_init ====>?kernel_init_freeable ====>
do_basic_setup ====>?do_initcalls
在內核啟動的最后一步,開啟一條內核線程來加載這些函數,從而成功裝載acpi驅動。
static void __init do_initcalls(void) {int level;for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)do_initcall_level(level); }接下來再接著看acpi_init函數,這個函數中會調用acpi_scan_init函數,acpi_scan_init函數會完成如下:
1、注冊ACPI的驅動模型
2、完成與apci相關的一系列初始化:
acpi_pci_root_init(); acpi_pci_link_init(); acpi_processor_init(); acpi_lpss_init(); acpi_apd_init(); acpi_cmos_rtc_init(); acpi_container_init(); acpi_memory_hotplug_init(); acpi_pnp_init(); acpi_int340x_thermal_init();3、重點:調用acpi_bus_scan函數
在這個函數中會繼續調用acpi_walk_namespace(ACPI_TYPE_ANY, handle, ACPI_UINT32_MAX,
acpi_bus_check_add, NULL, NULL, &device);
注冊acpi_bus_check_add函數,在acpi_bus_check_add函數中繼續調用:acpi_add_single_object函數:
static int acpi_add_single_object(struct acpi_device **child,
?acpi_handle handle, int type,
?unsigned long long sta)
在acpi_add_single_object函數中的主要操作:
1、調用acpi_init_device_object等完成acpi設備、電源管理相關等的初始化,詳情見后面分析
2、調用acpi_device_add獲取設備的HID信息,實際上是通過鏈表的遍歷形式去獲取
設置在/sys/devices/XXX下面的name
4、調用acpi_init_device_object函數:
void acpi_init_device_object(struct acpi_device *device, acpi_handle handle,
int type, unsigned long long sta)
(1)、初始化內核鏈表用來存儲pnp設備中關于的鏈表等其它的信息
INIT_LIST_HEAD(&device->pnp.ids);
device->device_type = type;
device->handle = handle;
....
(2)、調用acpi_set_pnp_ids將ids的保存到ids中,具體操作見后面的剖析
5、調用acpi_set_pnp_ids函數:
static void acpi_set_pnp_ids(acpi_handle handle, struct acpi_device_pnp *pnp,
int device_type)
首先會根據swicth語句來判斷設備類型:device_type,這里找到的是ACPI總線的設備類型ACPI_BUS_TYPE_DEVICE
switch (device_type)
{
...
case ACPI_BUS_TYPE_DEVICE:
...
}
case ACPI_BUS_TYPE_DEVICE:
在該選項ACPI_BUS_TYPE_DEVICE中:
5.1 ?首先會判斷acpi句柄是否為ACPI的根對象,如果是,則會直接添加id節點到pnp->ids的鏈表中去。
5.2 ?接下來,調用acpi_get_object_info函數:
????acpi_status acpi_get_object_info(acpi_handle handle,struct acpi_device_info **return_buffer)
????通過acpi_get_object_info這個函數得到設備的_HID和_CIDs信息,獲取之前需要對命名空間的句柄進行轉換,怎么轉?
????通過struct acpi_namespace_node *acpi_ns_validate_handle(acpi_handle handle)這個函數轉。
????acpi_ns_validate_handle ?對傳入的名字空間句柄轉換為名字空間節點,這是在處理根節點的特殊情況
這個句柄其實是: ? typedef void *acpi_handle; /* Actually a ptr to a NS Node */
????為什么是Object?在ACPI標準手冊上關于ASL語言中可以查詢到:
????ObjectType ?must have. A fixed list is written as ?( a , b , c , … ) ?where the number of arguments depends on the specific ?ObjectType , and some elements can be nested objects, that is ?(a, b, (q, r, s,t), d) .?大致意思是,對象類型一定要包含,它是一個固定的列表寫成(a,b,c...)參數,取決于特定的對象類型,有些元素也是可以嵌套的,比如(a,b,(q,r,s,t),d)。
????該函數中會嘗試去判斷函數傳過來的參數--句柄是否存在,或者句柄是否為根對象:
????if ((!handle) || (handle == ACPI_ROOT_OBJECT)),只要有一個成立,則會return (acpi_gbl_root_node);
接下來嘗試校驗句柄:
????if (ACPI_GET_DESCRIPTOR_TYPE(handle) != ACPI_DESC_TYPE_NAMED)
????如果該句柄不是描述命名空間類型的句柄,則會return (NULL);
以上條件都不滿足,則:return (ACPI_CAST_PTR(struct acpi_namespace_node, handle));
#define ACPI_CAST_PTR(t, p) ? ? ? ? ? ? ((t *) (acpi_uintptr_t) (p))
????它的原型是將命名空間句柄強制轉換為apci命名空間節點,因為只有這樣,才能正確的解析BIOS傳遞過來的關于_HID的信息。
(1)提供運行_HID/_UID/_SUB/_CID的方法,這里只看_HID的執行方法
if ((type == ACPI_TYPE_DEVICE) || (type == ACPI_TYPE_PROCESSOR)) {...//得到_HID的信息status = acpi_ut_execute_HID(node, &hid);if (ACPI_SUCCESS(status)) {info_size += hid->length;valid |= ACPI_VALID_HID;}...}(2)復制ID信息到返回緩沖區 or 保留區域 & 檢測id是否為PCI根橋????acpi_ns_copy_device_id ?//將HID、UID、SUB和CIDs復制到返回緩沖區,如果是可變長度的字符串則會被復制到保留區域
????acpi_ut_is_pci_root_bridge //對于HID和CID,會檢查ID是否為PCI根橋,如果是,則要info->flags |= ACPI_PCI_ROOT_BRIDGE;
3、保存HID等ID的信息到device的pnp->ids里,這里只分析HID,同樣是利用了內核鏈表的尾插機制,將id源源不斷的接在鏈表的尾部
例如:#define ACPI_BAY_HID "LNXIOBAY"
那么PNP設備又是如何被加載到ACPI中的呢?而Hardware ID傳進來的字符串又是如何被PNP識別的呢?接下來請看:
內核源碼/drivers/acpi/acpi_pnp.c
void __init acpi_pnp_init(void) {acpi_scan_add_handler(&acpi_pnp_handler); }acpi_pnp_init這個函數是在acpi_scan_init中被調用的,也就是前面講到的。接下來我們來看看acpi_scan_add_handler這個函數:
int acpi_scan_add_handler(struct acpi_scan_handler *handler) {if (!handler)return -EINVAL;list_add_tail(&handler->list_node, &acpi_scan_handlers_list);return 0; }? ? ?很明顯,這個函數完成的功能就是將節點插入到鏈表中去,插入的是什么節點?我們來看看acpi_pnp_handler:
static struct acpi_scan_handler acpi_pnp_handler = {.ids = acpi_pnp_device_ids,.match = acpi_pnp_match,.attach = acpi_pnp_attach, };? ? 這是一個結構體變量,這里的ids其實就是一個字符串,這個字符串就是acpi的設備id,只不過在這被初始化成了pnp設備id,其實是一個意思,因為PNP設備是注冊在ACPI之上的。
struct acpi_device_id {__u8 id[ACPI_ID_LEN];kernel_ulong_t driver_data; }; static const struct acpi_device_id acpi_pnp_device_ids[] = {/* pata_isapnp */{"PNP0600"}, /* Generic ESDI/IDE/ATA compatible hard disk controller *//* floppy */{"PNP0700"},/* ipmi_si */{"IPI0001"},...... };? ? ?而acpi_pnp_match是完成對BIOS傳遞過來的ID與這里的ID進行比較,如果存在這個ID,才會將對應的驅動注冊到內核中去,這樣內核才會去執行對應的驅動:
static bool matching_id(char *idstr, char *list_id) {int i;if (memcmp(idstr, list_id, 3)){return false;}for (i = 3; i < 7; i++) {char c = toupper(idstr[i]);if (!isxdigit(c)|| (list_id[i] != 'X' && c != toupper(list_id[i])))return false;}return true; }static bool acpi_pnp_match(char *idstr, const struct acpi_device_id **matchid) {const struct acpi_device_id *devid;for (devid = acpi_pnp_device_ids; devid->id[0]; devid++) {if (matching_id(idstr, (char *)devid->id)) {if (matchid)*matchid = devid;return true;}}return false; }static int acpi_pnp_attach(struct acpi_device *adev,const struct acpi_device_id *id) {return 1; }? ? 至此,我們已經完全明白內核是如何接收到BIOS傳過來的Hardware ID的整個流程,確實是非常難的,簡單的問題被復雜化,但沒有辦法,因為要統一管理的東西太多太多了,所以一定需要一個模型來進行管理。如果我們不想使用BIOS與ACPI的機制,完全也可以繞開這個流程,用標準的Linux驅動模型去實現,不過還是建議,還是使用標準的ACPI的流程,這樣才有助于軟件工程項目管理。
? ??
總結
以上是生活随笔為你收集整理的Linux x86架构下ACPI PNP Hardware ID的识别机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IT行业都能干什么事?为什么要学习IT技
- 下一篇: css代码样式大全(整理版)