1.前言
在arm嵌入式開發中,各個外設具有固定的實體地址,我們可以直接通過晶片手冊來編寫驅動配置後使用。但是在x86中有所不同,所有外設控制器整合在PCH(曾經的南橋)中,每個外設都是作為一個PCI裝置掛在PCH的PCI匯流排上,PCH再通過DMI與CPU相聯。對於標壓處理器H/K系列(也就是我們桌上型電腦),南橋還在主機板上,對於x86移動處理器(Y/U結尾系列),已將PCH和CPU整合到同一封裝中,與如今各類SOC類似,如下(詳見datasheet)。
由於x86中每個外設是一個PCI裝置,所以我們要使用某個外設就需要為其分配記憶體空間對映、IRQ和I/O基址,x86中這些資源配置是由BIOS(UEFI)完成的,因為每塊主機板設計和外設使用不一樣,就需要不一樣的配置,所以不同的主機板廠商需要定製自己主機板的BIOS 。
BIOS配置好主機板使用的外設後,一些BIOS(UEFI)通過ACPI(高階配置和電源介面)的DSDT來傳遞裝置資訊(類似arm裝置樹,但功能更強)給作業系統,作業系統解析獲取到這些裝置資訊後我們才能在驅動配置和使用這個外設,但ACPI對各個作業系統有相容性問題,這就會出現在Windows裝置管理器能看到該裝置,到linux下什麼就也沒有的現象,因為桌面CPU大多都是用的Windows系統,所以大部分X86硬體廠商的BIOS主要相容Windows為主。BIOS又是我們普通開發者無法接觸修改的東西,不相容怎麼辦。
本文說的GPIO就是這麼個問題,linux下無法使用,由於涉及的東西有點多,所以簡單介紹在如何將x86工控機引出的GPIO使用起來的(注意:是PCH的GPIO引腳,不是Super IO的GPIO)。
CPU :英特爾7代低壓處理器( Kaby Lake) i5-7200U/賽揚3865U
linux:linux 4.0以上
2.linux pinctrl子系統
要使用gpio需要先看一下linux系統PINCTRL子系統,層級如下所示(圖片來源蝸窩科技):
最底層是硬體控制器,其上是操作這些硬體的相關驅動(pin controller driver),不同的控制器有不同底層驅動,一般由晶片廠商BSP完成;pin controller driver初始化的時候會向pin control core模組註冊pin control裝置(通過pinctrl_register這個bootom level interface)。pin control core模組是一個硬體無關模組,它抽象了所有pin controller的硬體特性,僅僅從使用者(各個driver就是pin control subsystem的使用者)角度給出了top level的介面函式,這樣,各個driver不需要關注pin controller的底層硬體相關的內容,使用時直接向pinctrl子系統申請IO資源即可。關於linux GPIO與pinctrl子系統資訊,詳見蝸窩科技-GPIO子系統.
pin controller driver成功註冊到pin control core後,我們通過pin control core匯出到sysfs的檔案就可以直接操作一個GPIO,使其輸入輸出,而不需要專門去寫一個驅動模組。
3. pin controller driver
搞嵌入式的一定對platform bus非常熟悉,pin controller driver的註冊同樣離不開platform bus,driver與device必須經過某種匹配後,才能進一步執行probe註冊到系統中。
結合前言中對x86裝置的描述,platform bus可通過以下兩種方式來判斷driver和device是否匹配。
- 方式一,由BIOS通過ACPI 中DSDT傳遞控制器裝置節點描述給linux(可類比裝置樹),linux核心啟動過程中解析處理DSDT資訊,自動構造device裝置並新增到Platform bus,新增過程中匹配ACPI_ID,觸發執行pin controller driver 的
probe()
函式。 - 方式二,linux掃描PCI匯流排裝置建立裝置並新增,PCI驅動匹配vendor、device、class後觸發執行pin controller driver 的
probe()
函式。
別忘了前提,啟動時BIOS必須為使用的PCI裝置分配好裝置中斷號(中斷vector)、對映空間地址等我們才能用。那對於我們的GPIO裝置linux系統使用的是哪種方式呢,這需要到原始碼中來看,首先七代系列CPU linux pinctrl driver原始碼檔案為\drivers\pinctrl\intel\pinctrl-sunrisepoint.c
,看如下程式碼。
static const struct acpi_device_id spt_pinctrl_acpi_match[] = {
{ "INT344B", (kernel_ulong_t)&sptlp_soc_data },
{ "INT345D", (kernel_ulong_t)&spth_soc_data },
{ }
};
MODULE_DEVICE_TABLE(acpi, spt_pinctrl_acpi_match);
.....
static struct platform_driver spt_pinctrl_driver = {
.probe = spt_pinctrl_probe,
.driver = {
.name = "sunrisepoint-pinctrl",
.acpi_match_table = spt_pinctrl_acpi_match,
.pm = &spt_pinctrl_pm_ops,
},
};
可以看到使用的是ACPI模式,那麼驅動的註冊邏輯應該如下,
其中driver把系統中所有的pin描述出來,並將driver註冊到platform bus。driver需要對應的device才能工作,但是linux因為ACPI的相容性問題,linux並沒有解析DSDT並建立出GPIO 相關的device,所以沒有觸發執行probe來將pin controller driver註冊到pin control core中,pinctrl子系統沒有工作當然無法使用。到這裡我們去解決核心對ACPI的解析(或者說相容性問題)顯然是不太現實的(自己太菜(╯﹏╰)),有沒有其他辦法呢?
先閱讀原始碼看看,probe()執行過程中需要用到device的哪些resource,只要我們能獲取到這些resource,自己手動構造一個device註冊到platform bus不就行了,O(∩_∩)O哈哈~。
int intel_pinctrl_probe(struct platform_device *pdev,
const struct intel_pinctrl_soc_data *soc_data)
{
......
for (i = 0; i < pctrl->ncommunities; i++) {
......
res = platform_get_resource(pdev, IORESOURCE_MEM,
community->barno);//0
regs = devm_ioremap_resource(&pdev->dev, res);
......
}
......
irq = platform_get_irq(pdev, 0);
......
}
可以看到pin controller driver需要pincontrler 的地址空間和使用的中斷號兩部分資源,其中地址空間是三個,因為所有GPIO由三個GPIO控制器組成,三個GPIO控制器共享相同的中斷線,三個GPIO控制器作為一個PCI裝置。如何獲取這兩個資訊呢?
4.手動構造device
上面通過閱讀原始碼得知,intel-pinctrl需要pincontrler 地址空間、和使用的中斷號兩部分資源。
地址空間起始地址可通過PCI 裝置P2SB Bridge (D31:F1)獲得。中斷vector由BIOS配置,反編譯BIOS給linux傳遞的ACPI資訊,看是否有中斷vector相關資訊:
在板子上進入/sys/firmware/acpi/tables
,將目錄下所有檔案考出,使用acpi工具iasl
對DSDT檔案進行反編譯:
iasl -d DSDT.dat
得到AML
檔案 DSDT.dsl
,裡面包含BIOS開發的各裝置節點資訊。
開啟 DSDT.dsl
並找到pin controler裝置節點描述,只需要搜尋驅動裡的"INT344B"或"INT345D"就能定位到。到這裡我們也明白了,為什麼驅動裡的spt_pinctrl_acpi_match[]
有兩像,原來是一個代表標壓處理器(H),一個代表低壓處理器(U)。
Device (GPI0)
{
Method (_HID, 0, NotSerialized) // _HID: Hardware ID
{
If ((PCHV () == SPTH))
{
If ((PCHG == 0x02))
{
Return ("INT3451")
}
Return ("INT345D") //表示7代標壓處理器
}
Return ("INT344B") //表示7代低壓處理器
Name (LINK, "\\_SB.PCI0.GPI0")
Method (_CRS, 0, NotSerialized) // _CRS: Current Resource Settings
{
Name (RBUF, ResourceTemplate ()
{
Memory32Fixed (ReadWrite,
0x00000000, // Address Base
0x00010000, // Address Length 地址空間大小
_Y2E)
Memory32Fixed (ReadWrite,
0x00000000, // Address Base
0x00010000, // Address Length 地址空間大小
_Y2F)
Memory32Fixed (ReadWrite,
0x00000000, // Address Base
0x00010000, // Address Length 地址空間大小
_Y31)
Interrupt (ResourceConsumer, Level, ActiveLow, Shared, ,, _Y30)
{
0x0000000E, //中斷號
}
})
CreateDWordField (RBUF, \_SB.PCI0.GPI0._CRS._Y2E._BAS, COM0) // _BAS: Base Address
CreateDWordField (RBUF, \_SB.PCI0.GPI0._CRS._Y2F._BAS, COM1) // _BAS: Base Address
CreateDWordField (RBUF, \_SB.PCI0.GPI0._CRS._Y30._INT, IRQN) // _INT: Interrupts
COM0 = (SBRG + 0x00AF0000)
COM1 = (SBRG + 0x00AE0000)
CreateDWordField (RBUF, \_SB.PCI0.GPI0._CRS._Y31._BAS, COM3) // _BAS: Base Address
COM3 = (SBRG + 0x00AC0000)
IRQN = SGIR /* \SGIR */
Return (RBUF) /* \_SB_.PCI0.GPI0._CRS.RBUF */
}
你可能看不懂上面面的資訊,到底哪個是標壓哪個是低壓?沒關係,我們去pin controller driver中,裡面有註釋,反推一下就知道INT345D
代表的是標壓,INT344B
代表的是低壓。
/* Sunrisepoint-LP */
static const struct pinctrl_pin_desc sptlp_pins[] = {
....
}
static const struct intel_pinctrl_soc_data sptlp_soc_data = {
.pins = sptlp_pins,
...
}
.....
/* Sunrisepoint-H */
static const struct pinctrl_pin_desc spth_pins[] = {
....
}
static const struct intel_pinctrl_soc_data spth_soc_data = {
.pins = spth_pins,
...
}
static const struct acpi_device_id spt_pinctrl_acpi_match[] = {
{ "INT344B", (kernel_ulong_t)&sptlp_soc_data },
{ "INT345D", (kernel_ulong_t)&spth_soc_data },
{ }
};
回到正題,我們從 DSDT.dsl
獲取得到中斷號: 0xE,三個地址空間起始地址及大小。構建一個platform_device 如下:
#include <linux/debugfs.h>
#include <linux/ioport.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#define P2SB_PORTID_SHIFT 16
#define P2SB_PORT_GPIO3 0xAC
#define P2SB_PORT_GPIO2 0xAD /*未使用*/
#define P2SB_PORT_GPIO1 0xAE
#define P2SB_PORT_GPIO0 0xAF
#define sbreg_addr 0xfd000000 /*Address Base*/
/*Community 0*/
#define SPT_PINCTRL_COMMUNITY0_OFFSET sbreg_addr + (P2SB_PORT_GPIO0 << P2SB_PORTID_SHIFT)
#define SPT_PINCTRL_COMMUNITY0_SIZE 0x00010000
/*Community 1*/
#define SPT_PINCTRL_COMMUNITY1_OFFSET sbreg_addr + (P2SB_PORT_GPIO1 << P2SB_PORTID_SHIFT)
#define SPT_PINCTRL_COMMUNITY1_SIZE 0x00010000
/*Community 2*/
#define SPT_PINCTRL_COMMUNITY2_OFFSET sbreg_addr + (P2SB_PORT_GPIO2 << P2SB_PORTID_SHIFT)
#define SPT_PINCTRL_COMMUNITY2_SIZE 0x00010000
/*Community 3*/
#define SPT_PINCTRL_COMMUNITY3_OFFSET sbreg_addr + (P2SB_PORT_GPIO3 << P2SB_PORTID_SHIFT)
#define SPT_PINCTRL_COMMUNITY3_SIZE 0x00010000
static struct resource intel_pinctrl_dev_resources[] = {
/* iomem resource */
DEFINE_RES_MEM_NAMED(SPT_PINCTRL_COMMUNITY0_OFFSET, SPT_PINCTRL_COMMUNITY0_SIZE, NULL),
DEFINE_RES_MEM_NAMED(SPT_PINCTRL_COMMUNITY1_OFFSET, SPT_PINCTRL_COMMUNITY1_SIZE, NULL),
// DEFINE_RES_MEM_NAMED(SPT_PINCTRL_COMMUNITY2_OFFSET, SPT_PINCTRL_COMMUNITY2_SIZE, NULL),/*未使用*/
DEFINE_RES_MEM_NAMED(SPT_PINCTRL_COMMUNITY3_OFFSET, SPT_PINCTRL_COMMUNITY3_SIZE, NULL),
/* irq resource */
DEFINE_RES_IRQ(0x0E), /*反編譯BIOS DSDT獲取*/
};
static struct platform_device intel_pinctrl_device = {
.name = "sunrisepoint-pinctrl",
.id = -1,
.resource = intel_pinctrl_dev_resources,
.num_resources = ARRAY_SIZE(intel_pinctrl_dev_resources),
};
static int __init intel_spt_device_init(void)
{
return platform_device_register(&intel_pinctrl_device);
}
module_init(intel_spt_device_init);
static void __exit intel_spt_device_exit(void)
{
platform_device_unregister(&intel_pinctrl_device);
}
module_exit(intel_spt_device_exit);
MODULE_AUTHOR("wsg1100");
MODULE_DESCRIPTION("Intel sunrisepoint pinctrl device");
MODULE_LICENSE("GPL v2");
隨核心編譯後,載入模組,intel pinctrl子系統正常工作,(^o^)/。
注意,相同平臺,不同BIOS PCI資訊可能不同!文中提供的只是一種方法
版權宣告:本文為本文為博主原創文章,轉載請註明出處,部落格地址:https://www.cnblogs.com/wsg1100/。如有錯誤,歡迎指正。