Các tệp .mpy của MicroPython

MicroPython định nghĩa khái niệm tệp .mpy -- một định dạng tệp nhị phân chứa mã đã được biên dịch trước, và có thể được nhập như một mô-đun .py thông thường. Tệp foo.mpy có thể được nhập thông qua import foo, miễn là foo.mpy có thể được tìm thấy theo cách thông thường bởi cơ chế nhập. Thông thường, mỗi thư mục được liệt kê trong sys.path sẽ được tìm kiếm theo thứ tự. Khi tìm kiếm trong một thư mục cụ thể, foo.py được tìm trước và nếu không tìm thấy thì foo.mpy sẽ được tìm, sau đó tiếp tục tìm trong thư mục tiếp theo nếu cả hai đều không được tìm thấy. Do đó, foo.py sẽ được ưu tiên hơn foo.mpy.

Các tệp .mpy này có thể chứa bytecode thường được tạo ra từ các tệp nguồn Python (tệp .py) thông qua chương trình mpy-cross. Đối với một số kiến trúc, tệp .mpy cũng có thể chứa mã máy gốc, có thể được tạo ra theo nhiều cách khác nhau, đặc biệt là từ mã nguồn C.

Trình biên dịch mpy-cross

mpy-cross là trình biên dịch chéo chuyển đổi tệp nguồn .py thành tệp nhị phân .mpy sẵn sàng để nhập trên cam. Nó là một phần của cây mã nguồn MicroPython (cùng cây được dùng để xây dựng firmware của cam) và cũng được phát hành dưới dạng gói pip để sử dụng phía máy chủ mà không cần checkout đầy đủ firmware:

$ pip install --user mpy-cross

Hoặc thông qua pipx:

$ pipx install mpy-cross

Sau khi cài đặt, hãy gọi nó trên một tệp nguồn đơn lẻ:

$ mpy-cross foo.py

Lệnh này tạo ra foo.mpy trong thư mục hiện tại, sẵn sàng để sao chép vào hệ thống tệp của cam cùng với các mô-đun khác hoặc để đưa vào ảnh ROMFS.

Các tùy chọn dòng lệnh hữu ích nhất:

  • -o <path> -- đường dẫn đầu ra cho tệp .mpy được tạo (mặc định là tên tệp đầu vào với phần mở rộng được thay thế; -o - ghi ra stdout).

  • -O<n> -- mức tối ưu hóa 0 đến 3. Mức mặc định 0 giữ nguyên các xác nhận và thông tin vị trí nguồn đầy đủ; 3 loại bỏ các xác nhận và docstring, đồng thời viết lại các khối if __debug__. Mức này kiểm soát cùng bề mặt micropython.opt_level mà runtime cung cấp.

  • -march=<arch> -- kiến trúc gốc đích cho các hàm được trang trí @native@viper. Bắt buộc khi mã nguồn sử dụng các decorator đó. Giá trị phải khớp với lớp MCU của cam: chọn từ danh sách mà mpy-cross --help in ra, hoặc đọc từ cam trong thời gian chạy bằng sys.implementation._mpy.

  • -s <path> -- chuỗi đường dẫn nguồn được nhúng vào thông tin debug của .mpy. Hữu ích khi đường dẫn trên đĩa khác với đường dẫn nhập mà tệp nên hiển thị trong traceback.

  • -X emit=bytecode|native|viper -- chọn bộ phát mã mặc định cho toàn bộ mô-đun (thay thế từng hàm cho các decorator @native / @viper).

  • --version -- in phiên bản định dạng .mpy mà nhị phân này phát ra. Con số đó phải khớp với phiên bản mà runtime của cam hỗ trợ (xem bảng phát hành bên dưới) hoặc lệnh nhập sẽ báo lỗi ValueError('incompatible .mpy file').

Chạy mpy-cross --help để xem danh sách đầy đủ các cờ.

Gói pip cũng cung cấp một API mô-đun Python nhỏ để các tập lệnh xây dựng có thể điều khiển trình biên dịch trong cùng tiến trình thay vì tạo subprocess thủ công:

