BİR ÇUBUĞU PID KONTROLU İLE İSTENEN AÇIDA TUTMAK

Bir eksen etrafında hareket edebilecek şekilde bağlı bir çubuğu yatay durumda ya da istediğimiz herhangi bir eğimde sabit tutmak istiyoruz. Öyle ki bağlı olduğu platformu nasıl hareket ettirirsek ettirelim aynı eğimi korumaya devam etsin, çubuğu itip pozisyonunu bozsak da hemen kendini toparlayıp önceki eğimine geri dönsün.

Bu projede ortasındaki mafsal etrafında serbestçe hareket edebilen bir çubuktan oluşan “tahtırevalli” yi istediğimiz eğimde tutmaya çalışacağız. Bunun için kontrol işlemcimiz STM32F103, eğim/ivme sensörümüz ADXL345, aktivatörümüz de çubuğumuzun iki ucundaki pervaneli drone motorları olacak. Şimdilik iki motordan birisini kullanacağız, iki motorlu çalışma bir başka projeye…

Türevsel bileşeni bu projede de devreye sokmuyorum ama ne olduğunu, ne zaman kullanılması gerekeceğini matematiğe girmeden basitçe anlatıyorum.

ADXL345 ile eğim ölçme ve PID kontrolu altında DC motor kontrolu hakkında daha önce yayınlarım olmuştu, bu projeye girişmeden önce o yayınlara göz atmakta yarar var derim:

Bu proje çubuğun tek tarafındaki tek motor ile dengede tutmayı ele alıyor. Bunun devamı olan çalışmada aynı işi çubuğun iki ucundaki iki motor ile yapıyoruz. O yayına ulaşmak için aşağıdaki linki tıklayabilirsiniz. İkisi birbirinin tamamlayıcısı olduğundan önce bu ilk yayına göz atmalısınız):

KONFİGÜRASYON BİLGİLERİ

Yazılım : Szb_Mapple_ADXL345_1AxisPID_R4
Donanım : Mapple Adapter R2

PROJE HAKKINDA GENEL BİLGİ

Bu proje, daha önce bir DC motorun devir hızını kontrol etmekte kullandığımız PID kontrol tekniğinin bir başka uygulaması oluyor. İsterseniz bunu bir quadcopter drone yapmak için ilk adım olarak düşünebilirsiniz. Ben daha çok PID kontrol tekniğini pekiştirmek için bir egzersiz olarak görüyorum.

Projede çubuğun eğimini ADXL ivme sensörü ile ölçüyoruz. Çubuğun ucunu indirip kaldırmak için pervaneli minik bir drone motoru, motoru mikrodenetleyici ile sürmek için de basit bir MOSFET sürücü kullanıyoruz. Önceki projede motor sürmek için L298 sürücü kullanmıştık, bu sefer minik bir MOSFET yeterli olur diye düşündüm, yetti de. Kontrol sistemimiz de STM32F103C8 tabanlı bir geliştirme kiti. Bunun üzerinde bir TFT ekranımız var.

İstenen eğim açısını STM32 mikro denetleyiciye çok turlu bir potansiyometre ile bildiriyoruz.

MOTOR

Motor mini dronelarda kullanılan 3.3V DC, yüksek devirli 7.5×4.5cm plastik pervaneli tiplerden. Pervanenin 15-20 saniye çalıştıktan sonra milden çıkıp uçup gitmesi dışında gayet iyi performans veriyor.

Bu motor ve pervaneleri internetten ikili setler halinde bulunabiliyor. Aşağıdaki benim N11.com’dan bulup satın aldıklarım:

ADXL345 İVME ÖLÇER

ADXL345 üç eksende ivme ölçen bir sensör. Değişik skalalarda +-2g den +-16g ye seçilebilen aralıklarda 10 bit çözünürlükte ölçümler yapabiliyor. Mikro denetleyici ile iletişim I2C protokolu ile sağlanıyor. Açısal ivme ölçemiyor, yani “gyro” ölçümü yok.

İvme sensörü hakkında bir not:
ADXL ivme ölçeri 3 eksenli, her bir eksenin eksene dik yönde maruz kaldığı ivmeyi ölçüyor. Bu durumda X ekseni yatay konumda ve sabit iken yerçekimi nedeni ile 1g, yani 9.8m/s2 ivmeye maruz kalıyor. Yere paralel kalmasına rağmen aşağı yukarı hareket ettirildiğinde farklı ölçümler alıyorsunuz, örneğin serbest düşme sırasında 0 ivme okuyabilirsiniz. Dikey konumda iken ise 0g ölçüm sonucu alıyorsunuz. Diğer eksenler için de aynısı geçerli.  Bu durumda sensör hareket halinde olmasa da yerçekimi nedeni ile X,Y,Z eksenlerinin açısal konumunu algılayabiliyoruz. Ama titreşim ve hareket varsa eğim bilgisi açısından hatalı sonuçlar algılıyorsunuz. Bunun yeterli olduğu pek çok uygulama var, bu projede olduğu gibi.

