🌱 Data Structure và Algorithm trong Embedded Software

🌱 Data Structure và Algorithm trong Embedded Software

    Đối với các kỹ sư phần mềm nhúng, Data Structure và Algorithm (DSA) thường bị coi nhẹ so với các chủ đề khác như registers, interrupts hay communication protocols. Tuy nhiên, khi các sản phẩm firmware phát triển về độ phức tạp và khối lượng code, việc xử lý dữ liệu hiệu quả trở thành yêu cầu bắt buộc. DSA không chỉ dành cho phỏng vấn kỹ thuật hay lập trình thi đấu – trong embedded systems, chúng quyết định trực tiếp đến hiệu năng, độ ổn định, tính real-time và khả năng bảo trì của hệ thống.

    Bài viết này sẽ giới thiệu về các ngữ cảnh thực tế cần sử dụng DSA trong embedded development, từ UART communication, RTOS scheduling, memory management cho đến signal processing và bootloader mechanisms.



1. Tại sao Embedded Engineers cần hiểu DSA?

Hãy bắt đầu từ một số ngữ cảnh thực tế:

① RTOS Scheduler - Priority Task Management

   Trong hệ thống FreeRTOS hoặc Zephyr, scheduler phải liên tục tìm task có độ ưu tiên cao nhất để thực thi. Nếu sử dụng linear search qua tất cả tasks (O(n)), mỗi context switch sẽ tốn thời gian đáng kể, phá vỡ tính real-time.

⟹ Giải pháp: Sử dụng priority list hoặc binary heap để tìm task có ưu tiên cao nhất trong O(1) hoặc O(log n).

② ISR Data Handling - UART/SPI Reception

   Khi nhận dữ liệu liên tục từ UART trong interrupt, không thể xử lý ngay trong ISR vì sẽ block toàn bộ hệ thống. Cần lưu dữ liệu vào một bộ đệm tạm thời.

⟹ Giải pháp: Circular Buffer (Ring Buffer) – cấu trúc dữ liệu cho phép push/pop dữ liệu trong O(1) mà không cần dynamic allocation.

③ Event Queue & Message Passing

   Các task cần gửi message cho nhau (ví dụ: sensor data từ ISR đến processing task). Cần một cấu trúc an toàn, có thứ tự FIFO.

⟹ Giải pháp: Queue – đảm bảo dữ liệu được xử lý đúng thứ tự, không mất mát.

④ Memory Layout & Dynamic Allocation

   Embedded systems có memory hạn chế (kilobytes), nên fragmentation là vấn đề nghiêm trọng. Cần quản lý memory pool hiệu quả.

⟹ Giải pháp: Memory pools, fixed-size allocators – tránh fragmentation, đảm bảo deterministic timing.


2. Data Structures Phổ Biến trong Embedded

📦 Circular Buffer (Ring Buffer)

Ứng dụng: UART/SPI reception, ADC sampling, audio streaming

Ưu điểm: O(1) push/pop, không cần dynamic memory, no fragmentation

Thuộc tính Giá trị
Enqueue O(1)
Dequeue O(1)
Space Complexity O(n) - fixed
Memory Type Static allocation

💡 Ví dụ Code:

#define BUFFER_SIZE 256

typedef struct {
    uint8_t buffer[BUFFER_SIZE];
    uint16_t head;
    uint16_t tail;
    uint16_t count;
} CircularBuffer;

void cbWrite(CircularBuffer* cb, uint8_t data) {
    if (cb->count < BUFFER_SIZE) {
        cb->buffer[cb->head] = data;
        cb->head = (cb->head + 1) % BUFFER_SIZE;
        cb->count++;
    }
}

uint8_t cbRead(CircularBuffer* cb) {
    uint8_t data = cb->buffer[cb->tail];
    cb->tail = (cb->tail + 1) % BUFFER_SIZE;
    cb->count--;
    return data;
}

⚡ Priority Queue (Ready List)

Ứng dụng: RTOS scheduler, interrupt prioritization

Thực hiện trong FreeRTOS: Sử dụng bitmap để track ready tasks theo priority level. Mỗi priority level là 1 bit.

Thao tác Độ phức tạp
Find highest priority task O(1) - dùng bit counting instruction
Add task to ready list O(1) - set bit
Remove task O(1) - clear bit

📊 Arrays & Fixed-Size Buffers

Ứng dụng: Lookup tables, signal processing, sensor data storage

Ưu điểm: Dễ implement, O(1) access, predictable memory usage

// PWM duty cycle lookup table
#define NUM_TEMPS 10
uint8_t pwmLookup[NUM_TEMPS] = {
    0,   10,  20,  40,  60,  
    80,  100, 120, 150, 255
};

// Access là O(1)
uint8_t duty = pwmLookup[temp / 10];

🔗 Linked Lists

Ứng dụng: Dynamic event lists, task lists khi size không biết trước

Cảnh báo: Sử dụng cẩn thận trong embedded vì dynamic allocation dễ gây fragmentation. Nên dùng static pools.


🎯 State Machines & Bit Fields

Ứng dụng: Device state management, hardware register representation

// Bitfield để represent hardware register
typedef struct {
    unsigned ready : 1;      // Bit 0
    unsigned error : 1;      // Bit 1
    unsigned busy : 1;       // Bit 2
    unsigned reserved : 5;   // Bits 3-7
} DeviceStatus;

