Programación de sistemas de tiempo real con Ada — Control práctico de prioridad, periodo y tiempo de ejecución
· Actualizado el: · Go Komura · Ada, RealTime, Ravenscar, CeilingLocking, Tasking, Scheduling, PriorityInversion, ProgrammingLanguage, Tiempo real, Alta fiabilidad
1. Introducción — la profunda relación entre Ada y el tiempo real
En el artículo anterior, «Concurrencia segura en Ada», explicamos los fundamentos de la concurrencia segura mediante tareas y objetos protegidos de Ada. En este artículo avanzamos hacia una extensión de ese tema, un ámbito con restricciones aún más estrictas: los sistemas de tiempo real.
En un sistema de tiempo real, la «corrección» no solo significa que el resultado del cálculo sea lógicamente correcto, sino también que ese resultado se obtenga dentro del plazo establecido. Una respuesta correcta que llega un milisegundo tarde es tan peligrosa como una respuesta incorrecta.
Ante este requisito, Ada ofrece un conjunto integral de funciones de tiempo real estandarizadas como el Annex D (Real-Time Systems) de la especificación del lenguaje. No se trata de «una biblioteca añadida a posteriori», sino de garantías de tiempo real integradas en el propio runtime del lenguaje.
Funciones de tiempo real de Ada (Annex D):
- Prioridad de tareas y apropiación (FIFO_Within_Priorities)
- Protocolo Ceiling_Locking (prevención de la inversión de prioridad)
- Ejecución periódica en tiempo absoluto con delay until
- Perfil Ravenscar (subconjunto para seguridad crítica)
- Eventos de temporización (activación por tiempo sin sondeo)
- Monitorización del tiempo de ejecución (Ada.Execution_Time)
- Planificación multiperiódica
En este artículo explicamos estas funciones paso a paso mediante 8 ejemplos prácticos de código. Cada fragmento puede tratarse como un ejemplo independiente, pero los ejemplos 04/05, que contienen varias unidades de compilación, se dividen con gnatchop antes de compilarlos con gnatmake.
Público destinatario y conocimientos previos: se asume que el lector conoce los fundamentos de tareas, rendez-vous y objetos protegidos tratados en el artículo anterior. Está orientado a desarrolladores interesados en el software de control de dispositivos embebidos y sistemas de alta fiabilidad; no se trata de una introducción a la sintaxis de Ada en sí.
Entorno de verificación: se ha confirmado que los 8 ejemplos de este artículo compilan con GNAT 13.3.0 (Ubuntu 24.04, x86-64). Los resultados de ejecución mostrados en el texto (capítulos 3 y 5) se obtuvieron en el mismo entorno. Cómo se aplican realmente la prioridad y la apropiación depende del sistema operativo y del runtime de GNAT (capítulo 12), por lo que los detalles de la salida pueden variar según el entorno.
Los fragmentos de código que aparecen en este artículo están organizados por capítulos en un repositorio de referencia publicado en GitHub.
ada-real-time-systems - komurasoft-blog-samples (GitHub)
2. Qué es un sistema de tiempo real
Empecemos ordenando la terminología.
| Concepto | Descripción |
|---|---|
| Tiempo real estricto (hard real-time) | Superar el plazo supone un fallo fatal del sistema (control de vuelo, airbags, marcapasos) |
| Tiempo real flexible (soft real-time) | Superar el plazo no es deseable, pero se toleran excesos ocasionales (streaming de vídeo, videojuegos) |
| Deadline (plazo) | Instante absoluto en el que una tarea debe haberse completado |
| Periodo (period) | Intervalo de tiempo con el que se activa repetidamente una tarea |
| WCET (Worst-Case Execution Time) | Tiempo de ejecución en el peor caso de una tarea |
| Jitter | Variabilidad de la ejecución periódica |
| Planificabilidad (schedulability) | Propiedad de que «ese conjunto de tareas pueda ejecutarse cumpliendo todos los plazos». Verificar esto sobre el papel es el análisis de planificabilidad (análisis de tiempo de respuesta, etc.) |
| Perfil Ravenscar | Convención que restringe las funciones de tareas de Ada a un subconjunto fácil de analizar estáticamente. El nombre proviene de Ravenscar, el pueblo inglés donde se celebró la reunión en la que se definió (capítulo 6) |
En el diseño de sistemas de tiempo real, es un requisito necesario importante que se cumpla, para cada tarea, «WCET <= deadline». Sin embargo, eso por sí solo no garantiza que el sistema completo cumpla los plazos en su conjunto. También hace falta un análisis de tiempo de respuesta que tenga en cuenta el tiempo de bloqueo, la asignación de prioridades, el jitter, las interrupciones y el comportamiento del runtime y del sistema operativo. En la práctica, se busca WCET < deadline para dejar margen. Las funciones de tiempo real de Ada proporcionan, a nivel de lenguaje, un modelo de ejecución predecible que facilita ese análisis.
flowchart LR
HRT["Tiempo real estricto"] -->|"incumplir el plazo = fallo fatal"| Examples["Control de vuelo<br/>Airbags<br/>Marcapasos"]
SRT["Tiempo real flexible"] -->|"se toleran excesos ocasionales"| Examples2["Streaming de vídeo<br/>Videojuegos<br/>UI"]
Ada["Ada Annex D<br/>mecanismos que sostienen la predictibilidad"] --> Mechanism["FIFO_Within_Priorities<br/>Ceiling_Locking<br/>delay until"]
subgraph Requirements["Requisitos de tiempo real"]
D["Deadline<br/>instante absoluto de finalización"]
P["Periodo<br/>intervalo de repetición"]
W["WCET<br/>tiempo de ejecución en el peor caso"]
J["Jitter<br/>variabilidad del periodo"]
end
D --> Analysis["Análisis de planificabilidad"]
P --> Analysis
W --> Analysis
J --> Analysis
HRT --> Analysis
SRT --> Analysis
Mechanism --> Analysis
Analysis --> Constraint["Requisito necesario: WCET <= deadline<br/>la suficiencia se confirma con análisis de tiempo de respuesta"]
Figura 1: Requisitos de tiempo real y el papel de los mecanismos de Ada Annex D en el análisis de planificabilidad.
Uno de los fenómenos más peligrosos en los sistemas de tiempo real es la inversión de prioridad. Este problema ocurrió realmente en 1997 en la sonda Mars Pathfinder y provocó reinicios repetidos de la nave.
sequenceDiagram
participant S as Planificador
participant L as Tarea de baja prioridad
participant H as Tarea de alta prioridad
participant M as Tarea de prioridad media
participant R as Recurso compartido
L->>R: Adquiere el bloqueo
activate L
Note over L: Ejecutando la sección crítica
Note over S,L: El despertar de H hace que el planificador suspenda a L
deactivate L
activate H
H->>R: Intenta adquirir el bloqueo
Note over H: Bloqueada esperando el bloqueo (L lo mantiene)
deactivate H
Note over S,L: Como H espera el bloqueo, se reanuda L
activate L
Note over L: Continúa hacia la liberación del bloqueo...
Note over S,L: El despertar de M hace que el planificador suspenda a L
deactivate L
activate M
Note over L: L no puede liberar el bloqueo
Note over M: M sigue ejecutándose (ni H ni L pueden avanzar)
Note over H: [Inversión de prioridad] la de alta prioridad queda bloqueada indefinidamente
deactivate M
Figura 2: Secuencia de una inversión de prioridad clásica: la tarea de baja prioridad es apropiada por una de prioridad media mientras mantiene el bloqueo que necesita la de alta prioridad.
Una tarea de baja prioridad, mientras mantiene un bloqueo, es apropiada por una tarea de prioridad media, y la tarea de alta prioridad queda bloqueada indefinidamente. La medida realmente aplicada en Mars Pathfinder fue activar la herencia de prioridad (priority inheritance) de VxWorks, pero Ada ofrece, para el mismo tipo de problema, un enfoque distinto: Ceiling_Locking, integrado como característica del lenguaje.
3. Fundamentos de la prioridad de tareas — FIFO_Within_Priorities
FIFO_Within_Priorities es la política estándar de despacho basada en prioridad que puede especificarse en el Annex D de Ada. Cuando no se indica ninguna política de forma explícita, el comportamiento por defecto queda definido por la implementación, pero en GNAT muchas plataformas usan una política de este tipo. Dentro de la misma prioridad se ejecuta en orden FIFO (primero en entrar, primero en salir), y una tarea de mayor prioridad apropia (interrumpe) a las tareas de menor prioridad.
-- 01_task_priority.ada
-- Prioridad de tareas y forma básica de FIFO_Within_Priorities
-- Los pragmas de configuración se colocan antes de las cláusulas de contexto
pragma Task_Dispatching_Policy (FIFO_Within_Priorities);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Task_Priority_Demo is
task High_Priority_Task is
pragma Priority (Priority'Last);
pragma Storage_Size (4 * 1024);
end High_Priority_Task;
task Low_Priority_Task is
pragma Priority (Priority'First);
pragma Storage_Size (4 * 1024);
end Low_Priority_Task;
task body High_Priority_Task is
begin
Put_Line ("[T=0.0s] High priority task started");
delay until Clock + Milliseconds (100);
Put_Line ("[T=0.1s] High priority task completed");
end High_Priority_Task;
task body Low_Priority_Task is
begin
Put_Line ("[T=0.0s] Low priority task started");
delay until Clock + Milliseconds (500);
Put_Line ("[T=0.5s] Low priority task completed");
end Low_Priority_Task;
begin
Put_Line ("=== Task Priority Demo (FIFO_Within_Priorities) ===");
Put_Line ("Main: waiting for tasks to complete...");
delay until Clock + Milliseconds (800);
Put_Line ("Main: done");
end Task_Priority_Demo;
Puntos clave:
pragma Priorityasigna una prioridad estática a cada tarea.Priority'Lastes la más alta yPriority'Firstla más baja.- El cuerpo de las tareas de esta demo no realiza cálculos pesados; simplemente espera hasta un instante indicado con
delay until. Lo que se quiere confirmar aquí es que, cuando ambas quedan listas al mismo tiempo, la tarea de mayor prioridad obtiene primero la oportunidad de ejecutarse. - Este diagrama muestra el aspecto de la apropiación entre distintas prioridades dentro de
FIFO_Within_Priorities. Para confirmar el orden FIFO dentro de una misma prioridad haría falta otro ejemplo con varias tareas de igual prioridad. - En sistemas reales es habitual diseñar las prioridades relativas tomando
System.Default_Prioritycomo referencia.
sequenceDiagram
participant S as Planificador
participant Main as Tarea principal
participant HP as Tarea alta prioridad<br/>(Priority=Last)
participant LP as Tarea baja prioridad<br/>(Priority=First)
Main->>HP: Crea la tarea
Main->>LP: Crea la tarea
Note over HP,LP: T=0ms: ambas tareas están runnable
S->>HP: Selecciona HP, de mayor prioridad
activate HP
Note over HP: Escribe el registro de inicio
HP->>S: Se bloquea con delay until T+100ms
deactivate HP
S->>LP: A continuación ejecuta LP
activate LP
Note over LP: Escribe el registro de inicio
LP->>S: Se bloquea con delay until T+500ms
deactivate LP
Note over S: T=100ms: despierta HP
S->>HP: Ejecuta HP
activate HP
Note over HP: Escribe el registro de finalización
deactivate HP
Note over S: T=500ms: despierta LP
S->>LP: Ejecuta LP
activate LP
Note over LP: Escribe el registro de finalización
deactivate LP
Note over Main: (T=800ms) finaliza la tarea principal
Figura 3: Traza de planificación de la demo de prioridad de tareas: la tarea de alta prioridad se ejecuta primero en cada instante de despertar compartido.
Ejemplo de ejecución (GNAT 13.3.0 / Ubuntu 24.04, x86-64):
$ gnatchop -w 01_task_priority.ada . # → task_priority_demo.adb
$ gnatmake task_priority_demo.adb
$ ./task_priority_demo
[T=0.0s] High priority task started
[T=0.0s] Low priority task started
=== Task Priority Demo (FIFO_Within_Priorities) ===
Main: waiting for tasks to complete...
[T=0.1s] High priority task completed
[T=0.5s] Low priority task completed
Main: done
Observe que los dos registros de inicio de las tareas aparecen antes que la línea de cabecera de la tarea principal. Las tareas declaradas se activan antes de entrar en el cuerpo del subprograma que las contiene, por lo que este orden puede darse. Además, el orden de estas dos líneas de inicio puede intercambiarse en cada ejecución. Como se indica en el capítulo 12, la forma en que pragma Priority se traduce realmente en la planificación depende del sistema operativo y del runtime de GNAT, y en un Linux de propósito general no está garantizado el orden de arranque según la prioridad. Lo que sí se observa de forma estable en este ejemplo es el orden de finalización a los 0,1 y 0,5 segundos indicados con delay until.
Rango de prioridades de Ada (valores por defecto de GNAT):
Priority'First = 0 (la más baja)
Priority'Last = 30 (la más alta, aunque depende del sistema operativo)
4. Ceiling_Locking — el lenguaje evita la inversión de prioridad
Uno de los problemas más complicados en los sistemas de tiempo real es la inversión de prioridad (priority inversion). Es el fenómeno en el que una tarea de alta prioridad espera un bloqueo que mantiene una tarea de baja prioridad, y esta última, al ser apropiada por una tarea de prioridad media, provoca que la tarea de alta prioridad quede bloqueada indefinidamente.
Ante este problema, Ada integra directamente en los objetos protegidos el protocolo Ceiling_Locking.
-- 02_ceiling_locking.ada
-- Prevención de la inversión de prioridad mediante el protocolo Ceiling_Locking
-- Los pragmas de configuración se colocan antes de las cláusulas de contexto
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Ceiling_Locking_Demo is
Ceiling : constant System.Any_Priority := System.Any_Priority'Last;
protected Shared_Data is
pragma Priority (Ceiling);
procedure Write (V : Integer);
function Read return Integer;
private
Value : Integer := 0;
end Shared_Data;
protected body Shared_Data is
procedure Write (V : Integer) is
begin
Value := V;
end Write;
function Read return Integer is
begin
return Value;
end Read;
end Shared_Data;
task Producer is
pragma Priority (Priority'Last);
pragma Storage_Size (4 * 1024);
end Producer;
task Consumer is
pragma Priority (Priority'First);
pragma Storage_Size (4 * 1024);
end Consumer;
task body Producer is
begin
Put_Line ("[T=0.0s] Producer (high prio): about to write");
Shared_Data.Write (42);
Put_Line ("[T=0.0s] Producer (high prio): write done");
delay until Clock + Milliseconds (100);
end Producer;
task body Consumer is
begin
delay until Clock + Milliseconds (10);
Put_Line ("[T=0.01s] Consumer (low prio): about to read");
declare
V : Integer;
begin
V := Shared_Data.Read;
Put_Line ("[T=0.01s] Consumer (low prio): read done, got" &
Integer'Image (V));
end;
delay until Clock + Milliseconds (100);
end Consumer;
begin
Put_Line ("=== Ceiling_Locking Demo ===");
Put_Line ("Main: producer priority = Last, consumer priority = First");
Put_Line ("Ceiling = Any_Priority'Last, locking = Ceiling_Locking");
delay until Clock + Milliseconds (300);
Put_Line ("Main: done");
end Ceiling_Locking_Demo;
Cómo funciona Ceiling_Locking:
- Al objeto protegido se le asigna una prioridad techo con
pragma Priority (Ceiling). - Cualquier tarea que entre en el objeto protegido asciende automáticamente a la prioridad techo en el momento de entrar.
- Gracias a esto, mientras una tarea usa el objeto protegido, ninguna tarea de prioridad media puede apropiarla.
- Al salir del objeto protegido, la tarea vuelve a su prioridad original.
El siguiente diagrama no es una traza temporal exacta del código de ejemplo anterior, sino un esquema conceptual que muestra cómo Ceiling_Locking contiene el patrón de inversión de prioridad de la figura 2.
sequenceDiagram
participant S as Planificador
participant L as Tarea baja prioridad<br/>(prioridad=10)
participant M as Tarea prioridad media<br/>(prioridad=20)
participant H as Tarea alta prioridad<br/>(prioridad=30)
participant PO as Objeto protegido<br/>(prioridad techo=30)
Note over PO: Si la prioridad activa del llamante > prioridad techo, se produce Program_Error<br/>en el diagrama, H(30) puede entrar porque iguala el techo (30)
L->>PO: Entra en la operación protegida
activate L
Note over L,PO: La prioridad de ejecución asciende a 30
Note over S: Despierta M
Note over S,L: L se ejecuta con la prioridad techo 30<br/>M(20) no puede apropiarla
Note over S: Despierta H
Note over S,H: H(30) supera la comprobación del techo<br/>pero espera porque L usa el PO
L->>PO: Ejecuta la operación
L->>PO: Sale de la operación protegida
deactivate L
Note over L: La prioridad vuelve a 10
Note over S,H: Tras liberarse el PO, se ejecuta H
activate H
H->>PO: Entra en la operación protegida
Note over H,PO: H(30) = techo(30), por lo que puede entrar tras resolverse la contención
H->>PO: Sale de la operación protegida
deactivate H
Figura 4: Cómo Ceiling_Locking evita la inversión de prioridad ascendiendo automáticamente a la prioridad techo a la tarea que entra en el objeto protegido.
Pauta de diseño: la prioridad techo del objeto protegido debe fijarse en un valor igual o superior a la mayor prioridad de todas las tareas que usan ese objeto protegido. Si se incumple esto y una tarea con una prioridad activa superior a la prioridad techo invoca una operación protegida, Ada permite detectar el error de diseño mediante
Program_Error.
Para lograr algo equivalente con mutexes de pthread en C hace falta configurar explícitamente el atributo PTHREAD_PRIO_PROTECT, mientras que en Ada esto es una característica estándar del lenguaje.
5. delay until — ejecución periódica de tareas sin deriva
El patrón básico de un sistema de tiempo real es la tarea periódica. En una tarea que se ejecuta repetidamente a intervalos fijos, es fundamental evitar el error de temporización acumulado (deriva o drift).
El delay until de Ada resuelve este problema con elegancia.
-- 03_periodic_task.ada
-- Tarea periódica con delay until — evita la deriva acumulada
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Periodic_Task_Demo is
Period_MS : constant Time_Span := Milliseconds (200);
Cycles : constant Positive := 5;
task Sensor_Reader is
pragma Priority (Priority'Last - 2);
pragma Storage_Size (4 * 1024);
end Sensor_Reader;
task body Sensor_Reader is
Start_Time : constant Time := Clock;
Next_Release : Time := Start_Time + Period_MS;
Cycle_Count : Natural := 0;
begin
Put_Line ("[Sensor] Periodic task starts, period=" &
To_Duration (Period_MS)'Image & "s, cycles=" &
Natural'Image (Cycles));
for I in 1 .. Cycles loop
delay until Next_Release;
Cycle_Count := Cycle_Count + 1;
Put_Line ("[Sensor] Cycle" & Natural'Image (Cycle_Count) &
" at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
Next_Release := Next_Release + Period_MS;
end loop;
Put_Line ("[Sensor] Periodic task finished. Actual elapsed:" &
Duration'Image (To_Duration (Clock - Start_Time)) & "s");
end Sensor_Reader;
begin
Put_Line ("=== Periodic Task Demo (delay until) ===");
Put_Line ("Main: waiting for" & Natural'Image (Cycles) & " cycles...");
delay until Clock + Milliseconds (1500);
Put_Line ("Main: done");
end Periodic_Task_Demo;
Por qué usar delay until:
| Método | Problema |
|---|---|
delay Period; |
El tiempo de procesamiento de cada iteración se acumula y el periodo se desplaza progresivamente (deriva acumulada) |
delay until Next_Release; Next_Release := Next_Release + Period; |
Al tomar como referencia un tiempo absoluto, el siguiente instante de activación es correcto aunque un procesamiento se retrase |
Sin embargo, delay until no garantiza automáticamente que el tiempo de procesamiento quepa dentro del periodo. Si un procesamiento supera el siguiente instante de despertar, ese delay until retorna casi de inmediato, y el sistema entra en un estado que debería tratarse como deadline miss.
Con delay:
T=0ms → proceso(15ms) → delay 100ms → T=115ms → proceso(10ms) → ...
Intervalo real: 115ms, 110ms, ... (el tiempo de procesamiento se acumula)
Con delay until:
Next_Release: 100ms, 200ms, 300ms, ... (tiempo absoluto)
T=0ms → proceso(15ms) → delay until 100ms → T=100ms → proceso(10ms) → delay until 200ms
Intervalo real: 100ms, 100ms, ... (no depende del tiempo de procesamiento)
Ejemplo de ejecución (GNAT 13.3.0 / Ubuntu 24.04, x86-64):
$ gnatchop -w 03_periodic_task.ada . # → periodic_task_demo.adb
$ gnatmake periodic_task_demo.adb
$ ./periodic_task_demo
[Sensor] Periodic task starts, period= 0.200000000s, cycles= 5
=== Periodic Task Demo (delay until) ===
Main: waiting for 5 cycles...
[Sensor] Cycle 1 at 0.200326876s
[Sensor] Cycle 2 at 0.400159550s
[Sensor] Cycle 3 at 0.600183089s
[Sensor] Cycle 4 at 0.800327451s
[Sensor] Cycle 5 at 1.000260472s
[Sensor] Periodic task finished. Actual elapsed: 1.000297359s
Main: done
Cada ciclo presenta un retraso de despertar de unos 0,2 a 0,3 milisegundos, pero se aprecia que ese retraso no se traslada al periodo siguiente. Incluso en el quinto ciclo, la desviación respecto al instante de referencia es inferior a un milisegundo. Si se hubiera escrito con delay Period;, ese retraso se habría ido acumulando en cada ciclo y tras 5 ciclos habría producido una diferencia visible. Cabe señalar que estas cifras corresponden a un ejemplo en un Linux de propósito general y no son valores garantizados en un entorno de tiempo real estricto.
Este patrón con delay until se usará en el resto de tareas periódicas de este artículo.
flowchart TB
subgraph Bad["delay Period - deriva acumulada"]
B1["T=0ms: cálculo 15ms"] --> B2["delay 100ms → despierta a 115ms"]
B2 --> B3["cálculo 10ms → 125ms"]
B3 --> B4["delay 100ms → despierta a 225ms"]
B4 --> B5["Intervalo real: 115ms, 110ms..."]
end
subgraph Good["delay until - basado en tiempo absoluto"]
G1["próximo = T+100ms"] --> G2["cálculo 15ms"]
G2 --> G3["delay until T+100ms → despierta a 100ms"]
G3 --> G4["cálculo 10ms"]
G4 --> G5["próximo = T+200ms → despierta a 200ms"]
G5 --> G6["Intervalo real: 100ms, 100ms..."]
end
subgraph Overrun["Exceso de periodo - deadline miss"]
O1["próximo = T+100ms"] --> O2["cálculo 130ms"]
O2 --> O3["delay until T+100ms retorna casi de inmediato"]
O3 --> O4["se detecta el retraso y se trata como sobrecarga"]
end
Bad --> Drift["El error se acumula con el tiempo"]
Good --> Stable["Evita la deriva acumulada"]
Good --> Overrun
Figura 5: Comparación entre delay y delay until en la ejecución periódica, y el caso en que el procesamiento supera el periodo (deadline miss).
6. El perfil Ravenscar — un subconjunto de tiempo real verificable
Las funciones de tareas de Ada son potentes, pero en sistemas de seguridad extremadamente crítica esa potencia se convierte en un problema. La creación dinámica de tareas, la sentencia select, la sentencia abort y similares dificultan el análisis estático del tiempo de ejecución en el peor caso.
El perfil Ravenscar es la respuesta que Ada ofrece a este problema. Restringe las funciones de tareas a un subconjunto estáticamente analizable y determinista.
-- 04_ravenscar_profile.ada
-- Forma básica del perfil Ravenscar
-- En tiempo de compilación se especifica pragma Profile (Ravenscar); en gnat.adc
-- Compilación: gnatchop -w 04_ravenscar_profile.ada .
-- → se divide en ravenscar_state.ads / ravenscar_state.adb / ravenscar_demo.adb
-- preparar gnat.adc y luego gnatmake ravenscar_demo.adb
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
package Ravenscar_State is
protected Signal is
pragma Priority (System.Default_Priority + 5);
entry Wait_For_Release;
procedure Release;
private
Released : Boolean := False;
end Signal;
task Periodic_Worker is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Periodic_Worker;
task Monitor is
pragma Priority (System.Default_Priority);
pragma Storage_Size (4 * 1024);
end Monitor;
end Ravenscar_State;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
package body Ravenscar_State is
protected body Signal is
entry Wait_For_Release when Released is
begin
Released := False;
end Wait_For_Release;
procedure Release is
begin
Released := True;
end Release;
end Signal;
task body Periodic_Worker is
Start_Time : constant Time := Clock;
Next_Release : Time := Start_Time + Milliseconds (100);
Period : constant Time_Span := Milliseconds (100);
Cycle_Count : Natural := 0;
begin
Put_Line ("[Worker] Ravenscar periodic task starts");
for I in 1 .. 4 loop
delay until Next_Release;
Cycle_Count := Cycle_Count + 1;
Put_Line ("[Worker] Cycle" & Natural'Image (Cycle_Count) &
" at" & Duration'Image (To_Duration (Clock - Start_Time)) & "s");
Signal.Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Worker] Finished demo, waiting (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Periodic_Worker;
task body Monitor is
begin
Put_Line ("[Monitor] Waiting for signals...");
for I in 1 .. 4 loop
Signal.Wait_For_Release;
Put_Line ("[Monitor] Received signal" & Natural'Image (I));
end loop;
Put_Line ("[Monitor] All signals received, waiting (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Monitor;
end Ravenscar_State;
with Ravenscar_State; use Ravenscar_State;
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Real_Time; use Ada.Real_Time;
procedure Ravenscar_Demo is
begin
Put_Line ("=== Ravenscar Profile Demo ===");
Put_Line ("(compile with: gnatmake -gnatec=gnat.adc ravenscar_demo)");
Put_Line ("Main: waiting for Ravenscar tasks...");
delay until Clock + Milliseconds (800);
Put_Line ("Main: demo window elapsed; waiting forever (Ravenscar: No_Task_Termination)");
loop
delay until Clock + Seconds (1);
end loop;
end Ravenscar_Demo;
Restricciones del perfil Ravenscar:
| Función prohibida | Motivo |
|---|---|
Creación dinámica de tareas (new o tipos de acceso) |
La asignación de memoria en tiempo de ejecución no es determinista |
Sentencia select |
No solo las alternativas múltiples, sino la sentencia select en su conjunto dificulta el análisis del flujo de control |
Sentencia abort |
La interrupción asíncrona genera imprevisibilidad de estado |
Ada.Task_Attributes |
Comportamiento dinámico en tiempo de ejecución |
| Cambio dinámico de prioridad | Las premisas del análisis de planificación cambian en tiempo de ejecución |
delay relativo |
Tiende a generar deriva acumulada; se usa el delay until de tiempo absoluto |
| Varias entradas por objeto protegido | Aumentan las condiciones de bloqueo y los elementos a analizar |
| Terminación de tareas | En Ravenscar todas las tareas se tratan como no terminables |
Sentencia requeue |
Complica el seguimiento del flujo de control |
Esta restricción hace que un programa conforme a Ravenscar adopte una forma fácil de someter a análisis estático de temporización. Es una propiedad exigida por normas de seguridad como DO-178C (software aeronáutico) o ISO 26262 (seguridad funcional automotriz). La lista anterior es un extracto de las principales restricciones; el perfil real incluye además reglas adicionales relacionadas con el runtime y la analizabilidad, como No_Task_Hierarchy o Detect_Blocking.
flowchart TB
Full["Funciones completas de tareas de Ada"] --> Profile["Perfil Ravenscar"]
Profile --> Restrict["Restricciones"]
Profile --> Policy["Políticas obligatorias"]
Restrict --> R1["Prohibición de creación dinámica de tareas"]
Restrict --> R2["Prohibición de la sentencia select"]
Restrict --> R3["Prohibición de la sentencia abort"]
Restrict --> R4["Prohibición de Task_Attributes"]
Restrict --> R5["Límite de 1 entrada por objeto protegido"]
Restrict --> R6["Prohibición de la sentencia requeue"]
Restrict --> R7["Prohibición del delay relativo<br/>usar delay until"]
Restrict --> R8["Prohibición del cambio dinámico de prioridad"]
Restrict --> R9["Prohibición de la terminación de tareas<br/>todas las tareas no terminables"]
Policy --> P1["FIFO_Within_Priorities"]
Policy --> P2["Ceiling_Locking"]
Restrict --> Benefit["Lo que facilita:<br/>análisis estático de temporización"]
Policy --> Benefit
Benefit --> DO178["DO-178C<br/>software aeronáutico"]
Benefit --> ISO26262["ISO 26262<br/>seguridad funcional automotriz"]
Benefit --> IEC62304["IEC 62304<br/>software de dispositivos médicos"]
Figura 6: Restricciones y políticas obligatorias del perfil Ravenscar, y las normas de seguridad que su analizabilidad ayuda a cumplir.
Para activar el perfil Ravenscar, escriba lo siguiente en el archivo gnat.adc:
pragma Profile (Ravenscar);
7. Eventos de temporización — activación por tiempo sin sondeo
En muchos sistemas de tiempo real aparece con frecuencia el requisito de «despertar una tarea de alta prioridad cuando llegue un instante determinado». En una implementación ingenua esto se resolvería sondeando un temporizador, pero Ada ofrece un mecanismo más elegante: los eventos de temporización.
-- 05_timing_events.ada
-- Eventos de temporización (Ada.Real_Time.Timing_Events)
-- Mecanismo para despertar tareas de alta prioridad sin sondeo
-- Compilación: gnatchop -w 05_timing_events.ada .
-- → se divide en signal_pkg.ads / signal_pkg.adb / timing_events_demo.adb
-- gnatmake timing_events_demo.adb
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
package Signal_Pkg is
protected type Signal_Type is
pragma Priority (System.Interrupt_Priority'Last);
entry Wait_For_Event;
procedure Fire (Event : in out Timing_Event);
private
Fired : Boolean := False;
end Signal_Type;
S : Signal_Type;
end Signal_Pkg;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
package body Signal_Pkg is
protected body Signal_Type is
entry Wait_For_Event when Fired is
begin
Fired := False;
end Wait_For_Event;
procedure Fire (Event : in out Timing_Event) is
begin
Fired := True;
end Fire;
end Signal_Type;
end Signal_Pkg;
with Signal_Pkg; use Signal_Pkg;
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Real_Time.Timing_Events; use Ada.Real_Time.Timing_Events;
procedure Timing_Events_Demo is
pragma Priority (29);
Timer_1 : Timing_Event;
Timer_2 : Timing_Event;
task Reactor is
pragma Priority (System.Default_Priority + 5);
pragma Storage_Size (4 * 1024);
end Reactor;
task body Reactor is
begin
Put_Line ("[Reactor] Waiting for timing events...");
S.Wait_For_Event;
Put_Line ("[Reactor] Got event #1");
S.Wait_For_Event;
Put_Line ("[Reactor] Got event #2");
Put_Line ("[Reactor] Done");
end Reactor;
begin
Put_Line ("=== Timing Events Demo ===");
Put_Line ("Scheduling two timers at +100ms and +250ms...");
Set_Handler (Timer_1, Clock + Milliseconds (100), S.Fire'Access);
Set_Handler (Timer_2, Clock + Milliseconds (250), S.Fire'Access);
delay until Clock + Milliseconds (500);
Put_Line ("Main: done");
end Timing_Events_Demo;
Funcionamiento de los eventos de temporización:
1. Set_Handler(Timer_1, T+100ms, S.Fire'Access) ── registra un manejador para un tiempo absoluto
2. Transcurridos T+100ms ── el runtime invoca S.Fire **con la prioridad techo**
3. Fire pone la bandera Fired a True ── se abre la barrera
4. La tarea Reactor despierta desde Wait_For_Event
Lo importante es que, en este ejemplo, se especifica explícitamente Ceiling_Locking y, como el manejador Fire es un procedimiento de un objeto protegido, se ejecuta con la prioridad techo. El procedimiento protegido que se usa como manejador de un evento de temporización debe colocarse en un objeto protegido con una prioridad techo de nivel de interrupción, en este caso System.Interrupt_Priority'Last. Gracias a ello, no se produce inversión de prioridad durante el procesamiento del evento de temporización.
8. Colas de tiempo real mediante objetos protegidos
Un patrón habitual en los sistemas de tiempo real es el de productor-consumidor. Un sensor genera datos y una tarea de control los consume; en este caso es necesario gestionar de forma eficiente la exclusión mutua y el bloqueo del búfer.
Con los objetos protegidos y las barreras de entrada de Ada, esto puede implementarse como sincronización basada en barreras. Como el runtime gestiona internamente la exclusión mutua, no hace falta escribir directamente mutexes ni variables de condición en el código de la aplicación.
-- 06_protected_queue.ada
-- Compartición de datos de tiempo real mediante un objeto protegido
-- Pipeline: Producer -> Bounded_Buffer -> Consumer
-- En tiempo de compilación se especifica pragma Locking_Policy (Ceiling_Locking); en gnat.adc
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Protected_Queue_Demo is
Buffer_Size : constant := 4;
type Buf_Array is array (1 .. Buffer_Size) of Integer;
protected Bounded_Buffer is
pragma Priority (System.Any_Priority'Last);
entry Put (Item : Integer);
entry Get (Item : out Integer);
private
Buf : Buf_Array;
Count : Natural := 0;
Head : Positive := 1;
Tail : Positive := 1;
end Bounded_Buffer;
protected body Bounded_Buffer is
entry Put (Item : Integer) when Count < Buffer_Size is
begin
Buf (Tail) := Item;
Tail := (Tail mod Buffer_Size) + 1;
Count := Count + 1;
end Put;
entry Get (Item : out Integer) when Count > 0 is
begin
Item := Buf (Head);
Head := (Head mod Buffer_Size) + 1;
Count := Count - 1;
end Get;
end Bounded_Buffer;
task Producer is
pragma Priority (System.Default_Priority + 2);
pragma Storage_Size (4 * 1024);
end Producer;
task Consumer is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Consumer;
task body Producer is
Next_Release : Time := Clock + Milliseconds (50);
Period : constant Time_Span := Milliseconds (50);
begin
for I in 1 .. 6 loop
Bounded_Buffer.Put (I);
Put_Line ("[Producer] Put" & Integer'Image (I));
delay until Next_Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Producer] Done");
end Producer;
task body Consumer is
Item : Integer;
Next_Release : Time := Clock + Milliseconds (80);
Period : constant Time_Span := Milliseconds (80);
begin
delay until Clock + Milliseconds (30);
for I in 1 .. 6 loop
Bounded_Buffer.Get (Item);
Put_Line ("[Consumer] Got" & Integer'Image (Item));
delay until Next_Release;
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Consumer] Done");
end Consumer;
begin
Put_Line ("=== Protected Queue Demo (Ceiling_Locking) ===");
Put_Line ("Buffer size = 4; Producer every 50ms, Consumer every 80ms");
delay until Clock + Milliseconds (800);
Put_Line ("Main: done");
end Protected_Queue_Demo;
Puntos clave del diseño:
entry Put when Count < Buffer_Size— si el búfer está lleno, el Producer se bloquea automáticamente.entry Get when Count > 0— si el búfer está vacío, el Consumer se bloquea automáticamente.pragma Priority (System.Any_Priority'Last)— gracias al bloqueo por techo, no se produce inversión de prioridad entre Producer y Consumer.- La condición de barrera se define a partir del estado interno del objeto protegido (
Count) y se reevalúa automáticamente al liberarse el bloqueo.
En este código no aparecen mutexes, semáforos ni variables de condición del lado de la aplicación. La espera necesaria se expresa mediante las barreras de entrada del objeto protegido.
stateDiagram-v2
Empty: Vacío / Count=0
Partial: Parcial / Count=1..Buffer_Size-1
Full: Lleno / Count=Buffer_Size
[*] --> Empty: Estado inicial
Empty --> Partial: Put (se añade 1 elemento)
Partial --> Partial: Put / Get
Partial --> Empty: Get (se retira el último elemento)
Partial --> Full: Put (se ocupa el último hueco)
Full --> Partial: Get (se libera un hueco)
Empty --> Empty: Get se bloquea (barrera Count=0)
Full --> Full: Put se bloquea (barrera Count=Buffer_Size)
Figura 7: Estados del búfer acotado y transiciones provocadas por Put y Get; las barreras Count=0 y Count=Buffer_Size bloquean Get y Put respectivamente.
Cuando Put tiene éxito se reevalúa la barrera de Get, y cuando Get tiene éxito se reevalúa la barrera de Put. Esto ocurre al completarse la operación protegida, con independencia del estado en que se encuentre el diagrama.
9. Medición del tiempo de ejecución — primer paso hacia la monitorización
Para evaluar la planificabilidad de un sistema de tiempo real hace falta conocer con precisión el tiempo de ejecución (tiempo de CPU) de cada tarea. El paquete Ada.Execution_Time de Ada proporciona el tiempo de consumo de CPU por tarea.
-- 07_execution_time.ada
-- Control del tiempo de ejecución (Execution_Time)
-- Mide el tiempo de consumo de CPU de cada tarea
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
with Ada.Execution_Time;
use type Ada.Execution_Time.CPU_Time;
procedure Execution_Time_Demo is
package ET renames Ada.Execution_Time;
task Busy_Worker is
pragma Priority (System.Default_Priority + 1);
pragma Storage_Size (4 * 1024);
end Busy_Worker;
task body Busy_Worker is
Wall_Start : Time;
Cpu_Start : ET.CPU_Time;
Dummy : Integer := 0;
pragma Volatile (Dummy);
begin
Wall_Start := Clock;
Cpu_Start := ET.Clock;
Put_Line ("[Worker] Starting compute-bound work...");
for I in 1 .. 20_000_000 loop
Dummy := Dummy + 1;
end loop;
Put_Line ("[Worker] Dummy =" & Integer'Image (Dummy));
declare
Wall_Elapsed : constant Duration :=
To_Duration (Clock - Wall_Start);
Cpu_Span : constant Time_Span :=
ET.Clock - Cpu_Start;
begin
Put_Line ("[Worker] Done, wall time:" &
Duration'Image (Wall_Elapsed) & "s");
Put_Line ("[Worker] CPU time consumed:" &
Duration'Image (To_Duration (Cpu_Span)) & "s");
end;
end Busy_Worker;
Cpu_Start_Main : constant ET.CPU_Time := ET.Clock;
begin
Put_Line ("=== Execution Time Demo ===");
delay until Clock + Milliseconds (500);
declare
Cpu_Span : constant Time_Span := ET.Clock - Cpu_Start_Main;
begin
Put_Line ("Main: CPU time consumed after 500ms:" &
Duration'Image (To_Duration (Cpu_Span)) & "s");
end;
Put_Line ("Main: done");
end Execution_Time_Demo;
Tiempo de reloj de pared frente a tiempo de CPU:
Tiempo de reloj de pared (Wall Clock): Ada.Real_Time.Clock
→ Tiempo real transcurrido. Incluye el tiempo bloqueado o apropiado.
Tiempo de CPU (Execution Time): Ada.Execution_Time.Clock
→ Solo el tiempo en que esa tarea realmente se ejecutó en la CPU.
→ No se cuenta el tiempo bloqueado ni apropiado.
Esta distinción es el punto de partida de la monitorización del tiempo de ejecución y de la verificación del WCET. Mientras Busy_Worker espera con delay until, el tiempo de CPU no aumenta; solo aumenta durante el procesamiento de cálculo real. Durante el delay until Clock + Milliseconds(500) de la tarea principal, el tiempo de CPU también debería ser prácticamente cero. Sin embargo, la medición del tiempo de CPU no garantiza el verdadero WCET. El WCET, que incluye efectos de caché, del pipeline y de contención de memoria, requiere análisis estático y verificación por separado en el entorno de destino.
flowchart LR
subgraph Wall["Tiempo de reloj de pared"]
W1["Tiempo total transcurrido: 500ms"] --> W2["Desglose: cálculo + espera + bloqueo + apropiación"]
end
subgraph CPU["Tiempo de CPU"]
C1["Tiempo de CPU total: 120ms"] --> C2["Desglose: solo el cálculo real"]
end
Wall --> Diff["Diferencia = tiempo de espera, bloqueo y apropiación"]
CPU --> Diff
Diff --> Insight["El tiempo de CPU observa el coste de cálculo real<br/>es un apoyo para la verificación y monitorización del WCET<br/>excluye el tiempo de espera, bloqueo y apropiación"]
Insight --> Caveat["Advertencia<br/>la medición no garantiza el verdadero WCET<br/>hace falta análisis estático y verificación en el entorno de destino"]
Figura 8: Relación entre tiempo de reloj de pared y tiempo de CPU, y su papel como apoyo (no garantía) para la verificación del WCET.
10. Demo integral — sistema de tiempo real multiperiódico
Integramos ahora todos los elementos vistos hasta aquí —prioridad, Ceiling_Locking, delay until y objetos protegidos— para construir un sistema de tiempo real multiperiódico típico.
-- 08_multiperiodic.ada
-- Demo integral de un sistema de tiempo real multiperiódico
-- Tarea de lectura de sensor de periodo rápido (100ms)
-- Tarea de control de periodo lento (400ms)
-- Compartición de datos mediante Ceiling_Locking
pragma Locking_Policy (Ceiling_Locking);
with Ada.Text_IO; use Ada.Text_IO;
with System; use System;
with Ada.Real_Time; use Ada.Real_Time;
procedure Multiperiodic_Demo is
package Int_IO is new Ada.Text_IO.Integer_IO (Integer);
protected Shared_Sensor is
pragma Priority (System.Any_Priority'Last);
procedure Write (V : Integer);
function Read return Integer;
private
Value : Integer := 0;
end Shared_Sensor;
protected body Shared_Sensor is
procedure Write (V : Integer) is
begin
Value := V;
end Write;
function Read return Integer is
begin
return Value;
end Read;
end Shared_Sensor;
task Fast_Sensor is
pragma Priority (System.Default_Priority + 3);
pragma Storage_Size (4 * 1024);
end Fast_Sensor;
task body Fast_Sensor is
Next_Release : Time := Clock + Milliseconds (100);
Period : constant Time_Span := Milliseconds (100);
Cycle : Natural := 0;
begin
Put_Line ("[Fast] Sensor reader starts (100ms period)");
for I in 1 .. 12 loop
delay until Next_Release;
Cycle := Cycle + 1;
Shared_Sensor.Write (Cycle * 10);
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Fast] Done");
end Fast_Sensor;
task Slow_Controller is
pragma Priority (System.Default_Priority + 2);
pragma Storage_Size (4 * 1024);
end Slow_Controller;
task body Slow_Controller is
Next_Release : Time := Clock + Milliseconds (150);
Period : constant Time_Span := Milliseconds (400);
Cycle : Natural := 0;
Raw : Integer;
begin
Put_Line ("[Slow] Controller starts (400ms period)");
for I in 1 .. 3 loop
delay until Next_Release;
Cycle := Cycle + 1;
Raw := Shared_Sensor.Read;
Put_Line ("[Slow] Cycle" & Natural'Image (Cycle) &
" reads sensor =" & Integer'Image (Raw));
Next_Release := Next_Release + Period;
end loop;
Put_Line ("[Slow] Done");
end Slow_Controller;
begin
Put_Line ("=== Multiperiodic Real-Time System Demo ===");
Put_Line ("Fast sensor (100ms) x 12 + Slow controller (400ms) x 3");
Put_Line ("Ceiling_Locking prevents priority inversion on shared data");
delay until Clock + Milliseconds (2000);
Put_Line ("Main: done");
end Multiperiodic_Demo;
Estructura del sistema:
El siguiente diagrama es un ejemplo de planificación que combina los instantes de release del código de ejemplo (el sensor rápido tiene un periodo de 100 ms y el control lento un periodo de 400 ms con un offset de 150 ms) con tiempos de ejecución supuestos con fines ilustrativos. Como el código en sí no incluye los 80 ms de cálculo de control, no se trata de un diagrama con mediciones reales. Cuando la prioridad del sensor rápido es mayor, si su release llega mientras se ejecuta el control lento, este último se interrumpe temporalmente. Por claridad, el diagrama dibuja una única interrupción representativa por cada periodo de control lento, pero en realidad el sensor rápido se libera en cada frontera de 100 ms.
flowchart TB
Assumption["Suposición ilustrativa<br/>sensor rápido: procesamiento 10ms<br/>control lento: procesamiento 80ms"]
subgraph Cycle1["Control lento ciclo 1 (release=150ms)"]
direction LR
C1F1["100-110ms<br/>sensor rápido #1"] --> C1S1["150-200ms<br/>control lento #1 primera mitad"]
C1S1 --> C1F2["200-210ms<br/>sensor rápido #2<br/>como tiene P+3, interrumpe"]
C1F2 --> C1S2["210-240ms<br/>control lento #1 segunda mitad"]
end
subgraph Cycle2["Control lento ciclo 2 (release=550ms)"]
direction LR
C2S1["550-600ms<br/>control lento #2 primera mitad"] --> C2F6["600-610ms<br/>sensor rápido #6<br/>como tiene P+3, interrumpe"]
C2F6 --> C2S2["610-640ms<br/>control lento #2 segunda mitad"]
end
subgraph Cycle3["Control lento ciclo 3 (release=950ms)"]
direction LR
C3S1["950-1000ms<br/>control lento #3 primera mitad"] --> C3F10["1000-1010ms<br/>sensor rápido #10<br/>como tiene P+3, interrumpe"]
C3F10 --> C3S2["1010-1040ms<br/>control lento #3 segunda mitad"]
end
Assumption --> C1F1
C1S2 --> C2S1
C2S2 --> C3S1
Figura 9: Ejemplo ilustrativo de planificación multiperiódica: el sensor rápido interrumpe periódicamente al control lento en cada frontera de 100 ms.
Este patrón es la estructura típica de «captura de sensores rápida + bucle de control lento» que se observa con frecuencia en sistemas de control industrial y de robótica.
11. Ámbitos donde brillan las funciones de tiempo real de Ada
Las funciones de tiempo real de Ada aportan un valor especial en ámbitos como los siguientes.
flowchart TB
Ada["Ada Annex D<br/>funciones de tiempo real"] --> Aero["Aeroespacial<br/>DO-178C"]
Ada --> Rail["Ferroviario<br/>familia EN 50128"]
Ada --> Auto["Automotriz<br/>ISO 26262"]
Ada --> Medical["Dispositivos médicos<br/>IEC 62304"]
Ada --> Industrial["Control industrial<br/>familia IEC 61508"]
Ada --> Defense["Defensa y sistemas de alta fiabilidad"]
Aero --> A1["Control de vuelo<br/>ámbito de aplicación con amplio historial"]
Aero --> A2["Control de satélites y naves espaciales"]
Rail --> R1["Sistemas de señalización"]
Rail --> R2["Control automático de trenes"]
Auto --> Au1["Candidato para ECU relacionadas con la seguridad"]
Auto --> Au2["Aplicación limitada y selectiva<br/>en un ámbito dominado por C / MISRA-C"]
Medical --> M1["Marcapasos"]
Medical --> M2["Bombas de infusión"]
Industrial --> I1["Control de robots"]
Industrial --> I2["Máquinas herramienta de CNC"]
Defense --> D1["Computadoras de misión"]
Defense --> D2["Sistemas de operación a largo plazo"]
Figura 10: Ámbitos de aplicación de las funciones de tiempo real de Ada, organizados por sector regulatorio.
En el diagrama, solo el sector automotriz aparece marcado como «aplicación limitada y selectiva», y el motivo tiene más que ver con el tamaño del ecosistema existente que con la idoneidad del lenguaje en sí. El software de automoción se ha construido, en torno a C y MISRA-C, sobre especificaciones de API de estándares del sector como AUTOSAR, código suministrado por proveedores, compiladores y herramientas de verificación con certificación, e incluso la propia base de ingenieros disponibles. Cambiar de lenguaje, aunque solo afecte a un único componente, implica volver a preparar todo el instrumental que lo rodea junto con los procesos de aprovisionamiento y verificación. Por eso Ada tiende a posicionarse no como un estándar para todo el vehículo, sino como una opción usada de forma selectiva en determinados componentes que requieren garantías especialmente altas, o en organizaciones que ya cuentan con activos y una estructura de desarrollo en Ada. Dicho de otro modo, en ámbitos como el aeroespacial o el ferroviario, donde todo el ecosistema ya está orientado hacia la alta fiabilidad, esta barrera simplemente no existe.
12. Consideraciones y limitaciones
Las funciones de tiempo real de Ada son potentes, pero no son una solución universal.
1. Dependencia de la plataforma:
- La correspondencia real de
pragma Prioritydepende del entorno de ejecución (sistema operativo + runtime de GNAT). En Linux se traduce enSCHED_FIFO, pero en Windows puede no garantizarse una apropiación completa.
2. Restricciones de Ravenscar:
- Como está prohibida la creación dinámica de tareas, es necesario declarar todas las tareas de forma estática en el arranque del sistema. Esto limita la libertad de diseño.
3. Límites de la medición del WCET:
Ada.Execution_Timees una medición, no una garantía. El verdadero WCET, que incluye fallos de caché y riesgos del pipeline, debe verificarse por separado con herramientas de análisis estático.
4. Sobrecarga:
- La evaluación de las barreras de un objeto protegido se ejecuta automáticamente al completarse o cancelarse una entrada, y al salir del objeto protegido. En objetos protegidos invocados con alta frecuencia, hay que tener en cuenta esta sobrecarga.
5. Barrera de la cadena de herramientas:
- Para aprovechar al máximo las funciones de tiempo real de Ada hace falta un compilador cruzado y un runtime adecuados. Especialmente en plataformas embebidas, esto implica depender del runtime proporcionado por el fabricante.
13. Resumen
En este artículo hemos recorrido paso a paso, con 8 ejemplos de código, las funciones de tiempo real que ofrece el Annex D de Ada.
| Función | Valor que aporta |
|---|---|
| Prioridad de tareas | Planificación apropiativa basada en prioridad |
| Ceiling_Locking | Prevención de la inversión de prioridad integrada en el lenguaje |
delay until |
Ejecución periódica sin deriva acumulada |
| Perfil Ravenscar | Subconjunto de tareas fácil de analizar estáticamente |
| Eventos de temporización | Activación por tiempo sin necesidad de sondeo |
| Cola protegida | Sincronización basada en barreras de objetos protegidos |
| Medición del tiempo de ejecución | Monitorización del tiempo de CPU por tarea |
| Integración multiperiódica | Diseño que permite coexistir con seguridad tareas de distintos periodos |
La esencia de las funciones de tiempo real de Ada es que no son un añadido a posteriori. Las reglas de bloqueo que contienen la inversión de prioridad, la especificación de tiempo para la ejecución periódica, la monitorización del tiempo de ejecución: todo esto se ofrece como parte de la especificación del lenguaje. Por supuesto, el cumplimiento efectivo de los plazos sigue confirmándose mediante diseño y análisis, pero contar con un runtime de lenguaje que ya prepara esas premisas es una ventaja considerable.
Como siguiente paso, para probar el desarrollo de sistemas de tiempo real con Ada, instale la cadena de herramientas GNAT con Alire y compile el código de ejemplo de este artículo con gnatchop + gnatmake.
Para los fundamentos de la concurrencia en Ada (tareas, rendez-vous, objetos protegidos), consulte el artículo anterior, «Concurrencia segura en Ada».
14. Referencias
- Ada Reference Manual - Annex D: Real-Time Systems
- Ravenscar Profile Definition (ISO/IEC TR 24718:2005)
- GNAT Real-Time Topics (AdaCore)
- The Ravenscar Profile for High-Integrity Systems (AdaCore)
- Rate Monotonic Analysis (Liu & Layland, 1973)
- Alire - Ada Package Manager
- Colección de código de ejemplo de Ada (GitHub)
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Concurrencia segura en Ada — Guía práctica de tareas y objetos protegidos
Introducción a las tareas y objetos protegidos de Ada: rendezvous, aceptación selectiva, exclusión mutua, llamadas con tiempo de espera y...
Programación genérica en Ada ── escribir contratos con tipos y lograr la reutilización sin costo en tiempo de ejecución
Explica de forma sistemática los genéricos de Ada: subprogramas, paquetes, parámetros formales y categorías de tipo, con pautas de diseño...
El atractivo del lenguaje Ada ── Cuando los tipos expresan el diseño, el lenguaje que sostiene software que funciona durante décadas
Presentamos el atractivo del lenguaje Ada: tipado fuerte, restricciones de rango, paquetes que separan especificación e implementación, d...
Introducción a la verificación formal con SPARK ── De los contratos de Ada a la demostración matemática
Un artículo introductorio y práctico sobre la verificación formal con SPARK, el subconjunto de Ada. Repasa cómo pasar de los contratos (P...
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué es el Annex D de Ada?
- Es un conjunto de funciones para sistemas de tiempo real estandarizadas como parte de la especificación del lenguaje Ada. Incluye planificación apropiativa basada en prioridad mediante FIFO_Within_Priorities, el protocolo Ceiling_Locking para evitar la inversión de prioridad, ejecución periódica en tiempo absoluto mediante delay until, el perfil Ravenscar, eventos de temporización y la medición del tiempo de ejecución por tarea con Ada.Execution_Time. Su rasgo distintivo es que no se trata de una biblioteca añadida a posteriori, sino que está integrado en el propio runtime del lenguaje.
- ¿Qué es la inversión de prioridad? ¿Cómo la evita Ada?
- Es el fenómeno en el que una tarea de baja prioridad, mientras mantiene un bloqueo, es apropiada por una tarea de prioridad media, de modo que una tarea de alta prioridad que espera ese bloqueo queda bloqueada indefinidamente. Ocurrió realmente en la sonda Mars Pathfinder en 1997 y provocó reinicios repetidos de la nave. En Ada, el protocolo Ceiling_Locking se ofrece como una característica del lenguaje: una tarea que entra en un objeto protegido asciende automáticamente a la prioridad techo, lo que impide la apropiación por parte de tareas de prioridad media.
- ¿Qué es el perfil Ravenscar?
- Es un perfil que, para sistemas de seguridad crítica, restringe las funciones de tareas de Ada a un subconjunto estáticamente analizable y determinista. Prohíbe la creación dinámica de tareas, la sentencia select, la sentencia abort, el delay relativo y la sentencia requeue, entre otras cosas. Esta restricción facilita el análisis estático de temporización y ayuda a cumplir las características exigidas por normas de seguridad como DO-178C (software aeronáutico) o ISO 26262 (seguridad funcional automotriz). En GNAT se activa escribiendo pragma Profile (Ravenscar) en el archivo gnat.adc.
- ¿Por qué usar delay until en lugar de delay en tareas periódicas?
- Porque con delay, que especifica un tiempo relativo, el tiempo de procesamiento de cada iteración se va acumulando y el periodo se desplaza progresivamente (deriva acumulada). delay until determina el siguiente instante de activación a partir de un tiempo absoluto, de modo que aunque un procesamiento se retrase, los instantes de activación posteriores se mantienen correctos. Sin embargo, si el procesamiento supera el siguiente instante de despertar, delay until retorna casi de inmediato, por lo que es necesario diseñar por separado la detección de ese caso como deadline miss y tratarlo como sobrecarga.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.