Gyro ölçebilen sensör kullanıyorsak durum biraz farklı. Eksenler etrafındaki dönüş hareketlerini algıladığımız gyro ölçümleri dinamik ölçümler. Yani sensör hareketsiz iken "0" değeri algılıyorsunuz. Ama eksen etrafında döndüğünde ortaya çıkan açısal ivmeyi algılayabiliyorsunuz.

Bu iki ölçüm birbirinin tamamlayıcısı olarak kullanılıyor. Statik ivme ölçümü eksenlerin mutlak eğimi hakkında bilgi verirken, gyro ölçümü ile hareket halinde pozisyonumuzu daha doğru belirleyebiliyoruz. Sarsıntılı, titreşimli durumlarda statik ivme ölçümleri çok parazitli hale geliyor. Motorun yarattığı titreşim bile ölçümlere önemli miktarda parazit ivme ekleyebiliyor. Gyro ölçümleri ise daha temiz bilgi sağlıyor.

Bu nedenle drone yapmaya kalkarsak gyro da ölçebilen 6 eksenli bir sensör kullanmak zorundayız.

Bu hesaplamalar hakkında daha ayrıntılı bilgi için burayı tıklayabilirsiniz.

NOT: Bu açıklamadaki eksenlerin dik ve yatay konumları ADXL chip'inin kılıfının verdiği izlenimden farklı olduğunu belirtmeliyim. Benim X ekseni için yatay konum dediğim aslında PCB üzerindeki çizime göre dikey konum oluyor. O çizim ivmenin algılanma yönünü gösteriyor. İyisi mi data sheet'deki açıklayıcı diyagramı burada vereyim.

Howtomechatronics.com Sitesinden aldığım bu çizim bir eksen üzerindeki ivmenin nasıl algılandığını güzel anlatıyor. Umarım telif haklarından dolayı başım derde girmez, ne de olsa ticari bir kazancım yok bundan:

ADXL345 TAŞIYICI VE BAĞLANTI PLAKETİ
ADXL345 sensörü uygulama plaketi üzerinde
ADXL345 SENSÖR SATINALMA BİLGİLERİ
STM32 KONTROL MODÜLÜ ve Mosfet motor sürücü

Aşağıdaki fotoda STM32F103CB tabanlı minik geliştirme modülü, bunun için tasarlamış olduğum adaptör plaketi ve derme çatma MOSFET sürücü plaketciği görülüyor. STM32 adaptör kartının görevi modülün pinlerine daha az jumper kablo ile daha derli toplu erişebilmeyi sağlamak, TFT ekranın kolayca bağlanmasını sağlamaktan ibaret.

Aşağıdaki de STM32 denetçinin TFT tarafından görünümü. Sistem çalışır ve dengede çekilmiş olan bu fotoda potansiyometre ile ayarlanmış olan set point’in 217 olduğu, buna karşılık çubuğun o andaki eğiminin 229, son 10 ölçümün (ki bu son 100ms içinde demek) ortalamasının da 216 olduğunu gösteriyor. Bu periyod içindeki kümülatif hata da sadece “1”. İyi bir sonuç.

Bu rakamların ne anlama geldiğine gelince : X ekseninin +-90o aralığında maruz kaldığı yerçekimi ivmesi +-1g ve bu +-255 aralığında ölçüm sonuçları veriyor. Algoritmamda sadece pozitif büyüklüklerle çalışmak istediğimden ben bu veriyi 0..+512 arasına taşıyorum. Yani X ekseninin 0o konumu 255 çıkışı veriyor.

Bu ekranda görülen 217 set point’i ve buna yakın olan çubuk eğimleri, çubuğun tam yatay değil, biraz aşağıya bakar şekilde konumlandırıldığı anlamına geliyor. Yayının kapağındaki ve içindeki fotolarda da bu durum gözlenebiliyor.

Bu açıklamanın aşağıdaki algoritma ve kod bölümleri okunurken de göz önünde bulundurulması projenin daha kolay anlaşılmasını sağlayacaktır.

DEVRE ŞEMALARI

ADXL345 DEVRESİ

MOSFET (3055) MOTOR SÜRÜCÜ

Düşük Vgs gerilimli SO223 kılıflı 3055 MOSFET kullandım.
R1 = 220 Ohm
R5 = 2K2

GENEL KURULUM

Enerjisiz durumda iken sağ tarafın aşağıya çökmesini sağlamak için çubuğun o tarafını özellikle ağırlaştırdım. Yoksa motor dursa dahi eğim o tarafa kaymayabiliyor. Bu durumda dahi bağlantı kablolarının sertliğinden ve eksendeki sıkışmadan dolayı sistem rahat hareket etmiyor. Motor o tarafı aktif olarak yukarı çekerken sorun yok, ama aşağı inmesi gerektiğinde sistemin yapabileceği yegane şey motorun devrini düşürmek. İki motorun da kullanıldığı durumda bu sorun ortadan kalkacak. Her bir motor kendi tarafını yukarı kaldırırken diğer tarafı da aşağıya itebilecek.

