1 Tổng quan
Khi cấp nguồn 1 PC lên, trước khi hệ điều hành chiếm quyền điều khiển phần cứng thì phải có 1 phần mềm khác đã được chạy bởi phần cứng. Phần mềm được chạy đầu tiên đó chính là bootloader. Nhiệm vụ của bootloader chính là load hệ điều hành vào hệ thống.
Bootloader có 2 được chia ra thành 2 loại chính:
- Uboot: thiên về open source, miễn phí, phù hợp cho các kiến trúc arm – kiến trúc thiên về cho các thiết bị embedded nhỏ gọn
- Grub: cho kiến trúc x86
2. Quá trình khởi động của thiết bị embedded
Trước khi tìm hiểu về uboot, chúng ta cần tìm hiểu về quá trình khởi động của 1 thiết bị embedded. Quá trình này được chia ra làm 3 giai đoạn:
- First stage boot loader
- Second stage boot loader
- Third stage boot loader
First stage boot loader
Là chương trình được lưu trong vùng nhớ Rom của SoC, nằm tại địa chỉ 0x20000 (fuse ROM – lưu trữ vĩnh viễn và không thay đổi được)
Ngay sau khi power on, CPU sẽ tìm tới địa chỉ 0x20000 và thực thi chương trình trên – còn gọi là chương trình boot ROM.
Chương trình boot ROM có nhiệm vụ: Khởi tạo 1 số clock (cho RAM, chân Pin cho SD card), pin muxing, disable watchdog… và đọc giá trị của SYSBOOT_PIN để load second stage boot loader.
Ngoài ra, chương trình Boot Rom khởi tạo ra Booting device list, nơi chứa mức độ ưu tiên cho việc sử dụng thiết bị để booting cho con chip
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-21.png)
Xét chương trình boot ROM trên beaglebone black:
- Set up the booting device list: beaglebone black cho phép uboot được đọc từ 5 device khác nhau (MMC, SD card, SPI, Serial, USB). Vì vậy, cần có sự phân cấp mức độ ưu tiên boot từ các device được thực hiện trong bước Set up the booting device list
Beaglebone black có hệ thống SYSBOOT_PIN bao gồm 15 chân pin. Giá trị điện áp tại mỗi chân Pin sẽ quyết định 1 thuộc tính khi boot (Crystal Frequency, Pinmuxing, Boot sequence).
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-22.png)
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-23.png)
Trong đó 5 chân pin cuối cùng dùng để tạo mức độ ưu tiên cho các booting device.
- Sau đó, boot ROM tiến hành đọc dữ liệu ở thiết bị có mức độ ưu tiên cao nhất và check lỗi của dữ liệu đó. Nếu chương trình boot từ thiết bị không hợp lệ, nó sẽ thực hiện boot từ các thiết bị có mức độ ưu tiên thấp hơn liền kề.
Trên thực tế, để tránh việc bị thay đổi chương trình gốc của thiết bị, các hãng sản xuất đã dựa vào tính năng Secure boot để đảm bảo tính bảo mật của sản phẩm.
- ROM code lưu public key và private key
- Image lưu trên thiết bị nhớ dùng để boot cho con chip phải được mã hoá bằng public key.
- Sau khi load image và giải mã trên ram, chương trình ROM sẽ check tính hợp lệ của image – cho phép boot.
Do đặc thù kích thước nhỏ và không thể ghi đè lại. Nên chương trình boot ROM thường sẽ làm rất đơn giản. Tuy nhiên ngày nay, có nhu cầu về những cách thức boot phức tạp hơn. Ví dụ như cho phép boot OS từ ethernet, serial, hiển thị logo, boot song song nhiều OS…
Vì vậy cần phải có 1 chương trình boot loader phức tạp hơn. Chương trình sẽ được lưu ở bộ nhớ ngoài với kích thước lớn hơn. Uboot ra đời.
Second và third stage boot loader
Soucre code uboot được build xong sẽ được 2 file: SPL và TPL
Uboot sẽ đóng vai trò là second stage (SPL) và third stage (TPL) boot loader. Ví dụ uboot trong beaglebone black sẽ tướn ứng với 2 file: MLO – SPL và u-boot.img – TPL
SPL khởi tạo phần cứng và load tiếp TPL từ thiết bị boot vào chip.
TPL bao gồm các driver, thư viện, command. TPL thực hiện boot script là 1 tập hợp các command line làm nhiệm vụ đọc kernel image từ thiết bị boot, nạp nó vào RAM và giải nén. Sau đó, TPL chạy hàm start_kernel dựa vào địa chỉ trong kernel header của file image – hàm này tiến hành load ramdisk và run init process trong, init process sẽ tiến hành load tiếp các process khác.
Những tình huống trong dự án mà chúng ta phải làm việc với uboot:
- Check phần cứng. Một số lỗi xảy ra mà chúng ta không thể phân biệt được đó là lỗi của phần cứng hay phần mềm.
- Cấu hình lại chân pin: Board BBB đang có mặc định 4 cổng serial. Chúng ta muốn lấy chân pin của 3 cổng để dùng vào việc khác => Cấu hình lại chân pin.
- Sửa lại giao diện boot: Một số thiết bị cho phép hiển thị logo ngay khi bật nguồn.
Nhìn chung, đa số công việc mà chúng ta làm với uboot sẽ làm nhiệm vụ chuẩn bị môi trường để boot được linux từ thiết bị lưu trữ.
Kiến trúc của uboot khá tương đồng với kiến trúc của Linux
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-24.png)
Uboot được xây dựng với tư tưởng như sau:
- Tầng thấp nhất là các device driver. Chúng làm nhiệm vụ điều khiển phần cứng. Chức năng sẽ tập chung vào cấu hình clock, power, pinmuxing, đọc ghi dữ liệu từ thiết bị nhớ.
- Tầng thư viện: Là các library với những API như printf, debug_log, read(), write()…
- Tầng application: Là các command line. Như read, load, bootm…
Mục đích cuối cùng của uboot là thực hiện boot script (tương tự với script trên linux). Boot script sẽ gọi 1 loạt các command line để thực hiện việc boot hệ điều hành: đọc kernel từ thiết bị boot, nạp vào RAM và giải nén,..
Uboot source code
- Device tree.
- Syntax về device tree trong uboot follow theo đúng với Linux.
- Device tree trong uboot được đính kèm trong file uboot.img chứ không được build riêng ra file dtb
- Device driver
- Template về driver của uboot khá giống với Linux.
- Command line
Xét ví dụ vào giao diện của uboot trên BBB, khi thực hiện boot bằng thẻ nhớ và nhấn đồng thời nút S2 khi cấp nguồn. Sau đos, ta phải thực hiện nhấn Space ngay lập tức để có thể vào được giao diện của Uboot và tương tác với uboot qua các command line như sau:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-25.png)
Từ đây, bằng cmd “md” ta có thể đọc được memory từ một địa chỉ cụ thể
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-26.png)
Ví dụ: Đổi thời gian chờ nhấn Space để dừng ở giao diện command line của uboot
Khi uboot khởi động, muốn dừng ở giao diện đó thì phải nhấn Spacee trong vòng 3s. Bây giờ, ví dụ này sẽ thay đổi thời gian trên thành 30s bằng cách sửa config build uboot và build lại uboot rồi nạp vào BBB.
Các bước sửa code như sau:
- Dùng cmd:
1 |
grep -nrwI “Press SPACE to abort autoboot in” |
để tìm nơi define dòng log trên trong file config uboot cho BBB:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-27.png)
Ta thấy nó được define bởi CONFIG_AUTOBOOT_PROMPT trong configs/am335x_evm_defconfig
2. Tiếp tục tìm kiếm define AUTOBOOT_PROMPT ta thấy nó xuất hiện trong file Kconfig
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-28.png)
3. Đi vào file Kconfig ta thấy thời gian được hiện thị thông qua CONFIG_BOOTDELAY
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-29.png)
Bây giờ ta thực hiện việc sửa đổi CONFIG_BOOTDELAY trong file am335x_evm_defconfig thành 30
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-30.png)
Cuối cùng ta thực hiện build lại uboot và nạp vào BBB và sẽ thấy dòng log cho phép chờ nhấn Space đã được thay đổi
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-31.png)
3. Ví dụ bật tắt LED trong uboot
Để làm được ví dụ trên, cần:
- Viết 1 driver điều khiển đèn led
- Viết thêm 1 command line để bật tắt led thông qua driver
Ví dụ điều khiển đèn led tại P1:14 theo luồng hoạt động sau:
![](https://vinalinux.com.vn/wp-content/uploads/2023/12/image-32.png)
Muốn thêm 1 command line trong uboot, ta cần khởi tạo file code C “demo_led.c” cho command line đó trong thư mục u-boot/cmd/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
#include <common.h> #include <command.h> #include <dm.h> #include <led.h> #include <dm/uclass-internal.h> /* */ static const char *const state_label[] = { [LEDST_OFF] = "off", [LEDST_ON] = "on", [LEDST_TOGGLE] = "toggle", }; enum led_state_t get_demo_led_cmd(char *var) { int i; for (i = 0; i < LEDST_COUNT; i++) { if (!strncmp(var, state_label[i], strlen(var))) return i; } return -1; } static int show_led_state(struct udevice *dev) { int ret; ret = led_get_state(dev); if (ret >= LEDST_COUNT) ret = -EINVAL; if (ret >= 0) printf("%s\n", state_label[ret]); return ret; } static int show_label(void) { struct udevice *dev; int ret; for (uclass_find_first_device(UCLASS_LED, &dev); dev; uclass_find_next_device(&dev)) { struct led_uc_plat *plat = dev_get_uclass_platdata(dev); if (!plat->label) continue; printf("%s \n", plat->label); } return 0; } int do_demo_led(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { enum led_state_t cmd; const char *led_label; struct udevice *dev; int ret; if (argc < 2) return CMD_RET_USAGE; led_label = argv[1]; if (strncmp(led_label, "show", 4) == 0) return show_label(); cmd = argc > 2 ? get_demo_led_cmd(argv[2]) : LEDST_COUNT; ret = led_get_by_label(led_label, &dev); if (ret) { printf("LED '%s' not found (err=%d)\n", led_label, ret); return CMD_RET_FAILURE; } switch (cmd) { case LEDST_OFF: case LEDST_ON: case LEDST_TOGGLE: /* hàm gọi xuống driver điều khiển led */ ret = led_set_state(dev, cmd); break; case LEDST_COUNT: printf("LED '%s': ", led_label); ret = show_led_state(dev); break; } if (ret < 0) { printf("LED '%s' operation failed (err=%d)\n", led_label, ret); return CMD_RET_FAILURE; } return 0; } U_BOOT_CMD( demo_led, 4, 1, do_demo_led, "manage DEMO LEDs", "<led_label> on|off|toggle\tChange LED state\n" "demo_led <led_label>\tGet LED state\n" "demo_led show\t Show LED label" ); |
File code C cho command line tuân theo template trong uboot bằng cách khai báo macro U_BOOT_CMD
1 2 3 4 5 6 7 8 |
U_BOOT_CMD( demo_led, 4, 1, do_demo_led, "manage DEMO LEDs", "<led_label> on|off|toggle\tChange LED state\n" "demo_led <led_label>\tGet LED state\n" "demo_led show\t Show LED label" ); |
Với
- Tham số đầu tiên “demo_led” là tên của command line mình muốn thêm vào
- Tham số thứ hai “4” là số argument thêm vào cho command line
- Tham số thứ ba “1” là repeat (với 1 thì người dùng sau khi gõ câu lệnh chỉ cần nhấn Enter 1 lần nữa là sẽ thực hiện command line mà không cần gõ lại)
- Tham số thứ tư “do_demo_led” là con trỏ hàm trỏ vào hàm thực thi của command line đó
- Cuối cùng là các chuỗi kí tự gợi ý cho command line
Hàm “do_demo_led” 4 tham số truyền vào theo chuẩn của uboot. Trong đó argument truyền vào command line lưu trong mảng argv[]. Hàm này thực hiện phân tích argument truyền vào và thực hiện điều khiển led theo các yêu cầu của người dùng.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
int do_demo_led(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[]) { [...] /* Nếu chỉ gõ demo_led -> show ra các gợi ý sử dụng */ if (argc < 2) return CMD_RET_USAGE; /*lấy ra label của đèn led từ các argument truyền vào*/ led_label = argv[1]; /* nêú label này là "show" thì thực hiện */ if (strncmp(led_label, "show", 4) == 0) return show_label(); /* * Lấy ra các trạng thái mong muốn set cho led từ argument truyền vào: * on: bật led * off: tắt led * toggle: đảo trạng thái led */ cmd = argc > 2 ? get_demo_led_cmd(argv[2]) : LEDST_COUNT; /* check label của các led*/ ret = led_get_by_label(led_label, &dev); if (ret) { printf("LED '%s' not found (err=%d)\n", led_label, ret); return CMD_RET_FAILURE; } switch (cmd) { case LEDST_OFF: case LEDST_ON: case LEDST_TOGGLE: /* hàm gọi xuống driver điều khiển led */ ret = led_set_state(dev, cmd); break; case LEDST_COUNT: printf("LED '%s': ", led_label); ret = show_led_state(dev); break; [...] } |
Port file code cho cmd “demo_led.c” vào uboot bằng cách sửa file Kconfig và Makefile trong thư mục /u-boot/cmd:
Kconfig:
1 2 3 4 5 6 |
config CMD_DEMO_LED bool "demo_led" help Enable the 'demo_led' command which allows for control of LEDs. In this case, it is P1.14. |
Makefile:
1 |
obj-$(CONFIG_CMD_DEMO_LED) += demo_led.o |
Để file “demo_led.c” có thể chạy được trong uboot, ta cần phải thêm một số config vào configs/am335x_evm_defconfig để link source code vào soucre của uboot
1 2 3 4 5 6 7 |
CONFIG_DM=y CONFIG_DM_GPIO=y CONFIG_LED=y CONFIG_LED_GPIO=y CONFIG_DEMO_LED=y CONFIG_CMD_DEMO_LED=y |
Tạo thư mục chứa driver cho led trong thư mục /drivers của uboot: /u-boot/drivers/demo_led . Đầu tiên ta thêm vào Makefile trong thư mục /u-boot/drivers dòng sau:
1 |
obj-$(CONFIG_$(SPL_TPL_)DEMO_LED) += demo_led/ |
và chỉ định file source của driver trong /u-boot/drivers/Kconfig:
1 |
source "drivers/demo_led/Kconfig" |
Trong thư mục /u-boot/drivers/demo_led tạo Makefile và Kconfig để compile driver cho led
Makefile:
1 |
obj-$(CONFIG_$(SPL_)DEMO_LED) += demo_led.o |
Kconfig:
1 2 3 4 5 6 7 8 9 |
menu "LED DEMO Support" config DEMO_LED bool "Enable DEMO LED support" depends on DM && DM_GPIO help This is just a demo to show how to simply control a led in u-boot. endmenu |
Source file của driver “demo_led.c”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
#include <common.h> #include <dm.h> #include <errno.h> #include <led.h> #include <malloc.h> #include <asm-generic/gpio.h> #include <dm/lists.h> struct led_gpio_priv { struct gpio_desc gpio; }; static int gpio_led_set_state(struct udevice *dev, enum led_state_t state) { struct led_gpio_priv *priv = dev_get_priv(dev); int ret; if (!dm_gpio_is_valid(&priv->gpio)) return -EREMOTEIO; switch (state) { case LEDST_OFF: case LEDST_ON: break; case LEDST_TOGGLE: ret = dm_gpio_get_value(&priv->gpio); if (ret < 0) return ret; state = !ret; break; default: return -ENOSYS; } return dm_gpio_set_value(&priv->gpio, state); } static enum led_state_t gpio_led_get_state(struct udevice *dev) { struct led_gpio_priv *priv = dev_get_priv(dev); int ret; if (!dm_gpio_is_valid(&priv->gpio)) return -EREMOTEIO; ret = dm_gpio_get_value(&priv->gpio); if (ret < 0) return ret; return ret ? LEDST_ON : LEDST_OFF; } static int led_gpio_probe(struct udevice *dev) { struct led_uc_plat *uc_plat = dev_get_uclass_plat(dev); struct led_gpio_priv *priv = dev_get_priv(dev); int ret; /* Ignore the top-level LED node */ if (!uc_plat->label) return 0; ret = gpio_request_by_name(dev, "gpios", 0, &priv->gpio, GPIOD_IS_OUT); if (ret) return ret; return 0; } static int led_gpio_remove(struct udevice *dev) { /* * The GPIO driver may have already been removed. We will need to * address this more generally. */ #ifndef CONFIG_SANDBOX struct led_gpio_priv *priv = dev_get_priv(dev); if (dm_gpio_is_valid(&priv->gpio)) dm_gpio_free(dev, &priv->gpio); #endif return 0; } static int led_gpio_bind(struct udevice *parent) { struct udevice *dev; ofnode node; int ret; dev_for_each_subnode(node, parent) { struct led_uc_plat *uc_plat; const char *label; label = ofnode_read_string(node, "label"); if (!label) { debug("%s: node %s has no label\n", __func__, ofnode_get_name(node)); return -EINVAL; } ret = device_bind_driver_to_node(parent, "demo_led", ofnode_get_name(node), node, &dev); if (ret) return ret; uc_plat = dev_get_uclass_plat(dev); uc_plat->label = label; } return 0; } static const struct led_ops gpio_led_ops = { .set_state = gpio_led_set_state, .get_state = gpio_led_get_state, }; static const struct udevice_id led_gpio_ids[] = { { .compatible = "demo-uboot-leds" }, { } }; U_BOOT_DRIVER(demo_led) = { .name = "demo_led", .id = UCLASS_LED, .of_match = led_gpio_ids, .ops = &gpio_led_ops, .priv_auto = sizeof(struct led_gpio_priv), .bind = led_gpio_bind, .probe = led_gpio_probe, .remove = led_gpio_remove, }; |
Cuối cùng ta thực hiện thêm vào Device Tree node cho led mà ta muốn sử dụng (ở đây ta ví dụ led ở chân P1.14)
/u-boot/arch/arm/dts/am335x-boneblack.dts:
1 2 3 4 5 6 7 8 9 10 11 |
demo_leds { pinctrl-names = "default"; pinctrl-0 = <&demo_leds>; compatible = "demo-uboot-leds"; used_led@1 { label = "led-1_14"; gpios = <&gpio1 14 GPIO_ACTIVE_HIGH>; default-state = "off"; }; }; |
cấu hình pinmuxing cho chân pin P1.14 với chức năng GPIO
1 2 3 4 5 6 7 8 |
&am33xx_pinmux { demo_leds: demo_leds { pinctrl-single,pins = < 0x38 (PIN_OUTPUT_PULLDOWN | MUX_MODE7) /* gpmc_ad14.gpio1_14 */ >; }; }; |
Sau khi đã hoàn thành các bước trên, thực hiện build lại uboot và nạp vào board, ta thực hiện được các command line như đã thêm vào:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
=> demo_led show led:P1:14 => demo_led led:P1:14 LED 'led:P1:14': off => demo_led led:P1:14 on => demo_led led:P1:14 LED 'led:P1:14': on => demo_led led:P1:14 off => demo_led led:P1:14 LED 'led:P1:14': off => demo_led demo_led - manage DEMO LEDs Usage: demo_led <led_label> on|off|toggle Change LED state demo_led <led_label> Get LED state demo_led show Show LED label |