Xây dựng trình giả lập Game Boy bằng ngôn ngữ F#: Hành trình thấu hiểu phần cứng máy tính

30 tháng 4, 2026·6 phút đọc

Một kỹ sư phần mềm đã dành hàng trăm giờ để xây dựng trình giả lập Game Boy tên là "Fame Boy" sử dụng ngôn ngữ lập trình F#. Bài viết chia sẻ chi tiết về kiến trúc phần mềm, những thách thức trong việc giả lập CPU, PPU, APU và bài học kinh nghiệm về tối ưu hóa hiệu năng cũng như sử dụng AI trong quá trình phát triển.

Xây dựng trình giả lập Game Boy bằng ngôn ngữ F#: Hành trình thấu hiểu phần cứng máy tính

Làm việc với tư cách là một kỹ sư phần mềm hơn 8 năm, tôi thú nhận rằng mình chưa bao giờ thực sự hiểu máy tính hoạt động như thế nào ở cấp độ thấp. Vì vậy, tôi quyết định tìm hiểu bằng cách tự xây dựng một trình giả lập (emulator). Thay vì hàn các mạch điện tử như Ben Eater, tôi chọn con đường phần mềm.

PokemonPokemon

Tuổi thơ của tôi gắn liền với hàng trăm giờ bắt Pokémon trên Game Boy, vì vậy đây là ứng cử viên hoàn hảo: phần cứng thực, phạm vi tương đối đơn giản và mang nhiều ý nghĩa cá nhân. Trước khi bắt tay vào làm, tôi đã hoàn thành khóa học "From NAND to Tetris" để nắm vững các nguyên tắc cơ bản như thanh ghi (registers), bộ nhớ và ALU. Sau đó, để làm quen với việc xây dựng trình giả lập, tôi đã viết một trình giả lập CHIP-8 bằng F# tên là Fip-8.

Tại sao lại chọn F#?

Mục tiêu của tôi là trình giả lập hoạt động trên cả máy tính để bàn và trình duyệt web, vì vậy tôi tập trung vào việc tạo ra một giao diện đơn giản giữa lõi giả lập và giao diện người dùng (frontend).

Tôi đã mô hình hóa Fame Boy tương tự như phần cứng thực tế của Game Boy. CPU, giống như chip Sharp LR35902 thật, không biết gì về phần cứng ngoại trừ bản đồ bộ nhớ. Đây là phần "F#" nhất trong cơ sở mã của tôi, tận dụng tối đa khả năng mô hình hóa miền (domain modelling) của ngôn ngữ này.

ArchitectureArchitecture

Hệ thống kiểu (type system) mạnh mẽ của F# hoạt động cực kỳ hiệu quả để mô hình hóa các lệnh CPU. Ví dụ, thay vì xử lý 512 opcode riêng lẻ, tôi có thể nhóm chúng lại và sử dụng Discriminated Unions để giảm xuống chỉ còn 58 lệnh. Điều này không chỉ làm code gọn gàng hơn mà còn giúp trình biên dịch bắt được các trạng thái bất hợp pháp mà phần cứng không hỗ trợ.

Tuy nhiên, tôi phải xin lỗi những người theo chủ nghĩa lập trình hàm (functional programming purists). Mặc dù trình giả lập CHIP-8 trước đây của tôi hoàn toàn "tinh khiết", Fame Boy sử dụng tính khả biến (mutability) tự do. Việc sao chép hơn 16 kB bộ nhớ hàng triệu lần mỗi giây không phải là ý tưởng hay, vì vậy tôi đã chọn hiệu quả thay vì sự thuần túy về mặt lý thuyết.

Thách thức với PPU và APU

Game Boy không có GPU mà có PPU (Picture Processing Unit). Đây là phần khiến tôi ngạc nhiên vì nhiều bài viết khác chỉ dành vài đoạn ngắn cho nó. PPU ít liên quan đến việc thiết kế hệ thống mà nhiều hơn là thực hiện các bước cơ học để đưa điểm ảnh lên màn hình.

Debug ViewDebug View

Ban đầu, tôi gặp khó khăn trong việc hiểu cách hoạt động của pixel FIFO và đường ống PPU. Tôi đã giải quyết vấn đề bằng cách chỉ đọc dữ liệu ô (tile) và bản đồ nền từ bộ nhớ, sau đó hiển thị chúng lên màn hình. Khi thấy hình ảnh của Tetris hiện ra, tôi cảm thấy cực kỳ phấn khích.