ADXL345 MODÜL MONTAJI

MOTOR MONTAJI

Motoru izolebant ile yuvasına sıkıştırdım ama pervaneyi motor miline bir türlü yeterince sağlam iliştiremedim, 1 dakika çalışmadan uçup uçup gidiyor.

BAĞLANTILAR

  • ADXL SDA –> STM32 PB07 (I2C1 SDA)
  • ADXL SCL –> STM32 PB06 (I2C1 SCL)
  • ADXL INT1 –> STM32 PA02 (EXTI_2)
  • ADXL SDO –> GND
  • ADXL CS –> 3V3 (ADXL pin 9)
  • ADXL 5V –> 5V
  • POT Orta uç –> STM32 PA01 (ADC1_IN1)
  • MOSFET Input –> STM32 PA0 (TIM2 CH1 PWM Out)
  • MOSFET +Vcc –>5V. (STM32 den ayrı, ikinci güç kaynağı)
  • MOSFET GND –> STM32_GND ve 5V Power GND
  • MOSFET Out+ –> Motor+
  • MOSFET Out- –> Motor-

ALGORİTMA HAKKINDA BİLGİ

İVME OKUMA VE SAKLAMA

İvme okumaları için kesmelerden yararlanıyoruz. Bunun için açılış ayarlarında ADXL in “data hazır” kesmesini INT1 çıkışına yönlendiriyoruz. ADXL’in ölçüm aralığını da tam çözünürlükte +-2g olarak seçiyoruz. ADXL’in “Data Ready” kesmesini de etkinleştiriyoruz. Bunların yapılışını adxl345.c dosyası içindeki adxl_init() fonksiyonunda görebilirsiniz.

ADXL ard arda okumalar yapıyor, her okuma sonuçlandığında INT1 çıkışında bir kesme yaratıyor. Biz bu kesmeyi STM32 PA02 den algılıyoruz. Kesme fonksiyonumuz main.c içindeki HAL_GPIO_EXTI_Callback(). Bu kesmelerin periyodu burada verdiğim başlangıç ayarları ile 10ms.

Bu fonksiyon, okumalardan X eksenine ait olanlarını 10 elemanlı ICbuffer[] dizisine yerleştiriyor. (Bu projede sadece X eksenini kullanıyoruz.) Her yeni gelen okuma ile dizi elemanları birer pozisyon sağa kaydırılıp yeni gelene yer açılıyor, en sondaki de silinmiş oluyor. Yeni okunan değer de “0” ıncı indekse yerleştiriliyor. Bu şekilde ICValues[] dizisinde daima son okunan 10 ivme değeri bulunuyor.

İvme değerleri 10 bit olduğundan +-256 aralığında, biz de bu şekilde kullanıyoruz, sadece 0-512 aralığına taşıyoruz, g değerine ya da m/s2 ye çevirmeye gerek yok. İvme değerlerini m/s2 olarak veren bir fonksiyonumuz da var ama bunu kullanmıyoruz.

KESME FONKSİYONU
/**
  * @brief  EXTI line detection callbacks.
  * @param  GPIO_Pin: Specifies the pins connected EXTI line
  * @retval None
  */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
get_adxl_RawValues(&adxlRawValues);//PID kontrolunda ham değerlerle çalışacağız
/* ADXL den -256/+256 aralığında gelen okumayı 
 * 0..512 aralığına taşıyarak yerleştirelim */
	ICValue = adxlRawValues.x - rawMin;
	/* Çıkış ölçümünden (ADXL) gelen yeni değeri buffere koyalım */
for (uint8_t i = SAMPLECOUNT; i > 0; --i) { //birer pozisyon sağa kaydıralım
		ICbuffer[i] = ICbuffer[i - 1];
	}
	ICbuffer[0] = ICValue;   // yeni okumayı kaydedelim
}
ADXL FONKSİYONLARI
/*
 * adxl345.c
 *
 *  Created on: Jul 26, 2021
 *      Author: selcukozbayraktar
 */
#include "adxl345.h"
#define adxl_address 0x53<<1

I2C_HandleTypeDef hi2c1;

uint8_t data_rec[6];
uint8_t chipid=0;
//char x_char[3], y_char[3], z_char[3];

void adxl_write (uint8_t reg, uint8_t value)
{
	uint8_t data[2];
	data[0] = reg;
	data[1] = value;
	HAL_I2C_Master_Transmit (&hi2c1, adxl_address, data, 2, 100);
}

void adxl_read_values (uint8_t reg)
{
HAL_I2C_Mem_Read(&hi2c1,adxl_address,reg,1,(uint8_t*)data_rec,6,100);
}

