一锭部、GPIO簡介
i.MX6ULL 芯片的 GPIO 被分成 5 組,并且每組 GPIO 的數(shù)量不盡相同,例如 GPIO1 擁有 32 個引腳篙顺, GPIO2 擁有 22 個引腳偶芍, 其他 GPIO 分組的數(shù)量以及每個 GPIO 的功能請參考 《i.MX 6UltraLite Applications Processor Reference Manual》 第26章General Purpose Input/Output (GPIO)(P1133)。
通過 GPIO 硬件結(jié)構(gòu)框圖德玫,就可以從整體上深入了解 GPIO 外設(shè)及它的各種應(yīng)用模式匪蟀。
1.1 IO命名
打開 i.MX6ULL 參考手冊的第 32 章“Chapter 32: IOMUX Controller(IOMUXC)”
i.MX6ULL 的 IO 分為兩類:SNVS 域的和通用的,這兩類 IO 本質(zhì)上都是一樣的宰僧。
“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”的就是 GPIO 命名材彪,命名形式就是“IOMUXC_SW_MUC_CTL_PAD_XX_XX
”,后面的“XX_XX
”就是 GPIO 命名琴儿,比如:GPIO1_IO01段化、UART1_TX_DATA偶妖、JTAG_MOD 等等裙秋。他是 根據(jù)某個 IO 所擁有的功能來命名的动羽。比如我們一看到 GPIO1_IO01 就知道這個肯定能做 GPIO员咽,看到 UART1_TX_DATA 肯定就知道這個 IO 肯定能做為 UART1 的發(fā)送引腳捐友。
IO 復(fù)用功能佑附。 i.MX6ULL 除了 GPIO1_IO00~GPIO1_IO09 引腳外泼各,其它 IO 也是可以復(fù)用為 GPIO 功能履肃。同樣的成福,GPIO1_IO00~GPIO_IO09 也是可以復(fù)用為其它外設(shè)引腳荆残。
1.2 IO復(fù)用
IOMUX 譯為 IO 復(fù)用選擇器奴艾。i.MX6ULL 的芯片每個 GPIO 都通過 IOMUX 支持多種功能, 例如一個 IO 可用于網(wǎng)絡(luò)外設(shè) ENET 的數(shù)據(jù)接收引腳内斯,也可以被配置成 PWM 外設(shè)的輸出引腳蕴潦, 這樣的設(shè)計大大增加了芯片的適用性,這樣可選的功能就是由 IOMUX 實現(xiàn)的俘闯。IOMUX 相當于增加了多根內(nèi)部信號線與 IO 引腳相連潭苞,
最多有 8 根,也就是說一個 IO 最多可支持 8 種可選的功能
真朗。
以“IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00”這個 IO 為例此疹,打開參考手冊的 1568 頁。
可以看到有個名為:IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00 的寄存器遮婶,寄存器地址為 0X020E005C蝗碎,這個寄存器是 32 位的,但是只用到了最低 5 位旗扑,其中 bit0~bit3(MUX_MODE) 就是設(shè)置 GPIO1_IO00 的復(fù)用功能的蹦骑。GPIO1_IO00 一共可以復(fù)用為 9 種功能 IO,分別對應(yīng) ALT0~ALT8肩豁,其中 ALT5 就是作為 GPIO1_IO00脊串。GPIO1_IO00 還可以作為 I2C2_SCL、GPT1_CAPTURE1清钥、ANATOP_OTG1_ID 等琼锋。
1.3 IO配置
IOMUX 由其左側(cè)的 IOMUXC 控制(C表示Controler),IOMUXC 提供寄存器給用戶進行配置祟昭, 它又分成 MUX Mode(IO模式控制) 以及 Pad Settings(Pad配置) 兩個部分:
在 IOMUXC 外設(shè)中關(guān)于 MUX Mode 和 Pad Settings 寄存器命名格式如下:
IOMUXC控制類型 | 寄存器名稱 |
---|---|
MUX Mode | IOMUXC_SW_MUX_CTL_PAD_XXXX |
Pad Settings | IOMUXC_SW_PAD_CTL_PAD_XXXX |
每個引腳都包含這兩個寄存器缕坎,表中的XXXX表示引腳的名字
1.3.1 MUX Mode配置
MUX Mode 就是用來
配置引腳的復(fù)用功能
,即選擇引腳具體是用于網(wǎng)絡(luò)外設(shè) ENET 的數(shù)據(jù)接收篡悟, 還是用于 PWM 外設(shè)的輸出引腳谜叹,當然匾寝,也可以配置成普通的 IO 口,僅用于控制輸出高低電平荷腊。
以 GPIO1_IO04 引腳為例對 MUX 寄存器進行說明艳悔,該引腳相應(yīng)的 MUX 寄存器在參考手冊中的描述如下:
該寄存器主要有兩個配置域,分別是 SION 和 MUX_MODE女仰。
- SION: 用于設(shè)置引腳在輸出模式下同時開啟輸入通道猜年。
-
MUX_MODE: 使用 4 個寄存器位表示可選的 ALT0~ALT7 這 8 個模式。
- 如 ALT2 模式就是用于 USB 外設(shè)的 USB_OTG1_PWR 信號疾忍;
- 若配置為 ALT5 則引腳會用作普通的 GPIO 功能乔外, 用于輸出高、低電平一罩。
1.3.2 Pad Settings配置
Pad Settings 用于
配置引腳的屬性
杨幼,例如驅(qū)動能力,是否使用上下拉電阻聂渊, 是否使用保持器差购,是否使用開漏模式以及使用施密特模式還是CMOS模式等。
以 GPIO1_IO04 引腳中 PAD 寄存器在參考手冊中的描述如下:
相對來說 PAD 寄存器的配置項就更豐富了歧沪,而且圖中僅是該寄存器的部分說明歹撒,如 HYS 設(shè)置使用施密特模式的滯后功能莲组,PUS 配置上下拉電阻的阻值诊胞, 其它的還包含PUE、PKE锹杈、ODE撵孤、SPEED、DSE 及 SRE 的配置竭望。
1.3.3 PAD(可跳過不看)
PAD 代表了一個 i.MX6ULL 的 GPIO 引腳邪码。在它的左側(cè)是一系列信號通道及控制線,如 input_on 控制輸入開關(guān)咬清,Dir 控制引腳的輸入輸出方向闭专,Data_out 控制引腳輸出高低電平,Data_in 作為信號輸入旧烧,這些信號都經(jīng)過一個 IOMUX 的器件連接到左側(cè)的寄存器影钉。
①PAD引腳
代表一個i.MX6ULL的引腳。
②輸出緩沖區(qū)
當輸出緩沖區(qū)使能時掘剪,引腳被配置為輸出模式平委。在輸出緩沖區(qū)中,又包含了如下的屬性配置:
-
DSE驅(qū)動能力
當IO用作輸出的時候用來設(shè)置IO的驅(qū)動能力夺谁。
DSE可以調(diào)整芯片內(nèi)部與引腳串聯(lián)電阻R0的大小廉赔,從而改變引腳的驅(qū)動能力肉微。例如,R0的初始值為260歐姆蜡塌,在3.3V電壓下其電流驅(qū)動能力為12.69mA碉纳,通過DSE可以把R0的值配置為原值的1/2、1/3…1/7等馏艾。位設(shè)置 速度 000 輸出驅(qū)動關(guān)閉 001 R0(3.3V 下 R0 是 260Ω村象,1.8V 下 R0 是 150Ω,接 DDR 的時候是 240Ω) 010 R0/2 011 R0/3 100 R0/4 101 R0/5 110 R0/6 111 R0/7 SRE壓擺率配置
設(shè)置壓擺率攒至。
壓擺率是指電壓轉(zhuǎn)換速率厚者,可理解為電壓由波谷升到波峰的時間。增大壓擺率可減少輸出電壓的上升時間迫吐。i.MX6ULL的引腳通過SRE支持低速和高速壓擺率這兩種配置库菲。當此位為0的時候是低壓擺率,當為1的時候是高壓擺率志膀。壓擺率是大信號特性熙宇,下面的帶寬是小信號特性。-
SPEED帶寬配置
設(shè)置IO的帶寬溉浙。
分別可設(shè)置為50MHz烫止、100MHz以及200MHz。帶寬的意思是能通過這個IO口最高的信號頻率馆蠕,通俗點講就是方波不失真惊奇,如果超過這個頻率方波就變正弦波颂郎。但是這個帶寬要區(qū)別于IO的翻轉(zhuǎn)速率,IO的翻轉(zhuǎn)速率的信號來自于GPIO這個外設(shè)寺酪,而IO的帶寬只是限制了IO口引腳的物理特性替劈,IO口的信號可以來自于內(nèi)部定時器輸出的PWM信號寄雀,也可以來自于GPIO翻轉(zhuǎn)輸出的信號,兩者相比之下抬纸,PWM信號的頻率是遠遠高于GPIO翻轉(zhuǎn)輸出的信號頻率咙俩。位設(shè)置 速度 00 低速 50M 01 中速 100M 10 中速 100M 11 最大速度 200M ODE開漏輸出配置
設(shè)置引腳是否工作在開漏輸出模式。
在該模式時引腳可以輸出高阻態(tài)和低電平,此位為0的時候禁止開路輸出阿趁,當此位為1的時候就使能開路輸出功能膜蛔。輸出高阻態(tài)時可由外部上拉電阻拉至高電平。開漏輸出模式常用在一些通訊總線中脖阵,如I2C皂股。
③輸入緩沖區(qū)
當輸入緩沖區(qū)使能時,引腳被配置為輸入模式命黔。在輸入緩沖區(qū)中呜呐,又包含了如下的屬性配置:
- HYS滯后使能
用來使能遲滯比較器。
i.MX6ULL的輸入檢測可以使用普通的CMOS檢測或施密特觸發(fā)器模式(滯后模式)悍募。施密特觸發(fā)器具有滯后效應(yīng)蘑辑,對正向和負向變化的輸入信有不同的閾值電壓。如果需要對輸入波形進行整形的話可以使能此位坠宴。此位為0的時候禁止遲滯比較器喜鼓,為1的時候使能遲滯比較器豁翎。常被用于電子開關(guān)、波形變換等場合,其轉(zhuǎn)換特性和對比如下,如檢測按鍵時熏版,使用施密特模式即可起到消抖的功能。
④Pull/Keeper上下拉、保持器
引腳的控制邏輯中還包含了上下拉禾嫉、保持器的功能艳吠。芯片內(nèi)部的上拉和下拉電阻可以將不確定的信號鉗位在高、低電平,或小幅提高的電流輸出能力迈嘹,上拉提供輸出電流,下拉提供輸入電流神僵。注意這些上下拉配置只是弱拉,對于類似I2C之類的總線炮障,還是必須使用外部上拉電阻。i.MX6ULL芯片的電源模塊中包含轉(zhuǎn)換器,當轉(zhuǎn)換器停止工作時系馆,保持器會保持輸入輸出電壓。
上下拉爷狈、保持器可以通過如下屬性配置:
- PUS上下拉配置
設(shè)置上下拉電阻。
PUS可配置項可選為100K歐下拉以及22K歐土辩、47K歐及100K歐上拉。位設(shè)置 含義 00 100K 下拉 01 47K 上拉 10 100K 上拉 11 22K 上拉 - PUE上下拉、保持器選擇
上下拉功能和保持器功能是二選一的结洼,可以通過PUE來選擇。
當IO作為輸入的時候鸣峭,這個位用來設(shè)置 IO 使用上下拉還是狀態(tài)保持器。當為0的時候使用狀態(tài)保持器,當為1的時候使用上下拉浓镜。狀態(tài)保持器在IO作為輸入的時候才有用听隐,顧名思義风范,就是當外部電路斷電以后此IO口可以保持住以前的狀態(tài)锌半。 - PKE上下拉、保持器配置
用來使能或者禁止上下拉/狀態(tài)保持器功能。
為0時禁止上下拉/狀態(tài)保持器遍膜,為1時使能上下拉和狀態(tài)保持器。
注意挽懦,當引腳被配置為輸出模式時,不管上下拉、保持器是什么配置圃伶,它們都會被關(guān)閉。
1.4 GPIO配置
GPIO 模塊是每個 IO 都具有的外設(shè)侥猩,它具有 IO 控制最基本的功能,如輸出高低電平枫弟、檢測電平輸入等淡诗。 它也占用 IOMUX 分配的復(fù)用信號,也就是說使用 GPIO 模塊功能時同樣需要使用 IOMUX 選中 GPIO 外設(shè)宙攻,對其 GPIO 的功能進行配置。
1.4.1 GDIR方向寄存器
設(shè)置某個 IO 的工作方向。
控制一個 GPIO 引腳時形真,要先用 GDIR 方向寄存器配置該引腳用于輸出電平信號還是用作輸入檢測。 典型的例子是使用輸出模式可以控制LED燈的亮滅蛾坯,輸入模式時可以用來檢測按鍵是否按下。
GDIR 寄存器的每一個數(shù)據(jù)位代表一個引腳的方向,對應(yīng)的位被設(shè)置為0時該引腳為輸入模式呈驶,被設(shè)置為1時該引腳為輸出模式跌穗。
例如,對 GPIO1 的 GDIR 寄存器的 bit3 位被寫入為 1砌庄,那么 GPIO1.3 引腳的模式即為輸出缝彬。
1.4.2 DR數(shù)據(jù)寄存器
DR 數(shù)據(jù)寄存器直接代表了引腳的電平狀態(tài)
扒俯,它也使用 1 個數(shù)據(jù)位表示 1 個引腳的電平墩邀,每位用 1 表示高電平荔茬,用 0 表示低電平。
當 GDIR 方向寄存器設(shè)置引腳為輸出模式時坊萝,寫入 DR 數(shù)據(jù)寄存器對應(yīng)的位即可控制該引腳輸出的電平狀態(tài)十偶, 如這時 GPIO1 的 DR 寄存器的 bit4 被寫入為 1,則引腳為輸出高電平蛛勉。
當 GDIR 方向寄存器設(shè)置引腳為輸入模式時,讀取 DR 數(shù)據(jù)寄存器對應(yīng)的位即可獲取該引腳當前的輸入電平狀態(tài)侣诵,例如這里讀取 GPIO1 的DR寄存器的 bit4蘸炸,得到該位的值為 0穷当,表示當前引腳的輸入狀態(tài)為低電平。
1.4.3 PSR引腳狀態(tài)寄存器
讀取相應(yīng)的位即可獲取對應(yīng)的 GPIO 的狀態(tài)
,也就是 GPIO 的高低電平值。PSR 引腳狀態(tài)寄存器相當于 DR 寄存器的簡化版,它僅在 GDIR 方向寄存器設(shè)置為輸入模式時有效摇天,它的每個位表示一個引腳當前的輸入電平狀態(tài)。PSR 寄存器的權(quán)限是只讀的,對它進行寫操作是無效的纯丸。
特別地觉鼻,當引腳被配置成輸出模式時,若 IOMUXC 中的 MUX 寄存器使能了 SION 功能(輸出通道回環(huán)至輸入), 可以通過 PSR 寄存器讀取回引腳的狀態(tài)值。
二、引腳確定
我使用的是 野火_EBF6ULL S1 Pro
開發(fā)板
從原理圖可看到 RGB 燈的三個陰極 R、G、B 連接分別連接至標號
GPIO_4
溺蕉、CSI_HSYNC
伶丐、CSI_VSYNC
, 這些標號實際上與配套核心板上 i.MX6ULL 芯片的引腳相連疯特。由于引腳功能眾多哗魂, 繪制原理圖時不可避免地?zé)o法完全表示引腳信息的所有信息。而無論是具體的引腳名還是復(fù)用功能漓雅, 我們都無法直接得知這些具體是 i.MX6ULL 芯片的哪個引腳吃衅。我們需要知道這些引腳是對應(yīng)的具體 GPIO猪贪,這樣我們才能編寫程序進行控制牙寞。
由于還不清楚標號 GPIO_4
、CSI_HSYNC
、CSI_VSYNC
的具體引腳名署海,我們首先要在核心板原理圖中查看它與 i.MX6ULL 芯片的關(guān)系。打開 《野火_EBF6ULL S1 郵票孔核心板V1.0原理圖》羽圃,在PDF閱讀器的搜索框輸入前面的 GPIO_4
吨掌、CSI_HSYNC
、CSI_VSYNC
標號刀脏。
查找到了
GPIO_4
信號的具體引腳名為GPIO1_IO04
系奉。 但是當我們使用同樣的方法查找時發(fā)現(xiàn)只能找到CSI_HSYNC
、CSI_VSYNC
, 并沒有我們熟悉的 GPIOx_IOx 標注的引腳名汤踏。這兩個引腳默認情況下不用作 GPIO,而是用作攝像頭的某一功能引腳橱健,但是它可以復(fù)用為 GPIO,我們怎么找到對應(yīng)的 GPIO 呢?
-
方法一:
在《i.MX 6UltraLite Applications Processor Reference Manual》的第4章 External Signals and Pin Multiplexing 搜索引腳名
-
方法二:
在官方寫好的文件 fsl_iomuxc.h(路徑:SDK文件夾/devices/MCIMX6Y2/drivers/fsl_iomuxc.h) 中搜索引腳名
經(jīng)查閱事格,我們把以上連接 LED 燈的各個 i.MX6ULL 芯片引腳總結(jié)出如表:
LED燈 | 原理圖的標號 | 具體引腳名 | GPIO端口及引腳編號 |
---|---|---|---|
R燈 | GPIO_4 | GPIO1_IO04 | GPIO1_IO04 |
G燈 | CSI_HSYNC | CSI_HSYNC | GPIO4_IO20 |
B燈 | CSI_VSYNC | CSI_VSYNC | GPIO4_IO19 |
三、編程流程
1. 移植官方寄存器定義文件
2. 移植官方SDK引腳復(fù)用和引腳屬性定義文件
3. 移植野火PAD屬性配置文件
4. 編寫啟動文件
5. 編寫鏈接文件
6. 編寫makefile文件
7. 編寫C語言代碼
(1) 開啟GPIO時鐘
(2) 設(shè)置引腳的復(fù)用功能以及引腳屬性
(3) 設(shè)置引腳方向以及輸出電平
(4) 查詢按鍵輸入控制LED
四屿附、移植官方SDK寄存器定義文件
使用匯編語言和C語言實現(xiàn)點亮LED燈搁嗓。需要自己查找荐类、定義那么多寄存器季蚂。這樣做的缺點很明顯丽声,易錯刻诊、費時、代碼可讀性差则涯。NXP官方SDK中已經(jīng)將所有的寄存器以及所有可用引腳的復(fù)用功能定義好了复局。
添加官方SDK寄存器定義文件 MCIMX6Y2.h
,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2
目錄下是整。
在官方SDK頭文件
MCIMX6Y2.h
文件多達4萬多行肖揣,包含了i.MX6U芯片幾乎所有的寄存器定義以及中斷編號的定義。
這里只列 GPIO1相關(guān)寄存器 的部分代碼浮入。其他寄存器定義與此類似龙优。 添加這些定義之后我們就可以 直接使用 “GPIO1->DR”
語句操作GPIO1的DR寄存器。操作方法與STM32非常相似事秀。
typedef struct {
__IO uint32_t DR; /**< GPIO data register, offset: 0x0 */
__IO uint32_t GDIR; /**< GPIO direction register, offset: 0x4 */
__I uint32_t PSR; /**< GPIO pad status register, offset: 0x8 */
__IO uint32_t ICR1; /**< GPIO interrupt configuration register1,*/
__IO uint32_t ICR2; /**< GPIO interrupt configuration register2, */
__IO uint32_t IMR; /**< GPIO interrupt mask register, offset: 0x14 */
__IO uint32_t ISR; /**< GPIO interrupt status register, offset: 0x18 */
__IO uint32_t EDGE_SEL;/**< GPIO edge select register, offset: 0x1C */
} GPIO_Type;
/*********************以下代碼省略***************************8*/
/** Peripheral GPIO1 base address */
#define GPIO1_BASE (0x209C000u)
/** Peripheral GPIO1 base pointer */
#define GPIO1 ((GPIO_Type *)GPIO1_BASE)
五彤断、移植官方SDK引腳復(fù)用和引腳屬性定義文件
添加官方SDK引腳復(fù)用和引腳屬性定義文件 fsl_iomuxc.h
,位于 SDK_2.2_MCIM6ULL_EBF6ULL/devices/MCIMX6Y2/drivers
目錄下易迹。
使用每一個引腳之前我們都要選擇引腳的復(fù)用功能以及引腳的pad屬性宰衙。在官方SDK的頭文件
fsl_iomuxc.h
中定義了所有可用引腳以及這些引腳的所有復(fù)用功能,我們需要哪種復(fù)用功能只需要選擇即可睹欲,并且官方SDK中提供了初始化函數(shù)供炼。
-
定義引腳的復(fù)用功能
這里只列出了“GPIO1_IO00”引腳的復(fù)用功能,其他引腳類似窘疮。每個引腳對應(yīng)多個宏定義代表引腳的不同的復(fù)用功能袋哼,以宏“IOMUXC_GPIO1_IO00_I2C2_SCL”為例,它表示“GPIO1_IO00”引腳復(fù)用為“I2C2”的“SCL”引腳闸衫。這些宏定義將會用作某些函數(shù)的入口參數(shù)涛贯。
#define IOMUXC_GPIO1_IO00_I2C2_SCL \
0x020E005CU, 0x0U, 0x020E05ACU, 0x1U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_GPT1_CAPTURE1L \
0x020E005CU, 0x1U, 0x020E058CU, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_ANATOP_OTG1_IDL \
0x020E005CU, 0x2U, 0x020E04B8U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_ENET1_REF_CLK1L \
0x020E005CU, 0x3U, 0x020E0574U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_MQS_RIGHTL \
0x020E005CU, 0x4U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_GPIO1_IO00L \
0x020E005CU, 0x5U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_ENET1_1588_EVENT0_INL \
0x020E005CU, 0x6U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_SRC_SYSTEM_RESETL \
0x020E005CU, 0x7U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO00_WDOG3_WDOG_BL \
0x020E005CU, 0x8U, 0x00000000U, 0x0U, 0x020E02E8U
#define IOMUXC_GPIO1_IO01_I2C2_SDAL \
0x020E0060U, 0x0U, 0x020E05B0U, 0x1U, 0x020E02ECU
#define IOMUXC_GPIO1_IO01_GPT1_COMPARE1L \
0x020E0060U, 0x1U, 0x00000000U, 0x0U, 0x020E02ECU
#define IOMUXC_GPIO1_IO01_USB_OTG1_OCL \
0x020E0060U, 0x2U, 0x020E0664U, 0x0U, 0x020E02ECU
-
引腳復(fù)用功能設(shè)置函數(shù)
IOMUXC_SetPinMux()
擁有6個入口參數(shù), 但是前五個是通過上面的宏定義自動完成設(shè)置的蔚出。而第6個入口參數(shù)“inputOnfiled”用于設(shè)置是否開啟讀回引腳電平功能弟翘。
static inline void IOMUXC_SetPinMux(uint32_t muxRegister,
uint32_t muxMode,
uint32_t inputRegister,
uint32_t inputDaisy,
uint32_t configRegister,
uint32_t inputOnfield)
{
*((volatile uint32_t *)muxRegister) =
IOMUXC_SW_MUX_CTL_PAD_MUX_MODE(muxMode) |\
IOMUXC_SW_MUX_CTL_PAD_SION(inputOnfield);
if (inputRegister)
{
*((volatile uint32_t *)inputRegister) = \
IOMUXC_SELECT_INPUT_DAISY(inputDaisy);
}
}
-
引腳PAD屬性設(shè)置函數(shù)
IOMUXC_SetPinConfig()
函數(shù)共有6個入口參數(shù)虫腋,其中前五個是通過上面的宏定義自動完成設(shè)置的。而第6個參數(shù)用于設(shè)置PAD屬性稀余,根據(jù)每個引腳擁有一個32位PAD屬性寄存器悦冀。第六個參數(shù)就是設(shè)置要填入PAD屬性寄存器的值。
static inline void IOMUXC_SetPinConfig(uint32_t muxRegister,
uint32_t muxMode,
uint32_t inputRegister,
uint32_t inputDaisy,
uint32_t configRegister,
uint32_t configValue)
{
if (configRegister)
{
*((volatile uint32_t *)configRegister) = configValue;
}
}
代碼屏蔽 #include "fsl_common.h"
六滚躯、移植野火PAD屬性配置文件
添加 pad_config.h
雏门。
通常情況下一個引腳要設(shè)置8種PAD屬性,而這些屬性只能通過數(shù)字指定掸掏。為簡化PAD屬性設(shè)置野火編寫了一個PAD屬性配置文件 pad_config.h
(embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源碼下載:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】茁影,這里使用宏定義了引腳可選的PAD屬性值,并且通過宏定義的名字很容易知道宏代表的屬性值:
/* SPEED 帶寬配置 */
#define SPEED_0_LOW_50MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(0)
#define SPEED_1_MEDIUM_100MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(1)
#define SPEED_2_MEDIUM_100MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(2)
#define SPEED_3_MAX_200MHz IOMUXC_SW_PAD_CTL_PAD_SPEED(3)
/* PUE 選擇使用保持器還是上下拉 */
#define PUE_0_KEEPER_SELECTED IOMUXC_SW_PAD_CTL_PAD_PUE(0)
#define PUE_1_PULL_SELECTED IOMUXC_SW_PAD_CTL_PAD_PUE(1)
/* PUS 上下拉配置 */
#define PUS_0_100K_OHM_PULL_DOWN IOMUXC_SW_PAD_CTL_PAD_PUS(0)
#define PUS_1_47K_OHM_PULL_UP IOMUXC_SW_PAD_CTL_PAD_PUS(1)
#define PUS_2_100K_OHM_PULL_UP IOMUXC_SW_PAD_CTL_PAD_PUS(2)
#define PUS_3_22K_OHM_PULL_UP IOMUXC_SW_PAD_CTL_PAD_PUS(3)
完整的代碼請閱讀源文件丧凤,這里只列出了文件“pad_config.h”部分代碼(embed_linux_driver_tutorial_imx6_code/bare_metal/led_rgb_c/pad_config.h)【源碼下載:https://gitee.com/Embedfire/embed_linux_driver_tutorial_imx6_code.git】募闲。
七、編寫啟動文件
在 Ubuntu 下創(chuàng)建 start.S
文件用于編寫啟動文件愿待。
在匯編文件中設(shè)置“棧地址”
并執(zhí)行跳轉(zhuǎn)命令跳轉(zhuǎn)到main函數(shù)
執(zhí)行C代碼浩螺。
7.1 完整代碼
/***********************第一部分*********************/
.text //代碼段
.align 2 //設(shè)置2字節(jié)對齊
.global _start //定義一個全局標號
/*************************第二部分*************************/
_start: //程序的開始
b reset //跳轉(zhuǎn)到reset標號處
/*************************第三部分*************************/
reset:
mrc p15, 0, r0, c1, c0, 0 /* 將 CP15 協(xié)處理器中的寄存器數(shù)據(jù)讀到 ARM 寄存器中 */
bic r0, r0, #(0x1 << 12) /* 清除第12位(I位)禁用 I Cache */
bic r0, r0, #(0x1 << 2) /* 清除第 2位(C位)禁用 D Cache */
bic r0, r0, #0x2 /* 清除第 1位(A位)禁止嚴格對齊 */
bic r0, r0, #(0x1 << 11) /* 清除第11位(Z位)分支預(yù)測 */
bic r0, r0, #0x1 /* 清除第 0位(M位)禁用 MMU */
mcr p15, 0, r0, c1, c0, 0 /* 將 ARM 寄存器的數(shù)據(jù)寫入到 CP15 協(xié)處理器寄存器中 */
/***********************第四部分*********************/
ldr sp, =0x84000000 //設(shè)置棧地址64M
b main //跳轉(zhuǎn)到main函數(shù)
/***********************第五部分*******************/
/*進入死循環(huán)*/
loop:
b loop
7.2 分析代碼
-
第一部分
.text
定義代碼段。
.align 2
設(shè)置字節(jié)對齊仍侥。
.global _start
生命全局標號_start要出。
/*************************第一部分*************************/
.text //代碼段
.align 2 //設(shè)置2字節(jié)對齊
.global _start //定義一個全局標號
-
第二部分
_start:
定義標號_start: ,它位于匯編的最前面农渊,說以會首先被執(zhí)行患蹂。
b reset
使用b指令將程序跳轉(zhuǎn)到reset標號處。
/*************************第二部分*************************/
_start: //程序的開始
b reset //跳轉(zhuǎn)到reset標號處
-
第三部分
通過修改CP15寄存器(系統(tǒng)控制寄存器)
關(guān)閉 I Cache 砸紊、D Cache传于、MMU 等等。
我們暫時用不到的功能醉顽,如果開啟可能會影響我們裸機運行沼溜,為避免不必要的麻煩暫時關(guān)閉這些功能。
/*************************第三部分*************************/
reset:
mrc p15, 0, r0, c1, c0, 0 /* 將 CP15 協(xié)處理器中的寄存器數(shù)據(jù)讀到 ARM 寄存器中 */
bic r0, r0, #(0x1 << 12) /* 清除第12位(I位)禁用 I Cache */
bic r0, r0, #(0x1 << 2) /* 清除第 2位(C位)禁用 D Cache */
bic r0, r0, #0x2 /* 清除第 1位(A位)禁止嚴格對齊 */
bic r0, r0, #(0x1 << 11) /* 清除第11位(Z位)分支預(yù)測 */
bic r0, r0, #0x1 /* 清除第 0位(M位)禁用 MMU */
mcr p15, 0, r0, c1, c0, 0 /* 將 ARM 寄存器的數(shù)據(jù)寫入到 CP15 協(xié)處理器寄存器中 */
-
第四部分
ldr sp, =0x84000000
用于設(shè)置棧指針游添。野火i.MX6ULL開發(fā)板標配512M的DDR內(nèi)存系草,裸機開發(fā)用不了這么多。程序中我們將棧地址設(shè)置到DDR的64M地址處唆涝。 這個值也可以根據(jù)需要自行定義悄但。
b main
只用跳轉(zhuǎn)指令跳轉(zhuǎn)到main函數(shù)中執(zhí)行。
/***********************第四部分*********************/
ldr sp, =0x84000000 //設(shè)置棧地址64M
b main //跳轉(zhuǎn)到main函數(shù)
-
第五部分
b loop
是“無返回”的跳轉(zhuǎn)指令石抡。正常情況下,不會執(zhí)行第五部分代碼助泽。
/***********************第五部分*******************/
/*進入死循環(huán)*/
loop:
b loop
八啰扛、編寫鏈接腳本
寫好的代碼(無論是匯編還是C語言)都要經(jīng)過編譯嚎京、匯編、鏈接等步驟生成二進制文件或者可供下載的文件隐解。在編譯階編譯器會對每個源文件進行語法檢查并生成對應(yīng)的匯編語言鞍帝,匯編是將匯編文件轉(zhuǎn)化為機器碼。
使用
arm-none-eabi-gcc -g -c led.S -o led.o
命令完成源碼的編譯煞茫、匯編工作帕涌,生成了.o
文件。編譯和匯編是針對單個源文件续徽,也就編譯完成后一個源文件(.c
蚓曼,.S
或.s
)對應(yīng)一個.o
文件。程序鏈接階段就會將這些.o
鏈接成一個文件钦扭。鏈接腳本的作用就是告訴編譯器怎么鏈接這些文件纫版,比如那個文件放在最前面,程序的代碼段客情、數(shù)據(jù)段其弊、bss段分別放在什么位置等等。
在 Ubuntu 下創(chuàng)建 led.lds
鏈接腳本膀斋。
8.1 完整代碼
ENTRY(_start)
SECTIONS {
. = 0x80000000;
. = ALIGN(4);
.text :
{
start.o (.text)
*(.text)
}
. = ALIGN(4);
.data :
{
*(.data)
}
. = ALIGN(4);
.bss :
{
*(.bss)
}
}
8.2 分析代碼
-
指定程序的入口
ENTRY(_start)
用于指定程序的入口梭伐,ENTRY()
是設(shè)置入口地址的命令,“_start”
是程序的入口仰担,led程序的入口地址位于start.S
的“_start”
標號處糊识。
ENTRY(_start)
-
定義SECTIONS
SECTIONS
可以理解為是一塊區(qū)域,我們在這塊區(qū)域排布我們的代碼惰匙,鏈接時鏈接器就會按照這里的指示鏈接我們的代碼技掏。
SECTIONS {
···
···
}
-
定義鏈接起始地址
“.”
運算符代表當前位置。 我們在SECTION的最開始使用“.= 0x80000000”
就是將鏈接起始地址設(shè)置為0x80000000项鬼。
. = 0x80000000;
設(shè)置字節(jié)對齊
“. = ALIGN(4);”
它表示從當前位置開始執(zhí)行四字節(jié)對齊哑梳。假設(shè)當前位置為0x80000001,執(zhí)行該命令后當前地址將會空出三個字節(jié)轉(zhuǎn)到0x80000004地址處绘盟。設(shè)置代碼段
“.text :”
用于定義代碼段鸠真,固定的語法要求,我們按照要求寫即可龄毡。在“{}”中指定那些內(nèi)容放在代碼段吠卷。
將start.o
中的代碼放到代碼段的最前面。start.S
是啟動代碼應(yīng)當首先被執(zhí)行沦零,所以通常情況下要把它放到代碼段的最前面祭隔,其他源文件的代碼按照系統(tǒng)默認的排放順序即可,通配符“*”
在這里表示其他剩余所有的.o
文件路操。
. = ALIGN(4);
.text :
{
start.o (.text)
*(.text)
}
-
設(shè)置數(shù)據(jù)段
同設(shè)置代碼段類似疾渴,首先設(shè)置字節(jié)對齊千贯,然后定義代碼段。在數(shù)據(jù)段里使用“*”
通配符搞坝, 將所有源文件中的代碼添加到這個數(shù)據(jù)段中搔谴。
. = ALIGN(4);
.data :
{
*(.data)
}
-
設(shè)置BSS段
設(shè)置方法與設(shè)置數(shù)據(jù)段完全相同。
. = ALIGN(4);
.bss :
{
*(.bss)
}
九桩撮、編寫makefile文件
程序編寫完成后需要依次輸入編譯敦第、鏈接、格式轉(zhuǎn)換命令才能最終生成二進制文件店量。這種編譯方式效率低芜果、容易出錯。
使用
makefile
只需要在所在文件夾下執(zhí)行make
命令垫桂,makefile工具便會自動完成程序的編譯师幕、鏈接、格式轉(zhuǎn)換等工作诬滩。正常情況下我們可以在當前目錄看到生成的一些中間文件以及我們期待的.bin
文件霹粥。
在 Ubuntu 下創(chuàng)建 makefile
文件炬丸。
9.1 完整代碼
all: start.o led.o
arm-none-eabi-ld -Tled.lds $^ -o led.elf
arm-none-eabi-objcopy -O binary -S -g led.elf led.bin
%.o : %.S
arm-none-eabi-gcc -g -c $^ -o start.o
%.o : %.c
arm-none-eabi-gcc -g -c $^ -o led.o
.PHONY: clean
clean:
rm *.o *.elf *.bin
9.2 分析代碼
- 添加最終目標以及依賴文件
all: start.o led.o
-
添加鏈接命令
“-Tled.lds”
表示使用led.lds鏈接腳本鏈接程序漂羊。
“$^”
代表所有的依賴文件。
“-o”
指定輸出文件名幸冻。
arm-none-eabi-ld -Tled.lds $^ -o led.elf
-
添加格式轉(zhuǎn)換命令
“-O binary”
指定輸出二進制文件空镜。
“-S”
不從源文件中復(fù)制重定位信息和符號信息浩淘。
“-g”
不從源文件中復(fù)制可調(diào)試信息。
arm-none-eabi-objcopy -O binary -S -g led.elf led.bin
-
添加匯編文件編譯命令
“$^”
替代要編譯的源文件吴攒。
%.o : %.S
arm-none-eabi-gcc -g -c $^ -o start.o
-
添加編譯C文件的命令
“$^”
替代要編譯的源文件张抄。
%.o : %.c
arm-none-eabi-gcc -g -c $^ -o led.o
-
添加清理命令
“.PHONY”
定義了偽目標“clean”。偽目標一般沒有依賴洼怔,并且“clean”
偽目標一般放在Makefile文件的末尾署惯。
“clean”
為目標用于刪除make生成的文件。
.PHONY: clean
clean:
rm *.o *.elf *.bin
十镣隶、編寫C語言代碼
在 Ubuntu 下創(chuàng)建 led.c
文件用于驅(qū)動 RGB 燈极谊。
10.1 完整代碼
/*************************第一部分************************/
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"
/*************************第二部分************************/
/*LED GPIO端口、引腳號及IOMUXC復(fù)用宏定義*/
#define RGB_RED_LED_GPIO GPIO1
#define RGB_RED_LED_GPIO_PIN (4U)
#define RGB_RED_LED_IOMUXC IOMUXC_GPIO1_IO04_GPIO1_IO04
#define RGB_GREEN_LED_GPIO GPIO4
#define RGB_GREEN_LED_GPIO_PIN (20U)
#define RGB_GREEN_LED_IOMUXC IOMUXC_CSI_HSYNC_GPIO4_IO20
#define RGB_BLUE_LED_GPIO GPIO4
#define RGB_BLUE_LED_GPIO_PIN (19U)
#define RGB_BLUE_LED_IOMUXC IOMUXC_CSI_VSYNC_GPIO4_IO19
/*************************第三部分************************/
/* 所有引腳均使用同樣的PAD配置 */
#define LED_PAD_CONFIG_DATA (SRE_0_SLOW_SLEW_RATE| \
DSE_6_R0_6| \
SPEED_2_MEDIUM_100MHz| \
ODE_0_OPEN_DRAIN_DISABLED| \
PKE_0_PULL_KEEPER_DISABLED| \
PUE_0_KEEPER_SELECTED| \
PUS_0_100K_OHM_PULL_DOWN| \
HYS_0_HYSTERESIS_DISABLED)
/* 配置說明 : */
/* 轉(zhuǎn)換速率: 轉(zhuǎn)換速率慢
驅(qū)動強度: R0/6
帶寬配置 : medium(100MHz)
開漏配置: 關(guān)閉
拉/保持器配置: 關(guān)閉
拉/保持器選擇: 保持器(上面已關(guān)閉安岂,配置無效)
上拉/下拉選擇: 100K歐姆下拉(上面已關(guān)閉轻猖,配置無效)
滯回器配置: 關(guān)閉 */
/*************************第四部分************************/
/*簡單延時函數(shù)*/
void delay(uint32_t count)
{
volatile uint32_t i = 0;
for (i = 0; i < count; ++i)
{
__asm("NOP"); /* 調(diào)用nop空指令 */
}
}
int main()
{
/*************************第五部分************************/
CCM->CCGR1 |= CCM_CCGR1_CG13(0x3);//開啟GPIO1的時鐘
CCM->CCGR3 |= CCM_CCGR3_CG6(0x3); //開啟GPIO4的時鐘
/*************************第六部分************************/
/*設(shè)置 紅燈 引腳的復(fù)用功能以及PAD屬性*/
IOMUXC_SetPinMux(RGB_RED_LED_IOMUXC,0);
IOMUXC_SetPinConfig(RGB_RED_LED_IOMUXC, LED_PAD_CONFIG_DATA);
/*設(shè)置 綠燈 引腳的復(fù)用功能以及PAD屬性*/
IOMUXC_SetPinMux(RGB_GREEN_LED_IOMUXC,0);
IOMUXC_SetPinConfig(RGB_GREEN_LED_IOMUXC, LED_PAD_CONFIG_DATA);
/*設(shè)置 藍燈 引腳的復(fù)用功能以及PAD屬性*/
IOMUXC_SetPinMux(RGB_BLUE_LED_IOMUXC,0);
IOMUXC_SetPinConfig(RGB_BLUE_LED_IOMUXC, LED_PAD_CONFIG_DATA);
/*************************第七部分************************/
GPIO1->GDIR |= (1<<4); //設(shè)置GPIO1_04為輸出模式
GPIO1->DR |= (1<<4); //設(shè)置GPIO1_04輸出電平為高電平
GPIO4->GDIR |= (1<<20); //設(shè)置GPIO4_20為輸出模式
GPIO4->DR |= (1<<20); //設(shè)置GPIO4_20輸出電平為高電平
GPIO4->GDIR |= (1<<19); //設(shè)置GPIO4_19為輸出模式
GPIO4->DR |= (1<<19); //設(shè)置GPIO4_19輸出電平為高電平
/*************************第八部分************************/
while(1)
{
GPIO1->DR &= ~(1<<4); //紅燈亮
delay(0xFFFFF);
GPIO1->DR |= (1<<4); //紅燈滅
GPIO4->DR &= ~(1<<20); //綠燈亮
delay(0xFFFFF);
GPIO4->DR |= (1<<20); //綠燈滅
GPIO4->DR &= ~(1<<19); //藍燈亮
delay(0xFFFFF);
GPIO4->DR |= (1<<19); //藍燈滅
}
return 0;
}
10.2 分析代碼
-
第一部分:添加頭文件
文件“MCIMX6Y2.h”
和“fsl_iomuxc.h”
來自SDK。 文件“pad_config.h”
是野火編寫的文件域那,在其他工程中可直接使用咙边。
/*************************第一部分************************/
#include "MCIMX6Y2.h"
#include "fsl_iomuxc.h"
#include "pad_config.h"
- 第二部分:定義GPIO相關(guān)引腳以及復(fù)用功能
/*************************第二部分************************/
/*LED GPIO端口、引腳號及IOMUXC復(fù)用宏定義*/
#define RGB_RED_LED_GPIO GPIO1
#define RGB_RED_LED_GPIO_PIN (4U)
#define RGB_RED_LED_IOMUXC IOMUXC_GPIO1_IO04_GPIO1_IO04
#define RGB_GREEN_LED_GPIO GPIO4
#define RGB_GREEN_LED_GPIO_PIN (20U)
#define RGB_GREEN_LED_IOMUXC IOMUXC_CSI_HSYNC_GPIO4_IO20
#define RGB_BLUE_LED_GPIO GPIO4
#define RGB_BLUE_LED_GPIO_PIN (19U)
#define RGB_BLUE_LED_IOMUXC IOMUXC_CSI_VSYNC_GPIO4_IO19
-
第三部分:定義引腳的PAD屬性
PAD屬性宏定義保存在“pad_config.h”
文件中,這里使用“|”
運算符將所有屬性設(shè)置“合并”在一起败许,后面將作為函數(shù)參數(shù)友瘤。
/*************************第三部分************************/
/* 所有引腳均使用同樣的PAD配置 */
#define LED_PAD_CONFIG_DATA (SRE_0_SLOW_SLEW_RATE| \
DSE_6_R0_6| \
SPEED_2_MEDIUM_100MHz| \
ODE_0_OPEN_DRAIN_DISABLED| \
PKE_0_PULL_KEEPER_DISABLED| \
PUE_0_KEEPER_SELECTED| \
PUS_0_100K_OHM_PULL_DOWN| \
HYS_0_HYSTERESIS_DISABLED)
/* 配置說明 : */
/* 轉(zhuǎn)換速率: 轉(zhuǎn)換速率慢
驅(qū)動強度: R0/6
帶寬配置 : medium(100MHz)
開漏配置: 關(guān)閉
拉/保持器配置: 關(guān)閉
拉/保持器選擇: 保持器(上面已關(guān)閉,配置無效)
上拉/下拉選擇: 100K歐姆下拉(上面已關(guān)閉檐束,配置無效)
滯回器配置: 關(guān)閉 */
- 第四部分:簡單的延時函數(shù)
/*************************第四部分************************/
/*簡單延時函數(shù)*/
void delay(uint32_t count)
{
volatile uint32_t i = 0;
for (i = 0; i < count; ++i)
{
__asm("NOP"); /* 調(diào)用nop空指令 */
}
}
- 第五部分:開啟GPIO時鐘
/*************************第五部分************************/
CCM->CCGR1 |= CCM_CCGR1_CG13(0x3);//開啟GPIO1的時鐘
CCM->CCGR3 |= CCM_CCGR3_CG6(0x3); //開啟GPIO4的時鐘
- 第六部分:設(shè)置引腳的復(fù)用功能以及引腳PAD屬性
/*************************第六部分************************/
/*設(shè)置 紅燈 引腳的復(fù)用功能以及PAD屬性*/
IOMUXC_SetPinMux(RGB_RED_LED_IOMUXC,0);
IOMUXC_SetPinConfig(RGB_RED_LED_IOMUXC, LED_PAD_CONFIG_DATA);
/*設(shè)置 綠燈 引腳的復(fù)用功能以及PAD屬性*/
IOMUXC_SetPinMux(RGB_GREEN_LED_IOMUXC,0);
IOMUXC_SetPinConfig(RGB_GREEN_LED_IOMUXC, LED_PAD_CONFIG_DATA);
/*設(shè)置 藍燈 引腳的復(fù)用功能以及PAD屬性*/
IOMUXC_SetPinMux(RGB_BLUE_LED_IOMUXC,0);
IOMUXC_SetPinConfig(RGB_BLUE_LED_IOMUXC, LED_PAD_CONFIG_DATA);
- 第七部分:設(shè)置GPIO為輸出并設(shè)置初始電平為高電平
/*************************第七部分************************/
GPIO1->GDIR |= (1<<4); //設(shè)置GPIO1_04為輸出模式
GPIO1->DR |= (1<<4); //設(shè)置GPIO1_04輸出電平為高電平
GPIO4->GDIR |= (1<<20); //設(shè)置GPIO4_20為輸出模式
GPIO4->DR |= (1<<20); //設(shè)置GPIO4_20輸出電平為高電平
GPIO4->GDIR |= (1<<19); //設(shè)置GPIO4_19為輸出模式
GPIO4->DR |= (1<<19); //設(shè)置GPIO4_19輸出電平為高電平
- 第八部分:在while(1)中依次點亮紅燈、綠燈和藍燈
/*************************第八部分************************/
while(1)
{
GPIO1->DR &= ~(1<<4); //紅燈亮
delay(0xFFFFF);
GPIO1->DR |= (1<<4); //紅燈滅
GPIO4->DR &= ~(1<<20); //綠燈亮
delay(0xFFFFF);
GPIO4->DR |= (1<<20); //綠燈滅
GPIO4->DR &= ~(1<<19); //藍燈亮
delay(0xFFFFF);
GPIO4->DR |= (1<<19); //藍燈滅
}
十一束倍、編譯下載驗證
11.1 編譯代碼
make
執(zhí)行make命令被丧,生成led.bin文件。
11.2 代碼燒寫
編譯成功后會在當前文件夾下生成.bin文件绪妹,這個.bin文件也不能直接放到開發(fā)板上運行甥桂, 這次是因為需要在.bin文件缺少啟動相關(guān)信息。
為二進制文件添加頭部信息并燒寫到SD卡邮旷。查看 IMX6ULL學(xué)習(xí)筆記(12)——通過SD卡啟動官方SDK程序
進入燒寫工具目錄黄选,執(zhí)行 ./mkimage.sh <燒寫文件路徑>
命令,例如要燒寫的 led.bin 位于 home 目錄下婶肩,則燒寫命令為 ./mkimage.sh /home/led.bin
办陷。
執(zhí)行上一步后會列出linux下可燒寫的磁盤,選擇你插入的SD卡即可律歼。這一步 非常危險C窬怠!险毁!一定要確定選擇的是你插入的SD卡V迫Α!畔况,如果選錯很可能破壞你電腦磁盤內(nèi)容鲸鹦,造成數(shù)據(jù)損壞!u喂颉馋嗜! 確定磁盤后SD卡以“sd”開頭,選擇“sd”后面的字符即可域庇。例如要燒寫的sd卡是“sdb”則輸入“b”即可嵌戈。
11.3 實驗現(xiàn)象
將開發(fā)板設(shè)置為SD卡啟動,接入SD卡听皿,開發(fā)板上電熟呛,可以看到開發(fā)板上RGB紅、綠尉姨、藍三種顏色輪流閃爍庵朝。
? 由 Leung 寫于 2022 年 12 月 25 日
? 參考:6. 完善LED程序