- Bao gồm RV32I: thanh ghi, ABI và kiểm soát luồng với ecall.
- Luyện tập với Sao Mộc và các bài tập: phủ định, nhân tử, chuỗi, đệ quy.
- Master cross toolchain, kịch bản liên kết và gỡ lỗi với objdump.
Nếu bạn tò mò về trình biên dịch và cảm thấy RISC-V là giải pháp phù hợp, bạn đã đến đúng nơi rồi. Bắt đầu với ASM trên RISC-V có giá cả phải chăng hơn bạn nghĩ Nếu bạn hiểu các công cụ, mô hình của lập trình và một số quy tắc chính của kiến trúc.
Trong những dòng sau đây, tôi đã kết hợp những điều tốt nhất từ nhiều nguồn: thực hành với các trình mô phỏng loại Jupiter, các quy ước của tiết mục cơ bản RV32I, ví dụ về vòng lặp và đệ quy, lệnh gọi hệ thống và thậm chí là cái nhìn về thiết kế CPU RISC-V trong VHDL (với ALU, điều khiển bộ nhớ và máy trạng thái), cùng với phần đánh giá về chuỗi công cụ chéo và các tập lệnh liên kết.
Trình biên dịch RISC-V là gì và nó khác với ngôn ngữ máy như thế nào?
Mặc dù cả hai đều được gắn vào phần cứng, Ngôn ngữ máy là nhị phân thuần túy (số một và số không) mà CPU diễn giải trực tiếp, trong khi trình biên dịch sử dụng các thuật ngữ ghi nhớ và biểu tượng dễ đọc hơn trình biên dịch sau đó dịch sang nhị phân.
RISC-V định nghĩa một ISA mở với kho dữ liệu cơ sở rất sạch. Hồ sơ RV32I (32-bit) bao gồm 39 hướng dẫn sử dụng với tính trực giao đáng chú ý, tách biệt việc truy cập bộ nhớ khỏi tính toán thuần túy và có hỗ trợ tuyệt vời trong GCC/LLVM.
Hồ sơ, thỏa thuận và điểm vào
Trong RV32I bạn có 32 thanh ghi mục đích chung (x0–x31) 32-bit; x0 luôn được đọc là 0 và không thể ghi vào. Các bí danh như a0–a7 (đối số), t0–t6 (tạm thời) hoặc s0–s11 (đã lưu) cũng hữu ích để theo dõi ABI.
Tùy thuộc vào môi trường hoặc trình mô phỏng, chương trình có thể bắt đầu ở một nhãn cụ thể. Trong Jupiter, các chương trình bắt đầu bằng thẻ __start toàn cục., mà bạn phải khai báo là có thể nhìn thấy được (ví dụ: với .globl) để đánh dấu điểm vào.
các thẻ kết thúc bằng dấu hai chấm, bạn chỉ có thể đặt một lệnh trên mỗi dòng và các chú thích có thể được bắt đầu bằng # hoặc ; do đó trình biên dịch sẽ bỏ qua chúng.
Công cụ và Trình mô phỏng: Jupiter và Quy trình làm việc cơ bản
Để thực hành mà không gặp rắc rối, bạn có Trình mô phỏng/lắp ráp Jupiter, một công cụ đồ họa lấy cảm hứng từ SPIM/MARS/VENUS giúp chỉnh sửa, lắp ráp và thực thi dễ dàng trong một môi trường duy nhất.
Trong Jupiter, bạn có thể tạo, chỉnh sửa và xóa tệp trong tab Trình chỉnh sửa. Sau khi lưu, lắp ráp bằng F3 và chạy để gỡ lỗi từng lệnh một, sử dụng chế độ xem thanh ghi và bộ nhớ để hiểu trạng thái của máy.
Chương trình phải kết thúc bằng lời kêu gọi bảo vệ môi trường: thoát khỏi cuộc gọi thiết lập a0 với mã 10 (thoát). Trong RISC-V, lệnh gọi điện tử tương đương với lệnh gọi hệ thống hoặc lệnh bẫy đối với môi trường/hệ thống.
Cấu trúc tối thiểu của một chương trình và các lệnh gọi hệ thống
Cấu trúc điển hình trong các ví dụ học thuật xác định điểm bắt đầu, thực hiện công việc và kết thúc bằng lệnh gọi lại. các đối số ecall thường di chuyển theo hướng a0–a2 và bộ chọn dịch vụ trong a7, tùy thuộc vào môi trường.
Trong một Linux Ví dụ, RISC-V, bạn có thể in bằng lệnh syscall ghi và thoát bằng mã thích hợp. Để ghi a0 (fd), a1 (bộ đệm), a2 (chiều dài) và a7 được sử dụng với số dịch vụ. Cuối cùng, a0 được đặt thành mã trả về và a7 thành số thoát.
# Ejemplo mínimo (Linux RISC-V) para escribir y salir
.global _start
_start:
addi a0, x0, 1 # fd = 1 (stdout)
la a1, msg # a1 = &msg
addi a2, x0, 12 # a2 = longitud
addi a7, x0, 64 # write
ecall
addi a0, x0, 0 # return code
addi a7, x0, 93 # exit
ecall
.data
msg: .ascii "Hola mundo\n"
Nếu bạn làm việc bên ngoài Linux, chẳng hạn như trong các trình mô phỏng giáo dục có dịch vụ IoT riêng, thay đổi số ecall và đăng ký theo tài liệu môi trường.
Các bài tập ban đầu giúp bạn làm quen với các câu điều kiện, vòng lặp và bộ nhớ
Bài khởi động thông thường là phát hiện xem một số nguyên có phải là số âm hay không. Bạn có thể trả về 0 nếu là số dương và 1 nếu là số âm.; với RV32I, phép so sánh với 0 và đặt nhỏ hơn sẽ giải quyết vấn đề bằng một lệnh được cân nhắc kỹ lưỡng.
Một bài tập rất hữu ích khác là liệt kê các ước số của một số: duyệt từ 1 đến n, in ra các ước số và trả về số lượngBạn sẽ thực hành các phép chia có điều kiện, phép chia (hoặc phép trừ lặp lại) và phép lặp có phép cộng và phép so sánh.
Làm việc với chuỗi buộc bạn phải quản lý bộ nhớ: Truy cập từng ký tự của chuỗi trong bộ nhớ và chuyển đổi chữ thường thành chữ hoa tại chỗ nếu chúng nằm trong phạm vi ASCII. Sau khi hoàn tất, nó sẽ trả về địa chỉ gốc của chuỗi.
Vòng lặp, hàm và đệ quy: giai thừa, Fibonacci và Tháp Hà Nội
Khi thiết kế vòng lặp, hãy nghĩ đến ba khối: điều kiện, thân và bước. Với beq/bne/bge và các lệnh nhảy không điều kiện jal/j while/for được xây dựng không có bí ẩn, dựa vào addi và so sánh.
.text
.globl __start
__start:
li t0, 0 # i
li t1, 10 # max
cond:
bge t0, t1, end # si i >= max, salta
# cuerpo: usar a0/a1 segun servicio IO del entorno
addi t0, t0, 1 # i++
j cond
end:
li a0, 10
ecall
Trong các lệnh gọi hàm, hãy tôn trọng ABI: lưu lại nếu bạn định thực hiện nhiều cuộc gọi hơn, giữ nguyên s0–s11 nếu bạn sửa đổi chúng và sử dụng ngăn xếp với sp di chuyển theo bội số của một từ.
Giai thừa là phép đệ quy cổ điển: trường hợp cơ sở n==0 trả về 1; nếu không, hãy gọi factorial(n-1) và nhân với n. Bảo vệ ra và các thanh ghi được lưu trên ngăn xếp trước khi gọi và khôi phục chúng khi trả về.
factorial:
beq a0, x0, base
addi sp, sp, -8
sw ra, 4(sp)
sw s0, 0(sp)
mv s0, a0
addi a0, a0, -1
jal factorial
mul a0, a0, s0
lw s0, 0(sp)
lw ra, 4(sp)
addi sp, sp, 8
jr ra
base:
li a0, 1
jr ra
Fibonacci hữu ích cho việc thực hành cả hai đệ quy với hai cuộc gọi như một phiên bản lặp hiệu quả với các biến tích lũy. Và nếu bạn muốn thử thách với việc kiểm soát luồng và tham số, hãy dịch giải pháp sang trình biên dịch. Tháp Hà Nội với bốn đối số: đĩa, nguồn, đích và tháp phụ; nó tôn trọng thứ tự gọi và hiển thị từng chuyển động.
Truy cập bộ nhớ, mảng và thao tác chuỗi
Trong RISC-V, truy cập bộ nhớ được thực hiện bằng cách tải/lưu trữ: lw/sw cho các từ, lh/sh cho các nửa từ và lb/sb cho các byte, với các biến thể có dấu hoặc không dấu trong các khoản phí (lb so với lbu, lh so với lhu).
Để duyệt qua các mảng số nguyên, hãy sử dụng các độ lệch 4 byte cho mỗi chỉ mục; đối với chuỗi văn bản, tiến từng byte một cho đến khi tìm thấy ký tự kết thúc nếu quy ước yêu cầu (ví dụ: \0). Nhớ lưu địa chỉ cơ sở và xử lý con trỏ bằng addi/auipc/la nếu cần.
Thiết kế CPU RV32I từ đầu: Tổng quan cấp cao
Nếu bạn muốn đi sâu vào silicon, một dự án giáo dục sẽ xây dựng một CPU RV32I trong VHDL, có thể tổng hợp trong FPGA Dải trung bình thấp. Bao gồm ROM chương trình, RAM dữ liệu và một GPIO đơn giản để chiếu sáng đèn LED.
Hạt nhân thực hiện các mục cơ bản (không có phần mở rộng M/A/C hoặc CSR), sử dụng bus địa chỉ 32 bit và cho phép truy cập bộ nhớ mở rộng dấu 8/16/32 bit khi cần thiết. Thiết kế phân tách rõ ràng các thanh ghi, ALU, bộ điều khiển bộ nhớ và máy trạng thái.
ALU, sự thay đổi và ý tưởng về "tải chậm"
ALU được mô tả kết hợp với các hoạt động như phép cộng, phép trừ, XOR, OR, AND, phép so sánh (có dấu và không dấu) và sự thay đổi về mặt logic/số học.
Để lưu LUT trong FPGA, các dịch chuyển đa bit được triển khai lặp lại các dịch chuyển 1 bit được kiểm soát bởi máy trạng thái: bạn sử dụng nhiều chu kỳ, nhưng bạn lại giảm được tài nguyên logic.
Trong mạch đồng bộ, những thay đổi được quan sát thấy ở các cạnh xung nhịp. Khái niệm "tải chậm" gợi nhớ rằng những gì được bộ ghép kênh chọn sẽ tác động đến thanh ghi trong chu kỳ tiếp theo, một khía cạnh quan trọng khi thiết kế máy trạng thái lấy-giải mã-thực thi.
Bộ điều khiển và bản đồ bộ nhớ: ROM, RAM và GPIO
Một khối bộ nhớ tích hợp ROM và RAM vào một không gian liền kề, đơn giản hóa giao diện bộ xử lýBộ điều khiển nhận AddressIn (32 bit), DataIn, độ rộng (byte/nửa/từ), tín hiệu mở rộng dấu, WE (đọc/ghi) và Start để bắt đầu giao dịch.
Khi hoạt động kết thúc, ReadyOut được đặt thành 1 và nếu nó đã được đọc, DataOut chứa dữ liệu (được mở rộng dấu khi được yêu cầu). Nếu đã được ghi, dữ liệu vẫn nằm trong RAM.
Một ví dụ về bản đồ thực tế: ROM từ 0x0000 đến 0x0FFF, một byte GPIO ở 0x1000 (bit 0 đến một chân) và RAM từ 0x1001 đến 0x1FFFVới điều này, bạn có thể tạo ra đèn nháy bằng cách ghi và chuyển đổi bit đầu ra.
Thanh ghi, bộ ghép kênh và máy trạng thái
CPU định nghĩa 32 thanh ghi mục đích chung được khởi tạo bằng các mảng trong VHDL, với người giải mã để chọn đích ghi từ ALU và giữ nguyên phần còn lại.
Bộ ghép kênh điều khiển các đầu vào ALU (toán hạng và phép toán), các tín hiệu đến bộ điều khiển bộ nhớ (chiều rộng, địa chỉ, điều khiển bắt đầu và đọc/ghi) và các thanh ghi đặc biệt: PC, IR và bộ đếm phụ trợ cho các dịch chuyển lặp đi lặp lại.
Máy trạng thái bắt đầu bằng thiết lập lại, lấy lệnh được trỏ tới bởi PC (đọc 4 byte), nó được tải vào IR khi sẵn sàng và chuyển đến các nút thực thi: ALU (một lệnh trong 1 chu kỳ ngoại trừ các lệnh dịch chuyển), tải/lưu trữ, rẽ nhánh và nhảy, ngoài các lệnh đặc biệt như ebreak.
Chuỗi công cụ chéo, liên kết và gỡ lỗi
Để tạo các tệp nhị phân RV32I, hãy sử dụng Cross GCC (mục tiêu riscv32-none-elf). Bạn biên dịch các nguồn C/C++/ASM, liên kết với một tập lệnh xác định bản đồ bộ nhớ và chuyển đổi đầu ra thành dạng mà ROM/FPGA của bạn mong đợi.
Một tập lệnh móc đơn giản có thể đặt .text trong ROM từ 0x0000 và .data trong RAM từ 0x1004 (nếu 0x1000–0x1003 bị chiếm bởi các thanh ghi GPIO). Chương trình khởi động có thể được "trần trụi" và đặt con trỏ ngăn xếp ở cuối RAM (ví dụ: 0x1FFC) trước khi gọi main.
/* Mapa simple
* ROM: 0x00000000 - 0x00000FFF
* GPIO: 0x00001000 - 0x00001003
* RAM: 0x00001004 - 0x00001FFF
*/
SECTIONS {
. = 0x00000000;
.text : { *(.startup) *(.text) *(.text.*) *(.rodata*) }
. = 0x00001004;
.data : { *(.data) *(.data.*) }
}
Với riscv32-none-elf-objdump bạn có thể tháo rời ELF và kiểm tra địa chỉ; ví dụ, bạn sẽ thấy khởi động tại 0x00000000 với các lệnh như lui/addi/jal và chuyển sang main của bạn. Đối với mô phỏng VHDL, GHDL tạo ra các dấu vết mà bạn có thể mở bằng GtkWave.
Sau khi xác minh trong mô phỏng, hãy đưa thiết kế vào FPGA (Quartus hoặc chuỗi công cụ khác). Nếu RAM được suy ra là các khối bên trong và mã rõ ràng RTL, bạn có thể tổng hợp mà không có bất ngờ nào, ngay cả trên các thiết bị cũ.
Những lời nhắc nhở thực tế và những sai lầm thường gặp khi bắt đầu
Không được quên điều đó đấy x0 luôn bằng không; việc ghi vào đó không có tác dụng gì, còn việc đọc nó trả về 0. Hãy tận dụng điều này trong các phép cộng, so sánh và dọn dẹp sổ đăng ký.
Khi bạn triển khai các tính năng, lưu các bản ghi ra và sN mà bạn sửa đổivà quản lý ngăn xếp bằng các phép cộng/trừ được căn chỉnh theo từ vào sp. Khi trả về, nó khôi phục theo thứ tự ngược lại và nhảy với jr ra.
Trong các trình mô phỏng như Jupiter, hãy kiểm tra xem __start là toàn cục và bạn kết thúc nó bằng ecall đúng (a0=10 để thoát). Nếu có gì đó không khởi động, hãy kiểm tra nhãn, tính toàn cục và biên dịch lại (F3).
Trong các bài tập với IO, tôn trọng giao thức của môi trường: thanh ghi nào mang tham số, số hiệu dịch vụ và liệu có mong đợi giá trị địa chỉ hay giá trị tức thời hay không. Sử dụng trình mô phỏng hoặc tài liệu hướng dẫn hệ điều hành.
Với cơ sở ISA rõ ràng (RV32I, thanh ghi và ABI), một trình mô phỏng tiện lợi như Jupiter và các ví dụ ngày càng tăng (số âm, số thừa số, chữ hoa, vòng lặp, giai thừa, Fibonacci và Hanoi), trình biên dịch RISC-V không còn là một bức tường nữa mà trở thành một địa hình hấp dẫn để hiểu cách CPU suy nghĩ. Và nếu bạn dám tìm hiểu sâu hơn về VHDL, bạn sẽ thấy ALU, bộ nhớ và điều khiển kết hợp với nhau như thế nào.: từ việc lấy lệnh và tải chậm đến giao diện bộ nhớ và bản đồ với ROM, RAM và GPIO cho phép bạn nháy đèn LED bằng bộ xử lý của riêng bạn.
Người viết đam mê về thế giới byte và công nghệ nói chung. Tôi thích chia sẻ kiến thức của mình thông qua viết lách và đó là những gì tôi sẽ làm trong blog này, cho bạn thấy tất cả những điều thú vị nhất về tiện ích, phần mềm, phần cứng, xu hướng công nghệ, v.v. Mục tiêu của tôi là giúp bạn điều hướng thế giới kỹ thuật số một cách đơn giản và thú vị.