Writing Reusable Drivers (Phần 1)

“Phần mềm giống như entropy. Rất khó nắm bắt, không có khối lượng và tuân theo định luật thứ hai của nhiệt động lực học. Tức là nó luôn tăng. “

—Norman Ralph

1. Reusable Drivers

Viết một driver có thể được sử dụng từ application này sang application khác có thể rất hữu ích cho các embedded-software developer. Một khi driver được viết xong, các developer có thể tập trung vào application code và không phải lo lắng gì về các bit và byte. Các mẫu thiết kế (design pattern) driver có thể được sử dụng lại không chỉ trên cùng một phần cứng (hardware) mà còn trên nhiều nền tảng (platform) khác, chỉ bằng những thay đổi nhỏ cần thiết để điều chỉnh driver để truy cập các vùng bộ nhớ (memory region) khác nhau.

Trong chương này, chúng ta sẽ xem xét các phương pháp luận (methodology) khác nhau mà developer có thể sử dụng để ánh xạ (map) vào bộ nhớ ngoại vi (peripheral memory) và sau đó chúng ta sẽ chứng minh cách sử dụng từng kỹ thuật.

2. Giải Mã Từ Khóa extern và static

Liên kết mặc định (default linkage) cho một biến (variable) và hàm (function) trong ngôn ngữ lập trình C là extern. Một liên kết mặc định extern nghĩa là tất cả hàm và bất kỳ biến nào được định nghĩa (define) trong phạm vi (scope) của file đều là các biến và hàm toàn cục (global). Nói chung, có nhiều biến toàn cục và để tất cả hàm đều available trong một chương trình không phải là một thói quen lập trình tốt. Nếu mọi thứ trong application đều có nguy cơ bị các phần khác của ứng dụng chạm tới và thao túng, thì xác suất cao là nhiều chỗ trong application sẽ sử dụng một biến toàn cục mà không bảo vệ quyền truy cập của nó, và điều này có thể dẫn đến lỗi (bug). Những lỗi này thường khó tìm và khó mô phỏng lại (reproduce), điều này khiến chúng ta mất nhiều thời gian để sửa chữa.

Một thói quen lập trình ngôn ngữ hay nhất là giới hạn phạm vi (scope) của tất cả biến và hàm. Giữ cho các data và function được biết trong một giới hạn nhất định. Việc giới hạn phạm vi sẽ ngăn không cho một thành phần khác của application, hoặc một developer khác, vô tình lạm dụng hoặc giẫm qua data mà chúng được cho là không được phép sử dụng.

Các developer phần mềm nhúng ở trình độ junior thường sẽ nhận định việc sử dụng biến toàn cục là một việc làm khó chấp nhận và sẽ tránh sử dụng từ khóa extern. Vấn đề là theo mặc định, từ khóa extern được ngầm đặt trước các hàm và biến ở cấp phạm vi file. Điều này có nghĩa là nếu bạn không chỉ định kiểu liên kết thì toolchain của ngôn ngữ C sẽ tự biến mọi thứ thành toàn cục (global)!

Ví dụ, nhìn vào module đơn giản được hiển thị trong Figure 4-1. Module này trông hoàn toàn hợp lệ. Module sẽ biên dịch (compile) mà không có lỗi hoặc bất kỳ cảnh báo nào. Tuy nhiên, đối với trình biên dịch (compiler) và trình liên kết (linker), application trong Figure 4-1 trông giống như chương trình trong Figure 4-2.

#include <stdint.h>

int8_t myVariable = 0;

void myFunction(uint8_t myData);

void myFunction(uint8_t myData)
{
    myVariable = myData;
}

Figure 4-1. extern implicitly

#include <stdint.h>

extern int8_t myVariable = 0;

extern void myFunction(uint8_t myData);

extern void myFunction(uint8_t myData)
{
    myVariable = myData;
}

Figure 4-2. extern explicitly

