Bài 2: U-Boot là gì trong embedded Linux và vì sao nó quyết định boot chain

U-Boot là gì trong embedded Linux? Khi một board đã lên nguồn, DRAM đã được khởi tạo, kernel image vẫn còn nguyên nhưng hệ thống vẫn dừng ở Starting kernel..., boot nhầm root filesystem hoặc tự chuyển sang nhánh recovery, nguyên nhân nhiều khi lại không nằm ở kernel. Trong khá nhiều trường hợp, vấn đề đã xuất hiện từ U-Boot trước đó.

U-Boot thường được gọi là bootloader. Cách gọi này đúng, nhưng vẫn chưa nói hết vai trò của nó trong một hệ Linux nhúng. Ở đây không chỉ có chuyện kernel được nạp rồi quyền điều khiển được chuyển đi. Device tree sẽ được chọn, bootargs sẽ được dựng, nguồn boot sẽ được xác định, phân vùng active có thể được quyết định, còn logic recovery hoặc quy trình cập nhật cũng thường được móc vào ngay tại giai đoạn này.

Bởi vậy nếu boot chain được xem như một chuỗi bàn giao quyền điều khiển, thì U-Boot chính là nơi một phần rất lớn điều kiện bàn giao được thiết lập.

U-Boot là gì trong embedded Linux?

U-Boot là một bootloader rất phổ biến trong embedded Linux. Nó được chạy sau các bước khởi động sớm của SoC và trước khi quyền điều khiển được chuyển sang kernel Linux. Trong giai đoạn này, môi trường cần thiết để kernel có thể boot sẽ được chuẩn bị trước: image sẽ được chọn, DTB phù hợp sẽ được nạp, chuỗi tham số boot sẽ được tạo ra và nguồn boot cũng sẽ được xác định.

Nếu cần mô tả ngắn gọn, U-Boot có thể được hiểu là phần đứng giữa phần cứng vừa được đánh thức và kernel sắp được gọi lên.

Kernel chỉ boot đúng khi image đúng, DTB đúng, bootargs đúng và trạng thái phần cứng tối thiểu đã được chuẩn bị đúng từ trước.

Vị trí của U-Boot trong boot chain

Một boot chain điển hình trong embedded Linux thường được tổ chức như sau:

Boot ROM -> SPL / first-stage loader -> U-Boot -> Linux kernel -> init -> userspace

Trong chuỗi này có thể hình dung như sau:

  • Boot ROM được chạy đầu tiên từ trong chip.
  • SPL hoặc first-stage loader sẽ xử lý phần việc tối thiểu như khởi tạo DRAM hoặc chuẩn bị điều kiện để giai đoạn tiếp theo có thể chạy.
  • U-Boot sẽ tiếp quản phần logic boot có cấu hình.
  • Kernel Linux sẽ được nạp và bắt đầu boot lên userspace.

Như vậy, U-Boot không phải điểm kết thúc của quá trình khởi động. Tuy nhiên đây lại là giai đoạn mà rất nhiều quyết định quan trọng được chốt ngay trước khi kernel chạy. Vì thế khi lỗi boot xuất hiện, U-Boot thường phải được kiểm tra từ khá sớm, chứ không nên bị xem như một bước trung gian quá đơn giản.

Nếu cần nhìn lại bức tranh tổng thể, bài boot chain trong embedded Linux từ Boot ROM tới userspace nên được đọc trước. Khi đó từng stage sẽ được tách ra rõ hơn và việc khoanh vùng lỗi cũng dễ hơn nhiều.

Vì sao U-Boot không chỉ là nơi load kernel?

Nếu U-Boot chỉ làm mỗi việc nạp kernel thì việc debug boot đã đỡ mệt hơn khá nhiều. Thực tế lại khác. Tại U-Boot, hàng loạt quyết định quan trọng vẫn còn được giữ lại như:

  • kernel image nào sẽ được chọn
  • DTB nào sẽ được đi kèm theo board hoặc revision phần cứng
  • bootargs nào sẽ được truyền xuống kernel
  • root filesystem nào sẽ được mount
  • nhánh normal, rescue hay recovery sẽ được đi vào
  • slot A/B nào đang được đánh dấu là active

Chỉ cần một mắt xích trong số này bị lệch là toàn bộ quá trình khởi động có thể đổ vỡ. Vì thế nếu U-Boot chỉ được nhìn như một loader đơn thuần, vai trò thực sự của nó trong boot chain sẽ bị đánh giá thấp đi rất nhiều.

