Arm Cortex M7 – Cache và Buffer trong truyền tải dữ liệu

Cùng tìm hiểu cache, buffer là gì? Vai trò của cache, buffer? Vấn đề không đồng nhất dữ liệu Cache Coherency và hướng giải quyết trong truyền tải dữ liệu với vi điều khiển STM32 được thiết kế sử dụng vi xử lý Arm Cortex M7? Các nội dung trong bài viết được xem xét dựa trên thực tế trong quá trình thực hiện đề tài của nhóm nghiên cứu. 

CACHE
1. Cache là gì?
Cache là một bộ nhớ có kích thước nhỏ được cấu thành từ các cell SRAM và nằm  giữa vi xử lý và bộ nhớ RAM như hình dưới:

Hình 1: Bộ nhớ Cache giữa CPU và bộ nhớ RAM

2. Vì sao lại có cache?

Việc truy cập RAM tốn rất nhiều thời gian do 2 nguyên nhân chính sau: Khoảng cách từ RAM đến CPU (đặc biệt là RAM ngoài); nếu sử dụng DRAM thì RAM phải tốn một khoảng thời gian nhất định để refresh các cell RAM.

Các chương trình nói chung đều mang tính “locality” nghĩa là gần. Chúng có thể là “spatial locality” nghĩa là gần nhau về mặt “không gian” như việc truy cập các phần tử liên tiếp nhau trên một mảng, (hay/ hoặc) việc thực thi các assembly instruction nối tiếp nhau trong bộ nhớ. Chúng cũng có thể là “temporal locality” nghĩa là gần nhau về mặt “thời gian” ví dụ như việc truy cập vào biến đếm i trong vòng lặp “for(int i;…)”.

Chính vì thế, để giảm thời gian truy cập vào các dữ liệu mang tính chất “locality”, các nhà thiết kế bộ nhớ thêm vào một bộ nhớ nhỏ có tốc độ rất cao (thường là tương đương tốc độ CPU) là “Cache” để lưu các dữ liệu đó.

BUFFER
1. Buffer là gì?
Buffer trong bài viết này có thể hiểu một cách “đơn giản” nhưng chưa đẩy đủ lắm là “Cache của Cache”. Còn vì sao cách hiểu này không đầy đủ thì Buffer được đề cập ở đây còn được gọi là “Victim Buffer”/“Write Buffer” và bộ nhớ Buffer này chỉ phục vụ hoạt động ghi ngược (write back) vào trong memory như được trình bày trong hình 2 dưới. Các bạn có thể tự tìm hiểu đầy đủ hơn về Buffer, một dịp nào đó TAPIT có thể sẽ chia sẻ thêm cùng các bạn nội dung này. 

Hình 2: Vị trí của Write Buffer

2. Vì sao cần Buffer?
Có hai quá trình mà mọi người nên hiểu rõ khi tìm hiểu về Buffer:

  • Write through: khi CPU cần gán giá trị mới cho một biến thì CPU sẽ ghi lên cache (*nếu trong cache đang chứa dữ liệu của vị trí nhớ đó) và vị trí dữ liệu trong bộ nhớ RAM cũng sẽ được cập nhật. Quá trình write through cho một biến X = A thành A’ có thể được biểu diễn như hình 3 dưới đây:

Hình 3: Mô tả quá trình Write through

  • Write back: Khi cần ghi một giá trị vào một vị trí nhớ tồn tại trong Cache thì CPU chỉ ghi vào Cache như hình 4 dưới:

Hình 4: Quá trình Write back cho một biến tồn tại trong Cache

Vậy, giả sử bộ nhớ Cache đầy, và CPU muốn đọc một biến Y và Y sẽ phải được load lên vào vị trí của X. Câu hỏi được đặt ra là: Hệ thống bộ nhớ của chúng ta cần làm gì để đảm bảo biến X trong Cache và RAM được đồng nhất? Nếu chúng ta cứ mặc kệ và xoá biến X trong Cache, thì khi CPU cần đọc biến X từ RAM lên lại, chúng ta sẽ có X = A thay vì A’!

Cách giải quyết: Khi thay đổi giá trị biến X = A ➡ A’ trong Cache, Cache sẽ đánh dấu là giá trị biến đó đã bị sửa đổi (bằng một bit gọi là bit Dirty – bẩn). Khi chúng ta cần thay thế Y vào vị trí đang chứa X = A’, Cache cần phải ghi ngược biến X = A’ (biến đã bị đánh dấu Dirty) vào Memory rồi mới được đọc biến Y vào vị trí đó! Toàn bộ quá trình được mô tả như hình dưới:

Hình 5: Quá trình thay thế trong thủ tục Write back

Dựa vào 2 định nghĩa trên, ta sẽ thấy rằng nếu read/write miss occupied (khi CPU đọc dữ liệu nhưng dữ liệu đó không có trong cache và cần phải thay thế một vị trí nhớ trong cache để load dữ liệu mới lên) xảy ra thì write back tốn nhiều thời gian nếu vị trí nhớ hiện tại bị đánh dấu là dirty. Tuy nhiên, trên thực tế write back cho thấy tốc độ trung bình cao hơn do ít phải truy cập ngược lại vào RAM trong phần lớn thời gian.

