البرمجة المدمجة: برمجة المتحكمات الدقيقة
ما هي البرمجة المدمجة؟
في كل مصنع حولك، هناك عشرات المتحكمات الصغيرة التي لا تراها: متحكم يراقب حرارة الفرن، آخر يتحكم بسرعة المحرك، ثالث يقرأ مستوى السائل في الخزان. هذه كلها أنظمة مدمجة (Embedded Systems) — حواسيب صغيرة مخصصة لمهمة واحدة محددة.
البرمجة المدمجة هي كتابة البرنامج (Firmware) الذي يعمل مباشرة على هذه الرقاقات — بدون نظام تشغيل كامل، بدون شاشة، بدون لوحة مفاتيح. أنت تتحدث مباشرة مع العتاد: منافذ، مسجلات، مقاطعات.
المتحكمات الدقيقة: الأنواع والاختيار
Arduino (ATmega328P)
المنصة الأسهل للمبتدئين. المتحكم ATmega328P يعمل بتردد 16 MHz، ذاكرة 32 KB، وله 14 منفذ رقمي و6 منافذ تمثيلية.
نقاط القوة: سهل التعلم، مكتبات ضخمة، مجتمع كبير نقاط الضعف: بطيء، ذاكرة محدودة، لا يناسب المهام المعقدة
متى تستخدمه: نماذج أولية سريعة، قراءة حساسات بسيطة، مشاريع تعليمية
ARM Cortex-M (STM32, ESP32)
للمشاريع الصناعية الجدية. STM32F4 مثلاً يعمل بتردد 168 MHz، ذاكرة 1 MB، ويدعم DMA ومقاطعات متعددة المستويات.
نقاط القوة: سريع، منافذ كثيرة، يدعم RTOS نقاط الضعف: أعقد في البرمجة، يحتاج فهم أعمق للعتاد
متى تستخدمه: تحكم بمحركات، أنظمة مراقبة صناعية، IoT متقدم
ESP32
مع WiFi وBluetooth مدمجين، مناسب لمشاريع IoT. ثنائي النواة بتردد 240 MHz.
متى تستخدمه: ربط الحساسات بالسحابة، مراقبة عن بعد، لوحات تحكم لاسلكية
GPIO: بوابتك للعالم المادي
GPIO (General Purpose Input/Output) هي المنافذ التي تربط المتحكم بالعالم الخارجي. كل منفذ يمكن أن يكون:
- إدخال (Input): قراءة حالة زر، حساس، مفتاح نهاية حد
- إخراج (Output): تشغيل LED، مرحّل (Relay)، صمام
مثال: قراءة حساس حرارة والتحكم بمروحة
// Arduino: قراءة حساس حرارة NTC والتحكم بمروحة تبريد
const int TEMP_SENSOR = A0; // حساس حرارة على المنفذ التمثيلي 0
const int FAN_RELAY = 7; // مرحّل المروحة على المنفذ الرقمي 7
const float TEMP_THRESHOLD = 60.0; // درجة الحرارة الحدّية
void setup() {
pinMode(FAN_RELAY, OUTPUT);
Serial.begin(9600);
}
void loop() {
int raw = analogRead(TEMP_SENSOR); // قراءة 0-1023
float voltage = raw * (5.0 / 1023.0);
float temperature = (voltage - 0.5) * 100.0; // تحويل لدرجة مئوية
Serial.print("Temperature: ");
Serial.print(temperature);
Serial.println(" C");
if (temperature > TEMP_THRESHOLD) {
digitalWrite(FAN_RELAY, HIGH); // شغّل المروحة
Serial.println("Fan ON - Cooling active");
} else {
digitalWrite(FAN_RELAY, LOW); // أطفئ المروحة
}
delay(1000); // قراءة كل ثانية
}
المقاطعات: الاستجابة الفورية
المشكلة مع delay() و loop() هي أنك قد تفوّت حدثاً مهماً أثناء الانتظار. ماذا لو ضُغط زر الطوارئ بينما المتحكم في منتصف delay(1000)؟
المقاطعات (Interrupts) تحل هذه المشكلة: عندما يحدث حدث على منفذ معين، يتوقف البرنامج الرئيسي فوراً ويُنفَّذ كود المقاطعة، ثم يعود.
// Arduino: زر طوارئ بمقاطعة
const int EMERGENCY_BTN = 2; // المنفذ 2 يدعم مقاطعات خارجية
const int MOTOR_RELAY = 8;
volatile bool emergencyStop = false;
void setup() {
pinMode(EMERGENCY_BTN, INPUT_PULLUP);
pinMode(MOTOR_RELAY, OUTPUT);
// ربط المقاطعة: عند الضغط (هبوط الإشارة) نفّذ الدالة
attachInterrupt(
digitalPinToInterrupt(EMERGENCY_BTN),
onEmergencyPress,
FALLING
);
}
// دالة المقاطعة — يجب أن تكون قصيرة وسريعة
void onEmergencyPress() {
emergencyStop = true;
}
void loop() {
if (emergencyStop) {
digitalWrite(MOTOR_RELAY, LOW); // أوقف المحرك فوراً
Serial.println("EMERGENCY STOP ACTIVATED");
while (true) {
// ابقَ متوقفاً حتى إعادة تشغيل يدوية
}
}
// العمل الطبيعي للمحرك
digitalWrite(MOTOR_RELAY, HIGH);
}
قواعد مهمة للمقاطعات:
- اجعل دالة المقاطعة (ISR) قصيرة قدر الإمكان
- استخدم
volatileللمتغيرات المشتركة بين ISR والبرنامج الرئيسي - لا تستخدم
delay()أوSerial.print()داخل ISR
PWM: التحكم بالسرعة والشدة
PWM (Pulse Width Modulation) يُمكّنك من التحكم بسرعة محرك أو شدة إضاءة LED بتغيير نسبة زمن التشغيل للإشارة. بدلاً من تشغيل/إطفاء بسيط، تتحكم بالنسبة المئوية.
Duty Cycle 25%: ████____________████____________
Duty Cycle 50%: ████████________████████________
Duty Cycle 75%: ████████████____████████████____
Duty Cycle 100%: ████████████████████████████████
// التحكم بسرعة محرك DC عبر PWM
const int MOTOR_PWM = 9; // منفذ يدعم PWM
const int SPEED_POT = A0; // مقاومة متغيرة للتحكم بالسرعة
void setup() {
pinMode(MOTOR_PWM, OUTPUT);
}
void loop() {
int potValue = analogRead(SPEED_POT); // 0-1023
int motorSpeed = map(potValue, 0, 1023, 0, 255); // تحويل لـ 0-255
analogWrite(MOTOR_PWM, motorSpeed); // كتابة PWM
delay(50);
}
الاتصالات التسلسلية: UART و SPI و I2C
المتحكم يحتاج للتواصل مع الحساسات والشاشات والمتحكمات الأخرى. ثلاثة بروتوكولات أساسية:
UART (Serial)
اتصال نقطة-لنقطة بسيط بسلكين (TX و RX). يُستخدم للتواصل مع الحاسوب أو وحدات GPS/GSM.
// إرسال قراءات للحاسوب عبر UART
Serial.begin(115200);
Serial.println("Temp: 67.5C | Pressure: 4.8 bar");
I2C
ناقل بسلكين (SDA و SCL) يربط حتى 127 جهازاً على نفس الناقل. كل جهاز له عنوان فريد. مثالي لحساسات الحرارة والرطوبة والشاشات الصغيرة.
#include <Wire.h>
// قراءة حساس حرارة I2C (عنوان 0x48)
Wire.begin();
Wire.requestFrom(0x48, 2);
int msb = Wire.read();
int lsb = Wire.read();
float temp = ((msb << 8) | lsb) / 256.0;
SPI
أسرع بكثير من I2C لكن يحتاج أسلاك أكثر (MOSI, MISO, SCK, CS). يُستخدم لشاشات TFT، بطاقات SD، محولات ADC سريعة.
| البروتوكول | الأسلاك | السرعة | الأجهزة | الاستخدام |
|---|---|---|---|---|
| UART | 2 | بطيء | 1:1 | تواصل مع الحاسوب |
| I2C | 2 | متوسط | حتى 127 | حساسات، شاشات OLED |
| SPI | 4+ | سريع | محدود | شاشات TFT، بطاقات SD |
Firmware: البرنامج الثابت
الفرق بين كود عادي وFirmware:
- لا يوجد نظام تشغيل يحميك — إذا تعطل كودك، المتحكم يتجمد
- الموارد محدودة — كل بايت من الذاكرة مهم
- الزمن الحقيقي مطلوب — تأخر ميلي ثانية قد يعني خطراً
- لا إعادة تشغيل سهلة — الجهاز في الحقل، بعيد عنك
Watchdog Timer: شبكة الأمان
#include <avr/wdt.h>
void setup() {
wdt_enable(WDTO_2S); // إذا لم يُعاد ضبط المؤقت خلال 2 ثانية
// يُعاد تشغيل المتحكم تلقائياً
}
void loop() {
wdt_reset(); // أنا حي — أعد ضبط المؤقت
readSensors();
processData();
updateOutputs();
// إذا تعلّق البرنامج هنا ولم يصل لـ wdt_reset()
// سيُعاد تشغيل المتحكم بعد ثانيتين
}
مثال تكاملي: وحدة مراقبة صناعية
لنبني وحدة تقرأ 3 حساسات وترسل البيانات عبر Serial وتُشغّل إنذاراً:
// وحدة مراقبة صناعية مصغرة
#include <Wire.h>
struct SensorData {
float temperature;
float pressure;
float vibration;
bool alarm;
};
const float TEMP_MAX = 85.0;
const float VIB_MAX = 10.0;
const int ALARM_PIN = 13;
const int BUZZER_PIN = 6;
SensorData data;
void setup() {
Serial.begin(115200);
Wire.begin();
pinMode(ALARM_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
Serial.println("Industrial Monitor v1.0 Ready");
}
void loop() {
data.temperature = readTemperature();
data.pressure = readPressure();
data.vibration = readVibration();
data.alarm = (data.temperature > TEMP_MAX) ||
(data.vibration > VIB_MAX);
if (data.alarm) {
digitalWrite(ALARM_PIN, HIGH);
tone(BUZZER_PIN, 2000, 500);
} else {
digitalWrite(ALARM_PIN, LOW);
}
// إرسال البيانات كـ JSON عبر Serial
Serial.print("{\"temp\":");
Serial.print(data.temperature, 1);
Serial.print(",\"pres\":");
Serial.print(data.pressure, 1);
Serial.print(",\"vib\":");
Serial.print(data.vibration, 1);
Serial.print(",\"alarm\":");
Serial.print(data.alarm ? "true" : "false");
Serial.println("}");
delay(500);
}
خلاصة
| المفهوم | الوظيفة |
|---|---|
| GPIO | قراءة حساسات والتحكم بمخرجات |
| المقاطعات | استجابة فورية للأحداث الحرجة |
| PWM | تحكم تدريجي بالسرعة والشدة |
| UART/I2C/SPI | تواصل مع الأجهزة الخارجية |
| Watchdog | حماية من تجمّد البرنامج |
| Firmware | برنامج يعمل مباشرة على العتاد |
البرمجة المدمجة هي أقرب ما يكون المهندس فيه للعتاد. كل سطر كود يتحول مباشرة إلى إشارة كهربائية تُحرّك محركاً أو تقرأ حساساً. هذا هو الجسر بين عالم البرمجيات وعالم الآلات.