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à:
- 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/.
- 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/MainUIchạ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:
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).
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:
# 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
[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
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à:
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!
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);
}
}
}Đọ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 Type | Event Code | Ý Nghĩa & Hành Động |
|---|---|---|---|
| Nút A | EV_KEY (1) | 305 | Xác nhận / Chọn (Confirm) |
| Nút B | EV_KEY (1) | 304 | Hủy / Quay lại (Back/Cancel) |
| Nút X | EV_KEY (1) | 308 | Hành động phụ (Action) |
| Nút Y | EV_KEY (1) | 307 | Tùy chọn bổ sung (Options) |
| D-Pad Lên / Xuống | EV_KEY / EV_ABS | 103 / 108 (hoặc Hat 17) | Di chuyển danh sách / Menu dọc |
| D-Pad Trái / Phải | EV_KEY / EV_ABS | 105 / 106 (hoặc Hat 16) | Di chuyển ngang / Đổi trang |
| Nút L1 / R1 | EV_KEY (1) | 310 / 311 | Chuyển Tab trước / Tab sau |
| Nút MENU 🔴 | EV_KEY (1) | 316 | NÚ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 / SELECT | EV_KEY (1) | 315 / 314 | Tạm dừng / Mở bảng điều khiển |
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.
#!/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 0Cấ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:
/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)
{
"label": "My Rust App",
"icon": "",
"launch": "launch.sh",
"themecolor": "F97316",
"icontop": "icon.png"
}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ẽ:
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);
}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.
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é!