Sistemele integrate necesită adesea gestionarea datelor locale fără a fi incluse în lista de servere complete. Implementarea unui motor de bază de date ușor în C oferă dezvoltatorilor control direct asupra memoriei, performanței și stocării. Acest articol trece prin proiectarea și implementarea unui motor simplu de bază de date încorporat, acoperind structurile de date, operațiunile CRUD, indexarea și strategiile de persistență pentru mediile cu resurse limitate.

Cerințe de bază pentru un motor de bază încorporat

Un motor de baza de date încorporat trebuie să funcționeze în limite stricte pe RAM, flash, și viteza de procesare. Cerințele tipice includ comportament determinist, amprenta de cod minim, și nici dependențe externe. Motorul ar trebui să sprijine operațiuni de bază: inserați, prelua, actualiza, ștergeți și de căutare. Multe baze de date încorporate trebuie, de asemenea, să supraviețuiască pierderii de putere și stoca date pe memorie non-volatilă, cum ar fi EPROM, SPI flash, sau carduri SD.

Alegerea structurii corecte de date este prima decizie de proiectare. Array-urile sunt simple dar limitate de dimensiuni statice. Listele conectate permit o creștere dinamică, dar adaugă pointer deasupra capului. Pentru performanță echilibrată, o abordare hibridă folosind piscine fixe cu o listă liberă poate funcționa bine. SQLite's design principles oferă perspective utile chiar și pentru motoare mult mai simple.

Proiectarea stratului de stocare a înregistrărilor

Stratul de stocare gestionează modul în care înregistrările sunt prezentate în memorie sau pe disc. Un model comun este de a trata fiecare înregistrare ca o structură de lungime fixă pentru a simplifica aritmetica pointer și a permite indexarea directă. Înregistrările de lungime variabilă complica fragmentarea și necesită un manager de memorie.

Înregistrări fixe-Length cu o piscină de înregistrări

Defineşte un număr maxim de înregistrări (de exemplu, ) şi alocă un array static. Un bitmap sau o listă liberă de piese care sunt utilizate sloturi. Când un record este şters, slotul său revine la piscină. Această abordare evită alocarea dinamică şi garantează timpul de alocare O(1).

#define MAX_RECORDS 256
typedef struct {
 int id;
 char name[32];
 float value;
 int active; // 1 if slot in use
} Record;

Record pool[MAX_RECORDS];

Pentru depozitarea persistentă, piscina poate fi susținută de un fișier sau o regiune de flash. La pornire, motorul citește piscina din memorie non-volatilă în RAM, și pe închidere (sau periodic) o scrie înapoi.

Verificarea integrității datelor

Adăugați un câmp simplu de control la fiecare înregistrare pentru a detecta corupția. CRC-32] este o alegere bună pentru sistemele integrate, echilibrând complexitatea cu puterea de detectare a erorilor.

Punerea în aplicare a operațiunilor de bază CRUD

Cu piscina de înregistrare definite, implementa funcţii pentru a introduce, găsi, actualiza şi şterge înregistrări. Operaţiunile de căutare sunt adesea blocajul performanţă, astfel încât o căutare liniară naiv este acceptabil doar pentru baze de date mici (câteva sute de înregistrări).

Inserează cu Managementul Liberului List

Menţineţi o listă liberă de indici. Pe inserare, pop un index din lista liberă, umpleţi înregistrarea, şi marcaţi-l activ. Lista liberă în sine poate fi un stiva simplu folosind un array de numere întregi.

int free_list[MAX_RECORDS];
int free_count = MAX_RECORDS;
for (int i = 0; i < MAX_RECORDS; i++) free_list[i] = i;

int db_insert(int id, const char* name, float value) {
 if (free_count == 0) return -1; // no space
 int idx = free_list[--free_count];
 pool[idx].id = id;
 strncpy(pool[idx].name, name, sizeof(pool[idx].name)-1);
 pool[idx].value = value;
 pool[idx].active = 1;
 return idx;
}

Caută și actualizează

O simplă căutare iterează peste piscină, verificând doar înregistrările active. Pentru actualizări, localizați înregistrarea, modificați câmpurile și reverificați opțional lista liberă dacă înregistrarea este ștersă.

int db_find_by_id(int id) {
 for (int i = 0; i < MAX_RECORDS; i++) {
 if (pool[i].active && pool[i].id == id) return i;
 }
 return -1;
}

void db_update(int idx, float new_value) {
 if (idx >= 0 && idx < MAX_RECORDS && pool[idx].active)
 pool[idx].value = new_value;
}

Concepte avansate: Indexare și Persistență

Pe măsură ce numărul de înregistrări crește, căutarea liniară devine costisitoare. Adăugând un index simplu ? Ca o gamă sortate de indicii cheie sau un copac binar de căutare ?i timpul de recuperare. Pentru sistemele încorporate, un array static sortate pe chei cu căutare binară este adesea suficient dacă inserțiile sunt rare.

Index sortat cu căutare binară

