1. 程式人生 > >第六課:在LCD驅動中使用裝置樹

第六課:在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位於根目錄下面,它的compatiblesimple-bus,對於simple-bus下面的子節點它也會建立為一個平臺裝置,它的compatibledavicom,dm9000,我們以後將根據這個值找到對應的驅動程式,在這個節點裡面它指定了中斷的資訊,我們需要修改驅動程式為這個裝置節點新增一個platform_driver,在platform_driverprobe()函式裡面,把這個中斷號確定下來。
修改程式碼過程參考視訊。

觸控式螢幕裝置樹節點

觸控式螢幕的裝置樹節點如下:

    [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驅動的對比請看視訊