void adxl_read_address (uint8_t reg)
{
HAL_I2C_Mem_Read (&hi2c1, adxl_address, reg, 1, &chipid, 1, 100);
}

void adxl_init (void)
{
	adxl_read_address (0x00); // read the DEVID
	TFT_Printf(0,0,24,"CHIP_ID: %X",chipid);

adxl_write (0x31, 0x08);  //full resolution,right justified,+-2g
adxl_write (0x2D, 0x00);  //reset all bits
adxl_write (0x2D, 0x08);  //power_cntl measure and wake up 8hz
adxl_write (0x2F, 0x00);  //Map data ready interrupt to INT1
adxl_write (0x2E, 0x80);  //Enable data ready interrupt
}

void get_adxl_RawValues(axisValues *raws){
	adxl_read_values(0x32);
	  raws->x = ((data_rec[1]<<8)|data_rec[0]);
	  raws->y = ((data_rec[3]<<8)|data_rec[2]);
	  raws->z = ((data_rec[5]<<8)|data_rec[4]);
}

/* +-2g skalası için 0..9.8m/s2 arası ivme değerlerine çevirir.
 * Full resolution modunda LSB 4mg ye karşı düşer.0.0098x4=0.0392
 * Float değişkenlerden kaçınmak için sonucu 100x olarak int16
 * değişkenlere yüklüyoruz. (0.0392*100 = 3.92)
 */
void get_adxl_GValues(axisValues *gValues){
	axisValues raw;
	get_adxl_RawValues(&raw);
	  gValues->x = raw.x * 3.9;
	  gValues->y = raw.y * 3.9;
	  gValues->z = raw.z * 3.9;
}

PWM ÜRETİMİ

Motoru sürmek için TIM2 nin PWM üretim becerisinden yararlanıyoruz. PWM CHANNEL 1 kullanıyoruz, bu kanalın çıkışı PA0 a bağlı.
Sistem dahili osilatör ile 64MHz de çalışıyor. Kullandığımız TIM2 zamanlayıcısı aşağıdaki blok şemada görüldüğü gibi APB1 üzerinde. (Şema STM32F103RBT6 MCU Cortex M3 CD00161566 data sheet’inden.)

Projemizi yapılandırırken APB1’i de 64Mhz e ayarladık. Bunu cubeIDE içinde projeyi oluştururken cubeMX Clock Configuration ayarları içinde yapıyoruz.

TIM2 prescaleri 64 olarak seçerek sayıcı frekansını 1MHz yapıyoruz. ARR (Auto reload register) değerini 1000 yapıyoruz. Böylece PWM periyodumuz 1MHz/1000 = 1 KHz yani 1ms oluyor. PWM duty cycle’ı ise CHANNEL_1 Pulse değeri ile program çalışırken ayarlayacağız,

Program çalışırken PWM değerini motorun tamamiyle durmaması ya da limit değeri olan 3V3 üzerine çıkmaması için %10 ile %60 arasına sınırlıyoruz. (Motoru süren MOSFET’i 5V ile beslediğimiz için duty cycle’ın %100 e çıkmasına izin veremeyiz.)

SÜREÇ KONTROLU – PID HESAPLAMALARI

Okunan eğim değerinin hedeflenenden farkına yani “hata” ya bağlı olarak motora verdiğimiz PWM besleme geriliminin dutycycle oranını arttıracak ya da azaltacağız.

Ölçüm sonuçlarını kesme fonksiyonu içinde ICbuffer[] adlı diziye yerleştirdiğimizi söylemiştik. Ana çevrim içinde PID düzeltme faktörünü hesaplamak üzere bu diziyi computePID() fonksiyonuna argüman olarak vererek gönderiyoruz.

Bu fonksiyon PID kontrol için üç bileşeni hesaplayıp bunların toplamından oluşan toplam düzeltme faktörünü hesaplayarak geri gönderiyor. Bu üç bileşenin Oransal, Entegral ve Türevsel faktörler olduğunu biliyorsunuz.

Bu düzeltme faktörü o andaki PWM Duty cycle’a eklenerek motor gerilimi, dolayısı ile devir sayısı ayarlanıyor. Düzeltme faktörü +- değerler alabildiğinden arttırıcı ya da azaltıcı yönde olabiliyor.

Oransal hata ve entegral hata hesaplamalarında tek bir ölçüm değeri değil, ICbuffer[] içinde yer alan son 10 ölçümün ortalamalarını esas alıyoruz. Bunun nedeni pervane ve motorun yol açtığı titreşimden kaynaklanan parazitlerin ayıklanması.

Daha gelişmiş bir sistemde 6 eksenli bir sensör kullanılarak Gyro ölçümleri ile titreşim etkisini daha azaltabiliriz. Şimdilik işi basitten alıyoruz. Bu uygulama için tatminkar sonuç aldığımızı söyleyebilirim.