Menţineţi o matrice paralelă de indici de înregistrare sortaţi de tasta de căutare (de exemplu, ID). Când introduceţi un nou record, introduceţi indexul în array-ul sortate folosind o inserţie binară. Apoi căutarea devine O(log n) prin căutare binară. Deleţiile necesită schimbarea array-ului index, dar pentru baze de date mici acest lucru este acceptabil.

Stocare persistentă utilizând fișierul I/O

Pe microcontrolere fără sistem de fișiere, memoria flash brută scrie sunt comune. Pe sistemele integrate bazate pe Linux, standard POSIX // funcționează bine. Utilizați un format simplu de fișier: scrieți un antet (număr magic, versiune, număr de înregistrare) urmat de matricea de piscină brută. Pentru rezistența la accident, un jurnal de lucru (WAL) poate ajuta, dar pentru motoarele de bază este suficient un singur scriere atomică (care se potrivește într-o singură pagină flash). FreertOS+FAT este o opțiune de sistem de fișiere ușoare pentru sisteme adânc încorporate.

void db_save(const char* filename) {
 FILE* fp = fopen(filename, "wb");
 if (!fp) return;
 fwrite(pool, sizeof(pool), 1, fp);
 fclose(fp);
}

void db_load(const char* filename) {
 FILE* fp = fopen(filename, "rb");
 if (!fp) return;
 fread(pool, sizeof(pool), 1, fp);
 fclose(fp);
 // Rebuild free-list from pool
 free_count = 0;
 for (int i = 0; i < MAX_RECORDS; i++) {
 if (!pool[i].active) free_list[free_count++] = i;
 }
}

Gestionarea constrângerilor și a compromisurilor

Motoarele de baze de date integrate se confruntă cu un compromis constant între caracteristici și utilizarea resurselor. Alegerea caracteristicilor care să includă depinde de aplicație:

  • Concordanța ACID
  • Indexing
  • Contracte
  • Amprenta memoriei
  • Power loss

Exemplu practic: o bază de date logger de temperatură

Considerați un senzor de temperatură IoT care înregistrează fiecare minut și le stochează local timp de 24 de ore. Motorul bazei de date trebuie să mânuiască 1440 de înregistrări (unu pe minut). Fiecare înregistrare ar putea conține o marcă de timp (o epocă Unix), o temperatură (de pe linia de plutire) și un ID de senzor. Folosind piscina fixă cu 256 de sloturi este prea mică; aici avem nevoie de . Cu 28 de octeți pe înregistrare (4+4+4+4 pentru cheltuieli generale), piscina folosește aproximativ 40 KB, fezabilă pe multe microcontrolere cu 128 KB RAM.

Motorul poate stoca date într-un mod inel-buffer: atunci când piscina este plin, cel mai vechi record este suprascris. Implementa un "cap" pointer pentru următorul slot de scriere și un "coada" pentru cel mai vechi record activ. Aceasta evită logica de listă liberă și oferă O(1) inserare. Căutarea poate fi optimizată cu o căutare binară pe timbrul temporal dacă înregistrările sunt stocate în ordine cronologică.

typedef struct {
 uint32_t timestamp;
 float temp_c;
 uint8_t sensor_id;
 uint8_t active; // not needed if using ring buffer
} TemperatureRecord;

#define MAX_LOGS 1440
TemperatureRecord logs[MAX_LOGS];
uint16_t head = 0; // next write position
uint16_t count = 0; // number of valid records

Se introduce o lectură: scrie la ], increment modulo , increment (captat la . Căutarea unui anumit timbru temporal: dacă numărul == MAX LOGS, jurnalul este o secvență contiguă de la cap la cap-1 (împachetare). Se utilizează o căutare binară după calcularea startului virtual. Acest model este extrem de ușor și utilizat pe scară largă în sistemele de telemetrie. ]Bază de bază a tamponului de apel oferă opțiuni suplimentare de implementare.

Testarea și optimizarea hardware-ului țintă

Întotdeauna testați motorul de bază pe hardware-ul real încorporat. Emulatorii pierd constrângerile de sincronizare, în special pentru cicluri de scriere flash și scenariile de pierdere a puterii. Monitorizează utilizarea RAM cu un instrument de profilare și verifică cazurile de margine: stocare completă, date corupte, resetare mijlocul de scriere. Un ham simplu de testare ruleaza mii de inserții aleatoare, căutări, și șterge în timp ce se compară cu un model de aur.

  • Nivelizare uzură de fulgi
  • Power-fail safe
  • Memorie de punere în comun

Concluzie

Construirea unui motor baza de date de bază în C pentru sisteme integrate este o abordare practică pentru gestionarea datelor în dispozitivele limitate de resurse. Concentrându-se pe structuri simple de date, cum ar fi bazine fixe de înregistrare și tampoane inelare, dezvoltatorii realizează operațiuni eficiente CRUD cu cheltuieli minime. Adăugarea indexare opțională și persistență de bază transformă un simplu array într-un magazin de date locale fiabile. Tehnicile descrise aici scara de la mici jurnali senzori la sisteme mai sofisticate, și ei oferă o bază pentru înțelegerea modului mai mari baze de date încorporate cum ar fi SQLite sau Berkeley DB funcționează sub capota.