Tài Liệu Kỹ Thuật Độc Quyền Cho Cộng Đồng Việt Nam

Lập Trình Native Bằng Rust Trên Stock OS Của Máy TrimUI

Hướng dẫn chi tiết từ A-Z cách xây dựng ứng dụng Native siêu nhẹ, đạt chuẩn 60 FPS mượt mà, vẽ trực tiếp vào /dev/fb0 và bắt nút bấm qua Linux evdev trên chip Allwinner A133 (ARM64) mà không cần môi trường đồ họa X11/Wayland cồng kềnh.

🎯 Chip: Allwinner A133plus (ARM64)📺 Màn hình: 1280 × 720 (32bpp)🦀 Target: aarch64-unknown-linux-musl🎮 Input: /dev/input/event* (evdev)
1

Kiến Trúc Phần Cứng & Môi Trường Stock OS

TrimUI Smart Pro là một cỗ máy chơi game cầm tay mạnh mẽ trong phân khúc giá rẻ, nhưng môi trường hệ điều hành gốc (Stock OS) có những đặc thù mà lập trình viên cần hiểu rõ để ứng dụng chạy mượt mà:

Phần Cứng (Hardware Specs)
  • CPU: Allwinner A133plus, 4 nhân ARM Cortex-A53 @ 1.8GHz (kiến trúc 64-bit ARMv8-A / aarch64).
  • RAM: 1GB LPDDR4 — Rất dư dả cho ứng dụng Rust native (chỉ tốn từ 5MB - 15MB RAM).
  • Màn hình: 4.96 inch IPS, 1280 × 720 pixel (tỉ lệ 16:9), tần số quét 60Hz.
  • Thẻ nhớ: Thẻ MicroSD được mount tại đường dẫn tuyệt đối /mnt/SDCARD/.
Đặc Thù Stock OS (Linux Kernel 4.9)
  • Không có X11 hay Wayland: Không hề có hệ thống cửa sổ đồ họa hay desktop manager thông thường.
  • Truy cập Framebuffer trực tiếp: Toàn bộ hình ảnh hiển thị bắt buộc phải ghi trực tiếp vào thiết bị /dev/fb0.
  • Giao diện gốc (MainUI): Tiến trình /usr/trimui/bin/MainUI chạy liên tục để hiển thị menu máy và đọc tay cầm.
  • Hạn chế GLIBC: Stock OS dùng phiên bản GLIBC rất cũ (~2.28-2.31), gây lỗi nếu biên dịch GNU thông thường.

Cạm bẫy lớn nhất: Lỗi GLIBC Version Not Found

Nếu bạn biên dịch bằng target tiêu chuẩn aarch64-unknown-linux-gnu, máy TrimUI sẽ báo lỗi ngay khi khởi chạy:

/lib/libc.so.6: version 'GLIBC_2.34' not found (required by ./my-app)

Giải pháp vàng của Rust: Sử dụng target aarch64-unknown-linux-musl. Thư viện musl libc liên kết tĩnh (static linking) 100% mã máy vào một file nhị phân duy nhất, không phụ thuộc vào bất kỳ thư viện động nào của máy game, giúp ứng dụng chạy được trên mọi phiên bản Stock OS và cả Custom OS (CrossMix, Knulli, MinUI).

2

Cài Đặt Môi Trường Biên Dịch Chéo (Cross-Compile)

Bạn có thể biên dịch ứng dụng ngay trên máy tính của mình (Windows WSL2, Ubuntu, macOS) sang mã máy ARM64 của TrimUI chỉ với 2 công cụ đơn giản:

Cài đặt toolchain musl và công cụ cross
# 1. Thêm target musl ARM64 vào trình biên dịch Rust
rustup target add aarch64-unknown-linux-musl

# 2. Cài đặt công cụ cross (tự động xử lý môi trường Docker cross-compile)
cargo install cross
Cargo.toml (Tối ưu hóa dung lượng & hiệu năng tối đa)
[package]
name = "kv-trimui-app"
version = "0.1.0"
edition = "2021"