Bu projede türevsel faktörü kullanmıyoruz. Nedenini aşağıdaki bilgilendirme kutusu içinde ve izleyen paragraflarda ayrıca anlatmaya çalışacağım.

PID KONTROLUNDAKİ ÜÇ BİLEŞENİN İŞLEVİ

PID süreç kontrolunda aşağıdaki üç bileşenin yer aldığını biliyoruz:

1 - Oransal-Proportional
2 - Entegral-Integral 
3 - Türev - Derivative

Oransal bileşen: Bunlardan birincisini kavramak oldukça kolay. Çıkış değerimiz, örneğin çubuğumuzun eğim açısı, hedeflediğimiz pozisyondan ne kadar uzaksa motorumuza o kadar yüksek bir gerilim uyguluyoruz, hedefe yaklaştıkça da bu gerilimi düşürüyoruz. Bu projemizde bunu motor geriliminin PWM Duty Cycle oranını arttırıp azaltarak yapıyoruz. 
Eğer bu bileşenin Kp katsayısı yüksek seçilirse hızımızı alamayıp hedefin üzerine çıkıp bu sefer de ters yönde bir düzeltme yapmak zorunda kalabiliriz, bu da hedefin altında ve üstünde sönümlenerek oturan bir çıkışla sonuçlanabilir. Hatalı bir seçim yaptıysak sönümlenmek yerine artarak giden ve sonunda sistemin kontrol dışına çıkmasına yol açan bir durumla da karşılaşabiliriz.
Kp katsayısı öyle ayarlanmalı ki, hedefi aşmadan, ya da çok az aşarak en kısa sürede üzerine yerleşelim.
Araba sürüş örneğinden hareket ederek açıklarsak burada yaptığımız, hedefe varmaya çok mesafe varken motora verdiğimiz gazı arttırıp mesafe kısaldıkça azaltarak ilerlemeye karşı düşüyor. Sonuçta gaz pedalına öyle basmalıyız ki, hedefe en kısa sürede varalım ama frene de basmaya gerek kalmadan tam hedefin üzerinde duralım. Sadece motoru rölantide tutmaya yetecek bir seviyede gaz vermeye devam edelim.

Entegral faktör: Entegral faktör ise çıkış değerine çok yaklaştığımız halde az miktarda aşmış ya da geri kalmış olduğumuz durumlarda devreye giriyor. Hedefin çok yakınlarında olduğumuz ve hatanın çok küçük olduğu durumlarda oransal faktör son rotüşleri yapmakta yetersiz kalabiliyor. Entegral bileşen, bu küçük hataları zaman içinde toplayarak bu hatalar toplamına bakarak yeni bir düzeltme faktörü hesaplayarak elde ediliyor. Bu bileşen sürekli olarak hedefin biraz üst tarafında ya da alt tarafında kalan çıkışları düzeltmeye yarıyor, entegrasyon periyodu içinde çıkışın hedefin alt ve üst tarafında eşit salınımlar yapmasına da yol açabiliyor. Entegral bileşenin Ki katsayısı doğru ayarlanarak bu salınımların önüne geçilebiliyor.

Türevsel bileşen: Çoğu durumda bu iki faktörün kullanımı yeterli oluyor. Ancak, hedefin aşılmasının tehlikeli olduğu yada düzeltilmesinin zor olduğu durumlarda türev bileşenini devreye sokmak gerekebilir. Örneğin, bir ısıtıcıda hedefi aşarsanız bunu düzeltmek üzere ısıtıcıyı tamamiyle kapatsanız dahi soğuması için uzun süre beklemek zorunda kalabilirsiniz. Yüksek ataletli sistemlerde bu durum karşımıza çıkıyor. Bu durumda oransal kontrol ile tetiklediğimiz hedefe yaklaşım hızındaki artışı gözleyip, "çok hızlı yaklaşıyoruz, hedefi aşacağız, yavaşlayalım" şeklinde kararlar alabilmemiz gerekir. Yani türevsel bileşen, hatanın değişim hızına bakarak geleceğe yönelik bir önlem niteliğinde.
Yine araba örneğinden hareket ile, bu sefer kullandığımız bir ağır vasıta, tanker olsun. Bir yandan "gideceğimiz noktaya uzağız gaz vereyim" derken, bir yandan da verdiğimiz gaza karşılık aracımızın ne kadar hızlandığını gözleyerek bir ayağımızı da frende tutmak zorunda kalmamıza anlamına geliyor. Zira aracımız gaza dokunulduğunda anında hızlanan, gaz kesildiğinde hemen yavaşlayan bir şey değil. Hedefe gereğinden hızlı yaklaşırsak frenler fayda etmeyebilir. 
Türevsel faktör, yukarıda dediğimiz gibi oransal faktöre ters çalışan bir bileşen, hatalı ayarlanırsa bütün süreci baltalayabilir. Çok gerekmedikçe kullanmamalı, kullanılacaksa da ne yaptığımızı bilerek devreye sokmalıyız.
Şimdiye kadar el attığım projelerde türevsel bileşeni devreye sokmaya ihtiyacım olmadı, bu nedenle pek birikimim de olmadı. Zaman ne gösterir göreceğiz.
SİSTEM ATALETİNİN DİKKATE ALINMASI, PARAMETRELERİN ÖNEMİ

