مقاطعات العتاد وRing Buffer في الأنظمة المدمجة: تصميم UART لا يفقد البيانات
دراسة تقنية عميقة لبناء مسار استقبال UART يعتمد على ISR وRing Buffer مع تحليل التزامن، الفائض، إدارة الذاكرة، والاختبارات على المتحكمات الدقيقة.
Embedded SystemsUARTISRRing BufferBare MetalC/C++
مقدمة: لماذا يستحق UART تصميمًا دقيقًا؟
في الأنظمة المدمجة، يبدو UART واجهة بسيطة: خط استقبال، خط إرسال، وسرعة baud rate محددة. لكن هذا التصور ينهار عندما يعمل الجهاز في بيئة صناعية ويستقبل بيانات مستمرة من حساس أو مشغل أو وحدة اتصال لاسلكية.
قد تصل الرسائل في الوقت نفسه الذي ينفذ فيه المعالج حسابات تحكم، أو يكتب سجلًا في الذاكرة، أو يتعامل مع بروتوكول آخر. إذا صُمم الاستقبال بطريقة حاجزة أو اعتمد على polling بطيء، فسيصل بايت جديد قبل أن يقرأ البرنامج البايت السابق.
النتيجة هي Overrun Error أو رسالة ناقصة أو بروتوكول فقد تزامنه. والأسوأ أن الخطأ قد يظهر مرة كل عدة ساعات، لذلك لا يكفي اختبار الجهاز في ظروف هادئة.
هذا المقال يشرح تصميم طبقة استقبال عملية تعتمد على Interrupt Service Routine ومخزن دائري Ring Buffer، مع توضيح حدود الحل ومتى ننتقل إلى DMA.
1. تعريف المشكلة زمنيًا
لنفترض أن UART يعمل بسرعة 115200 baud مع 8 بت بيانات، وبت بداية، وبت توقف واحد. كل بايت يحتاج تقريبًا 10 بت على الخط.
زمن البايت الواحد يساوي تقريبًا:
10 / 115200 = 86.8 microseconds
هذا يعني أن المعالج قد يملك أقل من 87 ميكروثانية قبل وصول البايت التالي. في متحكم يعمل بسرعة منخفضة أو ينفذ مقطعًا حرجًا، هذه النافذة قصيرة جدًا.
عند سرعة 921600 baud يصبح الزمن قريبًا من 10.8 ميكروثانية فقط. هنا يصبح polling التقليدي مخاطرة حقيقية، حتى لو كان متوسط حمل المعالج منخفضًا.
المهم هو أسوأ زمن استجابة، وليس المتوسط. النظام الذي يستجيب بسرعة في 99% من الحالات قد يفشل في اللحظة التي تكون فيها الرسالة الحرجة قيد النقل.
2. لماذا يفشل Polling؟
في نموذج polling، تنفذ الحلقة الرئيسية شيئًا شبيهًا بهذا:
while (1) { if (UART_STATUS_REG & UART_RX_READY) { uint8_t byte = UART_DATA_REG; process_byte(byte); } read_sensor(); update_display(); run_control_loop();}
المشكلة ليست في السطر نفسه، بل في الزمن الذي تفصل فيه العمليات الأخرى بين فحص حالة UART وقراءة سجل البيانات.
إذا استغرقت update_display() وقتًا أطول من زمن بايت واحد، فقد يكتب العتاد البايت الجديد فوق سجل الاستقبال أو يعلن حالة overflow.
يمكن تقليل الخطر بتقصير الحلقة، لكن ذلك يحول المشكلة إلى سباق دائم بين البرنامج والعتاد. الحل الصحيح هو نقل لحظة الالتقاط إلى مقاطعة قريبة من مصدر الحدث.
3. ما الذي يجب أن تفعله ISR؟
القاعدة الذهبية هي أن ISR يجب أن تكون قصيرة وحتمية.
ينبغي أن تقوم بالخطوات التالية فقط:
قراءة سجل الحالة لتثبيت حالة المقاطعة.
قراءة سجل البيانات قبل فقدان البايت.
وضع البايت في المخزن الدائري.
تحريك مؤشر الكتابة.
تسجيل overflow إن لم توجد مساحة.
الخروج بأسرع وقت ممكن.
لا ينبغي أن تقوم ISR بتحليل JSON أو البحث عن نهاية رسالة أو تنفيذ printf أو الانتظار عبر delay.
كل عملية إضافية تزيد زمن حجب المقاطعات الأخرى وتخلق تذبذبًا في زمن الاستجابة.
4. نموذج Ring Buffer
المخزن الدائري هو مصفوفة ثابتة مع مؤشرين منطقيين: head للكتابة وtail للقراءة.
buffer: [A][B][C][D][ ][ ][ ][ ] ^ ^ tail head
تكتب ISR في موضع head ثم تزيده. تقرأ الحلقة الرئيسية من موضع tail ثم تزيده.
عند الوصول إلى نهاية المصفوفة يعود المؤشر إلى الصفر. لهذا السبب يسمى المخزن دائريًا.
يمكن استخدام عملية modulo، لكن في المسارات السريعة يفضل أن يكون الحجم قوة للعدد 2 حتى نستعمل قناعًا بتّيًا:
next = (index + 1u) & (BUFFER_SIZE - 1u);
هذه الطريقة تتطلب أن يكون BUFFER_SIZE مثل 64 أو 128 أو 256، وليس 100.
5. اختيار سياسة الامتلاء
عندما يصبح المخزن ممتلئًا، توجد عدة سياسات ممكنة.
السياسة الأولى: رفض البايت الجديد
يبقى المحتوى القديم سليمًا، ويسجل النظام حالة overflow. هذه السياسة مناسبة للرسائل التي لا يجوز فقد بدايتها.
السياسة الثانية: إسقاط أقدم بايت
يتقدم tail قبل كتابة البايت الجديد. هذه السياسة تحافظ على أحدث البيانات، لكنها قد تجعل الرسالة الحالية غير صالحة.
السياسة الثالثة: إيقاف الاستقبال
تستخدم عندما يكون فقدان البيانات أخطر من إيقاف المصدر وإطلاق إنذار للنظام الأعلى.
لا توجد سياسة عامة صحيحة لكل المشاريع. يجب أن يحددها بروتوكول النظام.
6. تنفيذ C آمن نسبيًا
المثال التالي يوضح بنية مخزن استقبال أحادي المنتج وأحادي المستهلك:
في نموذج single-producer/single-consumer، يكتب كل مؤشر طرف واحد فقط. هذا يقلل الحاجة إلى أقفال ثقيلة.
لكن يجب مراجعة حجم المؤشر وترتيب الذاكرة وفق نوع المعالج والمترجم.
9. ربط المقاطعة بالعتاد
يختلف اسم سجل UART بين STM32 وAVR وNXP وESP32، لكن التسلسل المنطقي واحد:
void USART1_IRQHandler(void) { uint32_t status = USART1->ISR; if ((status & USART_ISR_RXNE_RXFNE) != 0u) { uint8_t value = (uint8_t)USART1->RDR; uart_rx_push_from_isr(value); } if ((status & USART_ISR_ORE) != 0u) { uart_rx_error_count++; (void)USART1->RDR; }}
يجب قراءة دليل المتحكم لأن مسح أعلام الأخطاء قد يتطلب ترتيبًا محددًا لقراءة سجل الحالة والبيانات.
الاعتماد على مثال من متحكم مختلف قد يترك العلم مرفوعًا ويعيد تشغيل ISR بلا توقف.
10. التزامن والذاكرة المتطايرة
استخدام volatile يخبر المترجم بأن القيمة قد تتغير خارج التدفق العادي للبرنامج.
لكنه لا يجعل العمليات المركبة ذرية، ولا ينشئ حاجز ذاكرة كاملًا، ولا يحل مشكلة القراءة والكتابة المتزامنة تلقائيًا.
إذا كان المؤشر بحجم يمكن للمعالج قراءته في تعليمة واحدة، فقد يكون الوصول إليه آمنًا في هذا السيناريو. إذا لم يكن كذلك، نحتاج إلى تعطيل مقاطعة UART لحظيًا أو استخدام آلية ذرية مناسبة.
يجب ألا نعطل جميع المقاطعات لفترة طويلة من أجل قراءة بضعة بايتات. مدة المنطقة الحرجة يجب أن تكون قابلة للقياس وبأقل عدد من التعليمات.
11. تحليل حجم المخزن
حجم المخزن ليس رقمًا عشوائيًا. نحسبه من أعلى معدل وصول، وأسوأ زمن لا يستطيع فيه المستهلك معالجة البيانات.
اختيار CRC يعتمد على طول الإطار ونموذج الأخطاء المطلوب. لا تستخدم checksum بسيطًا لمجرد أنه أسهل إذا كان البروتوكول يحتاج كشف أخطاء متجاورة.
15. أخطاء شائعة في التنفيذ
تحديث المؤشر قبل البيانات
يؤدي إلى قراءة مستهلكة لبيانات غير مكتملة.
استخدام حجم غير مناسب مع modulo
المؤشر قد يقفز خارج المصفوفة إذا استعملنا قناعًا مع حجم ليس قوة للعدد 2.
تنفيذ printf داخل ISR
قد تكون printf حاجزة وثقيلة وتستخدم قفلًا داخليًا أو مخزنًا مشتركًا.
تجاهل overflow
الخطأ الذي لا يسجل لا يمكن تشخيصه. يجب عدّه وإظهاره في telemetry.
نسخ البيانات إلى مصفوفة غير محدودة
طول الرسالة القادم من الطرف الآخر لا يُوثق به. تحقق من الطول قبل النسخ.
افتراض أن volatile يحل كل شيء
هو أداة للملاحظة، وليس بروتوكول تزامن كاملًا.
16. الاختبارات الوحدوية
اختبر المخزن دون العتاد أولًا.
الحالات الأساسية هي:
القراءة من مخزن فارغ.
كتابة بايت واحد ثم قراءته.
الالتفاف من نهاية المصفوفة إلى بدايتها.
الامتلاء واحتساب overflow.
كتابة وقراءة متناوبتان.
كتابة متواصلة مع مستهلك أبطأ.
عدم تغيير البيانات بعد إخراجها.
يمكن تشغيل هذه الاختبارات على الحاسوب مع تنفيذ C عادي، ثم إضافة اختبارات تكامل على اللوحة.
17. اختبار الضغط
في اختبار الضغط، أرسل نمطًا متسلسلًا مثل 0, 1, 2 ... 255 لفترة طويلة.
يقرأ المستقبل البيانات ويقارن كل بايت بالمتوقع. عند حدوث فجوة يمكن معرفة هل الخطأ من النقل أو من parser أو من overflow.
يجب تنفيذ الاختبار أثناء تشغيل مهام تشبه الواقع، مثل تحديث شاشة أو قراءة ADC أو تسجيل Flash.
الاختبار الساكن في حلقة فارغة لا يمثل المنتج الحقيقي.
18. متى نستخدم DMA؟
إذا ارتفع baud rate أو كبرت الرسائل أو كان المعالج يتعامل مع عدة قنوات، قد تصبح مقاطعة لكل بايت مكلفة.
هنا نستخدم DMA مع مخزن دائري أو مخزن مزدوج.
DMA ينقل البيانات من سجل UART إلى RAM دون تدخل المعالج لكل بايت، ثم نستخدم مقاطعة عند نصف المخزن أو نهايته.
لكن DMA لا يلغي الحاجة إلى تصميم صحيح؛ يجب معرفة عدد البايتات الجديدة، ومعالجة التفاف المؤشر، وتزامن cache في المعالجات التي تحتوي على data cache.
19. مقارنة ISR لكل بايت مع DMA
العامل
ISR لكل بايت
DMA دائري
البساطة
أعلى
متوسطة
حمل المعالج
يزيد مع baud rate
منخفض
زمن الاستجابة
ممتاز للبايت
يعتمد على حجم الدفعة
التعقيد
منخفض
أعلى
مناسب لـ
رسائل قصيرة
تدفق مستمر وكبير
ابدأ بالحل الأبسط القابل للقياس، ثم انتقل إلى DMA عندما تثبت الأرقام الحاجة.
20. المراقبة في المنتج
أضف عدادات لا تستهلك وقتًا كبيرًا:
عدد البايتات المستلمة.
عدد overflow.
عدد أخطاء framing وparity.
أكبر استخدام للمخزن.
عدد الإطارات المرفوضة بسبب CRC.
متوسط زمن معالجة الإطار.
هذه العدادات تختصر ساعات من التخمين عند ظهور مشكلة في موقع العميل.
21. الخلاصة
تصميم UART الجيد ليس مجرد تفعيل interrupt. هو عقد واضح بين العتاد وISR وRing Buffer وparser.
تحافظ ISR على البايت، ويحافظ المخزن على الترتيب، وتتحقق آلة الحالات من الإطار، ويقرر البروتوكول ما يجب فعله عند الخطأ.
عندما تقاس أسوأ الحالات ويُختبر overflow وتُفصل طبقات النقل عن التحليل، تصبح الاتصالات مستقرة وقابلة للصيانة حتى في البيئات الصناعية الصعبة.
Giriş: UART neden dikkatli bir tasarım gerektirir?
Gömülü sistemlerde UART basit bir arayüz gibi görünür: bir alım hattı, bir gönderim hattı ve belirli bir baud rate. Ancak cihaz endüstriyel bir ortamda bir sensörden, aktüatörden veya kablosuz iletişim modülünden sürekli veri aldığında bu bakış açısı yetersiz kalır.
Mesajlar işlemcinin kontrol hesapları yaptığı, belleğe kayıt yazdığı veya başka bir protokolle ilgilendiği anda gelebilir. Alım yolu bloklayıcı tasarlanır ya da yavaş bir polling döngüsüne dayanırsa program önceki byte'ı okuyamadan yeni byte gelir.
Sonuç Overrun Error, eksik bir mesaj veya protokol senkronizasyonunun kaybıdır. Daha kötüsü, hata birkaç saatte bir görülebilir; bu yüzden cihazı yalnızca sakin koşullarda test etmek yeterli değildir.
Bu makale Interrupt Service Routine ve dairesel Ring Buffer kullanan pratik bir alım katmanını, çözümün sınırlarını ve ne zaman DMA'ya geçilmesi gerektiğini açıklar.
1. Problemi zaman açısından tanımlamak
UART'ın 8 veri biti, bir başlangıç biti ve bir durdurma biti ile 115200 baud hızında çalıştığını varsayalım. Her byte hat üzerinde yaklaşık 10 bit kaplar.
Bir byte'ın süresi yaklaşık olarak:
10 / 115200 = 86.8 microseconds
Bu, bir sonraki byte gelmeden önce işlemcinin 87 mikrosaniyeden daha az zamanı olabileceği anlamına gelir. Düşük hızlı bir mikrodenetleyicide veya kritik bir bölüm çalışırken bu pencere çok kısadır.
921600 baud hızında süre yalnızca yaklaşık 10.8 mikrosaniyeye düşer. Ortalama işlemci yükü düşük olsa bile geleneksel polling gerçek bir riske dönüşür.
Önemli olan ortalama değil, en kötü tepki süresidir. Sistem durumların %99'unda hızlı yanıt verse bile kritik mesaj aktarılırken başarısız olabilir.
2. Polling neden başarısız olur?
Polling modelinde ana döngü aşağıdakine benzer:
while (1) {
if (UART_STATUS_REG & UART_RX_READY) {
uint8_t byte = UART_DATA_REG;
process_byte(byte);
}
read_sensor();
update_display();
run_control_loop();
}
Sorun satırın kendisi değil, diğer işlemlerin UART durumunu kontrol etmek ile veri kaydını okumak arasına koyduğu süredir.
update_display() tek bir byte'ın süresinden uzun sürerse donanım yeni byte'ı alım kaydının üzerine yazabilir veya overflow durumunu bildirebilir.
Döngüyü kısaltmak riski azaltabilir, fakat problemi program ile donanım arasındaki sürekli bir yarışa dönüştürür. Doğru çözüm yakalama anını olay kaynağına yakın bir kesmeye taşımaktır.
3. ISR ne yapmalıdır?
Altın kural ISR'ın kısa ve deterministik olmasıdır.
Yalnızca şu adımları gerçekleştirmelidir:
Kesme durumunu sabitlemek için durum kaydını okumak.
Byte'ı kaybetmeden veri kaydını okumak.
Byte'ı dairesel belleğe koymak.
Yazma indeksini ilerletmek.
Yer yoksa overflow kaydetmek.
Mümkün olduğunca hızlı çıkmak.
ISR JSON ayrıştırmamalı, mesaj sonu aramamalı, printf çalıştırmamalı veya delay ile beklememelidir.
Her ek işlem diğer kesmelerin engellenme süresini artırır ve tepki süresinde değişkenlik oluşturur.
4. Ring Buffer modeli
Dairesel bellek, head yazma ve tail okuma için kullanılan iki mantıksal indisi olan sabit bir dizidir.
buffer: [A][B][C][D][ ][ ][ ][ ]
^ ^
tail head
ISR head konumuna yazar ve onu artırır. Ana döngü tail konumundan okur ve onu artırır.
Dizinin sonuna ulaşınca indeks sıfıra döner; yapı bu nedenle daireseldir.
Modulo işlemi kullanılabilir, ancak hızlı yollarda boyutun 2'nin kuvveti olması ve bit maskesi kullanılması tercih edilir:
next = (index + 1u) & (BUFFER_SIZE - 1u);
Bu yöntem BUFFER_SIZE değerinin 64, 128 veya 256 gibi olmasını gerektirir; 100 uygun değildir.
5. Doluluk politikasını seçmek
Bellek dolduğunda birkaç politika uygulanabilir.
Birinci politika: yeni byte'ı reddetmek
Eski içerik korunur ve sistem overflow durumunu kaydeder. Mesajın başlangıcının kaybedilemeyeceği durumlar için uygundur.
İkinci politika: en eski byte'ı düşürmek
Yeni byte yazılmadan önce tail ilerletilir. En güncel veriyi korur, ancak mevcut mesajı geçersiz kılabilir.
Üçüncü politika: alımı durdurmak
Veri kaybı kaynağı durdurmaktan daha tehlikeli olduğunda kullanılır ve üst katmana alarm gönderilir.
Her proje için doğru olan tek bir politika yoktur; bunu sistem protokolü belirlemelidir.
6. Görece güvenli bir C uygulaması
Aşağıdaki örnek tek üreticili ve tek tüketicili bir alım belleğinin yapısını gösterir:
Komutların sırası önemlidir. Yeni head değeri yayınlanmadan önce byte diziye yazılmalıdır.
head önce güncellenirse tüketici ISR'ın henüz yazmadığı bir konumu okuyabilir.
Bazı işlemcilerde 16 bit okuma ve yazma, veri yolu 8 bit ise atomik değildir. Böyle bir durumda işlemciye uygun bir tür seçilmeli veya erişim korunmalıdır.
Tek üreticili ve tek tüketicili modelde her indeksi yalnızca bir taraf yazar. Bu, ağır kilit ihtiyacını azaltır.
Yine de indeks genişliği ve bellek sıralaması işlemci ve derleyiciye göre incelenmelidir.
9. Kesmenin donanıma bağlanması
UART kayıtlarının adı STM32, AVR, NXP ve ESP32 arasında değişir, ancak mantıksal sıra aynıdır:
void USART1_IRQHandler(void) {
uint32_t status = USART1->ISR;
if ((status & USART_ISR_RXNE_RXFNE) != 0u) {
uint8_t value = (uint8_t)USART1->RDR;
uart_rx_push_from_isr(value);
}
if ((status & USART_ISR_ORE) != 0u) {
uart_rx_error_count++;
(void)USART1->RDR;
}
}
Hata bayraklarının temizlenmesi durum ve veri kayıtlarının belirli bir sırayla okunmasını gerektirebilir; bu yüzden mikrodenetleyici kılavuzu okunmalıdır.
Başka bir işlemcinin örneğine güvenmek bayrağın açık kalmasına ve ISR'ın durmadan yeniden çalışmasına yol açabilir.
10. Eşzamanlılık ve volatile bellek
volatile, derleyiciye değerin programın normal akışı dışında değişebileceğini söyler.
Ancak bileşik işlemleri atomik yapmaz, tam bir bellek bariyeri oluşturmaz ve eşzamanlı okumayı yazmayı kendiliğinden çözmez.
İndeks işlemcinin tek komutta okuyabileceği genişlikteyse bu senaryoda erişim güvenli olabilir. Değilse UART kesmesini kısa süreli devre dışı bırakmak veya uygun atomik mekanizma kullanmak gerekir.
Birkaç byte okumak için tüm kesmeler uzun süre kapatılmamalıdır. Kritik bölüm ölçülebilir olmalı ve mümkün olan en az komutu içermelidir.
11. Bellek boyutunu analiz etmek
Bellek boyutu rastgele seçilmez; en yüksek geliş hızından ve tüketicinin veriyi işleyemediği en kötü süreden hesaplanır.
CRC seçimi çerçeve uzunluğuna ve beklenen hata modeline bağlıdır. Protokol ardışık hataları yakalamayı gerektiriyorsa sırf kolay olduğu için basit checksum kullanılmamalıdır.
15. Yaygın uygulama hataları
İndeksi veriden önce güncellemek: Tüketicinin tamamlanmamış veriyi okumasına yol açar.
Modulo ile uyumsuz boyut kullanmak: 2'nin kuvveti olmayan boyutta maske indeksi dizi dışına taşıyabilir.
ISR içinde printf çalıştırmak:printf bloklayıcı ve ağır olabilir; iç kilit veya ortak bellek kullanabilir.
Overflow'u yok saymak: Kaydedilmeyen hata teşhis edilemez; sayaç telemetry'de görünmelidir.
Sınırsız diziye kopyalamak: Karşı taraftan gelen uzunluğa güvenilmemeli, kopyalamadan önce doğrulanmalıdır.
Volatile'ın her şeyi çözdüğünü sanmak: Bu yalnızca gözlem aracıdır, tam bir eşzamanlılık protokolü değildir.
16. Birim testleri
Önce belleği donanım olmadan test edin.
Boş bellekten okuma.
Tek byte yazıp okuma.
Dizinin sonundan başına sarma.
Doluluk ve overflow sayımı.
Dönüşümlü yazma ve okuma.
Tüketici daha yavaşken sürekli yazma.
Çıkarıldıktan sonra verinin değişmemesi.
Bu testler bilgisayarda normal C ile çalıştırılabilir, ardından kart üzerinde entegrasyon testleri eklenebilir.
17. Yük testi
Yük testinde uzun süre 0, 1, 2 ... 255 gibi sıralı bir desen gönderin.
Alıcı veriyi okuyup beklenen byte ile karşılaştırır. Aralık oluştuğunda hatanın taşımadan mı, parser'dan mı, yoksa overflow'dan mı geldiği anlaşılabilir.
Test ekran güncelleme, ADC okuma veya Flash kaydı gibi gerçeğe benzeyen görevler çalışırken yapılmalıdır.
Boş bir döngüdeki statik test gerçek ürünü temsil etmez.
18. DMA ne zaman kullanılır?
Baud rate yükselirse, mesajlar büyürse veya işlemci birden çok kanalla ilgilenirse byte başına kesme pahalı hale gelebilir.
Bu durumda dairesel veya çift bellekle DMA kullanılır.
DMA UART kaydından RAM'e her byte için işlemci müdahalesi olmadan taşır; sonra belleğin yarısında veya sonunda kesme kullanılır.
DMA doğru tasarım ihtiyacını ortadan kaldırmaz: yeni byte sayısı, indeks sarılması ve data cache bulunan işlemcilerde cache eşzamanlılığı bilinmelidir.
19. Byte başına ISR ve DMA karşılaştırması
Faktör
Byte başına ISR
Dairesel DMA
Basitlik
Daha yüksek
Orta
İşlemci yükü
Baud rate ile artar
Düşük
Tepki süresi
Byte için mükemmel
Blok boyutuna bağlı
Karmaşıklık
Düşük
Daha yüksek
Uygun olduğu yer
Kısa mesajlar
Sürekli ve büyük akış
Ölçülebilir en basit çözümle başlayın; sayılar ihtiyacı kanıtladığında DMA'ya geçin.
20. Üründe izleme
Çok fazla zaman harcamayan sayaçlar ekleyin:
Alınan byte sayısı.
Overflow sayısı.
Framing ve parity hata sayısı.
Belleğin en yüksek kullanım seviyesi.
CRC nedeniyle reddedilen çerçeve sayısı.
Çerçeve işleme süresinin ortalaması.
Bu sayaçlar müşteri sahasında sorun çıktığında saatler süren tahmini kısaltır.
21. Sonuç
İyi UART tasarımı yalnızca interrupt etkinleştirmek değildir. Donanım, ISR, Ring Buffer ve parser arasında açık bir sözleşmedir.
ISR byte'ı korur, bellek sırayı korur, durum makinesi çerçeveyi doğrular ve protokol hata durumunda ne yapılacağına karar verir.
En kötü durumlar ölçülür, overflow test edilir ve taşıma ile analiz ayrılırsa iletişim zor endüstriyel ortamlarda bile kararlı ve bakımı kolay olur.
Introduction: why UART deserves careful design
In embedded systems, UART looks simple: one receive line, one transmit line, and a defined baud rate. That view breaks down when an industrial device continuously receives data from a sensor, actuator, or wireless module.
Messages may arrive while the processor is running control calculations, writing a record to memory, or handling another protocol. If reception is blocking or relies on slow polling, a new byte can arrive before the previous byte is read.
The result is an Overrun Error, an incomplete message, or lost protocol synchronization. Worse, the failure may appear once every few hours, so testing only in calm conditions is not enough.
This article explains a practical receive layer based on an Interrupt Service Routine and a circular Ring Buffer, its limits, and when to move to DMA.
1. Define the problem in time
Assume UART runs at 115200 baud with 8 data bits, one start bit, and one stop bit. Each byte occupies roughly 10 bits on the line.
The time for one byte is approximately:
10 / 115200 = 86.8 microseconds
That means the processor may have less than 87 microseconds before the next byte arrives. On a slow microcontroller or inside a critical section, this window is very short.
At 921600 baud the time is only about 10.8 microseconds. Traditional polling becomes a real risk even when average CPU load is low.
The important value is worst-case response time, not the average. A system that responds quickly in 99% of cases can still fail while a critical message is in transit.
2. Why polling fails
A polling model may look like this:
while (1) {
if (UART_STATUS_REG & UART_RX_READY) {
uint8_t byte = UART_DATA_REG;
process_byte(byte);
}
read_sensor();
update_display();
run_control_loop();
}
The problem is not the line itself, but the time other operations place between checking UART status and reading the data register.
If update_display() takes longer than one byte time, hardware may overwrite the receive register or report an overflow.
Shortening the loop reduces the risk, but turns the problem into a permanent race between software and hardware. The correct solution moves capture close to the event source, into an interrupt.
3. What should the ISR do?
The golden rule is that an ISR must be short and deterministic.
It should perform only these steps:
Read the status register to latch the interrupt state.
Read the data register before the byte is lost.
Put the byte in the circular buffer.
Advance the write index.
Record overflow if there is no space.
Exit as quickly as possible.
The ISR should not parse JSON, search for an end marker, call printf, or wait with delay.
Every additional operation increases the time other interrupts are blocked and creates response-time jitter.
4. Ring Buffer model
A circular buffer is a fixed array with two logical indexes: head for writing and tail for reading.
buffer: [A][B][C][D][ ][ ][ ][ ]
^ ^
tail head
The ISR writes at head and advances it. The main loop reads at tail and advances it.
When the end of the array is reached, the index returns to zero; this is why the structure is circular.
Modulo can be used, but fast paths preferably use a power-of-two size and a bit mask:
next = (index + 1u) & (BUFFER_SIZE - 1u);
This requires BUFFER_SIZE to be 64, 128, or 256, for example, rather than 100.
5. Choose an overflow policy
When the buffer is full, several policies are possible.
Policy one: reject the new byte
Old content remains intact and the system records overflow. This suits messages whose beginning must not be lost.
Policy two: drop the oldest byte
Advance tail before writing the new byte. This preserves the newest data but may invalidate the current message.
Policy three: stop reception
Use this when losing data is more dangerous than stopping the source and raising an alarm to the higher-level system.
There is no universally correct policy; the system protocol must define it.
6. A relatively safe C implementation
The following example shows a single-producer, single-consumer receive buffer:
Instruction order matters. The byte must be written to the array before publishing the new head.
If head is updated first, the consumer may read a position that the ISR has not written yet.
On some processors, 16-bit reads and writes are not atomic when the data bus is 8-bit. In that case, use a processor-appropriate type or protect the access.
In a single-producer, single-consumer model, each index is written by only one side. This reduces the need for heavy locks.
Still, index width and memory ordering must be reviewed for the processor and compiler.
9. Connect the interrupt to hardware
UART register names differ across STM32, AVR, NXP, and ESP32, but the logical sequence is the same:
void USART1_IRQHandler(void) {
uint32_t status = USART1->ISR;
if ((status & USART_ISR_RXNE_RXFNE) != 0u) {
uint8_t value = (uint8_t)USART1->RDR;
uart_rx_push_from_isr(value);
}
if ((status & USART_ISR_ORE) != 0u) {
uart_rx_error_count++;
(void)USART1->RDR;
}
}
Read the microcontroller manual because clearing error flags may require a specific order for reading status and data registers.
Relying on an example from another controller can leave the flag asserted and restart the ISR continuously.
10. Concurrency and volatile memory
volatile tells the compiler that a value may change outside the program's normal flow.
It does not make compound operations atomic, create a full memory barrier, or automatically solve concurrent reads and writes.
If the index fits a width the processor can read in one instruction, access may be safe in this scenario. Otherwise, briefly disable the UART interrupt or use a suitable atomic mechanism.
Do not disable all interrupts for a long time just to read a few bytes. The critical section should be measurable and contain as few instructions as possible.
11. Analyze buffer size
Buffer size is not arbitrary. Calculate it from the highest arrival rate and the worst time during which the consumer cannot process data.
CRC choice depends on frame length and the required error model. Do not use a simple checksum merely because it is easier when the protocol needs to detect burst errors.
15. Common implementation mistakes
Updating the index before the data: The consumer can read incomplete data.
Using an incompatible modulo size: A mask with a non-power-of-two size can move the index outside the array.
Calling printf inside the ISR:printf may block, be expensive, and use an internal lock or shared buffer.
Ignoring overflow: An unrecorded error cannot be diagnosed; count it and expose it in telemetry.
Copying into an unbounded array: Never trust a remote length; validate before copying.
Assuming volatile solves everything: It is an observation tool, not a complete concurrency protocol.
16. Unit tests
Test the buffer without hardware first.
Reading from an empty buffer.
Writing and reading one byte.
Wrapping from the end of the array to the beginning.
Full condition and overflow counting.
Alternating writes and reads.
Continuous writes with a slower consumer.
Ensuring data does not change after it is removed.
Run these tests on a computer with ordinary C, then add integration tests on the board.
17. Stress testing
In a stress test, send a sequence such as 0, 1, 2 ... 255 for a long period.
The receiver compares every byte with the expected value. A gap reveals whether the failure came from transport, the parser, or overflow.
Run the test while realistic tasks such as display updates, ADC reads, or Flash logging are active.
A static test in an empty loop does not represent the real product.
18. When should DMA be used?
If baud rate rises, messages grow, or the processor handles several channels, an interrupt per byte can become expensive.
Use DMA with a circular or double buffer in that case.
DMA moves data from the UART register to RAM without processor intervention for each byte; then use an interrupt at the buffer half-way point or end.
DMA does not remove the need for sound design: know the number of new bytes, handle index wrapping, and manage cache coherency on processors with a data cache.
19. Per-byte ISR versus DMA
Factor
Per-byte ISR
Circular DMA
Simplicity
Higher
Medium
CPU load
Increases with baud rate
Low
Response time
Excellent per byte
Depends on batch size
Complexity
Low
Higher
Best for
Short messages
Large continuous streams
Start with the simplest measurable solution and move to DMA when the numbers prove it is needed.
20. Production monitoring
Add counters that do not consume much time:
Received byte count.
Overflow count.
Framing and parity error counts.
Peak buffer usage.
Frames rejected because of CRC.
Average frame-processing time.
These counters replace hours of guesswork when a field issue appears.
21. Conclusion
Good UART design is more than enabling an interrupt. It is a clear contract between hardware, the ISR, the Ring Buffer, and the parser.
The ISR preserves bytes, the buffer preserves order, the state machine validates the frame, and the protocol decides what to do on failure.
When worst cases are measured, overflow is tested, and transport is separated from analysis, communication remains stable and maintainable even in demanding industrial environments.