[dependencies]
libc = "0.2" # Thư viện FFI chuẩn để gọi mmap, ioctl và read evdev

[profile.release]
opt-level = 3        # Tối ưu hóa tốc độ thực thi cao nhất
lto = true           # Link-Time Optimization gom code toàn bộ crates
codegen-units = 1    # Cho phép LLVM phân tích sâu để tối ưu hóa triệt để
panic = "abort"      # Bỏ bảng unwinding giúp giảm kích thước file thực thi
strip = true         # Tự động loại bỏ debug symbols (binary xuất ra chỉ ~300KB - 500KB)

Lệnh biên dịch xuất file thành phẩm: cross build --target aarch64-unknown-linux-musl --release

3

Vẽ Trực Tiếp Lên Màn Hình Qua Framebuffer (/dev/fb0)

Linux Framebuffer là một thiết bị bộ nhớ đặc biệt. Khi bạn ghi các byte màu vào vùng nhớ của /dev/fb0, bộ điều khiển màn hình LCD sẽ quét và hiển thị ngay lập tức lên màn hình máy game.

Nguyên Lý Double-Buffering Tránh Xé Hình (Screen Tearing)

Màn hình TrimUI Smart Pro có độ phân giải 1280 × 720 pixel. Mỗi pixel chiếm 4 byte (32-bit màu ARGB/XRGB). Tổng kích thước một khung hình là:

1280 × 720 × 4 bytes = 3,686,400 bytes (xấp xỉ 3.51 MB)

Nếu bạn ghi từng pixel trực tiếp vào màn hình trong lúc màn hình đang quét, người chơi sẽ thấy hiện tượng xé hình (tearing). Bí quyết là tạo một mảng Vec<u32> trong bộ nhớ RAM làm Back Buffer. Bạn vẽ toàn bộ giao diện vào Back Buffer trước, sau đó chép một lần duy nhất sang Framebuffer bằng hàm ptr::copy_nonoverlapping. Quá trình sao chép 3.51 MB trên Cortex-A53 chỉ mất ~1.1 mili-giây, đảm bảo 60 FPS mượt mà tuyệt đối!

src/framebuffer.rs (Mở /dev/fb0 & mmap bộ nhớ video)
use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::ptr;

pub const SCREEN_WIDTH: usize = 1280;
pub const SCREEN_HEIGHT: usize = 720;

pub struct Framebuffer {
    pub width: usize,
    pub height: usize,
    pub buffer: Vec<u32>, // Back buffer vẽ trong RAM (32-bit ARGB)
    mmap_ptr: *mut u8,
    mmap_len: usize,
}

impl Framebuffer {
    pub fn new() -> Self {
        let width = SCREEN_WIDTH;
        let height = SCREEN_HEIGHT;
        let buffer = vec![0xFF000000; width * height];
        let mmap_len = width * height * 4;

        // Mở thiết bị đồ họa với cờ đồng bộ O_SYNC
        let file = OpenOptions::new()
            .read(true)
            .write(true)
            .custom_flags(libc::O_SYNC)
            .open("/dev/fb0")
            .expect("Không thể mở /dev/fb0");

        let fd = file.as_raw_fd();
        let ptr = unsafe {
            libc::mmap(
                ptr::null_mut(),
                mmap_len,
                libc::PROT_READ | libc::PROT_WRITE,
                libc::MAP_SHARED,
                fd,
                0,
            )
        };

        if ptr == libc::MAP_FAILED || ptr.is_null() {
            panic!("Không thể mmap /dev/fb0");
        }

        Self {
            width,
            height,
            buffer,
            mmap_ptr: ptr as *mut u8,
            mmap_len,
        }
    }

    /// Đẩy toàn bộ Back Buffer ra màn hình LCD (chỉ tốn ~1.2ms)
    pub fn present(&mut self) {
        unsafe {
            ptr::copy_nonoverlapping(
                self.buffer.as_ptr() as *const u8,
                self.mmap_ptr,
                self.mmap_len,
            );
        }
    }
}