import mpy_cross

mpy_cross.compile('foo.py', dest='build/foo.mpy', opt=3,
                  march=mpy_cross.NATIVE_ARCH_ARMV7EMSP)

mpy_cross.compile, mpy_cross.run, và mpy_cross.mpy_version là ba điểm vào; mpy_cross.CrossCompileError chứa stderr của trình biên dịch khi xảy ra lỗi. Các hằng số kiến trúc (NATIVE_ARCH_ARMV7EMSP, NATIVE_ARCH_ARMV7EMDP, v.v.) khớp với các chuỗi mà cờ -march chấp nhận.

Quản lý phiên bản và khả năng tương thích của tệp .mpy

Một tệp .mpy nhất định có thể hoặc không thể tương thích với một hệ thống MicroPython nhất định. Khả năng tương thích dựa trên các yếu tố sau:

  • Phiên bản của tệp .mpy: phiên bản của tệp phải khớp với phiên bản được hệ thống tải nó hỗ trợ.

  • Phiên bản phụ của tệp .mpy: nếu tệp .mpy chứa mã máy gốc thì phiên bản phụ của tệp phải khớp với phiên bản được hệ thống tải nó hỗ trợ. Ngược lại, nếu không có mã máy gốc trong tệp .mpy, thì phiên bản phụ bị bỏ qua khi tải.

  • Số bit cho số nguyên nhỏ: tệp .mpy sẽ yêu cầu số bit tối thiểu trong một small integer và hệ thống tải nó phải hỗ trợ ít nhất số bit này.

  • Kiến trúc gốc: nếu tệp .mpy chứa mã máy gốc thì nó sẽ chỉ định kiến trúc của mã máy đó và hệ thống tải nó phải hỗ trợ thực thi mã của kiến trúc đó.

Nếu hệ thống MicroPython hỗ trợ nhập tệp .mpy thì trường sys.implementation._mpy sẽ tồn tại và trả về một số nguyên mã hóa phiên bản (8 bit thấp hơn), các tính năng và kiến trúc gốc.

Cố gắng nhập tệp .mpy không vượt qua một trong bốn bài kiểm tra đầu tiên sẽ báo lỗi ValueError('incompatible .mpy file'). Cố gắng nhập tệp .mpy không vượt qua bài kiểm tra kiến trúc gốc (nếu nó chứa mã máy gốc) sẽ báo lỗi ValueError('incompatible .mpy arch').

Nếu nhập tệp .mpy thất bại, hãy thử các bước sau:

  • Xác định phiên bản .mpy và các cờ được hệ thống MicroPython của bạn hỗ trợ bằng cách thực thi:

    import sys
    sys_mpy = sys.implementation._mpy
    arch = [None, 'x86', 'x64',
        'armv6', 'armv6m', 'armv7m', 'armv7em', 'armv7emsp', 'armv7emdp',
        'xtensa', 'xtensawin', 'rv32imc', 'rv64imc'][(sys_mpy >> 10) & 0x0F]
    print('mpy version:', sys_mpy & 0xff)
    print('mpy sub-version:', sys_mpy >> 8 & 3)
    print('mpy flags:', end='')
    if arch:
        print(' -march=' + arch, end='')
    if (sys_mpy >> 16) != 0:
        print(' -march-flags=' + (sys_mpy >> 16), end='')
    print()
    
  • Kiểm tra tính hợp lệ của tệp .mpy bằng cách kiểm tra hai byte đầu của tệp. Byte đầu tiên phải là chữ 'M' viết hoa và byte thứ hai sẽ là số phiên bản, phải khớp với phiên bản hệ thống ở trên. Nếu không khớp thì hãy xây dựng lại tệp .mpy.

  • Kiểm tra xem phiên bản .mpy của hệ thống có khớp với phiên bản được phát ra bởi mpy-cross đã dùng để xây dựng tệp .mpy không, được tìm thấy bằng mpy-cross --version. Nếu không khớp thì hãy biên dịch lại mpy-cross từ kho Git được checkout tại tag (hoặc hash) được báo cáo bởi mpy-cross --version.

  • Đảm bảo bạn đang sử dụng đúng các cờ mpy-cross, được tìm thấy bằng đoạn mã trên, hoặc bằng cách kiểm tra biến Makefile MPY_CROSS_FLAGS cho port mà bạn đang sử dụng.

  • Nếu byte thứ ba của tệp .mpy có bit #6 được đặt, hãy kiểm tra xem vuint các bit cờ kiến trúc đặc thù được mã hóa có tương thích với mục tiêu mà bạn đang nhập tệp vào không.

