Lập trình hợp ngữ của ARM® Cortex™-M được thực hiện như thế nào ? Cú pháp của nó ra sao ?
Có rất nhiều bộ xử lý ARM khác nhau, tuy nhiên trong bài viết này chỉ tập trung vào dòng vi điều khiển Cortex-M thực hiện tập lệnh Thumb® được mở rộng với công nghệ Thumb-2. Tất nhiên là mình sẽ không mô tả cụ thể tất cả các tập lệnh Thumb® mà chỉ tập trung vào các tập con của nó, ngoài ra mình sẽ bỏ qua việc tối ưu kích thước code và thời gian thực thi.
Cú pháp
Ngôn ngữ hợp ngữ(Assembly) thì có 4 trường riêng biệt cách nhau bởi thẻ tab hoặc dấu cách.
Ví dụ luôn cho các bạn dễ hình dung
abel Opcode Operands Comment
Func MOV R0, #100 ; đặt R0 bằng 100
BX LR ; hàm return
label nằm ở cột đầu tiên dùng để xác định vị trí trong bộ nhớ của tập lệnh hiện tại, bắt buộc phải chọn tên duy nhất cho mỗi label.
opcode là mã máy chỉ cho bộ xử lý lệnh nào cần phải thực hiện.
operand là toán hạng xác định vị trí của dữ liệu để thực hiện lệnh. Với tập lệnh Thumb thì có 0,1,2,3, hoặc 4 operand (toán hạng) cách nhau bằng dấu phẩy.
comment là phần chú thích, nó thường được bỏ qua khi biên dịch code, nhưng nó sẽ mô tả giúp cho bạn có thể hiểu được cách phần mềm hoạt động.
Các comment như ở ví dụ trên không được tốt cho lắm, vì nó chỉ giải thích hoạt động của code. Người lập trình tốt thường thêm các comment để giải thích tại sao phần này lại được thực hiện, cách sử dụng nó như thế nào, làm thế nào để thay đổi và debug như ví dụ dưới đây
Func MOV R1,#100 ; R1=100
MUL R0,R0,R1 ; R0=100*input
ADD R0,#10 ; R0=100*input+10
BX LR ; return 100*input+10
Nếu thanh ghi R0 là thông số input thì hàm trên sẽ trả lại giá trị của thanh ghi R0 là 100*input+10
Source code của assembly thường là file text (file .s) gồm danh sách nhiều tập lệnh.
Assembler sẽ chuyển các code assembly thành object code để bộ xử lý có thể hiểu và thực hiện được. Tất cả object code thường là halfword-aligned. Điều này có nghĩa là tập lệnh có thể có 16 hoặc 32 bit, và chương trình đếm bit 0 sẽ luôn bằng 0.
Listing là file text gồm nhiều file object code được tạo bởi assembler với source code ban đầu.
Ví dụ:
Address Object code Label Opcode Operand comment
0x000005E2 F04F0164 Func MOV R1,#0x64 ; R1=100
0x000005E6 FB00F001 MUL R0,R0,R1 ; R0=100*input
0x000005EA F100000A ADD R0,R0,#0x0A ; R0=100*input+10
0x000005EE 4770 BX LR ; return 100*input+10
Khi thực hiện build một project thì tất cả các file sẽ được kết hợp hoặc biên dịch, sau đó sẽ được link với nhau. Linker sẽ quyết định chính xác vị trí mọi thứ trong bộ nhớ. Sau khi build xong project thành công thì có thể tải chương trình xuống flash ROM. Bạn có thể load chương trình xuống RAM, tuy nhiên với hệ thống nhúng thì không ai làm thế cả và khi bạn lưu dưới flash ROM thì trong quá trình debug bạn sẽ quan sát được địa chỉ chính xác của các biến.
Addressing mode và toán tử
Vấn đề thường gặp trong quá trình phát triển phần mềm là phân biệt sự khác nhau giữa dữ liệu và địa chỉ. Khi chúng ta đưa số 1000 vào trong thanh ghi R0 thì dữ liệu hoặc địa chỉ sẽ phụ thuộc vào cách bạn sử dụng số 1000 đó như thế nào. Để chương trình chạy hiệu quả thì cần phải truy cập vào thông tin trong thanh ghi, tuy nhiên chúng ta phải truy cập vào bộ nhớ để lấy các thông số hoặc lưu kết quả.
Addressing mode là dạng cấu trúc lệnh được dùng để xác định vị trí bộ nhớ để đọc/ ghi dữ liệu. Tất cả các lệnh sẽ bắt đầu bằng cách tìm các mã máy (opcode) hoặc toán tử được trỏ bởi PC(program counter). Khi mở rộng với công nghệ Thumb-2, một số mã máy là 16 bit hoặc 32 bit. Một số tập lệnh hoạt động trong bộ xử lý mà không cần nạp dữ liệu vào bộ nhớ.
Ví dụ: như lệnh ADD R1,R2 thực hiện phép toán R1+R2 và lưu lại kết quả trong R1.
Nếu dữ liệu được tìm trong chính tập lệnh của nó như MOV R0,#1 thì tập lệnh sẽ dùng mode Immediate addressing.
Một thanh ghi sẽ bao gồm địa chỉ hoặc vị trí của dữ liệu được gọi là pointer hoặc index register. Mode Indexed addressing sẽ sử dụng pointer để trỏ tới địa chỉ cần truy cập. Mode dùng PC như là con trỏ được gọi là PC-relative addressing. Cái này được dùng cho phân nhánh, gọi hàm, hoặc là truy cập vào các dữ liệu hằng số lưu trong ROM, sở dĩ nó có tên gọi này là vì mã máy có sự khác biệt giữa địa chỉ chương trình hiện tại và địa chỉ mà chương trình sẽ truy cập.
Lệnh MOV sẽ di chuyển dữ liệu trong bộ xử lý mà không truy cập vào bộ nhớ. Lệnh LDR sẽ đọc 32 bit word trong bộ nhớ và đưa dữ liệu vào thanh ghi. Với PC-relative addressing thì assembler sẽ tự động tính toán giá trị offset của PC.
Thanh ghi.Hầu hết các tập lệnh đều được thực hiện trên thanh ghi. Thông thường, luồng dữ liệu được thực hiện theo op code (operation code) từ trái sang phải, hay nói cách khác thì khi lập trình trên thanh ghi sẽ là gần nhất với op code để lấy kết quả của phép toán. Với mỗi lệnh như ví dụ dưới đây thì kết quả sẽ được lưu lại trong R2
MOV R2,#100 ; R2=100, immediate addressing
LDR R2,[R1] ; R2= giá trị của R1
ADD R2,R0 ; R2= R2+R0
ADD R2,R0,R1 ; R2= R0+R1
Danh sách thanh ghi. Tập lệnh stack push và stack pop có thể thực hiện trên một thanh ghi hoặc trên một danh sách thanh ghi. SP(stack pointer) giống như thanh ghi R13, LR(link register) giống thanh ghi R14, PC(program counter) giống thanh ghi R15 ở phần thanh ghi CPU bài các thành phần hệ thống nhúng.
PUSH {LR} ; lưu LR vào stack
POP {LR} ; xóa khỏi stack và đưa vào LR
PUSH {R1,R2,LR} ; lưu thanh ghi và trả lạ địa chỉ
POP {R1,R2,PC} ; khôi phục lại thanh ghi và return
Immediate addressing, với mode này thì dữ liệu sẽ tự nó tồn tại trong tập lệnh. Mỗi khi lệnh được gọi thì không cần chu kỳ để truy cập vào bộ nhớ để lấy dữ liệu.
Ví dụ như lệnh MOV R0,#100 ; R0=100, immediate addressing
Nói cho dễ hiểu thì giá trị 100(0x64) sẽ được lưu ở một địa chỉ nào đó trong EEPROM (0x00000264), PC sẽ truy cập vào địa chỉ này lấy giá trị 100 đưa vào R0.
Indexed addressing, với mode này thì dữ liệu nằm trong bộ nhớ và thanh ghi sẽ gồm con trỏ trỏ tới dữ liệu. Mỗi khi tập lệnh được thực hiện thì sẽ có một hoặc nhiều yêu cầu để đọc và ghi dữ liệu.
Ví dụ:
LDR R0,[R1] ; R0= trỏ tới giá trị R1
LDR R0,[R1,#4] ; R0= trỏ tới giá trị R1+4
R1 sẽ trỏ tới RAM, với lệnh LDR R0,[R1]
sẽ đọc giá trị 32 bit tại R1 và đặt nó vào R0. R1 có thể trỏ tới bất gì đối tượng hợp lệ nào nằm trong memory map (như RAM, ROM, I/O) và nó không bị điều chỉnh bởi lệnh.
Lệnh LDR R0, [R1,#4]
sẽ đọc giá trị 32 bit tại R1+4 và đặt nó vào R0. Ngay cả khi địa chỉ bộ nhớ được tính toán là R1+4 thì thanh ghi R1 vẫn không bị thay đổi.
PC-relative addressing. Mode này sử dụng PC như là một con trỏ (pointer). PC sẽ trỏ tới các lệnh được thực hiện tiếp theo, vì có sự thay đổi PC nên sẽ dẫn tới sự phân nhánh của chương trình. Một ví dụ cho trường hợp này là phân nhánh không điều kiện (unconditional branch). Trong ngôn ngữ lập trình assembly thì ta phải định nhãn cho nơi mà chúng ta muốn nhảy tới, và assembler sẽ mã hóa các tập lệnh với offset PC-relative tương ứng.
B Location ; nhảy tới Location, sử dụng PC-relative addressing
Một mode khác để gọi hàm là sử dụng lệnh BL, với lệnh này thì địa chỉ được trả lại sẽ được lưu trong thanh ghi liên kết (LR).
BL Subroutine ; gọi chương trình con, sử dụng PC-relative addressing
Thông thường cần 2 lệnh để truy cập vào dữ liệu trong RAM và I/O. Lệnh đầu tiên dùng PC-relative addressing để tạo con trỏ tới đối tượng, lệnh thứ 2 truy cập bộ nhớ sử dụng con trỏ.
Ví dụ:
LDR R1,=Count ; R1 trỏ tới biến Count, dùng PC-relative
LDR R0,[R1] ; R0= giá trị con trỏ tại R1
Hoạt động của 2 lệnh trên có thể được minh họa bằng hình dưới đây
Giả sử biến 32 bit Count được lưu trong RAM tại địa chỉ 0x2000.0000
. Trước tiên lệnh LDR R1,=Count
sẽ đưa R1 = 0x2000.0000
. Sau đó lệnh LDR R0,[R1]
đưa giá trị 32-bit tại địa chỉ 0x2000.0000
vào R0.
Flexible second operand . Rất nhiều lệnh có các toán tử linh hoạt thứ 2 (gọi là op2). op2 có thể là hằng dưới dạng #constant hoặc là thanh ghi dịch.
ADD Rd, Rn, #constant ;Rd = Rn+constant
Một dạng khác là dùng shift
ADD Rd, Rn, Rm {,shift} ;Rd = Rn+Rm
ADD Rn, Rm {,shift} ;Rn = Rn+Rm
Address Space
Memory map của TM4C123 được mô tả như hình sau
Memory map của hầu hét các dòng ARM® Cortex™-M đều giống như hình trên. Flash ROM bắt đầu từ địa chỉ 0x0000.0000
, RAM bắt đầu từ địa chỉ 0x2000.0000
, địa chỉ ngoại vi I/O từ 0x4000.0000
đến 0x5FFFF.FFFF
, các I/O trên bus ngoại vi riêng từ 0xE000.0000
đến 0xE00F.FFFF
. Sự khác biệt ở trong memory map giữa các dòng ARM là địa chỉ kết thúc của flash và RAM. Với nhiều bus có sẵn nghĩa là ARM có thể thực hiện được nhiều task cùng lúc.
ICode bus: lấy opcode ở ROM
DCode bus: đọc giá trị hằng từ ROM
System bus: Đọc/ ghi dữ liệu từ RAM hoặc I/O, lấy opcode từ RAM
PPB: Đọc/ghi dữ liệu từ ngoại vi nội như NVIC
AHB: Đọc/ghi dữ liệu từ I/O tốc độ cao hoặc các cổng song song (chỉ có trên M4)
Khi lưu 16-bit dữ liệu vào bộ nhớ thì cần 2 byte. Vì bộ nhớ trên máy tính là có địa chỉ tới từng byte nên có 2 cách để lưu dữ liệu trong 2 byte để tạo thành dữ liệu 16 bit.
Với freescale thì họ dùng big endian để lưu MSB tại địa chỉ thấp. Intel thì dùng little endian để lưu LSB tại địa chỉ thấp. Texas Instrument sử dụng little endian. Rất nhiều bộ xử lý ARM khác là biendian vì nó có thể cấu hình được cả 2 kiểu là big endian và little endian, tập lệnh của ARM luôn dùng little endian.
Ví dụ như mình lưu số 16 bit là 100 (0x03E8) vào địa chỉ 0x2000.0850
và 0x2000.0851
dưới dạng little endian và big endian
Ví dụ khác lưu số 32 bit là 0x12345678
vào địa chỉ từ 0x2000.0850
đến 0x2000.0853
.
Thông thường thì chúng ta sẽ không lấy từng dữ liệu mà sẽ lấy một chuỗi dữ liệu thông tin. Với chuỗi thông tin này thì khó xác định đâu là big endian hoặc little endian.
Ví dụ: giờ mình cần lưu chuỗi dữ liệu là “LM3S” (0x4C4D3353) tại địa chỉ từ 0x2000.0850
đến 0x2000.0853
thì chữ ‘L’ nằm trong mã ASCII = 0x4C
sẽ nằm ở 2 dạng big và little endian.
Tạm kết
Trong bài viết này vẫn còn khá nhiều khái niệm và lý thuyết, có 1 số khái niệm mình không dịch hết ra mà để tiếng anh để các bạn có thêm các từ khóa để tra khảo thêm. Ngoài ra về tập lệnh assembly của ARM bạn có thể tham khảo ở các tài liệu sau Cortex-M4 Instruction Set Technical User’s Manual, Cortex-M4 Technical Reference Manual, ARM® and Thumb-2 Instruction Set Quick Reference Card