Trong C, cách tốt nhất để kiểm soát liên kết ngoại mặc định (default external linkage) của một phần tử là sử dụng từ khóa static. Đây là từ khóa chỉ định lớp lưu trữ (storage-class). Nó cho compiler biết để giới hạn scope của biến hoặc của hàm, đồng thời nó cũng yêu cầu compiler cấp phát bộ nhớ (allocate storage) để biến tồn tại trong suốt thời gian sống của application.1 Static ghi đè (override) những từ khóa extern ngầm được tự động đặt trước các hàm và biến, khiến cho các biến và hàm trở thành liên kết nội (internally linkage). Kết quả: các biến và hàm chỉ khả dụng (available) trong một module đơn lẻ. Figure 4-3 cho thấy static sẽ hoạt động như thế nào trong chương trình mà trước đó nó có liên kết ngoại.

#include <stdint.h>

static int8_t myVariable = 0;

static void myFunction(uint8_t myData);

static void myFunction(uint8_t myData)
{
    myVariable = myData;
}

Figure 4-3. Explicitly limiting function and variable scope

3. Giải Mã Từ Khóa volatile

Đôi khi trong ứng dụng phần mềm nhúng sẽ cần phụ thuộc vào những thay đổi trong phần cứng vật lý. Phần mềm sẽ cần đọc cờ phần cứng (hardware flag), chẳng hạn như cờ báo hoàn thành quá trình truyền UART (transmission-complete flag). Một ví dụ minh họa đơn giản code này có thể giống như Figure 4-4. Đầu tiên, code định nghĩa (define) một pointer đến vị trí của UART_REGISTER trong memory. Sau đó, code sẽ đợi trong một vòng lặp while để UART_TX_FLAG trong UART_REGISTER được đặt (set).

#define UART_TX_FLAG 0x8

uint8_t* UART_REGISTER = 0x100000;

while((UART_REGISTER & UART_TX_FLAG) != UART_TX_FLAG);

Figure 4-4. Checking for the UART Tx Complete flag

Vấn đề của code trong Figure 4-4 là trình biên dịch (compiler) sẽ kiểm tra code và nhận ra rằng trong vòng lặp while, UART_REGISTER & UART_TX_FLAG là một biểu thức hằng số (constant expression). Không có nơi nào trong phần mềm mà giá trị đó thay đổi! Vì vậy, trình biên dịch sẽ làm theo những gì nó được thiết kế, tối ưu hóa code và kết quả sẽ như Figure 4-5.

#define UART_TX_FLAG 0x8

uint8_t* UART_REGISTER = 0x100000;

while(1);

Figure 4-5. The optimized UART Tx Check code

Kết quả được trình bày trong Figure 4-5 rõ ràng không phải là những gì developer dự định, nhưng nó dạy cho ta một bài học quan trọng. Khi truy cập phần cứng, các developer cần lấy hộp công cụ lập trình C và lôi từ khóa volatile ra. Điều này hướng dẫn trình biên dịch cần đọc lại giá trị của đối tượng (object) mỗi khi dùng tới nó, kể cả khi bản thân chương trình không thay đổi giá trị object từ lần truy cập trước đó.2 Developer có thể ngăn chặn việc tối ưu hóa code bằng cách khai báo giá trị được UART_REGISTER trỏ tới là volatile, như trình bày trong Figure 4-5. Bằng cách này, trình biên dịch sẽ nhận ra rằng biểu thức (expression) trong vòng lặp while có thể thay đổi bất cứ lúc nào và giá trị cần phải được đọc lại để xem liệu nó đã thay đổi hay chưa. Đoạn code được cập nhật có thể được thấy như trong Figure 4-6.

#define UART_TX_FLAG 0x8

uint8_t volatile* UART_REGISTER = 0x100000;

while((UART_REGISTER & UART_TX_FLAG) != UART_TX_FLAG);

Figure 4-6. Using the volatile keyword to prevent code optimization