Bảng sau đây hiển thị sự tương ứng giữa phiên bản phát hành MicroPython và phiên bản .mpy.

Phiên bản phát hành MicroPython

Phiên bản .mpy

v1.23.0 trở lên

6.3

v1.22.x

6.2

v1.20 - v1.21.0

6.1

v1.19.x

6

v1.12 - v1.18

5

v1.11

4

v1.9.3 - v1.10

3

v1.9 - v1.9.2

2

v1.5.1 - v1.8.7

0

Để đầy đủ, bảng tiếp theo hiển thị commit Git của kho MicroPython chính tại thời điểm phiên bản .mpy được thay đổi.

Thay đổi phiên bản .mpy

Commit Git

6.2 sang 6.3

bdbc869f9ea200c0d28b2bc7bfb60acd9d884e1b

6.1 sang 6.2

6967ff3c581a66f73e9f3d78975f47528db39980

6 sang 6.1

d94141e1473aebae0d3c63aeaa8397651ad6fa01

5 sang 6

f2040bfc7ee033e48acef9f289790f3b4e6b74e5

4 sang 5

5716c5cf65e9b2cb46c2906f40302401bdd27517

3 sang 4

9a5f92ea72754c01cc03e5efcdfe94021120531e

2 sang 3

ff93fd4f50321c6190e1659b19e64fef3045a484

1 sang 2

dd11af209d226b7d18d5148b239662e30ed60bad

0 sang 1

6a11048af1d01c78bdacddadd1b72dc7ba7c6478

phiên bản ban đầu 0

d8c834c95d506db979ec871417de90b7951edc30

Mã hóa nhị phân của tệp .mpy

Tệp .mpy của MicroPython là định dạng nhị phân chứa các đối tượng mã (bytecode và mã máy gốc) được lưu trữ bên trong theo cấu trúc phân cấp lồng nhau. Mã cho mô-đun ngoài cùng được lưu trữ trước, sau đó theo sau là các phần tử con. Mỗi phần tử con có thể có thêm các phần tử con, ví dụ trong trường hợp một lớp có các phương thức, hoặc một hàm định nghĩa một lambda hay comprehension. Để giữ kích thước tệp nhỏ trong khi vẫn cung cấp một dải giá trị lớn, định dạng sử dụng khái niệm số nguyên không dấu mã hóa biến đổi (vuint) ở nhiều nơi. Tương tự như mã hóa UTF-8, mã hóa này lưu trữ 7 bit mỗi byte với bit thứ 8 (MSB) được đặt nếu có một hoặc nhiều byte theo sau. Các bit của số nguyên không dấu được lưu trữ trong vuint theo dạng LSB.

Cấp độ cao nhất của tệp .mpy bao gồm ba phần:

  • Phần đầu (header).

  • Các bảng qstr và hằng số toàn cục.

  • Raw-code cho phạm vi ngoài cùng của mô-đun. Phạm vi ngoài cùng này được thực thi khi tệp .mpy được nhập.

Bạn có thể kiểm tra nội dung của tệp .mpy bằng cách sử dụng mpy-tool.py, ví dụ (chạy từ thư mục gốc của kho MicroPython chính):

$ ./tools/mpy-tool.py -xd myfile.mpy

Phần đầu (header)

Phần đầu .mpy là:

kích thước

trường

byte

giá trị 0x4d (ASCII 'M')

byte

số phiên bản chính .mpy

byte