// Bắt buộc: Tự động xóa màn hình về màu đen khi thoát để không để lại rác hình ảnh
impl Drop for Framebuffer {
    fn drop(&mut self) {
        unsafe {
            ptr::write_bytes(self.mmap_ptr, 0, self.mmap_len);
            libc::munmap(self.mmap_ptr as *mut libc::c_void, self.mmap_len);
        }
    }
}
4

Đọc Nút Bấm Gamepad Qua Hệ Thống Linux Evdev

Trên máy TrimUI Smart Pro, hệ điều hành Linux Kernel tự động gửi tín hiệu phím bấm vật lý tới các thiết bị /dev/input/event0 đến /dev/input/event4 dưới dạng các sự kiện libc::input_event.

Bảng Mã Keycode Chuẩn Cho TrimUI Smart Pro

Nút Vật LýLinux Event TypeEvent CodeÝ Nghĩa & Hành Động
Nút AEV_KEY (1)305Xác nhận / Chọn (Confirm)
Nút BEV_KEY (1)304Hủy / Quay lại (Back/Cancel)
Nút XEV_KEY (1)308Hành động phụ (Action)
Nút YEV_KEY (1)307Tùy chọn bổ sung (Options)
D-Pad Lên / XuốngEV_KEY / EV_ABS103 / 108 (hoặc Hat 17)Di chuyển danh sách / Menu dọc
D-Pad Trái / PhảiEV_KEY / EV_ABS105 / 106 (hoặc Hat 16)Di chuyển ngang / Đổi trang
Nút L1 / R1EV_KEY (1)310 / 311Chuyển Tab trước / Tab sau
Nút MENU 🔴EV_KEY (1)316NÚT THOÁT ỨNG DỤNG BẮT BUỘC! Khi người dùng bấm MENU, ứng dụng phải thoát và trả lại quyền cho máy.
Nút START / SELECTEV_KEY (1)315 / 314Tạm dừng / Mở bảng điều khiển
5

Bí Thuật launch.sh — Đóng Băng & Đánh Thức MainUI

Tại Sao 90% Lập Trình Viên Thất Bại Khi Viết App Cho Stock OS?

Khi bạn khởi chạy ứng dụng từ màn hình chính của TrimUI, tiến trình giao diện gốc /usr/trimui/bin/MainUI vẫn đang chạy ngầm! Nếu bạn không xử lý, MainUI sẽ tranh giành quyền đọc phím bấm tay cầm, phát ra tiếng động bíp bíp hệ thống, và tồi tệ nhất: khi bạn thoát ứng dụng, máy sẽ bị đen màn hình và đơ cứng!

Giải pháp chuẩn của KV-TrimUI: Sử dụng tín hiệu SIGSTOP để tạm dừng MainUI trước khi app chạy, và cài đặt hàm bẫy `trap cleanup EXIT` để phục hồi bằng SIGCONT khi app thoát.

launch.sh (Script khởi chạy an toàn tuyệt đối)
#!/bin/sh
# Chuyển thư mục làm việc về gốc ứng dụng trên thẻ nhớ MicroSD
cd "$(dirname "$0")"