Lưu ý, vị trí của từ khóa volatile trong code vừa cập nhật trên. Câu lệnh đang khai báo UART_REGISTER là một pointer đến một volatile uint8_t. Data là volatile, không phải là pointer. Đoạn code được thấy trong Figure 4-7 là một ví dụ về việc đặt SAI từ khóa volatile. Ví dụ đang trình bày một volatile pointer trỏ tới một uint8_t. Nói chung, việc có một pointer trỏ đến một thanh ghi phần cứng (hardware register) thay đổi, không phải là điều mà chúng ta muốn xảy ra trong một hệ thống nhúng.

#define UART_TX_FLAG 0x8

uint8_t* volatile UART_REGISTER = 0x100000;

while((UART_REGISTER & UART_TX_FLAG) != UART_TX_FLAG);

Figure 4-7. Improper volatile keyword location (Vị trí từ khóa volatile không đúng)

4. Giải Mã Từ Khóa const

Từ khóa const đôi khi có thể bị đánh lừa trong ngôn ngữ lập trình C. Một developer có thể nghĩ rằng const là một biến hằng và không thể sửa đổi được bằng application. Từ khóa const nói cho developer biết vị trí data đang được truy cập thông qua định danh (identifier) với từ khóa const là read-only.3 Nếu biến đang được định nghĩa là const tồn tại trong RAM, developer có thể tưởng tượng đến một pointer trỏ đến biến hằng (constant variable), viết tắt là const, và sau đó thay đổi giá trị. Trong nhiều trường hợp, những biến được khai báo const trong một hệ thống nhúng sẽ không được lưu trữ trong RAM mà thay vào đó sẽ để trong flash. Điều này ngăn không cho data bị sửa đổi và thực sự làm cho const data là hằng số.

Một thói quen tốt nhất để develop phần mềm nhúng là sử dụng từ khóa const thường xuyên nhất có thể.4 Từ khóa const cung cấp cho developer vài cách bảo vệ thông qua trình biên dịch (compiler) nếu có một nỗ lực nhằm thay đổi giá trị của một identifier. Những vị trí chủ yếu mà các developer cần xem xét sử dụng từ khóa const là:

  • Khi truyền data đến một function mà bên trong nó không cần sửa đổi data
  • Pointer trỏ đến hardware register thì không được thay đổi trong thời gian chạy

Nói chung, các hằng số luôn đúng như Pi hoặc các giá trị cấu hình không đổi, được định nghĩa không phải thông qua các identifier mà thông qua các kiểu liệt kê (enumeration) hoặc #define macro. Enumeration là phương pháp được ưu tiên hơn.

Trong phần trước, khi xem xét từ khóa volatile, chúng ta thấy một pointer đang được định nghĩa đã truy cập vào một hardware register. Một biến đang được sử dụng để truy cập phần cứng (hardware) đúng ra không nên thay đổi trong thời gian chạy. Đoạn code đó có thể được sửa đổi để pointer được định nghĩa là const và nó sẽ luôn trỏ đến vị trí chính xác trong bản đồ bộ nhớ phần cứng (hardware memory map) để truy cập UART_REGISTER. Ví dụ về code được cập nhật có thể như trong Figure 4-8. Trong ví dụ này, UART_REGISTER là một constant pointer trỏ đến data ở vị trị 0x100000, giá trị ở đây có thể thay đổi bất kỳ lúc nào (volatile) và là kiểu dữ liệu uint8_t.

#define UART_TX_FLAG  0x08

uint8_t volatile* const UART_REGISTER = 0x100000;

while((UART_REGISTER & UART_TX_FLAG) != UART_TX_FLAG);

Figure 4-8. A const pointer to a volatile uint8_t

5. Memory-Mapping Methodologies

Có vài lựa chọn cho các developer để ánh xạ code của họ vào memory region của vi điều khiển. Kỹ thuật được sử dụng sẽ phụ thuộc vào nhu cầu điều khiển của kỹ sư:

  • Kích thước code
  • Tốc độ thực thi
  • Hiệu quả
  • Khả năng port
  • Khả năng cấu hình

