第六課:在LCD驅動中使用裝置樹
按照計劃,本課會講解修改uboot和核心讓JZ2440支援裝置樹。
但前面修改uboot已經講解完了,修改核心也沒必要單獨講,可以直接看核心補丁,修改的方法也並不複雜。
核心補丁路徑:
doc_and_sources_for_device_tree/source_and_images/第5,6課的原始碼及映像檔案(使用了完全版的裝置樹)/第5課第4節_核心補丁及裝置樹/linux-4.19-rc3_device_tree_for_irq_jz2440.patch
對核心的修改並不多,裡面大部分是移植yaffs,yaffs是一個檔案系統,他適合在nand flash上使用,要是對移植yaffs感興趣的話,可以看看畢業班的視訊。
實際上涉及裝置樹的修改並不多,那我怎麼知道修改那些呢?
我使用最笨的方法——新增列印。在發現核心啟動卡住後,就沿著核心啟動流程呼叫的函式新增列印,比如在init.c
這裡列印使用的是
early_print()
,因為printk()
很可能還不能使用,early_print()
直接把資料寫到串口裡面,和硬體驅動沒有什麼關係。
第01節_使用裝置樹給DM9000網絡卡_觸控式螢幕指定中斷
在上一課我們把中斷體系講得很清楚了,我們先看一下核心裡的網絡卡驅動程式,所在路徑為:
drivers/net/ethernet/davicom/dm9dev9000.c
在這裡做了一件非常取巧的事情,以前中斷號和硬體繫結時,它的中斷號是IRQ_EINT7
,現在我直接偷懶將其賦值為7,實際上這種方法是非常不保險的。
從原理上我們可以知道網絡卡使用的是EINT7,對於EINT7它的hwirq是7,它就會從bit7開始查詢,bit7如果沒有被佔用,那麼它的虛擬中斷號就等於7。萬一有其它中斷程式使用了上一級的第7號中斷,後面EINT7的虛擬中斷號就不會等於7,所以我們在驅動程式裡指定中斷號存在風險,因此我們需要改正這種做法。
網絡卡裝置樹節點
我們可以先在裝置樹裡宣告使用哪一個中斷,在網絡卡中指定中斷:
srom-[email protected]20000000 {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x20000000 0x8000000>;
ranges;
[email protected]20000000 {
compatible = "davicom,dm9000";
reg = <0x20000000 0x2 0x20000004 0x2>;
interrupt-parent = <&gpf>;
interrupts = <7 IRQ_TYPE_EDGE_RISING>;
local-mac-address = [00 00 de ad be ef];
davicom,no-eeprom;
};
};
節點srom-cs
位於根目錄下面,它的compatible
是simple-bus
,對於simple-bus
下面的子節點它也會建立為一個平臺裝置,它的compatible
是davicom,dm9000
,我們以後將根據這個值找到對應的驅動程式,在這個節點裡面它指定了中斷的資訊,我們需要修改驅動程式為這個裝置節點新增一個platform_driver
,在platform_driver
的probe()
函式裡面,把這個中斷號確定下來。
修改程式碼過程參考視訊。
觸控式螢幕裝置樹節點
觸控式螢幕的裝置樹節點如下:
[email protected]5800000 {
compatible = "jz2440,ts";
reg = <0x58000000 0x100>;
reg-names = "adc_ts_physical";
interrupts = <1 31 9 3>, <1 31 10 3>;
interrupt-names = "int_ts", "int_adc_s";
clocks = <&clocks PCLK_ADC>;
clock-names = "adc";
};
該節點沒有指定interrupt-parent
,中斷將發給它的父節點(也就是根節點),在根節點有interrupt-parent = <0x1>;
,根據0x01
找到phandle
。
觸控式螢幕使用了兩個中斷,一個是按下/鬆開時產生的中斷,另外一個是ADC的中斷。一但觸控式螢幕產生訊號,就傳給子中斷控制器(sub interrupt),再由子中斷控制器發給頂級的中斷控制器(interrupt controller)。
interrupts
後面的4個32位數字含義如下:
- 第一個表示是發給主控制器還是子控制器,為1表示發給子控制器;
- 第二個表示子中斷控制器發給主控制器的哪一個;
- 第三個表示是這個中斷控制器裡的哪一個中斷;
- 第四個表示中斷的觸發方式;
通過第三個數字可以知道該節點的第0箇中斷資源是TC,第1箇中斷是ADC。
測試
兩個驅動程式修改完後,分別上傳到核心如下目錄:
drivers/net/ethernet/davicom
drivers/input/touchscreen
測試步驟如下:
- a. 編譯核心
- b. 使用新的uImage啟動
- c. 測試網絡卡:
ifconfig eth0 192.168.1.101
ping 192.168.1.1
d. 測試觸控式螢幕:
hexdump /dev/evetn0 // 然後點選觸控式螢幕
第02節_在裝置樹中時鐘的簡單使用
在本課裡,本來只打算講解兩節,分別是網絡卡、觸控式螢幕指定中斷和在裝置樹裡為LCD指定引數,後來發現LCD節點涉及clock和pinctrl,因此又擴充了兩節。
時鐘框圖
先來看看S3C2440時鐘的硬體框圖:
將該圖簡化如下:
我們只想作為消費者怎麼去使用這些時鐘,並不關心“提供者”內部的層級結構,只要知道“直接提供者”,也不關係“直接提供者”的實現,我們只需要發出請求就可以了。
晶振裝置樹描述
我們看看在2440的裝置樹裡怎麼描述這提供者和消費者。先來看看晶振:
xti: xti_clock {
compatible = "fixed-clock";
clock-frequency = <12000000>;
clock-output-names = "xti";
#clock-cells = <0>;
};
根據compatible
可以找到對應的驅動,驅動程式將晶振的頻率記錄下來,以後作為計算的基準。
然後再是PLL的裝置節點:
clocks: clock-[email protected]4c000000 {
compatible = "samsung,s3c2440-clock";
reg = <0x4c000000 0x20>;
#clock-cells = <1>;
};
裝置節點本身非常簡單,複雜的是它對應的驅動程式。在驅動程式裡面,肯定會根據reg
獲得暫存器的地址,然後設定各種內容。
大部分的晶片為了省電,它的外部模組時鐘平時都是關閉的,只有在使用某個模組時,才設定相應的暫存器開啟對應的時鐘。
這些使用者各有不同,要怎麼描述這些使用者呢?
我們可以為它們配上一個ID。在裝置樹中的#clock-cells = <1>;
表示 用多少個u32位來描述消費者。在本例中使用一個u32來描述。
這些ID值由誰提供的?
是由驅動程式提供的,該節點會對應一個驅動程式,驅動程式給硬體(消費者)都分配了一個ID,所以說複雜的操作都留給驅動程式來做。
LCD時鐘裝置樹描述
消費者想使用時鐘時,首先要找到時鐘的直接提供者,向它發出申請。以LCD為例:
fb0: [email protected]4d000000{
compatible = "jz2440,lcd";
reg = <0x4D000000 0x60>;
interrupts = <0 0 16 3>;
clocks = <&clocks HCLK_LCD>;
clock-names = "lcd";
……
}
在clock
屬性裡,首先要確定向誰發出時鐘申請,這裡是向clocks
發出申請,然後確定想要時鐘提供者提供哪一路時鐘,這裡是HCLK_LCD
,在驅動程式裡定義了該巨集,每種巨集對應了一個時鐘ID。
定義如下:
……
/* hclk-gates */
#define HCLK_LCD 32
#define HCLK_USBH 33
#define HCLK_USBD 34
#define HCLK_NAND 35
#define HCLK_CAM 36
……
因此,我們只需要在裝置節點定義clocks
這個屬性,這個屬性確定時鐘提供者,然後確定時鐘ID,也就是向時鍾提供者申請哪一路時鐘。
對應的核心文件可以參考這兩個檔案:
Documentation/devicetree/bindings/clock/clock-bindings.txt
Documentation/devicetree/bindings/clock/samsung,s3c2410-clock.txt
那麼我這個裝置驅動程式,怎麼去使用這些時鐘呢?
以前的驅動程式:clk_get(NULL, “name”);
clk_prepare_enable(clk);
現在的驅動程式:of_clk_get(node, 0);
clk_prepare_enable(clk);
總結
a. 裝置樹中定義了各種時鐘, 在文件中稱之為"Clock providers", 比如:
clocks: clock-[email protected]4c000000 {
compatible = "samsung,s3c2440-clock";
reg = <0x4c000000 0x20>;
#clock-cells = <1>; // 想使用這個clocks時要提供1個u32來指定它, 比如選擇這個clocks中發出的LCD時鐘、PWM時鐘
};
b. 裝置需要時鐘時, 它是"Clock consumers", 它描述了使用哪一個"Clock providers"中的哪一個時鐘(id), 比如:
fb0: [email protected]4d000000{
compatible = "jz2440,lcd";
reg = <0x4D000000 0x60>;
interrupts = <0 0 16 3>;
clocks = <&clocks HCLK_LCD>; // 使用clocks即[email protected]中的HCLK_LCD
};
c. 驅動中獲得/使能時鐘:
// 確定時鐘個數
int nr_pclks = of_count_phandle_with_args(dev->of_node, "clocks",
"#clock-cells");
// 獲得時鐘
for (i = 0; i < nr_pclks; i++) {
struct clk *clk = of_clk_get(dev->of_node, i);
}
// 使能時鐘
clk_prepare_enable(clk);
// 禁止時鐘
clk_disable_unprepare(clk);
第03節_在裝置樹中pinctrl的簡單使用
這節課講解在裝置樹中pinctrl的簡單使用,pinctrl從名字上就可以猜到它是控制引腳的。
基本概念
在講解使用方法前,先講解幾個概念。
Bank: 以引腳名為依據, 這些引腳分為若干組, 每組稱為一個Bank
比如s3c2440裡有GPA、GPB、GPC等Bank,
每個Bank中有若干個引腳, 比如GPA0,GPA1, …, GPC0, GPC1,…等引腳
Group: 以功能為依據, 具有相同功能的引腳稱為一個Group
比如s3c2440中串列埠0的TxD、RxD引腳使用 GPH2,GPH3, 那這2個引腳可以列為一組
比如s3c2440中串列埠0的流量控制引腳使用 GPH0,GPH1, 那這2個引腳也可以列為一組
State: 裝置的某種狀態, 比如核心自己定義的"default",“init”,“idel”,"sleep"狀態;
也可以是其他自己定義的狀態, 比如串列埠的"flow_ctrl"狀態(使用流量控制)
裝置處於某種狀態時, 它可以使用若干個Group引腳
當串列埠處於default狀態時,它是由pinctrl-0指定若干組(group)引腳;
當串列埠處於sleep狀態時,它是由pinctrl-1指定若干組(group)引腳;
裝置的pinctrl的設定時機
a. platform_device, platform_driver匹配時:
第3課第06節_platform_device跟platform_driver的匹配 中講解了platform_device和platform_driver的匹配過程,最終都會呼叫到 really_probe (drivers/base/dd.c)
really_probe:
/* If using pinctrl, bind pins now before probing */
ret = pinctrl_bind_pins(dev);
dev->pi ns->default_state = pinctrl_lookup_state(dev->pins->p,
PINCTRL_STATE_DEFAULT); /* 獲得"default"狀態的pinctrl */
dev->pins->init_state = pinctrl_lookup_state(dev->pins->p,
PINCTRL_STATE_INIT); /* 獲得"init"狀態的pinctrl */
ret = pinctrl_select_state(dev->pins->p, dev->pins->init_state); /* 優先設定"init"狀態的引腳 */
ret = pinctrl_select_state(dev->pins->p, dev->pins->default_state); /* 如果沒有init狀態, 則設定"default"狀態的引腳 */
......
ret = drv->probe(dev);
所以: 如果裝置節點中指定了pinctrl, 在對應的probe函式被呼叫之前, 先"bind pins", 即先繫結、設定引腳
b. 驅動中想選擇、設定某個狀態的引腳:
devm_pinctrl_get_select_default(struct device *dev); // 使用"default"狀態的引腳
pinctrl_get_select(struct device *dev, const char *name); // 根據name選擇某種狀態的引腳
pinctrl_put(struct pinctrl *p); // 不再使用, 退出時呼叫
第04節_使用裝置樹給LCD指定各種引數
a. 替換dts檔案:
把jz2440_irq.dts
放入核心 arch/arm/boot/dts目錄,
b. 替換驅動檔案:
把s3c2410fb.c
放入核心 drivers/video/fbdev/ 目錄,
修改 核心 drivers/video/fbdev/Makefile
:
obj-$(CONFIG_FB_S3C2410) += lcd_4.3.o
改為:
obj-$(CONFIG_FB_S3C2410) += s3c2410fb.o
c. 編譯驅動、編譯dtbs:
export PATH=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/work/system/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi/bin
cp config_ok .config
make uImage // 生成 arch/arm/boot/uImage
make dtbs // 生成 arch/arm/boot/dts/jz2440_irq.dtb
d. 使用上述uImage, dtb啟動核心即可看到LCD有企鵝出現
(1). 裝置樹中的描述:
fb0: [email protected]4d000000{
compatible = "jz2440,lcd";
reg = <0x4D000000 0x60>;
interrupts = <0 0 16 3>;
clocks = <&clocks HCLK_LCD>; /* a. 時鐘的處理 */
clock-names = "lcd";
pinctrl-names = "default"; /* b. pinctrl */
pinctrl-0 = <&lcd_pinctrl &lcd_backlight &gpb0_backlight>;/*新增一組背光控制引腳*/
status = "okay";
/* c. 根據LCD引腳特性設定lcdcon5, 指定lcd時序引數 */
lcdcon5 = <0xb09>;
type = <0x60>;
width = /bits/ 16 <480>;
height = /bits/ 16 <272>;
pixclock = <100000>; /* 單位: ps, 10^-12 S, */
xres = /bits/ 16 <480>;
yres = /bits/ 16 <272>;
bpp = /bits/ 16 <16>;
left_margin = /bits/ 16 <2>;
right_margin =/bits/ 16 <2>;
hsync_len = /bits/ 16 <41>;
upper_margin = /bits/ 16 <2>;
lower_margin = /bits/ 16 <2>;
vsync_len = /bits/ 16 <10>;
};
&pinctrl_0 {
gpb0_backlight: gpb0_backlight {
samsung,pins = "gpb-0";
samsung,pin-function = <1>;
samsung,pin-val = <1>;
};
};
(2) 程式碼中的處理:
a. 時鐘的處理:
info->clk = of_clk_get(dev->of_node, 0);
clk_prepare_enable(info->clk);
b. pinctrl:
程式碼中無需處理, 在 platform_device/platform_driver匹配之後就會設定default狀態對應的pinctrl
配置gpio引腳為lcd功能,檢視檔案jz2440_irq_all.dts
lcd_pinctrl {
samsung,pins = "gpc-8", "gpc-9", "gpc-10", "gpc-11", "gpc-12", "gpc-13", "gpc-14", "gpc-15", "gpd-0", "gpd-1", "gpd-2", "gpd-3", "gpd-4", "gpd-5", "gpd-6", "gpd-7", "gpd-8", "gpd-9", "gpd-10", "gpd-11", "gpd-12", "gpd-13", "gpd-14", "gpd-15", "gpc-1", "gpc-2", "gpc-3", "gpc-4";
samsung,pin-function = <0x2>;
phandle = <0x8>;
};
背光引腳,用來使能lcd電源
lcd_backlight {
samsung,pins = "gpg-4";
samsung,pin-function = <0x3>;
phandle = <0x9>;
};
背光引腳
gpb0_backlight {
samsung,pins = "gpb-0";
samsung,pin-function = <0x1>;//引腳功能為輸出
samsung,pin-val = <0x1>;//初始電平為1
phandle = <0xa>;
};
c. 根據LCD引腳特性設定lcdcon5, 指定lcd時序引數:
開啟wiki看新一期加強版lcd程式設計的介紹
引用lcd頁面
Vclk 每一個clk,電子槍移動一個畫素,我們需要設定lcd的時鐘引數,通過LCD晶片手冊的引數進行設定
時鐘
f=10M=10*10^6 = 10^7
週期
t=1/f=10^-7 = 10^-7 * 10^12 * 10^-12 s = 10^5皮秒
也就是pixclock值的由來
lcdcon5 = <0xb09>;
含義是指定了LCD訊號的極性
可以檢視老的lcd驅動裡面有註釋,這些值也是根據LCD極性來確定
程式碼中如何處理lcd裝置樹節點屬性?
直接讀裝置樹節點中的各種屬性值, 用來設定驅動引數
of_property_read_u32(np, "lcdcon5", (u32 *)(&display->lcdcon5));
of_property_read_u32(np, "type", &display->type);
of_property_read_u16(np, "width", &display->width);
of_property_read_u16(np, "height", &display->height);
of_property_read_u32(np, "pixclock", &display->pixclock);
of_property_read_u16(np, "xres", &display->xres);
of_property_read_u16(np, "yres", &display->yres);
of_property_read_u16(np, "bpp", &display->bpp);
of_property_read_u16(np, "left_margin", &display->left_margin);
of_property_read_u16(np, "right_margin", &display->right_margin);
of_property_read_u16(np, "hsync_len", &display->hsync_len);
of_property_read_u16(np, "upper_margin", &display->upper_margin);
of_property_read_u16(np, "lower_margin", &display->lower_margin);
of_property_read_u16(np, "vsync_len", &display->vsync_len);
新老版本lcd驅動的對比請看視訊