البرمجة في الزمن الحقيقي: عندما يكون التأخير غير مقبول
البرمجة في الزمن الحقيقي: عندما تكون الميلي ثانية مسألة حياة أو موت
في معظم البرامج العادية، إذا تأخر الرد نصف ثانية لا أحد يلاحظ. لكن في التحكم الصناعي، نصف ثانية قد تعني أن ذراع الروبوت تجاوز موضعه بعشرة سنتيمترات، أو أن الضغط في الأنبوب وصل مستوى خطيراً. هذا هو عالم البرمجة في الزمن الحقيقي (Real-Time Programming) — حيث الصحة الزمنية لا تقل أهمية عن صحة النتيجة.
نظام الزمن الحقيقي لا يعني بالضرورة نظاماً سريعاً — بل يعني نظاماً يمكن التنبؤ بزمن استجابته. محرك يستجيب خلال 10 ميلي ثانية دائماً أفضل من محرك يستجيب خلال 1 ميلي ثانية أحياناً و100 ميلي ثانية أحياناً أخرى.
الزمن الحقيقي الصلب مقابل المرن
الزمن الحقيقي الصلب (Hard Real-Time)
تجاوز الموعد النهائي = كارثة. لا يوجد مجال للتأخير أبداً.
أمثلة:
- نظام الفرامل المانعة للانغلاق (ABS): يجب أن يستجيب خلال 5ms حتماً
- التحكم بحقن الوقود في المحرك: توقيت دقيق لكل دورة
- نظام الأمان في المكابس الصناعية: إيقاف فوري عند اكتشاف يد العامل
الزمن الحقيقي المرن (Soft Real-Time)
التأخير مزعج لكنه ليس كارثياً — النظام يستمر بالعمل بأداء منخفض.
أمثلة:
- شاشة عرض بيانات المصنع: تأخير ثانية مقبول
- نظام تسجيل البيانات: فقدان نقطة بيانات واحدة ليس خطيراً
- واجهة المستخدم لنظام SCADA: بطء مؤقت مقبول
الزمن الحقيقي الحازم (Firm Real-Time)
النتيجة المتأخرة عديمة الفائدة لكنها لا تسبب كارثة:
- معالجة إطارات الفيديو في نظام رؤية صناعي: الإطار المتأخر يُهمل
| النوع | تجاوز الموعد | مثال صناعي |
|---|---|---|
| صلب | كارثة / خطر | فرامل، أنظمة أمان |
| حازم | النتيجة تُهمل | رؤية حاسوبية |
| مرن | أداء منخفض | شاشات مراقبة |
نظام التشغيل في الزمن الحقيقي (RTOS)
نظام التشغيل العادي (Windows, Linux) لا يضمن أوقات استجابة محددة — قد يقرر النظام فجأة تحديث برنامج مضاد الفيروسات أثناء عملية تحكم حرجة! لهذا نستخدم RTOS (Real-Time Operating System).
FreeRTOS: الأكثر شيوعاً في العالم الصناعي
FreeRTOS هو نظام تشغيل مفتوح المصدر يعمل على المتحكمات الدقيقة (مثل ESP32, STM32, Arduino). إليك كيف يبدو الكود:
#include "FreeRTOS.h"
#include "task.h"
// مهمة قراءة مستشعر الضغط — أولوية عالية
void vPressureTask(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(10); // كل 10ms
for (;;) {
float pressure = read_pressure_sensor();
if (pressure > MAX_SAFE_PRESSURE) {
activate_emergency_relief_valve();
}
// انتظار دقيق حتى الدورة التالية
vTaskDelayUntil(&xLastWakeTime, xPeriod);
}
}
// مهمة عرض البيانات — أولوية منخفضة
void vDisplayTask(void *pvParameters) {
for (;;) {
update_lcd_display();
vTaskDelay(pdMS_TO_TICKS(500)); // كل 500ms
}
}
int main(void) {
// إنشاء المهام مع تحديد الأولويات
xTaskCreate(vPressureTask, "Pressure", 256, NULL,
3, // أولوية عالية
NULL);
xTaskCreate(vDisplayTask, "Display", 512, NULL,
1, // أولوية منخفضة
NULL);
// بدء المُجدول
vTaskStartScheduler();
// لن نصل هنا أبداً
for (;;);
}
النقطة الجوهرية: مهمة الضغط (أولوية 3) تقطع دائماً مهمة العرض (أولوية 1) عندما يحين وقتها.
جدولة الأولويات (Priority Scheduling)
الجدولة الوقائية (Preemptive Scheduling)
المُجدول يسحب المعالج من المهمة ذات الأولوية المنخفضة ويعطيه للمهمة ذات الأولوية الأعلى فوراً:
الزمن ──────────────────────────────────>
مهمة الأمان (أولوية 5): ██ ██
مهمة التحكم (أولوية 3): ███ ████ ███
مهمة العرض (أولوية 1): ██ ███
^ ^
مقاطعة استئناف
انقلاب الأولوية (Priority Inversion)
مشكلة شهيرة حدثت في مركبة Mars Pathfinder على المريخ عام 1997:
المشكلة:
1. مهمة منخفضة الأولوية تحجز مورداً مشتركاً (قفل)
2. مهمة عالية الأولوية تحتاج نفس المورد — تنتظر
3. مهمة متوسطة الأولوية تقطع المهمة المنخفضة
4. النتيجة: المهمة العالية تنتظر المهمة المتوسطة — انقلاب!
الحل: وراثة الأولوية (Priority Inheritance) — المهمة المنخفضة ترث مؤقتاً أولوية المهمة العالية المنتظرة:
// FreeRTOS يدعم Mutex مع وراثة الأولوية
SemaphoreHandle_t xMutex;
void setup() {
// Mutex مع وراثة أولوية تلقائية
xMutex = xSemaphoreCreateMutex();
}
void vHighPriorityTask(void *pv) {
for (;;) {
// طلب القفل — إذا كان محجوزاً، ترتفع أولوية الحاجز
xSemaphoreTake(xMutex, portMAX_DELAY);
access_shared_resource();
xSemaphoreGive(xMutex);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
تزامن المهام (Task Synchronization)
عندما تعمل عدة مهام معاً، يجب تنسيقها لمنع التضارب.
الإشارات (Semaphores)
SemaphoreHandle_t xDataReady;
void vSensorTask(void *pv) {
for (;;) {
read_all_sensors();
// إخبار مهمة التحكم أن البيانات جاهزة
xSemaphoreGive(xDataReady);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
void vControlTask(void *pv) {
for (;;) {
// انتظار حتى تصبح البيانات جاهزة
if (xSemaphoreTake(xDataReady, pdMS_TO_TICKS(50))) {
compute_control_output();
} else {
// انتهاء المهلة — المستشعرات لم تستجب!
handle_sensor_timeout();
}
}
}
طوابير الرسائل (Message Queues)
QueueHandle_t xCommandQueue;
typedef struct {
uint8_t motor_id;
uint16_t target_rpm;
uint8_t direction;
} MotorCommand_t;
void vSupervisorTask(void *pv) {
MotorCommand_t cmd;
cmd.motor_id = 1;
cmd.target_rpm = 1200;
cmd.direction = FORWARD;
// إرسال أمر إلى مهمة التحكم بالمحرك
xQueueSend(xCommandQueue, &cmd, pdMS_TO_TICKS(100));
}
void vMotorControlTask(void *pv) {
MotorCommand_t received_cmd;
for (;;) {
if (xQueueReceive(xCommandQueue, &received_cmd, portMAX_DELAY)) {
set_motor_speed(received_cmd.motor_id,
received_cmd.target_rpm,
received_cmd.direction);
}
}
}
الحتمية: العدو الأول هو عدم التوقعية
الحتمية (Determinism) تعني أن نفس المدخلات تنتج نفس المخرجات في نفس الوقت دائماً. لتحقيقها:
ما يجب تجنبه
// سيئ: تخصيص ذاكرة ديناميكي — الوقت غير متوقع!
void bad_realtime_function() {
int *data = malloc(1000 * sizeof(int)); // قد يأخذ 1us أو 1ms
// ...
free(data);
}
// جيد: تخصيص ثابت مسبقاً
static int data_buffer[1000]; // محجوز عند بدء البرنامج
void good_realtime_function() {
// استخدام المخزن المحجوز مسبقاً
process_data(data_buffer, 1000);
}
قواعد الكود الحتمي
| افعل | لا تفعل |
|---|---|
| تخصيص ذاكرة ثابت عند البدء | malloc/free أثناء التشغيل |
| حلقات بعدد تكرارات معروف | حلقات while بشرط غير محدد |
| جداول بحث مُحسوبة مسبقاً | حسابات رياضية معقدة (sin, sqrt) |
| مخازن دوّارة بحجم ثابت | قوائم مرتبطة ديناميكية |
| أقفال مع حد زمني أقصى | انتظار لا نهائي بدون timeout |
متطلبات زمن الاستجابة لأنظمة التحكم
كل نظام صناعي له متطلبات زمنية مختلفة:
| التطبيق | زمن الاستجابة المطلوب | النوع |
|---|---|---|
| حلقة تحكم بالموضع (CNC) | 0.1 - 1 ms | صلب |
| تحكم بسرعة المحرك | 1 - 10 ms | صلب |
| قراءة مستشعرات حرارة | 10 - 100 ms | مرن |
| تحكم بعملية كيميائية | 100 ms - 1 s | مرن |
| نظام إنذار حريق | < 50 ms | صلب |
| تحديث شاشة HMI | 100 - 500 ms | مرن |
| نقل بيانات إلى SCADA | 1 - 5 s | مرن |
حساب زمن الاستجابة الأسوأ (WCET)
// كل مسار تنفيذ يجب أن يكون أقصر من الموعد النهائي
void control_cycle() {
// القراءة: ~50us
float temp = read_temperature();
float pressure = read_pressure();
// الحساب: ~100us
float output = pid_compute(temp, pressure);
// الكتابة: ~30us
write_analog_output(output);
// المجموع الأسوأ: ~180us
// الموعد النهائي: 1000us (1ms)
// الهامش: 820us — آمن
}
مثال عملي: نظام تحكم بخط تعبئة
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "semphr.h"
#define FILL_CYCLE_MS 10
#define SAFETY_CYCLE_MS 5
#define DISPLAY_CYCLE_MS 200
SemaphoreHandle_t xSafetyOk;
QueueHandle_t xFillLevel;
// مهمة الأمان — الأعلى أولوية
void vSafetyTask(void *pv) {
TickType_t xLast = xTaskGetTickCount();
for (;;) {
bool door_closed = read_safety_door();
bool e_stop_ok = !read_emergency_stop();
if (door_closed && e_stop_ok) {
xSemaphoreGive(xSafetyOk);
} else {
// إيقاف كل شيء فوراً
disable_all_outputs();
}
vTaskDelayUntil(&xLast, pdMS_TO_TICKS(SAFETY_CYCLE_MS));
}
}
// مهمة التعبئة — أولوية متوسطة
void vFillingTask(void *pv) {
TickType_t xLast = xTaskGetTickCount();
for (;;) {
if (xSemaphoreTake(xSafetyOk, 0) == pdTRUE) {
float level = read_level_sensor();
float valve_cmd = pid_fill_control(level, TARGET_LEVEL);
set_fill_valve(valve_cmd);
xQueueOverwrite(xFillLevel, &level);
}
vTaskDelayUntil(&xLast, pdMS_TO_TICKS(FILL_CYCLE_MS));
}
}
// مهمة العرض — أولوية منخفضة
void vDisplayTask(void *pv) {
float level;
for (;;) {
if (xQueuePeek(xFillLevel, &level, pdMS_TO_TICKS(1000))) {
update_hmi_display(level);
}
vTaskDelay(pdMS_TO_TICKS(DISPLAY_CYCLE_MS));
}
}
نصائح عملية لمبرمج الزمن الحقيقي
- قِس دائماً — لا تفترض أن الكود سريع بما يكفي، قِس الزمن الفعلي
- صمّم للحالة الأسوأ — ليس المتوسط، بل أسوأ حالة ممكنة
- اختبر تحت الحمل — النظام قد يعمل جيداً فارغاً ويفشل تحت الضغط
- استخدم watchdog timer — إذا توقفت مهمة، النظام يعيد نفسه تلقائياً
- افصل المهام الحرجة عن غير الحرجة — لا تخلط كود الأمان مع كود الواجهة
الخلاصة
البرمجة في الزمن الحقيقي هي العمود الفقري لكل نظام تحكم صناعي:
- الصلب مقابل المرن: حدد متطلباتك الزمنية بدقة
- RTOS: يضمن استجابة متوقعة عبر جدولة الأولويات
- تزامن المهام: إشارات وطوابير لتنسيق العمل بين المهام
- الحتمية: تجنب كل ما يجعل زمن التنفيذ غير متوقع
- زمن الاستجابة: كل تطبيق له متطلبات مختلفة — اعرفها واحترمها
عندما تكتب كوداً يتحكم بآلة حقيقية، تذكّر: الصحة وحدها لا تكفي — يجب أن تكون صحيحاً في الوقت المناسب.