Các kỹ thuật đơn giản nhất có xu hướng là không thể tái sử dụng hoặc không thể port, trong khi các kỹ thuật phức tạp hơn thường ngược lại. Có một số kỹ thuật ánh xạ bộ nhớ (memory-mapping) thường được sử dụng trong thiết kế driver. Các phương pháp này bao gồm những điều sau:

  • Ánh xạ bộ nhớ trực tiếp (direct memory mapping)
  • Sử dụng con trỏ (pointer)
  • Sử dụng cấu trúc (structure)
  • Sử dụng mảng con trỏ (pointer array)

Chúng ta hãy xem xét những phương pháp khác nhau có thể được sử dụng để ánh xạ một driver vào memory.

5.1. Mapping Memory Directly

Một khi developer đã suy nghĩ về các mô hình driver khác nhau có thể được sử dụng để control các peripheral của vi điều khiển, đã đến lúc bắt đầu viết code. Có nhiều kỹ thuật để developer sử dụng để ánh xạ driver của họ vào memory space của peripheral, chẳng hạn như ghi (write) trực tiếp vô các thanh ghi (register) hoặc sử dụng pointer, structure, pointer array.

Kỹ thuật đơn giản nhất để sử dụng – và cũng ít có khả năng tái sử dụng nhất – là ghi trực tiếp vô register của peripheral. Ví dụ, giả sử một developer muốn cấu hình GPIO Port C. Để thiết lập và đọc (read) port này, developer có thể kiểm tra file định nghĩa (define) register, tìm ID chính xác, sau đó viết code tương tự như trong Figure 4-9.

PORT_C_DIRECTION = 0x14;
PORT_C_OUTPUT = 0x51;

Figure 4-9. Direct register access

Viết code theo cách này rất thủ công và tốn nhiều công sức. Vì code được viết cho một setup duy nhất và rất cụ thể. Code vẫn có thể port được, nhưng sẽ có nguy cơ ghi sai giá trị, điều này có thể dẫn đến lỗi (bug) và sau đó tốn thời gian để gỡ lỗi (debug). Các application rất đơn giản, không cần tái sử dụng, thường sử dụng phương pháp ghi trực tiếp vô các register để setup và control các peripheral. Ghi trực tiếp vào các register, cách làm này nhanh chóng và hiệu quả, đồng thời nó cũng không yêu cầu nhiều dung lượng của flash.

5.2 Mapping Memory with Pointers

Mặc dù việc ghi trực tiếp vào register có thể hữu ích, kỹ thuật này thường được sử dụng cho phần mềm không cần tái sử dụng hoặc được viết trên hệ thống nhúng rất hạn chế về tài nguyên, chẳng hạn như một vi điều khiển 8-bit đơn giản. Một kỹ thuật thường dùng khi cần khả năng tái sử dụng là dùng pointer để ánh xạ vào memory. Một ví dụ khai báo để ánh xạ vào GPIO Port C register – giả sử đó là data register – như trong Figure 4-10.

/* GPIO Port C is located at 0x100000 */
uint32_t *Gpio_PortC = (uint32_t*)0x100000UL;

Figure 4-10. Mapping a pointer to GPIO Port C

Bây giờ, đoạn code trong Figure 4-10 có một vấn đề! Thực tế có một tình trạng là nếu chúng ta cố gắng viết code để đọc port hoặc một bit trên port, compiler sẽ tối ưu hóa (optimize out) việc đọc! Compiler sẽ thấy một vòng lặp while đang kiểm tra trạng thái bit của một register, như thể hiện trong Figure 4-11. Vì không có dòng nào trong vòng lặp while thay đổi những giá trị được lưu trữ ở chỗ mà Gpio_PortC đang trỏ đến. Nên compiler quyết định rằng, không có lý do gì để tiếp tục đọc giá trị này và việc đọc vị trí memory này có thể bị tối ưu hóa (bỏ qua).

