Până acum am scris programe care fac un singur lucru. Un ceas inteligent trebuie să deseneze ecranul, să citească accelerometrul, să întrețină conexiunea Bluetooth și să numere secundele - simultan, pe un singur procesor, cu 64 KB de memorie și o baterie care trebuie să țină o săptămână. În această lucrare studiem cum se rezolvă asta, pe un produs real al cărui cod sursă este public integral.
1Obiectivele lucrării
- Înțelegerea diferenței dintre o buclă principală și un planificator preemptiv
- Aplicarea criteriilor de planificabilitate: utilizare, limita Rate Monotonic, EDF
- Compilarea unui firmware complet, real, din codul sursă
- Crearea unui proces FreeRTOS propriu și alegerea corectă a priorității și a stivei
- Afișarea pe ceas a datelor produse de un proces propriu și verificarea lui în lista de procese a sistemului
- Comunicarea între procese prin cozi de mesaje, în locul variabilelor globale
- Provocarea și rezolvarea inversiunii de prioritate
- Legătura dintre planificare și consum: de ce un proces prost scris golește bateria
2Obiectul lucrării
Luăm firmware-ul InfiniTime - sistemul care rulează pe ceasul PineTime - îl compilăm din sursă, îi adăugăm un proces propriu și un cadran nou, apoi îl încărcăm pe ceas prin Bluetooth. La final măsurăm ce a costat, în consum, ceea ce am adăugat.
3Materiale necesare
- 1 PineTime (varianta sigilată e suficientă)
- 1 Calculator cu Linux sau WSL2
- 1 Docker instalat
- 1 Telefon cu Gadgetbridge (Android) sau InfiniLink (iOS) - vezi secțiunea 12
- 1 Cablu de încărcare magnetic (inclus cu ceasul)
- 1 ESP32-C6 din lucrarea precedentă (pentru partea de FreeRTOS fără ceas)
4De ce nu ajunge o buclă principală
Un program de microcontroler începe aproape întotdeauna așa:
void loop() {
citeste_accelerometru(); // 2 ms
actualizeaza_ecranul(); // 30 ms <-- lung
trateaza_bluetooth(); // 5 ms
numara_secundele(); // 1 ms
}Funcționează până în clipa în care una dintre sarcini devine lentă. Cât timp actualizeaza_ecranul()
desenează, celelalte trei nu se execută. Dacă un cadru Bluetooth sosește în acest interval, se pierde. Iar dacă
ecranul are nevoie ocazional de 200 ms, ceasul rămâne în urmă vizibil.
Se poate cârpi cu mașini de stare și verificări de millis() - exact tehnica de la laboratorul de
microcontrolere. Dar la a cincea sau a șasea sarcină, codul devine imposibil de urmărit. Soluția industrială este
un planificator care întrerupe o sarcină la mijloc și dă procesorul alteia, mai urgente.
Vocabularul planificării
| Termen | Notație | Înțeles |
|---|---|---|
| Perioadă | T | la ce interval trebuie reluată sarcina |
| Durată de execuție | C | cât timp de procesor îi trebuie, în cazul cel mai defavorabil |
| Termen limită | D | până când trebuie terminată; de obicei D = T |
| Utilizare | U = Σ C/T | ce fracțiune din procesor este cerută în total |
| Preempțiune | - | întreruperea unei sarcini în curs, în favoarea uneia mai urgente |
Cele două politici clasice
| Rate Monotonic (RM) | Earliest Deadline First (EDF) | |
|---|---|---|
| Prioritatea | fixă: perioadă mai mică → prioritate mai mare | dinamică: termenul cel mai apropiat câștigă |
| Garanție | U ≤ n·(21/n − 1) | U ≤ 1 |
| Limita pentru n mare | ≈ 69 % | 100 % |
| Cost la execuție | mic - prioritățile se calculează o dată | mai mare - termenele se recompară permanent |
| Comportare la supraîncărcare | previzibilă: procesele lente cedează primele | imprevizibilă: poate rata totul |
| Folosit de | FreeRTOS, Zephyr, majoritatea sistemelor încorporate | sisteme de cercetare, unele nuclee Linux în timp real |
U este sub limita RM, sistemul este garantat planificabil. Dacă o depășește, nu înseamnă
că nu funcționează - înseamnă doar că acest test nu poate decide, iar răspunsul trebuie căutat prin simulare
sau prin analiza timpului de răspuns. Este o distincție pe care simulatorul de mai jos o face explicită.5Simulator de planificare
Trei procese periodice, un procesor. Mișcați cursoarele pentru durata de execuție și urmăriți diagrama: săgețile în sus sunt momentele de lansare, dreptunghiurile colorate sunt execuția efectivă, iar un ✕ roșu marchează un termen ratat. Comutați între cele două politici și comparați.
- Găsiți limita RM. Creșteți durata procesului „ecran” treaptă cu treaptă. La ce utilizare apare primul termen ratat? Comparați cu limita teoretică de 78 % pentru trei procese.
- Zona incertă. Aduceți utilizarea între limita RM și 100 %. Verdictul devine „testul nu poate decide”, iar simularea arată totuși zero ratări. Aceasta este dovada practică a faptului că testul este suficient, dar nu necesar.
- Superioritatea EDF. Găsiți o configurație la care RM ratează, apoi comutați pe EDF fără să schimbați nimic altceva. Aceleași procese, același procesor, alt rezultat.
6Procese FreeRTOS în practică
Codul de mai jos rulează neschimbat pe ESP32-C6 și, cu modificări minime, pe orice sistem FreeRTOS - inclusiv pe PineTime.
// Prioritatile FreeRTOS: numar mai MARE = prioritate mai MARE.
// Le alegem dupa regula Rate Monotonic: perioada mai mica, prioritate mai mare.
#define PRIO_SENZOR 3 // perioada 20 ms
#define PRIO_BLUETOOTH 2 // perioada 100 ms
#define PRIO_ECRAN 1 // perioada 200 ms
void procesSenzor(void* param) {
// vTaskDelayUntil pastreaza o perioada EXACTA, indiferent cat a durat lucrul.
TickType_t ultima = xTaskGetTickCount();
for (;;) {
citeste_accelerometru();
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(20));
}
}
void procesBluetooth(void* param) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
trateaza_bluetooth();
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(100));
}
}
void procesEcran(void* param) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
deseneaza_ecranul(); // poate dura 30 ms - va fi intrerupt de senzor
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(200));
}
}
void setup() {
Serial.begin(115200);
// functia nume stiva param prioritate descriptor
xTaskCreate(procesSenzor, "senzor", 2048, NULL, PRIO_SENZOR, NULL);
xTaskCreate(procesBluetooth, "bluetooth", 4096, NULL, PRIO_BLUETOOTH, NULL);
xTaskCreate(procesEcran, "ecran", 4096, NULL, PRIO_ECRAN, NULL);
}
void loop() {
// Bucla principala este ea insasi un proces FreeRTOS, cu prioritate 1.
// O lasam sa doarma, ca sa nu fure timp de procesor.
vTaskDelay(pdMS_TO_TICKS(1000));
}Trei greșeli care se fac aproape întotdeauna
vTaskDelay în loc de vTaskDelayUntil
vTaskDelay(20) înseamnă „așteaptă 20 ms de acum”. Dacă lucrul a durat 5 ms, perioada reală
devine 25 ms, iar eroarea se acumulează. vTaskDelayUntil se raportează la momentul precedent
de lansare și menține perioada exactă. Pentru orice sarcină periodică, a doua variantă este cea corectă.delay() într-un proces
Pe ESP32, delay() este redirectat către vTaskDelay() și se comportă cuminte.
Pe alte sisteme FreeRTOS este o buclă de așteptare activă care blochează toate procesele de prioritate mai mică
pe toată durata ei. Folosiți întotdeauna funcțiile FreeRTOS.void procesDiagnostic(void* param) {
for (;;) {
// Intoarce numarul minim de cuvinte ramase LIBERE de la pornire.
// Daca se apropie de zero, stiva este subdimensionata.
UBaseType_t liber = uxTaskGetStackHighWaterMark(NULL);
Serial.printf("stiva ramasa: %u cuvinte (%u octeti)\n",
liber, liber * sizeof(StackType_t));
vTaskDelay(pdMS_TO_TICKS(5000));
}
}7Comunicarea între procese
Două procese care scriu în aceeași variabilă globală produc coruperi de date greu de reprodus. Soluția FreeRTOS este coada de mesaje: un tampon circular protejat, în care un proces depune și altul extrage, fără ca ele să se cunoască.
typedef struct {
uint32_t moment_ms;
float x, y, z;
} Masuratoare;
QueueHandle_t coada;
void procesSenzor(void* param) {
TickType_t ultima = xTaskGetTickCount();
for (;;) {
Masuratoare m = { millis(), citesteX(), citesteY(), citesteZ() };
// Al treilea parametru este cat asteptam daca coada e plina.
// Cu 0 nu asteptam deloc: mai bine pierdem o masuratoare decat sa
// intarziem un proces de prioritate mare.
if (xQueueSend(coada, &m, 0) != pdTRUE) {
// Coada plina inseamna ca cititorul nu tine pasul - o informatie utila.
Serial.println("masuratoare pierduta - consumatorul e prea lent");
}
vTaskDelayUntil(&ultima, pdMS_TO_TICKS(20));
}
}
void procesEcran(void* param) {
Masuratoare m;
for (;;) {
// portMAX_DELAY: procesul doarme pana soseste ceva. Cat doarme,
// NU consuma timp de procesor - exact ce ne trebuie pentru baterie.
if (xQueueReceive(coada, &m, portMAX_DELAY) == pdTRUE) {
deseneaza(m.x, m.y, m.z);
}
}
}
void setup() {
Serial.begin(115200);
coada = xQueueCreate(10, sizeof(Masuratoare)); // 10 mesaje in asteptare
if (coada == NULL) {
Serial.println("memorie insuficienta pentru coada");
return;
}
xTaskCreate(procesSenzor, "senzor", 2048, NULL, 3, NULL);
xTaskCreate(procesEcran, "ecran", 4096, NULL, 1, NULL);
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }- Este atomică - nu puteți citi jumătate dintr-o structură scrisă pe jumătate.
- Nu pierde date - o variabilă globală păstrează doar ultima valoare; coada le păstrează pe toate zece.
- Blochează eficient - procesul consumator doarme fără să consume procesor, în loc să verifice în buclă.
- Vă spune când sistemul nu face față - coada plină este un simptom măsurabil, nu o defecțiune tăcută.
8Inversiunea de prioritate
Aceasta este cea mai renumită capcană a sistemelor în timp real. A pus în pericol misiunea Mars Pathfinder în 1997: roverul de pe Marte repornea singur, la intervale neregulate, iar cauza a fost exact mecanismul descris mai jos.
Cum apare
- Un proces de prioritate mică ia o resursă comună (de exemplu magistrala I²C).
- Un proces de prioritate mare are nevoie de aceeași resursă și se blochează, așteptând-o.
- Un proces de prioritate medie, care nu are nicio legătură cu resursa, devine gata de execuție și îl întrerupe pe cel de prioritate mică.
- Rezultat: procesul de prioritate medie îl întârzie, indirect, pe cel de prioritate mare. Ierarhia priorităților s-a inversat.
Provocați-o singuri
SemaphoreHandle_t resursa;
void asteapta_activ(uint32_t ms) {
// Bucla de asteptare ACTIVA: tine procesorul ocupat, ca o prelucrare reala.
uint32_t t0 = millis();
while (millis() - t0 < ms) { }
}
void procesMic(void* p) { // prioritate 1
for (;;) {
xSemaphoreTake(resursa, portMAX_DELAY);
Serial.println(" [mic] am luat resursa");
asteapta_activ(300); // lucreaza cu resursa
Serial.println(" [mic] eliberez resursa");
xSemaphoreGive(resursa);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void procesMediu(void* p) { // prioritate 2
vTaskDelay(pdMS_TO_TICKS(50)); // porneste dupa cel mic
for (;;) {
Serial.println(" [mediu] ocup procesorul 800 ms");
asteapta_activ(800); // nu atinge resursa deloc!
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void procesMare(void* p) { // prioritate 3
vTaskDelay(pdMS_TO_TICKS(100)); // cere resursa dupa cel mic
for (;;) {
uint32_t t0 = millis();
Serial.println("[MARE] cer resursa");
xSemaphoreTake(resursa, portMAX_DELAY);
Serial.printf("[MARE] am asteptat %lu ms\n", millis() - t0);
xSemaphoreGive(resursa);
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
void setup() {
Serial.begin(115200);
delay(500);
// ---- SCHIMBATI DOAR ACEASTA LINIE ----
resursa = xSemaphoreCreateBinary(); // FARA mostenire de prioritate
xSemaphoreGive(resursa); // semafoarele binare pornesc goale
// resursa = xSemaphoreCreateMutex(); // CU mostenire de prioritate
// --------------------------------------
xTaskCreate(procesMic, "mic", 2048, NULL, 1, NULL);
xTaskCreate(procesMediu, "mediu", 2048, NULL, 2, NULL);
xTaskCreate(procesMare, "mare", 2048, NULL, 3, NULL);
}
void loop() { vTaskDelay(pdMS_TO_TICKS(1000)); }xSemaphoreCreateBinary(), procesul de prioritate mare așteaptă în jur de 1000 ms: cei 300 ms
în care resursa e ocupată, plus cei 800 ms în care procesul de prioritate medie îl întrerupe pe deținător.
Cu xSemaphoreCreateMutex() așteptarea scade la aproximativ 300 ms - doar cât durează efectiv
lucrul cu resursa.Regula practică: folosiți mutex pentru excludere mutuală (protejarea unei resurse) și semafor pentru semnalizare (anunțarea unui eveniment). Sunt lucruri diferite, deși API-ul seamănă.
9Compilarea firmware-ului InfiniTime
Trecem de la exemple la un firmware complet. InfiniTime are peste o sută de mii de linii de cod și rulează pe FreeRTOS cu interfața grafică LVGL.
doc/buildAndProgram.md din depozit. Pașii de mai jos reflectă procedura obișnuită,
dar depozitul este întotdeauna sursa de adevăr.- Descărcați codul sursădescărcare
git clone https://github.com/InfiniTimeOrg/InfiniTime.git cd InfiniTime git submodule update --init --recursive
- Compilați folosind imaginea Docker oficială
Este calea recomandată: aduce cu ea compilatorul ARM și pachetul de dezvoltare Nordic, deci nu trebuie să instalați nimic altceva.
compilaredocker run --rm -it \ -v "$(pwd)":/sources \ -u $(id -u):$(id -g) \ infinitime/infinitime-build
Prima rulare durează mult, pentru că descarcă imaginea. Rezultatul apare în
build/output/- fișierul cu extensia.zipși sufixul-dfueste pachetul care se încarcă prin Bluetooth. - Explorați structura proiectuluiorientare
ls src/systemtask/ # procesul central, care le coordoneaza pe celelalte ls src/displayapp/ # procesul de afisare si aplicatiile ls src/components/ # driverele: baterie, ecran, senzori, Bluetooth grep -rn "xTaskCreate" src/ # unde se creeaza procesele grep -rn "configTOTAL_HEAP_SIZE" src/FreeRTOSConfig.h
- Alternativa fără ceas: simulatorul
InfiniSim compilează același cod al interfeței pentru calculator și îl afișează într-o fereastră, cu ecran și atingere emulate. Este calea cea mai rapidă pentru a dezvolta un cadran.
simulatorgit clone https://github.com/InfiniTimeOrg/InfiniSim.git cd InfiniSim && git submodule update --init --recursive # urmati instructiunile din README pentru dependentele SDL2
Anatomia sistemului
Odată ce codul e compilat, căutați apelurile xTaskCreate. Veți găsi un tipar clar:
| Proces | Rol | Cum este dimensionat |
|---|---|---|
SystemTask | coordonează totul: pornire, somn, evenimente | prioritate mare, stivă generoasă |
DisplayApp | desenează ecranul și tratează atingerile | cea mai mare stivă - LVGL cere memorie |
| stiva Bluetooth | întreține conexiunea radio | prioritate mare, termene stricte impuse de protocol |
| proces inactiv | rulează când nu are cine altcineva | aici se intră în starea de consum redus |
vTaskDelay, procesul inactiv nu se mai execută niciodată,
consumul crește de zece ori și bateria ține o zi în loc de o săptămână. Un singur while(1){} plasat
greșit are acest efect.10Adăugarea unui proces propriu
Adăugăm în InfiniTime un proces care numără pașii făcuți în fiecare oră a zilei, iar rezultatul îl afișăm pe ceas, în aplicația de pași. Exemplul e mic, dar pune în mișcare trei procese și ambele mecanisme de comunicare de la secțiunea 7:
- SystemTask citește accelerometrul și, când totalul pașilor se schimbă, trimite un raport printr-o coadă. Coada transportă evenimente: „s-a întâmplat ceva”.
- statistici, procesul nostru, doarme blocat pe coadă. La fiecare raport se trezește, calculează câți pași s-au adăugat și îi trece în tabelul pe ore, apoi adoarme la loc.
- DisplayApp citește tabelul când desenează ecranul. Tabelul este stare partajată, deci citirea și scrierea lui sunt protejate de o secțiune critică.
#pragma once
#include <FreeRTOS.h>
#include <task.h>
#include <queue.h>
#include <array>
#include <cstdint>
#include "components/datetime/DateTimeController.h"
namespace Pinetime {
namespace Components {
class Statistici {
public:
struct Raport {
uint8_t ora; // 0...23
uint32_t pasi; // totalul zilei, asa cum il da senzorul
};
explicit Statistici(Controllers::DateTime& dateTime) : dateTime {dateTime} {}
void Start(); // creeaza coada si procesul
void RaporteazaPasi(uint32_t pasi); // apelat din procesul de sistem
uint32_t PasiInOra(uint8_t ora); // apelat din procesul de afisare
private:
static void Proces(void* param); // functia procesului
void Bucla();
Controllers::DateTime& dateTime; // de aici luam ora curenta
TaskHandle_t descriptor = nullptr;
QueueHandle_t coada = nullptr;
uint32_t ultimulTrimis = 0; // folosit DOAR de procesul de sistem
uint32_t ultimulPrimit = 0; // folosit DOAR de procesul statistici
bool amReferinta = false; // folosit DOAR de procesul statistici
std::array<uint32_t, 24> pe_ora {}; // scris de statistici, citit de afisare
};
}
}#include "Statistici.h"
using namespace Pinetime::Components;
void Statistici::Start() {
// Coada este mica: pastram doar cateva rapoarte in asteptare.
coada = xQueueCreate(4, sizeof(Raport));
if (coada == nullptr) {
return; // nu a fost memorie nici pentru coada
}
// Stiva de 512 de cuvinte = 2 KB. Procesul nu foloseste LVGL si nu
// are tampoane mari, deci atat ii ajunge. Verificati cu
// uxTaskGetStackHighWaterMark inainte de a considera valoarea finala.
if (xTaskCreate(Proces, "statistici", 512, this,
tskIDLE_PRIORITY + 1, &descriptor) != pdPASS) {
descriptor = nullptr; // nu a fost memorie pentru stiva
}
}
void Statistici::Proces(void* param) {
static_cast<Statistici*>(param)->Bucla();
}
void Statistici::Bucla() {
Raport r;
for (;;) {
// Procesul DOARME aici pana soseste un raport. Cat asteapta nu consuma
// nimic, iar procesul inactiv poate opri ceasul procesorului.
if (xQueueReceive(coada, &r, portMAX_DELAY) != pdTRUE) {
continue;
}
// Primul raport dupa pornire da doar punctul de plecare: pasii facuti
// inainte de repornirea ceasului nu stim in ce ora au fost facuti.
if (!amReferinta) {
ultimulPrimit = r.pasi;
amReferinta = true;
continue;
}
// Senzorul da totalul zilei; diferenta fata de totalul precedent
// este numarul de pasi facuti intre timp.
uint32_t noi;
if (r.pasi >= ultimulPrimit) {
noi = r.pasi - ultimulPrimit;
} else {
// Totalul a scazut: a trecut miezul noptii si numaratorul a luat-o
// de la zero. Incepem o zi noua de statistici.
noi = r.pasi;
taskENTER_CRITICAL();
pe_ora.fill(0);
taskEXIT_CRITICAL();
}
ultimulPrimit = r.pasi;
if (r.ora < pe_ora.size()) {
taskENTER_CRITICAL(); // afisarea poate citi tabelul oricand
pe_ora[r.ora] += noi;
taskEXIT_CRITICAL();
}
}
}
void Statistici::RaporteazaPasi(uint32_t pasi) {
// Trimitem doar cand s-a schimbat ceva - altfel am umple coada degeaba.
if (coada == nullptr || pasi == ultimulTrimis) {
return;
}
Raport r {dateTime.Hours(), pasi};
// Fara asteptare: daca coada e plina, raportul se pierde. Nu pierdem insa
// pasi - urmatorul raport contine din nou totalul zilei.
if (xQueueSend(coada, &r, 0) == pdTRUE) {
ultimulTrimis = pasi;
}
}
uint32_t Statistici::PasiInOra(uint8_t ora) {
if (ora >= pe_ora.size()) {
return 0;
}
taskENTER_CRITICAL();
uint32_t valoare = pe_ora[ora];
taskEXIT_CRITICAL();
return valoare;
}- Prioritatea
tskIDLE_PRIORITY + 1- cea mai mică peste procesul inactiv. Statisticile nu au termen limită; nu au voie să întârzie desenarea ecranului sau stiva Bluetooth. - Stiva de 512 de cuvinte - valoarea de pornire, care trebuie verificată cu
uxTaskGetStackHighWaterMark. Pe un sistem cu 64 KB, fiecare kilooctet alocat degeaba este luat de la altcineva. Secțiunea 11 vă arată unde se vede pe ceas. - Procesul așteaptă pe coadă, nu se trezește periodic -
xQueueReceivecuportMAX_DELAYîl scoate complet din planificare până sosește un raport. Cât stați pe loc, procesul nu rulează deloc; o buclă cuvTaskDelays-ar trezi degeaba de mii de ori pe zi. - Timp de așteptare zero la trimitere - procesul de sistem are prioritate mai mare și nu are voie să fie blocat de o coadă plină aparținând unuia neimportant. Dacă un raport se pierde, nu se pierd și pașii: fiecare raport conține totalul zilei, deci următorul recuperează tot.
- Fiecare variabilă are un singur proprietar -
ultimulTrimise folosit doar de procesul de sistem,ultimulPrimitdoar de procesul statistici. Singurul lucru folosit de două procese este tabelulpe_ora, și numai el este protejat de secțiunea critică.
configASSERT este un macro care, în versiunea compilată pentru ceas, dispare cu totul.
O linie ca BaseType_t r = xTaskCreate(...); configASSERT(r == pdPASS); lasă în urmă o variabilă
care nu mai este citită nicăieri, iar proiectul compilează cu opțiunea -Werror, care transformă
orice avertisment în eroare: compilarea se oprește cu unused variable. Mai grav, verificarea
dispare și ea. De aceea rezultatul se testează explicit, cu if, iar dacă ceva nu s-a putut
crea, procesul rămâne pur și simplu inactiv în loc să blocheze ceasul.11Conectarea procesului și recompilarea
Cele două fișiere din secțiunea anterioară nu fac încă nimic. Dacă le copiați în proiect și
compilați, compilarea reușește fără nicio eroare, ceasul pornește, iar procesul nu există. Motivul: compilatorul
nu știe că fișierul nou trebuie compilat, nimeni nu creează obiectul și nu îi apelează Start(), iar
nimic nu afișează rezultatul. Urmează patru pași, apoi recompilarea.
.cpp care nu este trecut în
CMakeLists.txt nu produce nicio eroare: pur și simplu nu este compilat. Dacă ați modificat codul
și pe ceas nu se schimbă nimic, verificați întâi acest pas.undefined referenceMesajul vine de la editorul de
legături (ld), nu de la compilator: fiecare fișier s-a compilat corect, dar la final cineva
apelează o funcție al cărei cod nu a fost compilat în acel executabil. Priviți ce țintă eșuează - numele
apare în linia CMakeFiles/pinetime-recovery.dir/.... Cele câteva rânduri
dangerous relocation care o însoțesc sunt doar consecințe ale aceleiași funcții lipsă.- Înregistrați fișierul nou în CMake
Deschideți
src/CMakeLists.txtși adăugați calea fișierului.cpp, relativă lasrc/, lângă celelalte componente. Fișierul.hnu se trece în listă. Atenție: linia trebuie pusă în două liste, nu într-una.Pe lângă firmware-ul obișnuit, InfiniTime compilează și un firmware de recuperare - o variantă minimă, încărcată de bootloader dacă aplicația principală nu mai pornește. Și el conține
SystemTask.cpp, deci și el apeleazăStart()șiRaporteazaPasi(). Lista lui de surse se numeșteRECOVERY_SOURCE_FILES. Dacă adăugați fișierul doar înSOURCE_FILES, compilarea trece, dar legarea firmware-ului de recuperare se oprește cuundefined reference to Pinetime::Components::Statistici::Start().src/CMakeLists.txt# 1. firmware-ul obisnuit list(APPEND SOURCE_FILES ... components/datetime/DateTimeController.cpp components/statistici/Statistici.cpp # <-- linia adaugata ... ) # 2. firmware-ul de recuperare - mai jos in acelasi fisier list(APPEND RECOVERY_SOURCE_FILES ... components/datetime/DateTimeController.cpp components/statistici/Statistici.cpp # <-- aceeasi linie, inca o data ... )
- Creați obiectul în procesul de sistem
Procesul nostru are nevoie de controllerul de dată și oră ca să știe ora curentă (vedeți constructorul din
Statistici.h).SystemTaskare deja o referință la el, deci obiectul se declară acolo. Ordinea contează: membrii unei clase se construiesc în ordinea în care sunt declarați, decistatisticitrebuie declarat dupădateTimeController. FuncțiaGetStatistici()îi dă aplicației de pași acces la obiect, la pasul 4.Atenție unde o puneți: imediat sub
enum class SystemTaskStateîncepe declarația constructoruluiSystemTask(...), care se întinde pe vreo 20 de rânduri. Dacă funcția ajunge înăuntrul listei lui de parametri, compilatorul raportează sute de erori (expected ')' before '{' token, apoiuninitialized reference memberpentru fiecare membru), deși greșeala este o singură linie. Puneți-o dupăPushMessage, ca mai jos.src/systemtask/SystemTask.h#include "systemtask/Messages.h" #include "components/statistici/Statistici.h" // <-- adaugat // ... in sectiunea public, IMEDIAT DUPA declaratia lui PushMessage. // ATENTIE: nu in lista de parametri a constructorului SystemTask(...), // care ocupa vreo 20 de randuri chiar deasupra si se termina cu ");" void Start(); void PushMessage(Messages msg); Pinetime::Components::Statistici& GetStatistici() { return statistici; } // ... in sectiunea private, IMEDIAT DUPA linia cu dateTimeController: Pinetime::Controllers::DateTime& dateTimeController; Pinetime::Components::Statistici statistici {dateTimeController}; - Porniți procesul și trimiteți-i datele
Start()se apelează o singură dată, la pornire, după celelalte inițializări dinSystemTask::Work(). Pașii vin din funcția care citește accelerometrul,UpdateMotion()- acolo îi trimitem mai departe prin coadă.src/systemtask/SystemTask.cppvoid SystemTask::Work() { // ... initializarile existente ... motionController.Init(motionSensor.DeviceType()); settingsController.Init(); statistici.Start(); // <-- porneste procesul nostru // ... } void SystemTask::UpdateMotion() { // ... auto motionValues = motionSensor.Process(); motionController.Update(motionValues.x, motionValues.y, motionValues.z, motionValues.steps); statistici.RaporteazaPasi(motionValues.steps); // <-- totalul zilei, catre coada // ... restul functiei ramane neschimbat ... } - Afișați rezultatul în aplicația de pași
Aplicația Steps (pictograma cu pantof) primește obiectul prin
AppControllers, care are deja un pointer spreSystemTask, și adaugă un rând nou în partea de sus a ecranului. FuncțiaRefresh()rulează în procesul de afișare de zece ori pe secundă cât timp aplicația este deschisă - de aici se citește tabelul prinPasiInOra().În
Steps.hsunt patru modificări, marcate (a)–(d). Cea mai ușor de uitat este prima,#include-ul de sus. Fără ea compilatorul nu știe ce esteStatisticiși raportează'Pinetime::Components::Statistici' has not been declared, apoiinvalid use of incomplete type 'class Pinetime::System::SystemTask'- adică știe căSystemTaskexistă, dar nu i-a văzut încă definiția.src/displayapp/screens/Steps.h// (a) SUS, la includeri - fara ea, Steps.h nu stie ce e Statistici // si nici ce contine SystemTask: #include "Symbols.h" #include "systemtask/SystemTask.h" // <-- adaugat // (b) constructorul primeste doua referinte in plus: Steps(Controllers::MotionController& motionController, Controllers::Settings& settingsController, Components::Statistici& statistici, Controllers::DateTime& dateTime); // (c) in sectiunea private, sub settingsController: Controllers::Settings& settingsController; Components::Statistici& statistici; Controllers::DateTime& dateTime; lv_obj_t* lOra; // (d) iar in AppTraits<Apps::Steps>, functia Create devine: static Screens::Screen* Create(AppControllers& controllers) { return new Screens::Steps(controllers.motionController, controllers.settingsController, controllers.systemTask->GetStatistici(), controllers.dateTimeController); };src/displayapp/screens/Steps.cppSteps::Steps(Controllers::MotionController& motionController, Controllers::Settings& settingsController, Components::Statistici& statistici, Controllers::DateTime& dateTime) : motionController {motionController}, settingsController {settingsController}, statistici {statistici}, dateTime {dateTime} { // ... tot codul existent al constructorului ... // eticheta noua, in partea de sus a ecranului - chiar inainte de lv_task_create: lOra = lv_label_create(lv_scr_act(), nullptr); lv_obj_set_style_local_text_color(lOra, LV_LABEL_PART_MAIN, LV_STATE_DEFAULT, Colors::orange); lv_label_set_text_static(lOra, ""); lv_obj_align(lOra, nullptr, LV_ALIGN_IN_TOP_MID, 0, 25); taskRefresh = lv_task_create(RefreshTaskCallback, 100, LV_TASK_PRIO_MID, this); } void Steps::Refresh() { // ... codul existent ... lv_arc_set_value(stepsArc, int16_t(500 * stepsCount / settingsController.GetStepsGoal())); uint8_t ora = dateTime.Hours(); lv_label_set_text_fmt(lOra, "Ora %02d: %lu", int(ora), statistici.PasiInOra(ora)); lv_obj_align(lOra, nullptr, LV_ALIGN_IN_TOP_MID, 0, 25); } - Recompilați
Exact aceeași comandă ca la secțiunea 9. Compilarea este incrementală: se recompilează doar fișierele modificate și cele care depind de ele, deci durează de obicei sub un minut, nu cât prima dată.
recompilare# din radacina depozitului InfiniTime docker run --rm -it \ -v "$(pwd)":/sources \ -u $(id -u):$(id -g) \ infinitime/infinitime-build # unde a aparut pachetul pentru telefon: find build -name "*-dfu*.zip"
Dacă apare o eroare, citiți prima eroare din listă - celelalte sunt de obicei consecințe ale ei. Dacă ați modificat
CMakeLists.txtși compilarea pare să ignore schimbarea, ștergeți directorulbuild/și compilați din nou de la zero.
grep -l "Statistici" build/src/*.map.
Trebuie să apară atât pinetime-app, cât și pinetime-recovery. Dacă nu apare nimic,
fișierul nu a fost compilat - reveniți la pasul 1.- În aplicația Steps apare sus un rând portocaliu,
Ora 14: 0. Primul raport după pornire stabilește doar punctul de plecare, deci numărătoarea începe cu primii pași făcuți după repornire. Mergeți câteva zeci de pași și redeschideți aplicația: valoarea crește. La ora fixă rândul trece la ora nouă și pornește de la zero, iar la miezul nopții tot tabelul se golește. - În Settings → About, pe ecranul 4 (lista proceselor FreeRTOS) apare un rând nou,
sta. Numele e tăiat la trei caractere pentru că InfiniTime seteazăconfigMAX_TASK_NAME_LENla 4 - la fel aparMAIșidis. Coloana S arată aproape mereu B (Blocked): procesul doarme pe coadă, exact cum am vrut. Coloana Free arată câte cuvinte din stivă nu au fost folosite niciodată - cu ea răspundeți la întrebarea de la secțiunea 10: ce valoare ați alege în locul lui 512?
lab03-statistici-ro.patch. Se aplică pe
InfiniTime 1.16.0, din rădăcina depozitului: întâi git apply --check lab03-statistici-ro.patch
(verifică fără să modifice nimic), apoi git apply lab03-statistici-ro.patch. Folosiți-o ca să comparați,
după ce ați încercat singuri - git diff vă arată exact unde diferă codul vostru.SOURCE_FILES: ecranele se înregistrează în src/displayapp/apps/CMakeLists.txt
și se activează la compilare, adăugând comenzii Docker -e ENABLE_USERAPPS="..." sau
-e ENABLE_WATCHFACES="...". Citiți doc/code/Apps.md din depozit pentru pașii exacți ai
versiunii pe care o compilați.12Încărcarea pe ceas
Ceasul se actualizează prin Bluetooth, fără fire și fără programator. Bootloaderul păstrează versiunea anterioară și revine la ea automat dacă noul firmware nu confirmă că a pornit corect. Procedura diferă puțin între Android și iOS - alegeți coloana care se aplică telefonului vostru.
- Încărcați ceasul complet
O actualizare întreruptă de o baterie descărcată este singurul mod realist de a strica un PineTime. Bateria trebuie să fie peste 50 %.
- Instalați aplicația companion potrivită telefonului
Android iOS (iPhone / iPad) Aplicație Gadgetbridge InfiniLink Sursă F-Droid (gratuită, open source) App Store (gratuită, open source) Ce mai poate face notificări, vreme, control muzică ceas/dată, baterie, puls, pași, muzică Apple, HealthKit Un singur nume de evitat: nRF Connect for iOS Este aplicația care apare cel mai des în căutări pentru actualizat plăci Nordic, dar pentru PineTime nu mai funcționează: versiunile ulterioare 4.24.3 au rupt compatibilitatea, iar App Store instalează mereu ultima versiune. Nu există cale simplă de a instala varianta veche care mai mergea. Folosiți InfiniLink. - Transferați pachetul
.zippe telefonFișierul se află în
build/output/și are sufixul-dfu. Nu îl dezarhivați - formatul arhivat este cel pe care îl așteaptă bootloaderul. Pe iPhone, cel mai simplu e să-l trimiteți spre aplicația Fișiere (AirDrop de pe calculator, sau descărcare directă în Safari) - InfiniLink îl poate încărca direct din acolo. - Porniți actualizarea
Android (Gadgetbridge): conectați ceasul, apoi
Firmware/App installși alegeți fișierul.
iOS (InfiniLink): conectați ceasul din ecranul principal, apoi din meniul de firmware alegeți „Update from file” și selectați arhiva.zipdin Fișiere. Alternativ, InfiniLink poate verifica singur pagina de versiuni InfiniTime de pe GitHub și propune direct ultima variantă, fără să mai descărcați nimic manual.
Transferul durează câteva minute pe orice platformă. Nu îndepărtați telefonul de ceas în acest timp. - Confirmați noua versiune
După repornire, ceasul cere confirmarea că firmware-ul funcționează. Dacă nu confirmați, bootloaderul revine la versiunea anterioară la următoarea repornire - o plasă de siguranță elegantă, care merită studiată ca mecanism în sine.
13Sarcini de lucru
- Folosind simulatorul de la secțiunea 5, determinați utilizarea maximă la care setul de trei procese rămâne planificabil sub RM. Comparați cu limita teoretică și explicați diferența.
- Găsiți o configurație la care RM ratează termene iar EDF nu. Copiați ambele diagrame în referat și explicați exact ce decizie diferită ia EDF.
- Încărcați
procese_baza.inope ESP32-C6. ModificațiprocesEcransă dureze 250 ms (mai mult decât perioada lui) și descrieți ce se întâmplă cu celelalte procese. - Adăugați
procesDiagnosticși notați stiva rămasă pentru fiecare proces. Reduceți alocările până la limita sigură și calculați câtă memorie ați recuperat. - Rulați
inversiune.inoîn ambele variante. Notați timpul de așteptare al procesului de prioritate mare în fiecare caz și explicați diferența prin mecanismul de moștenire a priorității. - Compilați InfiniTime. Găsiți toate apelurile
xTaskCreateși realizați un tabel cu procesele sistemului: nume, prioritate, dimensiunea stivei și rolul fiecăruia. - Căutați în
FreeRTOSConfig.hopțiuneaconfigUSE_TICKLESS_IDLEși explicați, pe baza codului, ce se întâmplă atunci când toate procesele dorm. - Adăugați procesul
statisticiurmând secțiunile 10 și 11, încărcați firmware-ul pe ceas și fotografiați ecranul aplicației Steps înainte și după câteva zeci de pași. - În Settings → About, ecranul 4, notați valoarea din coloana Free pentru procesul
sta. Alegeți o dimensiune a stivei care păstrează o rezervă de 25 % față de cât s-a folosit efectiv, recompilați și verificați din nou. Câți octeți de RAM ați recuperat față de 512 cuvinte? - Explicați de ce pierderea unui raport din coadă nu duce la pași pierduți. Ce s-ar întâmpla dacă
RaporteazaPasi()ar trimite doar diferența față de raportul precedent, în loc de totalul zilei?
14Aplicație de aprofundare
Partea interesantă nu este desenul, ci bugetul de reîmprospătare. Fiecare redesenare a ecranului trezește procesorul și costă energie. Măsurați, cu
uxTaskGetStackHighWaterMark și cu numărătorul de treziri:
- De câte ori pe minut se redesenează cadranul dumneavoastră?
- Care este numărul minim de redesenări necesar ca să afișeze ora corectă?
- Cu cât se prelungește autonomia dacă redesenați doar cifra care s-a schimbat, în loc de tot ecranul?
PasiInOra(0) ... PasiInOra(23). Indicii: un obiect lv_chart de tip
LV_CHART_TYPE_COLUMN cu 24 de puncte; ecranul se înregistrează ca aplicație de utilizator
(doc/code/Apps.md) și se activează prin ENABLE_USERAPPS.
Două întrebări la care trebuie să răspundeți în referat: De ce nu este nevoie de o coadă nouă pentru acest ecran? Și cât de des trebuie reîmprospătat graficul, dacă datele se schimbă cel mult la câteva secunde - sunt necesare cele 10 reîmprospătări pe secundă pe care le face aplicația Steps?