GPIO والإشارات الرقمية: التحكم بالعالم الخارجي
ما هو GPIO؟ أطراف الأغراض العامة
GPIO (General Purpose Input/Output) هي الأطراف التي تربط المتحكم الدقيق بالعالم الخارجي. كل طرف يمكن ضبطه كمدخل لقراءة حالة حساس أو زر، أو كمخرج لتشغيل LED أو مرحل (Relay) يتحكم بمعدات صناعية.
في المتحكمات الصناعية مثل STM32، تُنظَّم الأطراف في مجموعات (Ports): GPIOA، GPIOB، وهكذا. كل مجموعة تحتوي حتى 16 طرفاً. قبل استخدام أي طرف، يجب تفعيل ساعة المجموعة التابع لها.
مستويات الجهد
- مستوى منخفض (0): 0V إلى 0.8V
- مستوى مرتفع (1): 2.0V إلى 3.3V
- منطقة غير محددة: 0.8V إلى 2.0V (يجب تجنبها)
تعمل معظم المتحكمات الحديثة بجهد 3.3V. عند التعامل مع أجهزة 5V صناعية، تحتاج محوّل مستوى (Level Shifter).
إعداد طرف كمخرج: تشغيل LED ومرحل
عند ضبط طرف كمخرج، يمكنك التحكم بجهده: مرتفع (3.3V) أو منخفض (0V). هذا يكفي لتشغيل LED مباشرة أو قيادة ترانزستور يشغّل مرحلاً.
// إعداد طرف PA5 كمخرج وتشغيل مرحل
#include "stm32f4xx_hal.h"
#define RELAY_PIN GPIO_PIN_5
#define RELAY_PORT GPIOA
void relay_init(void) {
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = RELAY_PIN;
gpio.Mode = GPIO_MODE_OUTPUT_PP; // مخرج Push-Pull
gpio.Pull = GPIO_NOPULL;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(RELAY_PORT, &gpio);
}
void relay_on(void) {
HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_SET);
}
void relay_off(void) {
HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_RESET);
}
أنماط المخرج
- Push-Pull: يقود المخرج بفاعلية نحو HIGH أو LOW. الأكثر استخداماً.
- Open-Drain: يسحب نحو LOW فقط. يحتاج مقاومة رافعة خارجية. يُستخدم مع I2C وبعض الدوائر الصناعية.
إعداد طرف كمدخل: قراءة زر وحساس
عند ضبط طرف كمدخل، يقرأ المتحكم حالة الجهد عليه: مرتفع أو منخفض. هذا يُستخدم لقراءة أزرار التشغيل، حساسات القرب، ونهايات المشوار (Limit Switches).
// قراءة حساس قرب صناعي على PB0
#define SENSOR_PIN GPIO_PIN_0
#define SENSOR_PORT GPIOB
void sensor_init(void) {
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = SENSOR_PIN;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULLUP; // مقاومة رافعة داخلية
HAL_GPIO_Init(SENSOR_PORT, &gpio);
}
uint8_t is_object_detected(void) {
// الحساس يعطي LOW عند اكتشاف جسم (NPN)
return (HAL_GPIO_ReadPin(SENSOR_PORT, SENSOR_PIN) == GPIO_PIN_RESET);
}
المقاومات الرافعة والساحبة
عندما لا يكون هناك شيء متصل بطرف المدخل، يكون في حالة "عائمة" (Floating) ويقرأ قيماً عشوائية. المقاومات الرافعة (Pull-up) والساحبة (Pull-down) تحل هذه المشكلة.
المقاومة الرافعة (Pull-up)
تربط الطرف بـ VCC عبر مقاومة (عادة 10KΩ). الحالة الافتراضية HIGH. عند الضغط على الزر، يصبح LOW. هذا هو النمط الشائع في الأزرار الصناعية.
المقاومة الساحبة (Pull-down)
تربط الطرف بـ GND عبر مقاومة. الحالة الافتراضية LOW. عند الضغط، يصبح HIGH.
معظم المتحكمات توفر مقاومات داخلية يمكن تفعيلها برمجياً (كما في المثال السابق: GPIO_PULLUP)، لكن في البيئات الصناعية يُفضَّل استخدام مقاومات خارجية لمتانة أعلى.
مشكلة الارتداد Debouncing
عند الضغط على زر ميكانيكي، لا ينتقل فوراً من حالة لأخرى. بل يرتد عدة مرات خلال ميلي ثوانٍ، مما يُنتج عدة نبضات بدل واحدة. في البيئات الصناعية، هذا قد يسبب أوامر مكررة خطيرة.
حل برمجي بسيط
#define DEBOUNCE_MS 50
uint8_t read_button_debounced(GPIO_TypeDef *port, uint16_t pin) {
if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET) {
HAL_Delay(DEBOUNCE_MS);
if (HAL_GPIO_ReadPin(port, pin) == GPIO_PIN_RESET) {
return 1; // ضغطة مؤكدة
}
}
return 0;
}
حل بآلة حالة (أفضل للتطبيقات الصناعية)
typedef struct {
uint8_t state;
uint8_t last_raw;
uint32_t last_change_tick;
} Debouncer;
uint8_t debounce_update(Debouncer *d, uint8_t raw, uint32_t now_ms) {
if (raw != d->last_raw) {
d->last_change_tick = now_ms;
d->last_raw = raw;
}
if ((now_ms - d->last_change_tick) >= DEBOUNCE_MS) {
d->state = d->last_raw;
}
return d->state;
}
مثال عملي: تشغيل مروحة تبريد بحساس حرارة ثنائي
في هذا المثال، نستخدم حساس حرارة ثنائي الحالة (Thermal Switch) يعطي إشارة رقمية عند تجاوز درجة محددة، ونشغّل مروحة تبريد عبر مرحل.
#include "stm32f4xx_hal.h"
#define TEMP_ALERT_PIN GPIO_PIN_0 // PB0 - حساس الحرارة
#define TEMP_ALERT_PORT GPIOB
#define FAN_RELAY_PIN GPIO_PIN_5 // PA5 - مرحل المروحة
#define FAN_RELAY_PORT GPIOA
#define LED_STATUS_PIN GPIO_PIN_13 // PC13 - LED الحالة
#define LED_STATUS_PORT GPIOC
void system_init(void) {
HAL_Init();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitTypeDef gpio_out = {
.Mode = GPIO_MODE_OUTPUT_PP,
.Pull = GPIO_NOPULL,
.Speed = GPIO_SPEED_FREQ_LOW
};
gpio_out.Pin = FAN_RELAY_PIN;
HAL_GPIO_Init(FAN_RELAY_PORT, &gpio_out);
gpio_out.Pin = LED_STATUS_PIN;
HAL_GPIO_Init(LED_STATUS_PORT, &gpio_out);
GPIO_InitTypeDef gpio_in = {
.Pin = TEMP_ALERT_PIN,
.Mode = GPIO_MODE_INPUT,
.Pull = GPIO_PULLUP
};
HAL_GPIO_Init(TEMP_ALERT_PORT, &gpio_in);
}
int main(void) {
system_init();
Debouncer temp_db = {0};
while (1) {
uint8_t raw = HAL_GPIO_ReadPin(TEMP_ALERT_PORT, TEMP_ALERT_PIN);
uint8_t overtemp = debounce_update(&temp_db, !raw, HAL_GetTick());
if (overtemp) {
HAL_GPIO_WritePin(FAN_RELAY_PORT, FAN_RELAY_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_STATUS_PORT, LED_STATUS_PIN, GPIO_PIN_SET);
} else {
HAL_GPIO_WritePin(FAN_RELAY_PORT, FAN_RELAY_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(LED_STATUS_PORT, LED_STATUS_PIN, GPIO_PIN_RESET);
}
HAL_Delay(10);
}
}
الخلاصة
أطراف GPIO هي الواجهة الأساسية بين المتحكم والعالم المادي. التعامل الصحيح مع المدخلات والمخرجات الرقمية، مع مراعاة مشاكل الارتداد ومستويات الجهد، هو أساس كل مشروع مدمج صناعي. في الدرس القادم سننتقل إلى عالم الإشارات التناظرية وقراءة المستشعرات عبر ADC.