while((*Gpio_PortC & BIT0) == 0)
{
  /* Execute the loop code */
}

Figure 4-11. Checking a register bit

Để giải quyết vấn đề này, developer cần sử dụng từ khóa volatile. Về cơ bản, volatile báo với compiler rằng data đang đọc bị thay đổi liên tục, bất kỳ lúc nào mà không phải do bất kì đoạn code nào thay đổi giá trị đó. Có ba trường hợp mà volatile thường được sử dụng:

  • Các biến đang được ánh xạ tới các hardware register
  • Data được chia sẻ giữa các trình phục vụ ngắt (interrupt service routine) và application code
  • Data được chia sẻ giữa nhiều luồng (multiple thread)

Về cơ bản, volatile báo cho compiler biết là không được tối ưu hóa việc đọc, nó đảm bảo rằng data được lưu trữ tại vị trí memory đó được đọc mỗi khi biến được gọi.

Vị trí volatile xuất hiện trong khai báo rất quan trọng để ánh xạ chính xác một peripheral register. Khai báo một pointer làm một register bằng cách sử dụng câu lệnh sau, nó báo cho compiler biết rằng pointer là volatile, chứ không phải là data được trỏ tới. Đoạn code trong Figure 4-12 cho thấy pointer có thể thay đổi bất kỳ lúc nào trong khi thực tế thì data trong register được trỏ tới mới có thể thay đổi.

uint32_t* volatile Gpio_PortC = (uint32_t*)0x100000UL;

Figure 4-12. Incorrectly using the volatile keyword for pointer data

Cách khai báo ĐÚNG là đặt từ khóa volatile ngay sau data pointer chứ không phải ngay sau pointer, như được hiển thị trong Figure 4-13.

uint32_t volatile* Gpio_PortC = (uint32_t*)0x100000UL;

Figure 4-13. Correctly using the volatile keyword for pointer data

Code này cho compiler biết rằng Gpio_PortC là một pointer đến một volatile uint32_t. Hãy nhớ rằng, khi đọc một khai báo như thế này, hãy bắt đầu đọc từ bên trái của định danh và đọc từ phải sang trái. Điều này giúp cung cấp sự rõ ràng cho việc khai báo thực tế. (Tôi đề nghị bạn nên đọc phần “Complex Declarators” từ cuốn sách Expert C Programmers,5 cung cấp những lời khuyên tổng quát giúp tìm hiểu ý nghĩa của một khai báo).

Với từ khóa volatile ở đúng vị trí, giờ đây chúng ta biết compiler sẽ không tối ưu hóa việc đọc giá trị của biến. Tuy nhiên, vẫn còn một vấn đề với cách viết khai báo. Hãy dành một chút thời gian để kiểm tra đoạn code được hiển thị trong Figure 4-14.

/* Set the 0 bit high on PortC */
*Gpio_PortC |= 0x1;
Gpio_PortC++;

Figure 4-14. Accessing memory to a non-constant pointer

Phép toán tăng pointer Gpio_PortC như trên hoàn toàn hợp lệ. Sau khi tăng, pointer có thể sẽ trỏ đến Port D, một register khác với Port C, hoặc thậm chí là một SPI hoặc IIC peripheral. Một khi pointer được ánh xạ vào memory, developer không được phép tăng, giảm hoặc sửa đổi vị trí của pointer. Điều này là cực kỳ nguy hiểm! Vì vậy, thay vào đó, trong khai báo, chúng ta nên khai báo pointer là hằng số (constant), như trong Figure 4-15.

uint32_t volatile* const Gpio_PortC = (uint32_t*)0x100000UL;

Figure 4-15. Constant memory-pointer declaration