// Tiết kiệm memory, set/check trạng thái nhanh
DeviceStatus status = {0};
status.ready = 1;  // Set ready flag
if (status.error) { /* handle error */ }

3. Các Algorithms Thực Tế cho Embedded Systems

🔍 Searching Algorithms

Linear Search - O(n): Đơn giản, dùng cho unsorted data nhỏ

int findCommand(uint8_t* commands, int size, uint8_t target) {
    for (int i = 0; i < size; i++) {
        if (commands[i] == target) return i;
    }
    return -1;
}

Binary Search - O(log n): Nhanh hơn nhiều cho sorted data

int binarySearchPWM(uint16_t* tempTable, int size, uint16_t tempValue) {
    int low = 0, high = size - 1;
    while (low <= high) {
        int mid = (low + high) / 2;
        if (tempTable[mid] == tempValue) return mid;
        if (tempTable[mid] < tempValue) low = mid + 1;
        else high = mid - 1;
    }
    return -1;
}

✅ Cyclic Redundancy Check (CRC)

Ứng dụng: Data integrity verification trong UART, CAN, SPI communication

#define CRC8_POLY 0x07

uint8_t crc8(uint8_t* data, uint8_t length) {
    uint8_t crc = 0;
    for (uint8_t i = 0; i < length; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x80) {
                crc = (crc << 1) ^ CRC8_POLY;
            } else {
                crc = crc << 1;
            }
        }
    }
    return crc;
}

🔘 Button Debouncing

Ứng dụng: Stabilize mechanical switch inputs

bool debounceButton() {
    static uint8_t count = 0;
    
    if (GPIO_ReadInputDataBit(BUTTON_PORT, BUTTON_PIN)) {
        count++;
        if (count >= 20) {  // ~20ms delay
            count = 20;
            return true;
        }
    } else {
        count = 0;
        return false;
    }
    return false;
}

⚙️ PID Control Algorithm

Ứng dụng: Motor speed control, temperature regulation, robotics

typedef struct {
    float kp, ki, kd;
    float lastError;
    float integral;
    float output;
} PIDController;

float computePID(PIDController* pid, float setpoint, float measured, float dt) {
    float error = setpoint - measured;
    
    // Proportional term
    float p = pid->kp * error;
    
    // Integral term (with anti-windup)
    pid->integral += error * dt;
    if (pid->integral > 100) pid->integral = 100;
    if (pid->integral < -100) pid->integral = -100;
    float i = pid->ki * pid->integral;
    
    // Derivative term
    float d = pid->kd * (error - pid->lastError) / dt;
    pid->lastError = error;
    
    pid->output = p + i + d;
    return pid->output;
}

📡 Signal Filtering

Moving Average Filter: Reduce noise from sensor readings

#define FILTER_SIZE 5

typedef struct {
    uint16_t buffer[FILTER_SIZE];
    uint8_t index;
    uint32_t sum;
} MovingAverageFilter;

uint16_t filterUpdate(MovingAverageFilter* filter, uint16_t newValue) {
    filter->sum -= filter->buffer[filter->index];
    filter->buffer[filter->index] = newValue;
    filter->sum += newValue;
    filter->index = (filter->index + 1) % FILTER_SIZE;
    return filter->sum / FILTER_SIZE;
}

4. Big O Notation & Performance Analysis

   Big O notation mô tả cách thời gian thực thi hoặc memory usage tăng khi input size tăng. Trong embedded, điều này cực kỳ quan trọng vì:

  • Real-time constraints: O(n) loop trong ISR có thể violation timing requirements
  • Power consumption: Thuật toán O(n²) consume gấp đôi power so với O(n)
  • Memory: Stack/heap growth phải predictable
Complexity Loại Ví dụ Embedded?
O(1) Constant Array access, bit operations ✅ Excellent
O(log n) Logarithmic Binary search, binary tree ✅ Good
O(n) Linear Loop through array ⚠️ Careful
O(n log n) Log-linear Merge sort, quicksort ⚠️ Avoid in ISR
O(n²) Quadratic Bubble sort, nested loops ❌ Avoid

Kết luận

    Data Structure và Algorithm trong embedded không phải lý thuyết trừu tượng – chúng là những công cụ thiết thực để xây dựng các hệ thống ổn định, hiệu quả và real-time. Mỗi quyết định về cấu trúc dữ liệu ảnh hưởng trực tiếp đến:

  • Latency: Context switch time, ISR execution time
  • Memory: Stack/heap usage, fragmentation risk
  • Power: CPU cycles, battery life
  • Reliability: Buffer overflow, deadlock risks

   Hãy học DSA với tư duy của một embedded engineer – không phải để giải bài toán phức tạp, mà để thiết kế hệ thống hiệu quả, deterministic và maintainable. Khi bạn hiểu rõ về circular buffers, priority queues, và complexity analysis, bạn sẽ viết firmware code chất lượng cao hơn rất nhiều.


>>>>>> Follow ngay <<<<<<<

Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//
Avatar

Đăng nhập

Người dùng mới? Đăng ký ngay

hoặc

Bằng việc tạo tài khoản, bạn đồng ý với Chính sách bảo mật & Chính sách Cookie.