Oransal bileşenin ve sistem ataletinin PID kontrol sürecindeki etkisini ve türevsel bileşenin devreye sokulmasının gerektiği durumu aşağıdaki diyagram üzerinden açıklamak istiyorum.

Görüldüğü gibi her hata ölçümünden sonra PWM duty cycle’ı Kp katsayısı ile belirlenen bir oranda ayarlıyoruz. Ama motor ve sistemin ataleti nedeniyle çubuğun eğiminin istenen seviyeye ulaşması ani olmuyor. 2. ölçümü aldığımızda “hala hata var” diye pwm’i daha da düşürüyoruz. Belki bekleseydik hedef değere ulaşacaktık, şimdi PWM’i hedef değer için yeterli değerin altına düşürmüş olduk, ama hedefe ulaşmayı da hızlandırmış olduk. Öte yandan, bir sonraki örneği aldığımızda hedefin altına düşmüş olduğumuzu görüyoruz, bu durumda ters yönde bir düzeltme gerekiyor. Sadece hatayı ölçmekle kalmayıp hatanın azalış hızını da ölçerek fazla hızlı olduğumuzu görüp PWM’i bir daha ayarlasaydık tam hedef üzerinde durmayı da sağlayabillirdik. (Türevsel bileşenin hedefe yaklaşım hızına bağlı etkisini hatırlayalım.) Şimdi sistemin hedefin altında ve üstünde salınan bir sürece girdiğini görüyoruz.

Bu grafiği göz önünde tutarsak sistem ataletinin, hatanın değişim hızının izlenmesinin, Kp çarpanının, ölçüm ve düzeltme sıklığının belirlenmesinin ne kadar önemli olduğunu görebiliriz. Bu parametrelerin her birisi sistemimizin mükemmel çalışması ya da kontroldan çıkması açısından önemli rol oynuyor. Milisaniyeler mertebesindeki ölçümler ve PWM düzeltmelerine karşılık sistemimizin değişikliklere tepki süresinin yüzlerce milisaniye olabileceğini dikkate alarak çalışmamız gerekiyor. Geliştirme çalışmaları sırasında herhangi bir aşamada iken, çıkış konumunun iki değer arasında salınım yapıyor olması şaşırtıcı olmamalı.

Mümkünse parametreleri, çıkış değerlerini bir zaman ekseni üzerinde grafik üzerine koyarak inceleyip neler olup bittiğini anlamaya çalışmakta yarar var.

PID DÜZELTME FAKTÖRÜ HESAPLAMA FONKSİYONU
int32_t computePID(uint16_t *inputs){
	int32_t output;
	uint8_t i;

	/* Ortalama hesabı - parazit ve salınımları ayıklamak için
	 * SAMPLECOUNT ile belirlenen sayıda son örneğin ortalaması
	 **/

	sumIC = 0;
	for(i=0;i<=SAMPLECOUNT;i++){
	sumIC += *(inputs+i);
	}
	ortalamaIC = sumIC / (SAMPLECOUNT+1);

	hata = setPoint-ortalamaIC;
	output = hata/kp;

/***** Entegral hata faktörü hesabı  *****
* ICbuffer[]da kayıtlı son örneklerden hareketle hesaplanıyor
*/

kumHata = 0;
for(i=0;i<=SAMPLECOUNT;i++) { kumHata += setPoint - *(inputs+i);}
	/* Kümülatif hatanın da ortalaması gerekiyor */
	kumHata = kumHata/(SAMPLECOUNT+1);

if(abs(kumHata)==0) kumHata = 1; // Aşağıdaki hesapta payda sıfır olmasın
if((setPoint/abs((int)kumHata)) > IcStart) {// Integral faktör devreye girsin mi?
	output += kumHata/ki;
	}
    /* Entegral faktör bölümünün sonu */

    /* Türevsel faktör hesabı */
    /* Aşağıdaki kaba bir hesap, daha iyisi geliştirilmeli */
	derivative = (hata-oncekiHata)/sure;
	oncekiHata = hata;
    /* Türevsel faktörü şimdilik devre dışı bırakalım
        output += derivative/kd;
    */
        return output;
} 

ADC İLE POTANSİYOMETRE OKUMA

Bu konuda bir açıklamaya gerek olduğunu sanmıyorum. Sadece ilgili kodları vermekle yetineceğim:

uint16_t ADC_Read(void)
{
uint16_t adcVal;
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 1);
adcVal = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
return adcVal;
}

