Sử dụng U-Boot trong Phát triển Hệ thống Nhúng

Tổng quan về U-Boot

Để khởi động một hệ điều hành Linux trên các hệ thống nhúng, cần có một chương trình bootloader. Bootloader là phần mềm đầu tiên chạy khi chip được cấp điện. Chức năng chính của nó là khởi tạo các thiết bị ngoại vi cơ bản như DDR RAM, sau đó sao chép nhân Linux từ bộ nhớ flash (NAND, NOR FLASH, SD, EMMC) vào DDR và cuối cùng là khởi động nhân Linux. Vai trò của bootloader tương tự như BIOS trên máy tính cá nhân trong việc khởi động Windows.

Có nhiều phiên bản U-Boot khác nhau:

  • U-Boot chính thức (Official U-Boot): Được cộng đồng U-Boot duy trì, cập nhật thường xuyên và hỗ trợ nhiều chip phổ biến.
  • U-Boot của nhà sản xuất bán dẫn (Semiconductor Vendor's U-Boot): Các nhà sản xuất chip (như STMicroelectronics, NXP) cung cấp phiên bản U-Boot tùy chỉnh, tối ưu hóa cho chip của họ và thường có sự hỗ trợ tốt hơn cho các tính năng độc quyền của chip.
  • U-Boot của nhà sản xuất bo mạch (Board Vendor's U-Boot): Dựa trên U-Boot của nhà sản xuất bán dẫn, các nhà sản xuất bo mạch tích hợp thêm hỗ trợ cho các thiết bị ngoại vi và cấu hình phần cứng cụ thể của bo mạch phát triển của họ.

Trong thực tế, người dùng thường sử dụng U-Boot do nhà sản xuất bán dẫn hoặc nhà sản xuất bo mạch cung cấp vì chúng được tối ưu hóa tốt hơn cho phần cứng cụ thể. Sử dụng U-Boot chính thức có thể yêu cầu nhiều công đoạn porting (chuyển đổi) hơn để hỗ trợ đầy đủ thiết bị ngoại vi trên bo mạch.

Biên dịch U-Boot

1. Chuẩn bị môi trường biên dịch

Trước tiên, cần cài đặt các thư viện cần thiết trên hệ thống Ubuntu của bạn:

sudo apt-get install libncurses5-dev bison flex

Giải nén mã nguồn U-Boot vào thư mục làm việc mong muốn của bạn. Giả sử tệp mã nguồn là u-boot-stm32mp-2020.01-v1.x.tar.bz2:

tar -vxf u-boot-stm32mp-2020.01-v1.x.tar.bz2

2. Cấu hình và biên dịch

Quá trình biên dịch U-Boot bao gồm các bước sau:

make distclean
make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- stm32mp157d_atk_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabihf- DEVICE_TREE=stm32mp157d-atk all
  • ARCH: Chỉ định kiến trúc nền tảng, ở đây là arm.
  • CROSS_COMPILE: Tiền tố của bộ biên dịch chéo (cross-compiler) được sử dụng, ví dụ: arm-none-linux-gnueabihf-.
  • DEVICE_TREE: Tệp Device Tree (cây thiết bị). U-Boot cũng sử dụng Device Tree để mô tả phần cứng, vì vậy cần chỉ định tệp Device Tree phù hợp với bo mạch của bạn, ví dụ: stm32mp157d-atk.

Để đơn giản hóa lệnh biên dịch, bạn có thể chỉnh sửa tệp Makefile trong thư mục gốc của U-Boot để gán giá trị mặc định cho ARCHCROSS_COMPILE. Tìm và sửa đổi các dòng tương ứng:

ARCH        = arm
CROSS_COMPILE   = arm-none-linux-gnueabihf-

Sau khi chỉnh sửa Makefile, các lệnh biên dịch sẽ ngắn gọn hơn:

make distclean
make stm32mp157d_atk_defconfig
make V=1 DEVICE_TREE=stm32mp157d-atk all

Lệnh make V=1 sẽ hiển thị chi tiết quá trình biên dịch. Để tăng tốc độ, bạn có thể sử dụng biên dịch đa luồng bằng tùy chọn -j, ví dụ sử dụng 8 luồng:

make V=1 DEVICE_TREE=stm32mp157d-atk all -j8

Sau khi biên dịch thành công, các tệp ảnh U-Boot quan trọng sẽ được tạo, đặc biệt là u-boot.bin (tệp nhị phân thực thi) và u-boot.stm32 (tệp nhị phân có thêm 256 byte thông tin tiêu đề). Tệp u-boot.stm32 thường được sử dụng để flash lên thiết bị.

Ghi U-Boot vào thiết bị

Để ghi U-Boot đã biên dịch vào bộ nhớ EMMC của bo mạch, bạn có thể sử dụng công cụ như STM32CubeProgrammer. Cần cập nhật tệp tf-a.tsv (nếu bạn đang sử dụng TF-A) để thêm chỉ thị ghi U-Boot. Thêm dòng sau vào cuối tệp:

# U-Boot
-       0x00020000      u-boot.stm32

Sao chép tệp u-boot.stm32 vừa biên dịch vào thư mục images mà bạn đã sử dụng cho các bước flash trước đó. Sau đó, sử dụng STM32CubeProgrammer để flash U-Boot qua kết nối USB OTG. Khi quá trình hoàn tất, thiết lập các công tắc trên bo mạch để khởi động từ EMMC.

Kết nối cổng USB\_TTL của bo mạch với máy tính qua cáp USB Type-C và mở một chương trình terminal (ví dụ MobaXterm hoặc PuTTY) để theo dõi và tương tác với U-Boot. Khi bạn thấy thông báo "Hit any key to stop autoboot: " với bộ đếm ngược, nhấn phím Enter để vào chế độ dòng lệnh của U-Boot. Nếu không nhấn, U-Boot sẽ tự động cố gắng khởi động nhân Linux (nếu có).

Phân tích thông báo khởi động U-Boot

Khi U-Boot khởi động, nó sẽ hiển thị một loạt thông tin:

U-Boot 2020.01-stm32mp-r1 (Jul 30 2023 - 12:37:27 +0800) // Phiên bản U-Boot và thời gian biên dịch.
CPU: STM32MP157DAA Rev.Z                                 // Thông tin về CPU.
Model: STMicroelectronics STM32MP157D eval daughter      // Mô tả bo mạch, có thể là từ thiết kế tham chiếu.
Board: stm32mp1 in trusted mode (st,stm32mp157d-atk)    // Chế độ hoạt động của bo mạch.
DRAM:  1 GiB                                             // Dung lượng bộ nhớ DDR.
Clocks:                                                 // Tần số các khối chức năng.
- MPU : 800 MHz
- MCU : 208.878 MHz
- AXI : 266.500 MHz
- PER : 24 MHz
- DDR : 533 MHz
WDT:   Started with servicing (32s timeout)              // Thông tin về Watchdog Timer.
NAND:  0 MiB                                             // Dung lượng NAND (0 nếu không có).
MMC:   STM32 SD/MMC: 0, STM32 SD/MMC: 1                  // Các thiết bị MMC/SD có sẵn (SD/MMC0 thường là SD card, SD/MMC1 thường là EMMC).
Loading Environment from MMC... OK                      // Tải biến môi trường từ MMC.
In:    serial                                            // Kênh đầu vào chuẩn là serial.
Out:   serial                                            // Kênh đầu ra chuẩn là serial.
Err:   serial                                            // Kênh lỗi chuẩn là serial.
invalid MAC address in OTP 00:00:00:00:00:00            // Lỗi địa chỉ MAC không hợp lệ (cần thiết lập).
Net:
Error: ethernet@5800a000 address not set.
No ethernet found.                                      // Thông tin mạng ban đầu, có thể thiếu MAC address.

lcd_id  = 02                                            // Một biến môi trường nào đó.
Hit any key to stop autoboot:  0                       // Bộ đếm ngược tự động khởi động.
Boot over mmc1!
switch to partitions #0, OK
mmc1(part 0) is current device
** Unrecognized filesystem type **
STM32MP>                                                // Dấu nhắc lệnh U-Boot.

Sử dụng các lệnh U-Boot

Trong chế độ dòng lệnh U-Boot, bạn có thể nhập help hoặc ? theo sau là tên lệnh để xem chi tiết cách sử dụng. Ví dụ: ? bootz.

1. Lệnh truy vấn thông tin

  • bdinfo: Hiển thị thông tin tổng quan về bo mạch, bao gồm địa chỉ và kích thước DRAM, địa chỉ tham số khởi động, tốc độ baud, địa chỉ con trỏ ngăn xếp (SP), v.v.
  • printenv: Hiển thị tất cả các biến môi trường hiện tại. U-Boot hỗ trợ tự động hoàn thành bằng phím TAB. Các biến môi trường là các chuỗi giá trị có thể cấu hình, ví dụ bootdelay=1 (thời gian chờ khởi động mặc định 1 giây).
  • version: Hiển thị phiên bản U-Boot.

2. Lệnh thao tác biến môi trường

Các lệnh chính để quản lý biến môi trường là setenvsaveenv. Các biến môi trường được lưu trữ trong flash và được đọc vào DRAM khi U-Boot khởi động. Mọi thay đổi với setenv chỉ có hiệu lực trong DRAM cho đến khi bạn sử dụng saveenv để lưu chúng vào flash.

  • Thay đổi biến môi trường: Để thay đổi giá trị của một biến, ví dụ đặt bootdelay thành 5 giây:
    setenv bootdelay 5
    saveenv
            

    Sau khi lưu và khởi động lại, U-Boot sẽ chờ 5 giây trước khi tự động khởi động.

  • Tạo biến môi trường mới: Sử dụng setenv tương tự để tạo biến mới. Ví dụ, tạo biến kernel_params:
    setenv kernel_params 'console=ttySTM0,115200 root=/dev/mmcblk2p2 rootwait rw'
    saveenv
            

    Lưu ý sử dụng dấu nháy đơn nếu giá trị chứa khoảng trắng.

  • Xóa biến môi trường: Để xóa một biến, gán cho nó một giá trị rỗng:
    setenv kernel_params
    saveenv
            

    Sau khi lưu và khởi động lại, biến kernel_params sẽ không còn.

3. Lệnh thao tác bộ nhớ (DRAM)

Các lệnh này dùng để đọc và ghi trực tiếp vào bộ nhớ DRAM. Các số trong lệnh U-Boot luôn được hiểu là hệ thập lục phân (hexadecimal).

  • md[.b, .w, .l] address [# of objects]: Hiển thị nội dung bộ nhớ.
    • .b, .w, .l: Chỉ định hiển thị theo byte (1 byte), word (2 byte) hoặc long (4 byte).
    • address: Địa chỉ bắt đầu.
    • # of objects: Số lượng đối tượng để hiển thị (không phải số byte). Ví dụ, 0x10 đối tượng hiển thị theo .b sẽ là 16 byte, theo .w sẽ là 32 byte, theo .l sẽ là 64 byte.
    md.b C0100000 10   // Hiển thị 0x10 (16) byte từ địa chỉ 0xC0100000
    md.w C0100000 10   // Hiển thị 0x10 (16) word (32 byte) từ địa chỉ 0xC0100000
    md.l C0100000 10   // Hiển thị 0x10 (16) long (64 byte) từ địa chỉ 0xC0100000
            
  • nm[.b, .w, .l] address: Sửa đổi giá trị bộ nhớ tại địa chỉ chỉ định. Nhập giá trị mới sau dấu nhắc ?, sau đó nhập q để thoát. Địa chỉ sẽ không tự động tăng.
    nm.l C0100000    // Sửa 4 byte tại 0xC0100000
    // Nhập giá trị mới, ví dụ: 12345678, sau đó nhấn Enter, rồi nhấn q
            
  • mm[.b, .w, .l] address: Sửa đổi giá trị bộ nhớ, địa chỉ sẽ tự động tăng. Tương tự nm nhưng tiện lợi hơn cho việc sửa nhiều vùng nhớ liên tiếp.
    mm.l C0100000    // Sửa 4 byte tại 0xC0100000, sau đó địa chỉ sẽ tăng lên C0100004
    // Nhập giá trị mới, ví dụ: 05050505, sau đó nhấn Enter, rồi nhấn q
            
  • mw[.b, .w, .l] address value [count]: Điền một giá trị cụ thể vào một vùng bộ nhớ.
    • address: Địa chỉ bắt đầu.
    • value: Giá trị cần điền.
    • count: Số lượng đối tượng cần điền.
    mw.l C0100000 0A0A0A0A 10 // Điền giá trị 0x0A0A0A0A vào 0x10 (16) long (64 byte) từ 0xC0100000
            
  • cp[.b, .w, .l] source target count: Sao chép dữ liệu từ một vùng bộ nhớ này sang vùng bộ nhớ khác.
    • source: Địa chỉ nguồn.
    • target: Địa chỉ đích.
    • count: Số lượng đối tượng cần sao chép.
    cp.l c0100000 c0100100 10 // Sao chép 0x10 long (64 byte) từ 0xC0100000 đến 0xC0100100
            
  • cmp[.b, .w, .l] addr1 addr2 count: So sánh dữ liệu giữa hai vùng bộ nhớ.
    • addr1: Địa chỉ bắt đầu vùng nhớ thứ nhất.
    • addr2: Địa chỉ bắt đầu vùng nhớ thứ hai.
    • count: Số lượng đối tượng cần so sánh.
    cmp.l c0100000 c0100100 10 // So sánh 0x10 long (64 byte) từ 0xC0100000 và 0xC0100100
            

    U-Boot sẽ báo cáo nếu hai vùng giống nhau hoặc khác nhau.

4. Lệnh thao tác mạng

U-Boot hỗ trợ nhiều chức năng mạng, rất hữu ích cho việc gỡ lỗi nhân Linux qua mạng. Trước khi sử dụng, cần cấu hình các biến môi trường mạng:

setenv ipaddr 192.168.1.106         // Địa chỉ IP của bo mạch
setenv ethaddr b8:ae:1d:01:01:00   // Địa chỉ MAC của bo mạch (phải là duy nhất trong mạng)
setenv gatewayip 192.168.1.1       // Địa chỉ gateway
setenv netmask 255.255.255.0       // Subnet mask
setenv serverip 192.168.1.105      // Địa chỉ IP của máy chủ (ví dụ: máy tính Ubuntu)
saveenv

Đảm bảo rằng bo mạch và máy chủ Ubuntu của bạn ở cùng một dải mạng.

  • ping [IP]: Kiểm tra kết nối mạng với một máy chủ khác.
    ping 192.168.1.105
            

    Lưu ý: U-Boot chỉ có thể ping các thiết bị khác, các thiết bị khác không thể ping U-Boot vì U-Boot không xử lý các yêu cầu ping đến.

  • dhcp: Lấy địa chỉ IP tự động từ router DHCP. Chỉ hoạt động khi bo mạch được kết nối với router.
    dhcp
            

    Lệnh này cũng có thể được cấu hình để khởi động nhân Linux qua TFTP.

  • nfs [loadAddress] [[hostIPaddr:]bootfilename]: Tải tệp qua NFS (Network File System) vào DRAM. Hữu ích cho việc gỡ lỗi nhân và device tree mà không cần flash.

    Để sử dụng NFS, bạn cần thiết lập máy chủ NFS trên Ubuntu. Ví dụ, tạo thư mục /home/nguoidung/linux/nfs và export nó.

    nfs C2000000 192.168.1.105:/home/nguoidung/linux/nfs/uImage
            

    C2000000 là địa chỉ DRAM để lưu tệp, 192.168.1.105 là IP máy chủ, và phần còn lại là đường dẫn tệp trên máy chủ NFS.

  • tftpboot [loadAddress] [[hostIPaddr:]bootfilename]: Tải tệp qua TFTP (Trivial File Transfer Protocol) vào DRAM.

    Cần cài đặt và cấu hình máy chủ TFTP trên Ubuntu:

    sudo apt-get install tftp-hpa tftpd-hpa xinetd
    mkdir /home/nguoidung/linux/tftpboot
    chmod 777 /home/nguoidung/linux/tftpboot
            

    Tạo tệp cấu hình /etc/xinetd.d/tftp với nội dung:

    server tftp
    {
        socket_type = dgram
        protocol = udp
        wait = yes
        user = root
        server = /usr/sbin/in.tftpd
        server_args = -s /home/nguoidung/linux/tftpboot/
        disable = no
        per_source = 11
        cps = 100 2
        flags = IPv4
    }
            

    Cấu hình tệp /etc/default/tftpd-hpa:

    # /etc/default/tftpd-hpa
    
    TFTP_USERNAME="tftp"
    TFTP_DIRECTORY="/home/nguoidung/linux/tftpboot"
    TFTP_ADDRESS=":69"
    TFTP_OPTIONS="-l -c -s"
            

    Khởi động lại dịch vụ TFTP:

    sudo service tftpd-hpa restart
            

    Sao chép tệp cần thiết vào thư mục TFTP và cấp quyền:

    cp uImage /home/nguoidung/linux/tftpboot/
    chmod 777 /home/nguoidung/linux/tftpboot/uImage
            

    Sử dụng lệnh tftpboot trong U-Boot (chỉ cần tên tệp, không cần đường dẫn đầy đủ trên máy chủ):

    tftp C2000000 uImage
            

    Các lỗi "Permission denied" thường do thiếu quyền truy cập vào thư mục TFTP hoặc tệp.

5. Lệnh thao tác với EMMC và SD Card

U-Boot cung cấp nhiều lệnh để tương tác với các thiết bị MMC/SD.

  • mmc info: Hiển thị thông tin về thiết bị MMC/SD hiện tại.
  • mmc rescan: Quét lại các thiết bị MMC/SD để nhận diện các thiết bị mới hoặc thay đổi.
  • mmc list: Liệt kê tất cả các thiết bị MMC/SD được U-Boot phát hiện.
  • mmc dev [devnum] [partnum]: Chọn thiết bị MMC/SD và phân vùng làm việc. devnum là chỉ số thiết bị (ví dụ 0 cho SD card, 1 cho EMMC), partnum là chỉ số phân vùng.
  • mmc read addr blk# cnt: Đọc cnt khối (block) từ vị trí blk# của thiết bị MMC/SD đã chọn vào địa chỉ addr trong DRAM.
  • mmc write addr blk# cnt: Ghi cnt khối từ địa chỉ addr trong DRAM vào vị trí blk# của thiết bị MMC/SD đã chọn.
  • fatls mmc <dev>:<part> [<directory>]: Liệt kê các tệp trên phân vùng FAT của thiết bị MMC/SD.
  • ext4ls mmc <dev>:<part> [<directory>]: Liệt kê các tệp trên phân vùng EXT4 của thiết bị MMC/SD.
  • fatload mmc <dev>:<part> <addr> <filename>: Tải tệp filename từ phân vùng FAT vào địa chỉ addr trong DRAM.
  • ext4load mmc <dev>:<part> <addr> <filename>: Tải tệp filename từ phân vùng EXT4 vào địa chỉ addr trong DRAM.
    ext4load mmc 1:2 C2000000 uImage  // Tải uImage từ phân vùng 2 của EMMC (mmc 1) vào C2000000
            

    Đây là một lệnh quan trọng để tải nhân Linux hoặc Device Tree từ hệ thống tệp EXT4 trên thẻ nhớ/EMMC.

6. Lệnh khởi động (Boot)

Các lệnh này được dùng để khởi động nhân Linux sau khi đã tải nó vào DRAM.

  • bootm [addr [arg ...]]: Khởi động tệp ảnh nhân Linux định dạng uImage.
    • addr: Địa chỉ nhân Linux trong DRAM.
    • arg...: Các tham số tùy chọn. Nếu có initrd, tham số thứ hai là địa chỉ initrd. Nếu sử dụng Device Tree, tham số thứ ba là địa chỉ Device Tree trong DRAM. Dùng - nếu không có initrd.

    Ví dụ, tải nhân và device tree qua TFTP, sau đó khởi động:

    tftp C2000000 uImage
    tftp C4000000 stm32mp157d-atk.dtb
    bootm C2000000 - C4000000
            
  • bootz [addr [initrd[:size]] [fdt]]: Tương tự như bootm nhưng dùng cho tệp ảnh nhân Linux định dạng zImage. Các tham số tương tự bootm.
  • bootbootd: Hai lệnh này thực chất cùng thực hiện một hàm, thường được sử dụng để khởi động hệ thống theo biến môi trường bootcmd.

    bootcmd là một biến môi trường lưu trữ chuỗi các lệnh khởi động. Bạn có thể định cấu hình bootcmd để tự động khởi động Linux khi bộ đếm ngược kết thúc. Ví dụ:

    setenv bootcmd 'tftp C2000000 uImage;tftp C4000000 stm32mp157d-atk.dtb;bootm C2000000 - C4000000'
    saveenv
    boot
            

    Lệnh boot sẽ thực thi chuỗi lệnh được lưu trong bootcmd.

7. Lệnh ums (USB Mass Storage)

Lệnh ums cho phép bo mạch hoạt động như một thiết bị lưu trữ USB (USB Mass Storage), cho phép bạn truy cập bộ nhớ flash của bo mạch (như EMMC hoặc SD card) từ máy tính của mình như một USB drive thông thường.

ums <USB_controller> [<devtype>] <dev[:part]>
  • USB_controller: Chỉ số của bộ điều khiển USB (thường là 0 cho cổng USB_OTG).
  • devtype: Loại thiết bị lưu trữ (mặc định là mmc).
  • dev[:part]: Thiết bị flash cần gắn kết, cùng với phân vùng tùy chọn. Ví dụ 1 cho EMMC, hoặc 1:2 cho phân vùng 2 của EMMC.

Kết nối cổng USB_OTG của bo mạch với máy tính qua cáp Type-C, sau đó thực thi:

ums 0 mmc 1

Lệnh này sẽ biến EMMC của bo mạch thành một thiết bị lưu trữ USB mà máy tính có thể nhận diện.

8. Các lệnh thông dụng khác

  • reset: Khởi động lại bo mạch.
  • go addr [arg ...]: Nhảy đến địa chỉ addr trong DRAM và thực thi mã tại đó.
  • run <env_var>: Thực thi chuỗi lệnh được lưu trữ trong biến môi trường <env_var>. Điều này rất hữu ích để tạo các kịch bản khởi động tùy chỉnh.

    Ví dụ, tạo các biến môi trường để khởi động từ EMMC hoặc qua mạng:

    setenv mybootemmc 'ext4load mmc 1:2 C2000000 uImage;ext4load mmc 1:2 C4000000 stm32mp157d-atk.dtb;bootm C2000000 - C4000000'
    setenv mybootnet 'tftp C2000000 uImage;tftp C4000000 stm32mp157d-atk.dtb;bootm C2000000 - C4000000'
    saveenv
            

    Sau đó, bạn có thể chạy:

    run mybootemmc
            

    hoặc

    run mybootnet
            

    để chuyển đổi giữa các phương pháp khởi động dễ dàng.

  • mtest [start [end [pattern [iterations]]]]: Một lệnh đơn giản để kiểm tra đọc/ghi bộ nhớ DDR.
    • start: Địa chỉ bắt đầu kiểm tra.
    • end: Địa chỉ kết thúc kiểm tra.
    mtest C0000000 C0001000 // Kiểm tra vùng nhớ từ 0xC0000000 đến 0xC0001000
            

    Nhấn Ctrl+C để dừng kiểm tra.

9. Lệnh MII

Các lệnh MII (Media Independent Interface) được sử dụng để đọc và ghi vào các thanh ghi của chip PHY mạng. Điều này rất hữu ích khi gỡ lỗi chức năng Ethernet.

Trên một số nền tảng (như STM32MP157), đồng hồ ETHMAC có thể bị tắt sau mỗi lần giao tiếp mạng, khiến các lệnh MII không hoạt động. Để khắc phục, bạn cần kích hoạt lại đồng hồ ETHMAC trước khi sử dụng lệnh MII bằng cách đặt các bit 8-10 của thanh ghi 0x50000218 thành 1. Các bước thực hiện:

  1. Ping một thiết bị mạng khác để xác nhận kết nối mạng cơ bản.
  2. Kích hoạt đồng hồ ETHMAC (sử dụng lệnh mw hoặc mm để thiết lập các bit thanh ghi).
  3. Sử dụng các lệnh MII để truy vấn thanh ghi PHY.

Ví dụ về lệnh MII:

  • mii info <addr>: Hiển thị thông tin về chip PHY tại địa chỉ <addr>. Địa chỉ PHY thường là 0x00 hoặc 0x01 tùy thuộc vào chip cụ thể (ví dụ, YT8511 thường là 0x00, RTL8211 thường là 0x01).
mii info 0

Thẻ: U-Boot Embedded Linux Bootloader stm32mp1 Cross-Compilation

Đăng vào ngày 5 tháng 6 lúc 16:46