Việc thêm từ khóa const bây giờ làm cho Port C trở thành một con trỏ hằng loại volatile uint32_t (constant pointer to a volatile uint32_t), và bất kỳ ý định nào nhằm tăng hoặc giảm pointer này trong source code sẽ dẫn đến lỗi trình biên dịch (compiler error). Sử dụng const theo cách này là rất quan trọng để viết code an toàn, tuy nhiên nếu bạn chịu khó đọc kỹ những code ví dụ hoặc những định nghĩa (#define) register được hỗ trợ bởi nhà cung cấp vi điều khiển, bạn sẽ thấy phần lớn họ bỏ qua chuyện này và cho phép con trỏ ánh xạ bộ nhớ (memory-mapped pointer) được chỉnh sửa trong source code.

5.3. Mapping Memory with Structures

Kỹ thuật tiếp theo, và có lẽ là kỹ thuật phổ biến nhất được cung cấp bởi các nhà sản xuất vi điều khiển, là sử dụng các structure để ánh xạ vào memory. Structure đưa cho các developer một cách để tạo các data member ánh xạ trực tiếp đến một vị trí memory (memory location). Tiêu chuẩn C đảm bảo rằng nếu tôi tạo các data member trong một structure, chúng sẽ xuất hiện theo cùng một thứ tự mà không có phần đệm (padding). Kết quả là khả năng tạo ra các structure pointer ánh xạ trực tiếp vào memory space của peripheral, như thể hiện trong Figure 4-16.

Figure 4-16. Mapping a structure into 32-bit memory

Structure phải có mỗi member tương ứng theo thứ tự để các peripheral register ánh xạ đúng. Đồng thời lưu ý trong phần khai báo rằng structure đang trừu tượng hóa các chi tiết để tạo một pointer đến structure. Với structure được khai báo theo cách này, developer có thể truy cập peripheral bằng cách sử dụng code trong Figure 4-17.

#define PORTC_BASE_PTR    ((GPIO_MemMapPtr)0x300000UL)

PORTC_BASE_PTR->PDIR |= (1UL << 23);

Figure 4-17. Declaring a peripheral base pointer based on structure

Tôi thực sự không phải là một người thích sử dụng macro theo cách này, mặc dù khi tìm hiểu code của nhiều hãng vi điều khiển cung cấp, bạn sẽ thấy nó khá tràn lan. Một giải pháp thay thế sẽ là khai báo PORTC_BASE_PTR làm định danh chuẩn bằng cách sử dụng code được hiển thị trong Figure 4-18.

GPIO_MemMapPtr PORTC_BASE_PTR = ((GPIO_MemMapPtr)0x300000UL);
PORTC_BASE_PTR->PDIR |= (1UL << 23);

Figure 4-18. Defining and using the memory-mapped structure

Sử dụng structure để ánh xạ memory có thể hiệu quả và mang đến cho developer một cách để tạo ra những driver được ánh xạ có khả năng tái sử dụng. Sử dụng các tiêu chuẩn như Tiêu chuẩn Giao diện Phần mềm ARM® Cortex® (Software Interface Standard – CMSIS) có thể cung cấp một phương pháp phổ biến và có thể tái sử dụng để truy cập các peripheral register nhằm cải thiện khả năng port. Thật không may, kể từ khi viết bài này, nhiều nhà cung cấp vẫn sẽ sử dụng các quy ước đặt tên của riêng họ, điều này vẫn đòi hỏi một lượng công việc hợp lý để thích ứng với các bộ vi điều khiển khác nhau.

5.4. Using Pointer Arrays in Driver Design

Một phương pháp duy nhất để ánh xạ bộ nhớ (mapping memory) là sử dụng một mảng con trỏ (pointer array). Pointer array là một array mà mỗi phần tử (element) của array là một pointer. Đối với một kỹ sư đang develop một driver, mỗi element trong pointer array sẽ trỏ đến một peripheral register với cùng một kiểu (type) register duy nhất. Ví dụ: một developer sẽ tạo một pointer array để set data output của các GPIO port bằng cách include từng pointer đến các data register PORTA, PORTB, PORTC, v.v. Một pointer array thứ hai sẽ được tạo ra để nắm giữ tất cả những GPIO direction register (thanh ghi điều khiển chức năng input/output của GPIO) cho các port. Một pointer array sẽ được tạo cho mỗi kiểu register trên peripheral, với mỗi entry đại diện cho một kênh (channel).

Có nhiều lợi ích khi sử dụng pointer array để ánh xạ memory trong hệ thống nhúng. Đầu tiên, nó cho phép developer nhóm (group) các register thành các kênh logic. Thứ hai, các hàm khởi tạo (initialization function) có thể được viết theo cách chạy trong vòng lặp với mỗi index trong array, điều này giúp đơn giản hóa đáng kể hàm khởi tạo. Không chỉ đơn giản hóa việc khởi tạo mà việc sử dụng pointer array còn tạo ra một mẫu thiết kế (design pattern) có thể dễ dàng tái sử dụng và port từ application này sang application tiếp theo và từ platform này sang platform tiếp theo.

Pointer array cũng giúp trừu tượng hóa phần cứng và chuyển đổi các register thành một thứ gì đó dễ đọc và dễ hiểu hơn đối với các lập trình viên – con người. Developer có thể tạo ra những tên function dễ hiểu để truy cập các pointer array và xử lý phần chi tiết đằng sau. Các structure khởi tạo thậm chí có thể được tạo để cho phép một table được pass vào một driver để khởi tạo peripheral, một lần nữa tạo ra một framework chung, tiêu chuẩn có thể được tái sử dụng và được port dễ dàng.

Mặc dù có những khả năng mạnh mẽ và tính port mà pointer array mang lại cho bảng lập trình (programming table), có một vài nhược điểm mà các developer cần cảnh giác. Đầu tiên, việc tạo những pointer array sẽ làm tăng kích thước chương trình khi so sánh với structure hoặc những phương pháp ánh xạ memory truy cập trực tiếp. Lý do cho sự gia tăng chương trình là vì bây giờ đã có thêm những array đang lưu trữ các pointer, và trên đó có một bảng cấu hình (configuration table) sẽ được lưu trữ trong flash, chứa thông tin khởi tạo cho mọi peripheral và channel. Sự gia tăng kích thước chương trình không đáng kể lắm, nhưng nếu developer bị giới hạn bởi vi điều khiển có vài nghìn kilobyte dung lượng flash thì nó sẽ nhanh chóng lấp đầy dữ liệu khởi tạo (initialization data).

Thứ hai, vì các peripheral đang được truy cập thông qua một pointer array, có khả năng tốn hiệu suất một vài clock cycle khi truy cập các driver cấp thấp. Nếu một developer đang sử dụng một bộ vi điều khiển 8-bit cũ chạy ở tốc độ 8 MHz, đó có thể là một vấn đề lớn. Sử dụng bộ vi xử lý hiện đại như 32-bit ARM Cortex-M, sự khác biệt về hiệu suất là không đáng kể trong hầu hết các application. Điều đó nói rằng, developer vẫn cần đảm bảo rằng họ giám sát được hiệu suất hệ thống của họ.

Khi so sánh chi phí và thời gian develop của việc sử dụng structure hoặc phương pháp ánh xạ memory trực tiếp, pointer array cung cấp cho developer một mẫu thiết kế linh hoạt và có khả năng tái sử dụng. Do đó dễ dàng có khả năng mở rộng (scalable) và thích ứng (adaptable). Hãy cùng kiểm nghiệm làm thế nào để chúng ta có thể ánh xạ memory tới một timer peripheral bằng kỹ thuật ánh xạ pointer array.

6. Creating a Timer Driver Overview


1C in a Nutshell, pages 156, 165

2C in a Nutshell, pages 53, 127

3C in a Nutshell, page 57

4Barr Group Best Practices (Embedded C Coding Standard, page 23)

5Expert C Programming: Deep C Secrets, Peter Linden (Prentice Hall, 1994)

Icons made by Freepik from www.flaticon.com