Những nhiệm vụ chính thường được giao cho U-Boot

  • Kernel image sẽ được nạp vào đúng địa chỉ bộ nhớ.
  • DTB phù hợp với phần cứng sẽ được chọn và nạp.
  • bootargs sẽ được tạo ra để truyền xuống kernel.
  • Nguồn boot như eMMC, SD card, NAND, NOR, USB hoặc network sẽ được lựa chọn.
  • Phân vùng hoặc slot boot sẽ được quyết định.
  • Nhánh recovery, rescue hoặc cập nhật sẽ được điều phối nếu cần.
  • Giao diện lệnh phục vụ debug boot cũng thường được mở ra tại đây.

Chính vì một shell debug thường có mặt ở U-Boot nên phần này rất hay bị đụng đến trong quá trình phát triển sản phẩm. Nhiều lỗi boot chỉ thực sự lộ mặt sau khi printenv được chạy, thiết bị lưu trữ đích được kiểm tra lại hoặc chuỗi lệnh boot thực tế được đối chiếu với những gì đang được ghi trong tài liệu.

Kiến trúc nhìn theo đường đi của dữ liệu boot

[Boot source]
   |- eMMC / SD / NAND / NOR / Network
   v
[Storage access]
   |- partition / filesystem / raw offset
   v
[Boot selection]
   |- kernel image
   |- DTB
   |- initramfs (nếu có)
   |- active slot / fallback path
   v
[Environment]
   |- bootcmd
   |- bootargs
   |- boot_targets
   |- bootdelay / rescue flag
   v
[Kernel handoff]
   |- memory address
   |- command line
   |- device tree
   v
[Linux kernel boot]

Khi boot flow được nhìn theo sơ đồ này, một điều sẽ hiện ra khá rõ: lỗi boot không nhất thiết phải nằm ở kernel. Lỗi có thể đã xuất hiện từ trước đó, khi source boot bị chọn sai, partition bị trỏ nhầm, DTB không khớp hoặc bootargs đang chỉ đến một root filesystem không tồn tại.

Environment, bootcmd và bootargs: nơi lỗi hay bắt đầu lộ ra

Một đặc trưng lớn của U-Boot nằm ở cơ chế environment variables. Tập biến này được dùng để điều khiển boot flow, nhờ đó cách boot có thể được thay đổi mà chưa cần sửa mã nguồn C ngay lập tức.

  • bootcmd là chuỗi lệnh sẽ được U-Boot thực thi để khởi động hệ thống.
  • bootargs là chuỗi tham số sẽ được truyền xuống kernel Linux.
  • environment là nơi các biến cấu hình boot được lưu trữ.

Ưu điểm của mô hình này là tính linh hoạt rất cao. Một nguồn boot khác có thể được thử, một rootfs khác có thể được chọn hoặc một tham số debug có thể được thêm vào chỉ bằng vài thay đổi ở environment.

Nhưng đổi lại, đây cũng là nơi trạng thái cũ bị tích tụ rất dễ. Một biến còn sót từ lần test trước, một giá trị bị lưu nhầm hoặc một nhánh bootcmd không còn khớp với tài liệu cũng đủ để việc phân tích lỗi đi chệch hướng. Trong nhiều ca debug, lỗi “kernel không boot” thực ra chỉ là hậu quả của một điều kiện boot sai đã được truyền xuống từ U-Boot.

SPL là gì và vì sao U-Boot thường được chia thành nhiều giai đoạn?

Trên nhiều SoC, U-Boot đầy đủ không thể được chạy ngay từ đầu. Lúc này DRAM có thể chưa được khởi tạo, SRAM vẫn còn rất ít và nhiều khối phần cứng cần thiết cũng chưa sẵn sàng. Vì thế một stage nhỏ hơn thường sẽ được đưa vào trước để mở đường.

Phần này thường được gọi là SPL. Các nhiệm vụ tối thiểu như init DRAM, chuẩn bị truy cập storage hoặc kéo giai đoạn tiếp theo vào RAM sẽ được xử lý tại đây. Sau đó U-Boot đầy đủ mới được chạy để tiếp quản phần logic boot phức tạp hơn.

Chi tiết này rất quan trọng khi debug. Nếu U-Boot prompt còn chưa xuất hiện thì lỗi chưa chắc đã nằm trong U-Boot đầy đủ. Hoàn toàn có thể board đã dừng lại ở SPL hoặc ở một giai đoạn còn sớm hơn nữa.

U-Boot khác bootloader MCU ở điểm nào?

Khác biệt lớn nhất nằm ở phạm vi công việc.

  • Trong hệ MCU, bootloader thường tập trung vào validate image, copy image hoặc nhảy sang application.
  • Trong embedded Linux, U-Boot còn phải quan tâm đến kernel, DTB, root filesystem, sơ đồ storage, đường đi cập nhật, nhánh recovery và các nhánh boot có điều kiện.