Vậy bây giờ làm sao để tiếp tục giảm số lần mà write back phải truy cập bộ nhớ? Hoặc chính xác hơn là làm sao để chúng ta có thể thay thế dữ liệu X = A’ trong trường hợp hình 5 nhưng không cần ngay lập tức ghi ngược X = A’ về RAM?

Lúc này, ta add thêm một bộ nhớ Buffer như hình 2! Nếu Cache cần Writeback về RAM thì nó sẽ ghi vào Buffer trước! Nếu Buffer đầy thì nó sẽ ghi một phần dữ liệu về RAM trong khi Cache vẫn có thể thay thế các vị trí nhớ với các biến dữ liệu khác. Nếu trong quá trình ghi/đọc, CPU truy cập vào một biến đang được ghi trong Buffer thì biến đó sẽ lại được load từ Buffer lên Cache và CPU một cách nhanh chóng.

Đến đây, ta đã hiểu được một cách đơn giản Cache và Buffer và tác dụng của chúng, giờ ta sẽ đi khám phá vấn đề của hệ thống nhớ có Cache trong hệ thống multi master (multi core, DMA)

Cache Coherency (Không đồng nhất dữ liệu)
Trong vi điều khiển STM32, thông thường sẽ có 1 đến 2 DMA master để hỗ trợ giảm tải cho CPU trong việc di chuyển dữ liệu từ ngoại vi/bộ nhớ đến bộ ngoại vi/bộ nhớ thông qua ma trận bus. Tuy nhiên, khi sử dụng DMA với Cache, ta có thể gặp phải trường hợp sau: 

Hình 6: Sự không đồng nhất dữ liệu giữa Cache và RAM do DMA gây ra

Ta thấy rằng, do có DMA cũng là một master có khả năng thay đổi dữ liệu trên RAM nên đã có sự không đồng nhất giữa RAM và Cache! Điều này có thể gây ra những lỗi rất nghiêm trọng và khó giải quyết vì nó không hiện hữu rõ ràng trong phần mềm mà do phần cứng bên dưới gây ra! Chính vì thế, ở phần tiếp theo ta sẽ thảo luận những cách giải quyết vấn đề này!

Hướng giải quyết Cache Coherency
1. Phần mềm:

  • Một cách đơn giản để giải quyết vấn đề này là sử dụng phần mềm để load lại toàn bộ Cache trước khi đọc!
  • Chúng ta có thể tưởng tượng như thế này: Có biến X được truy cập bởi cả CPU và DMA. DMA sẽ thường xuyên ghi vào vị trí của biến X trong khi CPU sẽ thường xuyên phải đọc giá trị của X. Mỗi lần CPU muốn đọc biến X, để đảm bảo đồng nhất dữ liệu, CPU sẽ flush toàn bộ cache, các biến đã bị modified ví dụ như Y = B ➡ B’ sẽ được ghi ngược lại RAM trong khi biến không bị modified sẽ được xóa. Sau đó, Cache lại bắt đầu đọc lại biến X từ RAM. Mô tả chi tiết hơn về Cache Flush có thể đọc tại [https://www.st.com/resource/en/data_brief/stm32cubeprog.pdf]
  • Để thực hiện Invalidate trong arm thì chúng ta dùng lệnh sau:
    • Để flush toàn bộ Cache: void SCB_CleanInvalidateDCache (void)
    • Để flush cache cho một biến/mảng với kích thước mong muốn:
      void SCB_CleanInvalidateDCache_by_Addr (uint32_t *addr, int32_t dsize)
  • Nếu hệ thống có sự tham gia của hệ điều hành như RTOS, Linux thì các hệ điều hành này có thể đã có sẵn cơ chế quản lý đồng bộ Cache một cách tự động!
  • Với phương án này, trách nhiệm đảm bảo đồng bộ dữ liệu giữa Cache và RAM thuộc về lập trình viên! Ngoài ra, phương này cũng yêu cầu CPU phải dành một số chu kì máy để thực hiện việc flush cache và load lại dữ liệu mới từ RAM trước khi có thể đọc được dữ liệu!

2. Phần cứng:

  • Để khắc phục những nhược điểm của phương pháp sử dụng phần mềm, một số hệ thống có cơ chế để đảm bảo RAM và Cache cùng chứa các bản dữ liệu giống nhau.
  • Phương pháp tối ưu nhất là hệ thống phần cứng được thiết kế với các protocol như MSI (Modified-Shared-Invalid), MESI (Modified-Exclusive-Shared-Invalid), MESIF, QPI, Distributed Cache (Cache với các phần vùng độc lập và chung khác nhau cho mỗi master)
  • Tuy nhiên, board mà nhóm nghiên cứu sử dụng là STM32F746 DISCO với vi điều khiển STM32F746NGH6U không hỗ trợ bất kì giao thức nào được nêu trên. Tuy nhiên, vi điều khiển này lại có một ngoại vi khác là Memory Protection Unit (MPU) cho phép ta điều chỉnh những thuộc tính/quy tắc cho các vùng nhớ.

Chi tiết về MPU sẽ được trình bày trong bài viết tiếp theo. 

Nhóm tác giả
Ng.Q.Phương, Ng.H.Phúc, Ng.H.N.Thương

Tìm hiểu thêm:
Fanpage Cộng đồng Kỹ thuật TAPIT: TAPIT – Learning, Research and Sharing Community