# 1. Cấp quyền thực thi cho toàn bộ file nhị phân
chmod -R +x ./bin ./*.sh 2>/dev/null || true

# 2. BẪY TÍN HIỆU THOÁT AN TOÀN: Bất kể app crash hay thoát bình thường,
# hàm cleanup() sẽ luôn được gọi để đánh thức MainUI, giúp máy KHÔNG BAO GIỜ bị đen màn hình!
cleanup() {
    sync
    echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true
    echo "Khôi phục MainUI..."
    killall -CONT MainUI 2>/dev/null || true
    if ! pgrep -x "MainUI" >/dev/null 2>&1; then
        if [ -x "/usr/trimui/bin/MainUI" ]; then
            /usr/trimui/bin/MainUI >/dev/null 2>&1 &
        fi
    fi
}
trap cleanup EXIT INT TERM HUP

# 3. ĐÓNG BĂNG TIẾN TRÌNH MAINUI CỦA MÁY (Tránh trùng lặp phím bấm và tiếng bíp)
killall -STOP MainUI 2>/dev/null || true

# 4. KHỞI CHẠY ỨNG DỤNG RUST NATIVE CỦA BẠN
./bin/my-app

exit 0
6

Cấu Trúc Đóng Gói Lên Thẻ Nhớ MicroSD

Để máy TrimUI Smart Pro tự động nhận diện và hiển thị ứng dụng của bạn trong mục Apps trên màn hình chính, hãy đặt thư mục theo cấu trúc sau:

Cấu Trúc Thư Mục Thẻ Nhớ
/mnt/SDCARD/Apps/MyRustApp/
├── bin/
│   └── my-app        <-- Binary ARM64 (musl)
├── config.json       <-- File cấu hình icon & nhãn
├── launch.sh         <-- Script khởi chạy bảo vệ
└── icon.png          <-- Biểu tượng ứng dụng (120x120)
config.json
{
  "label": "My Rust App",
  "icon": "",
  "launch": "launch.sh",
  "themecolor": "F97316",
  "icontop": "icon.png"
}
7

Mã Nguồn Mẫu Hoàn Chỉnh: Minimal "Hello TrimUI"

Dưới đây là mã nguồn đầy đủ của một chương trình Rust native hoàn chỉnh: mở Framebuffer 1280×720, hiển thị hiệu ứng dải màu gradient sống động, lắng nghe phím bấm MENU hoặc B để thoát sạch sẽ:

src/main.rs (Copy & Chạy ngay trên TrimUI Smart Pro)
use std::fs::{File, OpenOptions};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::io::AsRawFd;
use std::ptr;
use std::thread;
use std::time::{Duration, Instant};

const SCREEN_WIDTH: usize = 1280;
const SCREEN_HEIGHT: usize = 720;

fn main() {
    println!("Khởi động TrimUI Rust Native App...");

    let mmap_len = SCREEN_WIDTH * SCREEN_HEIGHT * 4;
    let mut back_buffer = vec![0u32; SCREEN_WIDTH * SCREEN_HEIGHT];

    // 1. Mở Framebuffer /dev/fb0 với cờ O_SYNC
    let fb_file = match OpenOptions::new()
        .read(true)
        .write(true)
        .custom_flags(libc::O_SYNC)
        .open("/dev/fb0")
    {
        Ok(f) => f,
        Err(e) => {
            eprintln!("Lỗi: Không thể mở /dev/fb0: {}", e);
            return;
        }
    };

    let fd = fb_file.as_raw_fd();
    let mmap_ptr = unsafe {
        libc::mmap(
            ptr::null_mut(),
            mmap_len,
            libc::PROT_READ | libc::PROT_WRITE,
            libc::MAP_SHARED,
            fd,
            0,
        )
    };

    if mmap_ptr == libc::MAP_FAILED {
        eprintln!("Lỗi: mmap thất bại");
        return;
    }

    // 2. Mở thiết bị tay cầm non-blocking
    let input_file = OpenOptions::new()
        .read(true)
        .custom_flags(libc::O_NONBLOCK)
        .open("/dev/input/event0")
        .ok();

    let mut frame_count: u32 = 0;
    let start_time = Instant::now();

    println!("Bắt đầu Game Loop 60 FPS. Bấm MENU hoặc B để thoát.");

    'main_loop: loop {
        let frame_start = Instant::now();
        frame_count = frame_count.wrapping_add(1);

        // A. Kiểm tra sự kiện nút bấm từ evdev
        if let Some(ref dev) = input_file {
            let mut ev: libc::input_event = unsafe { std::mem::zeroed() };
            let size = std::mem::size_of::<libc::input_event>();
            let bytes = unsafe {
                libc::read(dev.as_raw_fd(), &mut ev as *mut _ as *mut libc::c_void, size)
            };

            if bytes as usize >= size && ev.type_ == 1 && (ev.value == 1 || ev.value == 2) {
                // Mã 316 = Phím MENU, Mã 304 = Phím B
                if ev.code == 316 || ev.code == 304 {
                    println!("Nhận phím thoát (code: {}). Đang đóng app...", ev.code);
                    break 'main_loop;
                }
            }
        }

        // B. Vẽ đồ họa vào Back Buffer trong RAM (Hiệu ứng Gradient sống động)
        let t = (frame_count as f32 * 0.05).sin();
        let r_shift = ((t + 1.0) * 127.0) as u32;

        for y in 0..SCREEN_HEIGHT {
            let row_offset = y * SCREEN_WIDTH;
            let g = (y * 255 / SCREEN_HEIGHT) as u32;
            for x in 0..SCREEN_WIDTH {
                let b = (x * 255 / SCREEN_WIDTH) as u32;
                // Định dạng màu 32-bit: 0xFF_RR_GG_BB
                let color = 0xFF000000 | (r_shift << 16) | (g << 8) | b;
                back_buffer[row_offset + x] = color;
            }
        }

        // C. Sao chép tức thời từ RAM sang LCD Framebuffer (Double-buffering)
        unsafe {
            ptr::copy_nonoverlapping(
                back_buffer.as_ptr() as *const u8,
                mmap_ptr as *mut u8,
                mmap_len,
            );
        }

        // D. Khóa nhịp 60 FPS (~16.6ms mỗi frame)
        let elapsed = frame_start.elapsed();
        if elapsed < Duration::from_millis(16) {
            thread::sleep(Duration::from_millis(16) - elapsed);
        }
    }

    // 3. DỌN DẸP SẠCH SẼ KHI THOÁT
    unsafe {
        ptr::write_bytes(mmap_ptr, 0, mmap_len); // Xóa màn hình đen
        libc::munmap(mmap_ptr, mmap_len);        // Hủy ánh xạ bộ nhớ
    }

    let total_secs = start_time.elapsed().as_secs_f32();
    let avg_fps = frame_count as f32 / total_secs;
    println!("Ứng dụng đã thoát an toàn. FPS trung bình: {:.1}", avg_fps);
}
8

Hỏi Đáp Kỹ Thuật & Những Cạm Bẫy Thường Gặp

Q: Tại sao thoát app xong máy bị đen màn hình, phải tắt nguồn bằng phím cứng?

👉 Nguyên nhân: Trong file launch.sh bạn đã gửi tín hiệu killall -STOP MainUI nhưng chưa gửi killall -CONT MainUI khi app kết thúc, khiến giao diện gốc của máy vẫn bị treo trong trạng thái đóng băng. Hãy dùng hàm trap cleanup EXIT như trong mục 5.

Q: Làm sao để render chữ tiếng Việt (UTF-8) và hình ảnh PNG trong Rust?

👉 Giải pháp: Sử dụng crate fontdue (vô cùng nhẹ và nhanh hơn 10 lần so với freetype) kết hợp lệnh include_bytes!("../assets/BeVietnamPro.ttf") để nhúng thẳng font tiếng Việt vào binary. Với hình ảnh, dùng crate image với cờ default-features = false, features = ["png"] để đọc file ảnh và ghi các byte pixel vào Back Buffer.

Q: Màu sắc hiển thị bị ngược (Màu Đỏ biến thành Xanh Dương)?

👉 Nguyên nhân: Thứ tự kênh màu của màn hình là BGRA thay vì RGBA. Hãy đảo vị trí bit dịch khi tính toán màu pixel: thay vì (r << 16) | (g << 8) | b, hãy thử (b << 16) | (g << 8) | r.

Chung Tay Phát Triển Cộng Đồng TrimUI Việt Nam

Bạn Đã Sẵn Sàng Xây Dựng Ứng Dụng Native Của Riêng Mình?

Nếu tài liệu kỹ thuật này giúp bạn mở mang kiến thức và tự tay làm được ứng dụng chạy trên máy game TrimUI, đừng quên ủng hộ tác giả Khoa Võ một ly cà phê qua ZaloPay để có thêm động lực ra mắt nhiều công cụ hay hơn nữa nhé!