Vì thế cách làm việc với U-Boot thường gần với một môi trường boot có thể cấu hình được hơn là một đoạn mã chỉ có nhiệm vụ chuyển quyền sang application. Độ linh hoạt được tăng lên, nhưng độ phức tạp khi vận hành cũng tăng theo.

Những lỗi hay gặp khi làm việc với U-Boot

  • Kernel image đúng file nhưng lại được nạp sai địa chỉ bộ nhớ.
  • DTB bị chọn sai nên kernel lên được một đoạn rồi treo hoặc thiết bị ngoại vi không hoạt động.
  • bootargs bị sai nên root filesystem không thể được mount.
  • Environment bị hỏng hoặc còn sót trạng thái từ lần test trước.
  • Partition, slot A/B hoặc thiết bị lưu trữ đích bị trỏ nhầm.
  • bootcmd thực tế đang đi theo một nhánh khác với thứ vẫn được mô tả trong tài liệu.
  • Logic fallback sau cập nhật chưa được xử lý chặt nên thiết bị cứ quay về image cũ sau khi reboot.

Điều khó chịu là các triệu chứng do những lỗi này tạo ra thường không nói thẳng nguyên nhân gốc. Một kernel panic có thể khiến sự chú ý bị dồn hết vào kernel, trong khi vấn đề gốc lại nằm ở chuỗi root=. Tương tự, nếu hệ thống treo sau khi kernel đã được nạp, DTB hoặc memory handoff cũng cần được nghi ngờ trước.

Nên nhìn U-Boot theo thứ tự nào khi debug boot lỗi?

  • Trước hết cần xác định board đang dừng ở Boot ROM, SPL, U-Boot hay kernel.
  • Nếu đã vào được U-Boot, environment nên được kiểm tra bằng printenv trước.
  • Kernel, DTB và initramfs nào đang được nạp cần được xác nhận lại.
  • Storage source, partition và slot boot thực tế cần được đối chiếu.
  • bootargs cần được so với root filesystem mong muốn.
  • Nếu có A/B update hoặc recovery, trạng thái fallback cũng phải được kiểm tra.

Thứ tự này thường giúp việc khoanh vùng nhanh hơn so với cách nhảy thẳng vào kernel config. Trong boot chain, stage nào chạy trước thì stage đó cũng nên được nghi ngờ trước nếu dữ liệu bàn giao sang stage sau chưa đúng.

Hướng thiết kế nào thường tối ưu hơn cho sản phẩm thực tế?

Nếu nhìn từ góc độ production, một hướng thiết kế thường an toàn hơn là: U-Boot nên đủ thông minh để việc boot được giữ ổn định, recovery vẫn có thể thực hiện và quy trình cập nhật vẫn phối hợp được; nhưng độ phức tạp không nên bị dồn vào đây quá nhiều.

  • Boot flow nên được giữ rõ ràng và ít nhánh ẩn.
  • Tên biến environment nên được thống nhất.
  • Trạng thái tạm dùng để test nên được hạn chế tối đa.
  • Logic A/B và fallback nên được mô tả minh bạch.
  • U-Boot nên được xem là một phần của kiến trúc độ tin cậy, không chỉ là bước khởi động.

Đây thường là điểm cân bằng tốt giữa tính linh hoạt và chi phí vận hành. Nếu U-Boot bị làm quá đơn giản, khả năng cứu hệ thống khi có sự cố sẽ giảm. Nhưng nếu quá nhiều chính sách và nhánh rẽ bị nhét vào đây, trạng thái ẩn, bug khó tái hiện và chi phí debug sẽ tăng lên rất nhanh.

Kết luận

U-Boot là một thành phần trung tâm trong boot chain của embedded Linux. Tại đây, kernel image, DTB, bootargs, nguồn boot, phân vùng boot, nhánh recovery và nhiều quyết định vận hành khác sẽ được xác định trước khi kernel thực sự chạy.

Vì thế nếu muốn hiểu vì sao một board Linux nhúng boot được hoặc không boot được, U-Boot gần như luôn phải được phân tích kỹ. Trong rất nhiều trường hợp, nguyên nhân gốc không nằm ở nơi hệ thống dừng lại, mà nằm ở nơi điều kiện boot đã được thiết lập từ trước.

Bài tiếp theo nếu đi tiếp theo mạch này nên được dành riêng cho environment, bootcmdbootargs, bởi đây là nơi rất nhiều lỗi boot thực tế bắt đầu lộ ra.

Phản hồi về bài viết

Cùng thảo luận chút nhỉ!

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.