Site icon TAPIT

RAM MEMORY: Cấu trúc, chức năng và thực hành debug trên VĐK STM32 (Phần 2)

Bộ nhớ truy cập ngẫu nhiên (RAM) là thành phần không thể thiếu trong mọi hệ thống nhúng, đóng vai trò là nơi lưu trữ dữ liệu và các biến chương trình cần thiết cho hoạt động tức thời. Tiếp nối Phần 1 mình đã giới thiệu Tổng quan về RAM, cấu trúc và chức năng của phân vùng Data và BSS (xem phần 1 tại đây), Phần 2 này sẽ đi sâu vào hai phân vùng bộ nhớ cực kỳ quan trọng: HeapStack. Đây là những thành phần cốt yếu cho việc cấp phát bộ nhớ động và quản lý ngăn xếp chương trình.

Việc nắm vững cách hoạt động của Heap và Stack không chỉ giúp bạn tối ưu hóa hiệu suất ứng dụng mà còn hỗ trợ đắc lực trong quá trình debug, đặc biệt khi gặp phải các lỗi như tràn bộ nhớ (memory overflow) hay xung đột vùng nhớ.

–o–

Phần 2. Cấu trúc và chức năng của phân vùng Heap và Stack.

4. Kiến trúc chung của RAM

4.3. Phân vùng Heap

Hình 1. Định nghĩa địa chỉ và kích thước của các phân vùng bộ nhớ RAM

Kích thước tối đa của Heap = Kích thước tối đa của RAM – Kích thước phân vùng Data – Kích thước phân vùng BSS – Kích thước tối thiểu của Stack

= (1010*102410) – 5416 – 16C16  – 40016 = 224016

Vậy địa chỉ của phân vùng Heap có địa chỉ bắt đầu là 0x200001C0 và địa chỉ kết thúc tại 0x20002400 (0x200001C0 + 0x2240).

Ví dụ về Heap Exhaution: Trong ví dụ này, vòng lặp vô hạn liên tục cấp phát bộ nhớ mà không giải phóng, dẫn đến tràn heap.

Trong đoạn chương trình trên, chúng ta có một vòng lặp vô hạn while (1). Trong mỗi lần lặp, CPU triển khai lệnh malloc cấp phát một khối bộ nhớ 800 Byte (200*4 – Với vi điều khiển STM32, kích thước của int bằng 4) trên Heap.

Vậy điều kiện để thoát khỏi vòng lặp là gì? Với điều kiện ptr == NULL, malloc() sẽ trả về NULL trong trường hợp việc cấp phát bộ nhớ thất bại, cụ thể hơn khi Heap không còn đủ bộ nhớ để cấp phát. Chúng ta đã biết được kích thước tối đa của Heap là 224016 = 876810. Sau mỗi vòng lặp, Heap cấp phát 80010 byte dữ liệu và 8 byte metadata, vậy theo lý việc tính toán, có thể cấp phát thành công 10 lần (8768 / 808 = 10.85).

Mình sẽ debug chương trình trên, chúng ta sẽ quan sát sự thay đổi giá trị của:

Như chúng ta đã biết, địa chỉ bắt đầu của Heap là 0x200001C0. Với mỗi lần cấp phát, Heap dành 8 byte cho Metadata, dữ liệu khối bộ nhớ đầu tiên sẽ bắt đầu từ 0x200001C8. 

Hình 2. Địa chỉ và dữ liệu của khối bộ nhớ Heap được cấp phát đầu tiên

Chúng ta đã tính toán với phạm vi lưu trữ tối đa của Heap (với VĐK STM32F103C6T6), Heap có thể cấp phát tối đa 10 khối 800 Byte. Kết quả debug đã chứng thực điều này.

Địa chỉ của khối bộ nhớ thứ 10 = 0x200001C0 + 810 + 910*80010 = 0x20001E30.

Hình 3. Địa chỉ và dữ liệu của khối bộ nhớ Heap được cấp phát cuối cùng

Khi Heap không đủ bộ nhớ để cấp phát, malloc() sẽ trả về NULL, việc cấp phát thất bại và chương trình kết thúc. Quan sát hình ảnh dưới đây, giá trị con trỏ ptr là NULL (tương đương 0x0).

Hình 4. Cấp phát thất bại do Heap không đủ bộ nhớ để cấp phát

Cách giải quyết việc tràn Heap: thêm free + điều kiện cho vòng lặp while.

Tại chương trình trên, chúng ta sử dụng free để giải phóng bộ nhớ không sử dụng nữa. Bên cạnh đó MAX_LOOP là điều kiện để số lượng vòng lặp là tối đa, nhằm đảm bảo tránh trường hợp việc cấp phát thất bại. Các bạn quan sát kết quả Debug ở hình ảnh dưới đây. Các bạn chú ý, việc sử dụng free() đã giải phóng bộ nhớ cấp phát sau mỗi vòng lặp nên giá trị của con trỏ ptr luôn luôn là 0x200001C8.