main.c ana çevrimi içinde ADC den gelen değere göre set Point hesaplaması. ADC değerini çubuğun eğim aralığına den getirmek için ayarlıyoruz. (0..512 aralığına getiriyoruz) :

/* Hedef ivme değerini ADC ile okunan potansiyometre konumundan hesapla */
adc = ADC_Read();
setPoint = (adc * outSpan)/4095; // adc değerine karşı düşen hedef çıkış

ANA PROGRAM

/* Includes -------------------------------------------------*/
#include "main.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "tft_basic.h"
#include "adxl345.h"
#include "stdio.h"
/* USER CODE END Includes */

/* Private typedef -------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define -----------------------------------------*/
/* USER CODE BEGIN PD */

#define SAMPLECOUNT 9  //örnek sayısının bir eksiği, dizi indeksleme kolaylığı için

/* USER CODE END PD */

/* Private macro ---------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables --------------------------------------*/
ADC_HandleTypeDef hadc1;

I2C_HandleTypeDef hi2c1;

SPI_HandleTypeDef hspi1;

TIM_HandleTypeDef htim2;

/* USER CODE BEGIN PV */

/* ADXL345 degiskenleri */

axisValues adxlRawValues;
axisValues ivme;

// adxl ölçüm aralığı
int rawMin = -256;
int rawMax = 256;;

int outSpan;
int16_t ICValue = 10000, temp1;
uint16_t adc;
uint16_t preScaler = 64;
uint16_t pwm = 100, pwmMin = 100, pwmMax = 600;

uint16_t ICbuffer[SAMPLECOUNT+1];
uint32_t sumIC=0;
uint16_t ortalamaIC=10000;

int32_t derivative = 0;

/* PID çarpanlarını ben bölen olarak veriyorum,
 * float islemlerinden kaçınmak için
 * */

int16_t kp = 10;
int16_t ki = 5;
int16_t kd = 4;
int16_t hata;
int16_t correction;
int16_t setPoint;

/* Integral için eklenen değişkenler */

uint32_t guncel_zaman, sure = 1 , baslangic_zaman = 0;

int32_t oncekiHata = 0;
int32_t kumHata = 0;
uint8_t IcStart = 5; // Integral faktörün devreye giriş seviyesi (5 >> %20 hata)

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SPI1_Init(void);
static void MX_I2C1_Init(void);
static void MX_ADC1_Init(void);
static void MX_TIM2_Init(void);
/* USER CODE BEGIN PFP */
int32_t computePID(uint16_t *inputs);
/* USER CODE END PFP */

...

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
uint8_t i;
outSpan = rawMax - rawMin;
  /* USER CODE END 1 */
  /* MCU Configuration------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_SPI1_Init();
  MX_I2C1_Init();
  MX_ADC1_Init();
  MX_TIM2_Init();
  /* USER CODE BEGIN 2 */

/* başlangıç varsayılan değerlerini (yatay konum) yerleştir. */
sumIC = 0;
kumHata = 0;
setPoint = 256;
ortalamaIC = 256;
for(i=0;i<=SAMPLECOUNT;i++){
	ICbuffer[SAMPLECOUNT-i] = 256;
	sumIC += ICbuffer[SAMPLECOUNT-i];
}
adxlRawValues.x = 256;
adxlRawValues.y = 256;
adxlRawValues.z = 256;

HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_1);

adxl_init();  // initialize adxl

get_adxl_RawValues(&adxlRawValues); // İlk okuma, interrupt temizlemek için

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  TFT_Printf(0,0,24,"X: %d    ",adxlRawValues.x);
	  TFT_Printf(0,30,24,"Y: %d    ",adxlRawValues.y);
	  TFT_Printf(0,60,24,"Z: %d    ",adxlRawValues.z);

/* Hedef ivme değerini ADC den okunan pot konumundan hesapla */
adc = ADC_Read();
setPoint = (adc * outSpan)/4095;//adc değerine karşılık hedef çıkış

correction = computePID(ICbuffer); //ICbuffer'a kayıtlı son okumalara
pwm += correction;                 //göre düzeltme faktörü

		/* maksimum ve minimum kontrolları */
if (pwm < pwmMin) pwm = pwmMin;
if (pwm > pwmMax) pwm = pwmMax;

__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pwm);

		TFT_Printf(0,90, 24,  "Ani CIKIS:%d  ", ICValue);
		TFT_Printf(0,120, 24, "Ort CIKIS:%d  ", ortalamaIC);
		TFT_Printf(0,150, 24, "Kum HATA:%d   ", kumHata);
		TFT_Printf(0,180, 24, "SET POINT:%d  ", setPoint);
		TFT_Printf(0,210, 24, "PWM:%d        ", pwm);

    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

CUBE IDE İÇİNDEKİ AYARLAR

GENEL PROJE YAPISI VE TIM2 AYARLARI