cờ tính năng, kiến trúc gốc, số phiên bản phụ (trước đây là cờ tính năng trong các phiên bản cũ hơn)

byte

số bit trong một số nguyên nhỏ

Byte thứ ba được chia như sau (MSB trước):

bit

ý nghĩa

7

dành riêng, phải là 0

6

một vuint cờ kiến trúc đặc thù theo sau phần đầu

5..2

số kiến trúc gốc

1..0

số phiên bản phụ

Cờ kiến trúc đặc thù

Nếu bit #6 của byte cờ tính năng trong phần đầu được đặt, thì một vuint chứa thông tin kiến trúc đặc thù tùy chọn sẽ theo sau phần đầu. Nội dung của số nguyên này phụ thuộc vào kiến trúc gốc mà tệp được thiết kế cho.

Điều này hiện được sử dụng để lưu trữ các phần mở rộng bộ xử lý RISC-V mà tệp MPY cần để hoạt động đúng ngoài I, M, C và Zicsr. Các biến thể khác nhau của ArmV7 được xác định bằng số kiến trúc gốc của chúng, nhưng tái sử dụng cơ chế đó sẽ làm phức tạp mọi thứ đối với RV32 và RV64.

Các tệp MPY nhắm đến RV32 hoặc RV64 mà không cần bất kỳ phần mở rộng bộ xử lý cụ thể nào không cần cung cấp số nguyên cờ (cùng với việc đặt bit thích hợp trong phần đầu). Việc thiếu giá trị cờ cho các tệp MPY RV32 và RV64 được dùng để chỉ ra rằng không cần phần mở rộng cụ thể nào, và tiết kiệm một byte trong nhị phân đầu ra cuối cùng.

Xem thêm tùy chọn dòng lệnh -march-flags trong cả mpy-tool.pympy-cross, và tùy chọn dòng lệnh --arch-flags trong mpy_ld.py để đặt giá trị này khi tạo tệp MPY.

Các bảng qstr và hằng số toàn cục

Tệp .mpy chứa một bảng qstr duy nhất và một bảng đối tượng hằng số duy nhất. Các bảng này là toàn cục đối với tệp .mpy, chúng được tham chiếu bởi tất cả các đối tượng raw-code lồng nhau. Bảng qstr ánh xạ số qstr nội bộ (nội bộ của tệp .mpy) sang số qstr đã giải quyết của runtime mà tệp .mpy được nhập vào. Điều này liên kết tệp .mpy với phần còn lại của hệ thống mà nó thực thi trong đó. Bảng đối tượng hằng số được điền với các tham chiếu đến tất cả các đối tượng hằng số mà tệp .mpy cần.

kích thước

trường

vuint

số lượng qstr

vuint

số lượng đối tượng hằng số

...

dữ liệu qstr

...

các đối tượng hằng số được mã hóa

Các phần tử raw-code

Một phần tử raw-code chứa mã, có thể là bytecode hoặc mã máy gốc. Nội dung của nó là:

kích thước

trường

vuint

loại, kích thước và liệu có các phần tử sub-raw-code không

...

mã (bytecode hoặc mã máy)

vuint

số lượng phần tử sub-raw-code (chỉ khi khác không)

...

các phần tử sub-raw-code

Vuint đầu tiên trong một phần tử raw-code mã hóa loại mã được lưu trữ trong phần tử này (hai bit ít quan trọng nhất), liệu raw-code này có phần tử con không (bit thứ ba ít quan trọng nhất), và độ dài của mã theo sau (lượng RAM cần cấp phát cho nó).

Theo sau vuint là chính mã đó. Trừ khi loại mã là mã viper có các relocation, mã này là dữ liệu hằng số và không cần được sửa đổi.

Nếu raw-code này có bất kỳ phần tử con nào (được chỉ ra bằng một bit trong vuint đầu tiên), theo sau mã là một vuint đếm số lượng phần tử sub-raw-code.

Cuối cùng, bất kỳ phần tử sub-raw-code nào được lưu trữ một cách đệ quy.