بروتوكولات الاتصال: UART و SPI و I2C — كيف تتحدث المكونات
UART: الاتصال التسلسلي الأبسط
UART (Universal Asynchronous Receiver/Transmitter) هو أقدم وأبسط بروتوكول اتصال تسلسلي. يستخدم سلكين فقط: TX للإرسال و RX للاستقبال. لا يحتاج إشارة ساعة مشتركة لأن الطرفين يتفقان مسبقاً على سرعة الاتصال (Baud Rate).
معاملات الاتصال
- Baud Rate: عدد البتات في الثانية (9600, 115200, الخ)
- Data Bits: عادة 8 بت
- Parity: None, Even, أو Odd (للكشف عن الأخطاء)
- Stop Bits: 1 أو 2
التكوين الأشهر صناعياً: 9600 8N1 (9600 baud, 8 data bits, No parity, 1 stop bit) لاتصالات Modbus، و 115200 8N1 لأغراض التشخيص.
#include "stm32f4xx_hal.h"
UART_HandleTypeDef huart2;
void uart_init(uint32_t baudrate) {
__HAL_RCC_USART2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_2 | GPIO_PIN_3; // PA2=TX, PA3=RX
gpio.Mode = GPIO_MODE_AF_PP;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &gpio);
huart2.Instance = USART2;
huart2.Init.BaudRate = baudrate;
huart2.Init.WordLength = UART_WORDLENGTH_8B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&huart2);
}
void uart_send(const char *data, uint16_t len) {
HAL_UART_Transmit(&huart2, (uint8_t *)data, len, 100);
}
RS-485: UART للمسافات الطويلة
في المصانع، يُحوَّل UART إلى RS-485 عبر شريحة محوّل (مثل MAX485). RS-485 يدعم مسافات حتى 1200 متر وربط 32 جهازاً على نفس الخط. بروتوكول Modbus RTU يعمل فوق RS-485.
SPI: اتصال سريع مع عدة أجهزة
SPI (Serial Peripheral Interface) بروتوكول متزامن يستخدم 4 أسلاك:
- MOSI: بيانات من المتحكم (Master) إلى الطرفي (Slave)
- MISO: بيانات من الطرفي إلى المتحكم
- SCK: إشارة الساعة
- CS/SS: اختيار الجهاز (طرف لكل جهاز)
SPI سريع جداً (حتى عشرات MHz) ويُستخدم مع شاشات TFT، وبطاقات SD، ومحولات ADC الخارجية عالية الدقة.
SPI_HandleTypeDef hspi1;
void spi_init(void) {
__HAL_RCC_SPI1_CLK_ENABLE();
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
HAL_SPI_Init(&hspi1);
}
uint8_t spi_transfer(uint8_t tx_byte) {
uint8_t rx_byte;
HAL_SPI_TransmitReceive(&hspi1, &tx_byte, &rx_byte, 1, 10);
return rx_byte;
}
I2C: ناقل بسلكين لعشرات المكونات
I2C (Inter-Integrated Circuit) يستخدم سلكين فقط:
- SDA: خط البيانات
- SCL: خط الساعة
كل جهاز على الناقل له عنوان فريد (7-bit: حتى 127 جهاز). سرعته أبطأ من SPI (100-400 KHz عادة) لكنه يوفر الأسلاك. يُستخدم مع مستشعرات الحرارة والرطوبة، شاشات OLED، وساعات RTC.
I2C_HandleTypeDef hi2c1;
void i2c_init(void) {
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_6 | GPIO_PIN_7; // PB6=SCL, PB7=SDA
gpio.Mode = GPIO_MODE_AF_OD;
gpio.Pull = GPIO_PULLUP;
gpio.Speed = GPIO_SPEED_FREQ_HIGH;
gpio.Alternate = GPIO_AF4_I2C1;
HAL_GPIO_Init(GPIOB, &gpio);
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
HAL_I2C_Init(&hi2c1);
}
HAL_StatusTypeDef i2c_write_reg(uint8_t addr, uint8_t reg, uint8_t val) {
uint8_t buf[2] = {reg, val};
return HAL_I2C_Master_Transmit(&hi2c1, addr << 1, buf, 2, 100);
}
uint8_t i2c_read_reg(uint8_t addr, uint8_t reg) {
uint8_t val;
HAL_I2C_Master_Transmit(&hi2c1, addr << 1, ®, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, addr << 1, &val, 1, 100);
return val;
}
متى تستخدم كل بروتوكول؟
| المعيار | UART | SPI | I2C |
|---|---|---|---|
| عدد الأسلاك | 2 | 4+ | 2 |
| السرعة | حتى 1 Mbps | حتى 50 MHz | حتى 3.4 MHz |
| عدد الأجهزة | نقطة لنقطة | عدة (بأسلاك CS) | حتى 127 |
| المسافة | قصيرة (RS-485 للطويلة) | قصيرة جداً | قصيرة |
| الاستخدام الصناعي | Modbus، تشخيص | شاشات، ADC خارجي | مستشعرات رقمية |
مكتبات HAL: تبسيط التعامل مع البروتوكولات
في Rust مع embedded-hal، يتم تجريد البروتوكولات في سمات (Traits) موحدة تعمل مع أي متحكم:
use embedded_hal::i2c::I2c;
fn read_temperature<I: I2c>(i2c: &mut I, addr: u8) -> Result<f32, I::Error> {
let mut buf = [0u8; 2];
i2c.write_read(addr, &[0xE3], &mut buf)?;
let raw = ((buf[0] as u16) << 8) | buf[1] as u16;
let temp = 175.72 * (raw as f32) / 65536.0 - 46.85;
Ok(temp)
}
هذا الكود يعمل مع STM32 أو ESP32 أو أي متحكم يدعم embedded-hal دون تعديل. هذه هي قوة التجريد في Rust المدمج.
مثال عملي: قراءة مستشعر BME280 عبر I2C وإرسال البيانات عبر UART
مستشعر BME280 يقيس الحرارة والرطوبة والضغط الجوي. نقرأه عبر I2C ونرسل البيانات عبر UART للتشخيص.
#define BME280_ADDR 0x76
typedef struct {
float temperature;
float humidity;
float pressure;
} BME280_Data;
void bme280_init(void) {
// إعادة ضبط المستشعر
i2c_write_reg(BME280_ADDR, 0xE0, 0xB6);
HAL_Delay(10);
// وضع القياس: حرارة x2، رطوبة x2، ضغط x2
i2c_write_reg(BME280_ADDR, 0xF2, 0x02);
i2c_write_reg(BME280_ADDR, 0xF4, 0x4B);
i2c_write_reg(BME280_ADDR, 0xF5, 0x08);
}
BME280_Data bme280_read(void) {
BME280_Data data;
uint8_t raw[8];
uint8_t reg = 0xF7;
HAL_I2C_Master_Transmit(&hi2c1, BME280_ADDR << 1, ®, 1, 100);
HAL_I2C_Master_Receive(&hi2c1, BME280_ADDR << 1, raw, 8, 100);
// معالجة البيانات الخام (مبسّطة)
int32_t adc_t = ((int32_t)raw[3] << 12) | ((int32_t)raw[4] << 4) | (raw[5] >> 4);
data.temperature = adc_t / 100.0f; // تحتاج معاملات المعايرة الفعلية
data.pressure = 1013.25f;
data.humidity = 50.0f;
return data;
}
int main(void) {
HAL_Init();
i2c_init();
uart_init(115200);
bme280_init();
while (1) {
BME280_Data d = bme280_read();
char buf[80];
int len = snprintf(buf, sizeof(buf),
"T=%.1f C H=%.1f %% P=%.1f hPa\r\n",
d.temperature, d.humidity, d.pressure);
uart_send(buf, len);
HAL_Delay(1000);
}
}
الخلاصة
بروتوكولات UART و SPI و I2C هي اللغات التي تتحدث بها المكونات الإلكترونية مع بعضها. اختيار البروتوكول المناسب يعتمد على السرعة المطلوبة وعدد الأجهزة والمسافة. في الدرس القادم سنتعلم المؤقتات و PWM للتحكم الدقيق بالتوقيت وسرعة المحركات.