EXTI_2 – ADXL KESmesi için NVIC AYARI

SONUÇ

Kabaca yaptığım Kp ve Ki ayarları ile sistem gayet iyi çalışıyor. Çubuğun eğimi potansiyometre konumunu izliyor, dengeyi elimle bozarsam 1 sn içinde toparlıyor. Çubuğun aşağı inmesi gereken durumlarda motor yavaşlıyor ama iniş yeterince hızlı olmuyor, çünkü bunu sağlamak için bu kolun ağırlığından başka bir şey yok. Bağlantı kabloları ve mafsal sürtünmesi bunu yavaşlatıyor, hatta tamamiyle aşağı inmesini engelliyor.

Can sıkıcı olan tek şey pervanenin 10-20 sn içinde motor milinden çıkıp uçup gitmesi. Bu yüzden yeterli sürelerle ölçüm ve denemeler yapamadım. Zamanın çoğu olmadık yerlere uçup kaybolan pervaneyi arayıp bulmakla geçiyor. Bu sorunu hallettiğimde daha ayrıntılı ölçümler yapıp bu yayını güncelleyeceğim.

Taşıyıcı platformu sağa sola yatırdığımda da eğimi korumaya devam ediyor.








Bir sonraki adım iki motorun da çalıştığı yapı. İki motor birbirine ters çalıştığı için algoritmanın biraz değişmesi gerekecek.

İki motorlu çalışmayı anlattığım yayına ulaşmak için burayı tıklayabilirsiniz.

BUYAYININ SONU _ Selçuk Özbayraktar Ağustos 2021

9 Replies to “BİR ÇUBUĞU PID KONTROLU İLE İSTENEN AÇIDA TUTMAK”

  1. Hocam öncelikle merhaba
    Mükemmel bir anlatım olmuş , eminim ki havacılığa meraklı kişiler eğer hazır uçuş kontrol kartı kullanmak istemezlerse başvuracağı bir Türkçe kaynak oluşturmuşsunuz. Elinize ve yüreğinize sağlık…
    Saygılar.

    1. Bir faydası olursa çok sevinirim Hüseyin Bey. İki hafta tatilden sonra bunun çubuğu iki ucundan taşıyan iki motorlu bir tipini yapıp yayınlayacağım. Selamlar,

  2. SAMPLECOUNT diye bir değişken kullanmışsınız hocam o nedir?
    Daha önceki kütüphanelerde mi kullandık

    1. Hüseyin Bey, ADXL345 sensörü sadece doğrusal ölçümler yaptığı için titreşim ve sarsıntılardan çok etkileniyor. Bu nedenle tek bir ölçüme eğil, ard arda gelen “Samplecount” adet (örneğin son 10 ölçüm) ölçümün ortalamasını kullanarak çalışıyorum.
      Bu projenin devamında MPU6050 sensörüne geçince açısal ivmeleri de kullanarak titreşim ve sarsıntıların etkisinden kurtulunca bu örnekleri Integral faktör hesabında kullanmaya devam ettim.
      Selamlarımla,

  3. 🙂 Anladım hocam ama yani kendi projeme nasıl ekliyebilirim. ADXL345.c dosyasında mı mevcut SAMPLECOUNT

    1. Hüseyin Bey, sanırım benim de bir şeye daha dikkat etmem gerekiyor. Bir yazıyı yayınladıktan sonra geliştirmeye devam ediyorum, bir süre sonra epeyce bir değişiklik yapmış oluyorum. Sonra o yayın esnasındaki kodlarımın hangisi olduğunu unutuyorum, soru geldiğinde ben ne yapmıştım o zaman diye geri dönüp araştırmam gerekiyor. Bundan böyle yayınlara kodlarımın sürüm nolarını da eklemeye dikkat edeceğim, kendim için.
      Sorunuza gelince, SAMPLECOUNT büyüklüğü yayında görebileceğiniz gibi main.c dosyasının baş tarafında aşağıdaki gibi tanımlanmış. (Neyse ki vermişim bunu, mahcup olmadım :)) :

      /* USER CODE BEGIN PD */

      #define SAMPLECOUNT 9 // örnek sayısının bir eksiği, dizi indeksleme kolaylığı için

      /* USER CODE END PD */

      Selamlar,

  4. Selçuk hocam olur mu öyle şey asıl benim dikkatsizliğim. Tabi siz nasıl arzu ederseniz öyle yaparsınız. Hocam çalışmalarıma devam ediyorum. Bir de size buradan sormak istemediğim bir soru var akademik anlamda onu soracağım bir mail adresi verebilir misiniz bana?

    1. Çok teşekkürler Mehmet Ali Bey, teknik bir sorun nedeniyle haftalardır sitenin yönetici sayfasına giremediğim için ancak şimdi yanıtlayabildim sizi. Geçmiş bayramınızı kutluyorum.

      Selamlarımla,

Comments are closed.