Hình 5. Bổ sung free() và điều kiện cho việc cấp phát bộ nhớ Heap

4.4. Phân vùng Stack

Địa chỉ của SP sẽ được tính toán như sau:

Ví dụ về dữ liệu được lưu trữ trong Stack:

Dựa theo các kiến thức về Stack mình vừa trình bày, các bạn cùng phân tích ví dụ trên và xác định những dữ liệu nào sẽ được lưu trữ vào Stack khi thực thi chương trình trên nhé!

Chúng ta cùng phân tích với 4 giai đoạn:

a. Hàm main() bắt đầu thực thi

Dữ liệu trong Stack Kích thước Ghi chú
Địa chỉ trả về của main() 4 Thanh ghi lr lưu địa chỉ trả về của main() được push vào Stack
value1 4 Biến cục bộ của main()
value2 4 Biến cục bộ của main()
result 4 Sẽ được gán giá trị sau khi sum thực thi xong

b. Hàm Sum() được gọi

Dữ liệu trong Stack Kích thước Ghi chú
Địa chỉ trả về của main() 4 Thanh ghi lr lưu địa chỉ trả về của main() được push vào Stack
value1 4 Biến cục bộ của main()
value2 4 Biến cục bộ của main()
result 4 Sẽ được gán giá trị sau khi sum thực thi xong
a 4 Tham số được truyền vào sum()
b 4 Tham số được truyền vào sum()
Địa chỉ trả về của Sum() 4 Thanh ghi lr lưu địa chỉ trả về của Sum() được push vào Stack
res 4 Biến cục bộ của Sum()

c. Sum() trả về dữ liệu

Dữ liệu trong Stack Kích thước Ghi chú
Địa chỉ trả về của main() 4 Thanh ghi lr lưu địa chỉ trả về của main() được push vào Stack
value1 4 Biến cục bộ của main()
value2 4 Biến cục bộ của main()
result 4 Được gán với giá trị Sum() trả về

d.Kết thúc hàm main

Dữ liệu trong Stack Kích thước Ghi chú
Empty 0

Khi chương trình hàm main kết thúc, các biến cục bộ trong main (value1, value2, result) được giải phóng. Địa chỉ trả về của main() được lấy ra và CPU nhảy về tại địa chỉ đó.

Kết quả trên chỉ dựa theo sự phân tích dựa theo lý thuyết mình trình bày. Để có thể thấy được sự hoạt động của Stack trên Vi điều khiển STM32, chúng ta sẽ phân tích chương trình trên dưới dạng kết quả của giai đoạn 2 trong 4 giai đoạn của quy trình biên dịch (phân tích cú pháp và chuyển đổi file về ngôn ngữ bậc thấp assembly). Các bạn cần lưu ý ý nghĩa của một số từ khoá dưới đây:

Sau khi áp dụng đoạn chương trình trên và biên dịch trên vi điều khiển STM32F103, trình IDE STM32Cube đã chuyển đổi và sinh ra được chương trình Assembly dưới đây.

Để thực sự hiểu sâu và ghi nhớ cách RAM hoạt động, bạn nên kết hợp lý thuyết với việc debug trực tiếp trên vi điều khiển. Bạn có thể sử dụng bất kỳ dòng vi điều khiển nào, nhưng STM32 là lựa chọn ưu tiên. Lý do là bạn sẽ được làm việc với bộ thư viện đầy đủ và các hướng dẫn sử dụng chi tiết do hãng cung cấp, giúp việc phân tích các định nghĩa giá trị liên quan đến bộ nhớ trở nên dễ dàng hơn rất nhiều.

Chuỗi bài viết này không chỉ cung cấp lý thuyết mà còn hướng dẫn chi tiết cách debug. Dù bạn làm việc trong lĩnh vực lập trình nhúng hay lập trình trong bất kỳ lĩnh vực nào khác, RAM luôn là bộ nhớ cực kỳ quan trọng, chúng ta cần hiểu và biết cách quản lý dữ liệu trên RAM. Mình hy vọng chuỗi bài viết này sẽ giúp bạn có cái nhìn tổng quan về RAM, hiểu rõ các phân vùng, biết cách debug, phân tích và sử dụng RAM hiệu quả.

Chúc các bạn thành công!

N.T.Nhien

Tìm hiểu thêm:
Tổng hợp hướng dẫn Internet of Things với NodeMCU ESP8266 và ESP32
Tổng hợp các bài hướng dẫn Lập trình vi điều khiển STM32
[Học trực tuyến: NGÔN NGỮ C CHO LẬP TRÌNH NHÚNG & IOTS]
Fanpage TAPIT: TAPIT – AIoT Learning