Âm thanh (APU) thậm chí còn khó khăn hơn. Tôi đã dành nhiều thời gian để hiểu cách các thanh ghi âm thanh và kênh hoạt động. Thách thức lớn nhất là đồng bộ hóa. Ban đầu, tôi cố gắng để tốc độ khung hình (frame rate) điều khiển trình giả lập, nhưng điều này gây ra hiện tượng "popping" trong âm thanh. Cuối cùng, tôi phải chuyển sang để âm thanh điều khiển trình giả lập để đảm bảo sự đồng bộ hoàn hảo, dù điều này làm cho giao diện giữa giả lập và frontend trở nên phức tạp hơn.

Đem F# lên trình duyệt với Fable

Sau khi có phiên bản hoạt động trên máy tính để bàn, tôi muốn chuyển Fame Boy sang web. Tôi đã thử Blazor WebAssembly nhưng gặp vấn đề về hiệu năng (chỉ đạt khoảng 8 FPS). Tôi quay lại với Fable – một công cụ biên dịch F# sang JavaScript.

Tuy nhiên, tôi đã gặp một lỗi rất thú vị. Các thanh ghi CPU trong Game Boy là số nguyên 8-bit (0-255), nhưng khi kiểm tra trên trình duyệt, tôi thấy giá trị của chúng là những con số âm khổng lồ như -15565461. Nguyên nhân là do các thao tác bitwise trong JavaScript hoạt động trên 32-bit và không cắt ngắn kết quả như F# mong đợi. Sau khi tìm và sửa tất cả các chỗ cần cắt ngắn 8-bit, phiên bản web đã hoạt động trơn tru với gói cài đặt chỉ khoảng 100 kB.

Bài học về tối ưu hóa hiệu năng

Khi thêm các tính năng, hiệu suất của trình giả lập dần giảm xuống còn 45 FPS. Tôi đã sử dụng trình profiler trong JetBrains Rider và phát hiện ra hàm mapAddress là nguyên nhân chính.

Vào thời điểm đó, tôi đang say sưa với phát triển hướng miền (domain-driven design) và áp dụng nó vào bộ nhớ. Mọi lần đọc hoặc ghi bộ nhớ đều tạo ra một đối tượng MemoryRegion cần được ánh xạ. Điều này dẫn đến việc cấp phát hàng triệu đối tượng lên heap mỗi giây và các nhánh (branching) thêm cho trình biên dịch JIT.

Khi tôi loại bỏ các đối tượng này và truy cập trực tiếp vào mảng, hiệu suất đã tăng gấp đôi. Bài học lớn nhất là đôi khi bạn phải từ bỏ các mô hình lập trình "đẹp" để đạt được hiệu năng cần thiết.

Vai trò của AI trong dự án

Dự án này nhằm mục đích học hỏi, nhưng tôi vẫn sử dụng AI như một công cụ hỗ trợ. Tôi sử dụng AI để viết các trường hợp kiểm tra (unit tests) dựa trên tài liệu kỹ thuật, cho phép tôi thực hiện phát triển hướng kiểm thử (TDD) thực sự mà không phải tốn công viết test thủ công.

Một trường hợp đáng chú ý là khi tôi gần như muốn bỏ cuộc vì một lỗi liên quan đến bộ đếm thời gian (timer). Tôi đã dành hơn 20 giờ để debug mà không thành công. Cuối cùng, Claude Opus đã tìm ra vấn đề chỉ trong vài phút: bộ đếm thời gian chỉ tăng lên một lần cho mỗi lệnh thay vì dựa trên số chu kỳ (cycles) thực tế.

Kết luận

Mục tiêu chính của tôi là học cách máy tính hoạt động, và xét về mặt đó, dự án này là một thành công lớn. Tôi đã có khoảng thời gian tuyệt vời, thường xuyên ngồi làm việc đến 2 giờ sáng vì quá say sưa sửa lỗi.

Dự án này có thể không biến tôi thành một kỹ sư phần mềm giỏi hơn ngay lập tức, nhưng cảm giác hiểu rõ hơn về công cụ mà tôi sử dụng hàng ngày là vô giá. Nếu bạn quan tâm đến lập trình cấp thấp hoặc muốn thử sức với F#, việc xây dựng một trình giả lập là một trải nghiệm học tập tuyệt vời.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