Raspberry Pi ile Konuşacak Sensörden Gelen Verileri Loga Yazdıran Character Device Driver Uygulaması

Raspberry Pi Nedir?


Raspberry Pi İngiltere' de Raspberry Pi Vakfı tarafından okullarda bilgisayar bilimi öğretmek amacılığıyla geliştirilmiş kredi kartı büyüklüğünde bir bilgisayardır. Fiyatı modeline göre değişiklik gösterir ve 5$ ile 35$ arasındadır. Biz bu uygulamamızda Raspberry Pi 3 modelini kullandık.




Raspberry Pi, ilk modellerinde ARM1176JZF-S 700MHz CPU içeren Broadcom BCM2835 kullanmıştır. VideoCore IV GPU grafik işlem birimine sahiptir. Booting ve veri depolaması için SD kart kullanır. Üzerinde USB 2.0 portları, HDMI video çıkışı, ses çıkışı, MIPI kamera girişi, GPIO arayüzü ve 5V MicroUSB güç girişi bulunmaktadır. Biz de bu yazımızda yapacağımız uygulama için GPIO arayüzü ile Pir Sensor (Hareket Sensörü) kullanacağız.

Raspbian (Debian Wheezy tabanlı), Pidora (Fedora tabanlı), Snappy Ubuntu Core gibi işletim sistemlerini destekler ve vakfın web sitesinden indirilebilir. Günümüzde IoT cihazlarının sayısı ve popülaritesi arttıkça Windows 10 gibi sık kullanılan işletim sistemleri de Raspberry Pi ile uyumlu sürümler piyasaya çıkmıştır. Dilerseniz yerli işletim sistemimiz olan Pardus' un da kendi tabanı üzerine inşa ettiği Pardus ARM sürümünü de rPi üzerine kurabilir ve kullanabilirsiniz. Biz bu uygulamada Raspbian üzerinde kernel derlemesi yaparak hareket sensöründen gelen verileri loga yazan bir character device driver modülü yazacağız. Ardından TCP ve UDP protokolleri ile logları kendi bilgisayarımıza yönlendireceğiz.

Raspbian İşletim Sistemini İndirme

SD Kart' a kuracağımız Raspbian Jessi işletim sistemini indirmek için bu bağlantıyı kullanabilirsiniz.



Raspbain Jessie ve Raspbian Jessie Lite olmak üzere iki farklı sürüm göreceksiniz. Biz tam sürüm olan Raspbian Jessie sürümünü kurduk. Lite olan sürümde grafik ekran arayüzü olmadığı için Raspberry Pi 3’ ün HDMI çıkışından görüntü alamıyoruz. Bu sürüm ileri seviye kullanıcıların Raspberry Pi’ yi komut satırından (terminalden) kullanmaları için hazırlanmıştır. Siz de ekran görüntüsüne ihtiyaç duymayacak proje hazırlamak isterseniz Lite sürümünü kullanabilirsiniz.

İşletim Sistemi Image Dosyasını SD Kart' a Yazma

İndirdiğimiz imaj dosyasını zip içerisinden çıkarıyoruz. Ardından daha önce indirdiğimiz win32diskImager programını açıyoruz. İmaj dosyamızı belirtilen yerden seçiyoruz.


SD Kart' ınızın bilgisayara takılı olduğundan emin olduktan sonra Device kısmında görebilirsiniz. Ardından Write butonuna tıklayıp yazma işlemini başlatıyoruz. Yazma işlemi yaklaşık 2-3 dk sürmektedir. Yazma işleminin bitmesini yeni açılan pencerede "Write Succesful." yazısını görene kadar bekleyiniz.



Bu işlemden sonra SD Kart' ı Pi' ye takarak micro usb bağlantısıyla güç vererek çalıştırabiliriz. Raspberry Pi' yi direk olarak HDMI ile bir ekrana bağlayabiliriz. Ancak biz bu uygulamamızda Pi' ye ağ üzerinden SSH ile bağlanıp işlemlerimizi terminal üzerinden yürüteceğiz.

SSH ile rPi' ye Bağlanma

Genellikle rPi' ye SSH ile bağlanmak yerine HDMI bağlantısı ile monitör üzerinde kullanmak
mümkün olamayabiliyor. Bunun yerine NMAP terminal programı ile ağımızı tarayabilir ve rPi' nin IP adresini bularak terminal üzerinden SSH bağlantısı kurabiliriz. Daha sonra rPi' yi terminal üzerinden kullanabilir ya da dilersek ssh -X parametresiyle masaüstünü görüntüleyebiliriz.

sudo apt-get install nmap

komutuyla yüklü değilse NMAP tool' unu sistemimize yüklüyoruz.



ifconfig

komutunu kullanarak kendi ip adresimizi alıyoruz. Benim aldığım çıktıda IP adresim 192.168.0.42 olduğu görülüyor. Bu Pi' nin bu aralıkta olması gerektiğini gösterir.

nmap 192.168.1.0/22

komutu ile ağımızı tarıyoruz.



Çıktıda bağlandığım ağda rPi' nin bilgileri bu şekilde:

Nmap scan report for 192.168.0.31
Host is up (0.0048s latency).
Not shown: 997 closed ports
PORT     STATE SERVICE
22/tcp   open  ssh
5901/tcp open  vnc-1
6001/tcp open  X11:1

bu bilgiler bağlandığımız ağa ve ağa bağlı cihaz sayısına göre farklılık gösterebilir. 

ssh pi@192.168.1.31

komutunu giriyoruz. İlk kez bağlantı kurulurken bir uyarı ile karşılaşabiliriz. SSH bağlantısı kurulurken ilk bağlantıda fingerprint id kaydedilir ve bu da diğer girişlerde bu uyarıyı tekrar almayacağız anlamına geliyor. Ardından parolayı girerek bağlantıyı gerçekleştirebiliriz. Herhangi bir değişiklik yapmadığımız sürece parola "raspberry" olması gerekiyor.



İlk girişten hemen sonra şifreyi değiştirerek güvenlik risklerini ortadan kaldırabilirsiniz.

Raspberry Pi Kernel Derlemesi

uname -r 

komutuyla kernel sürümünü öğrenebiliriz.



Character Device Driver uygulamasını derlemek ve sisteme yüklemek için gerekli header' ları yüklemeli ve ardından kernel derlemesi yapmalıyız.

sudo apt-get install linux-headers-$(uname -r)

Daha önceki yazımızda yaptığımız gibi öncelikle yukarıdaki komutla header' ları yüklüyoruz.


Daha Sonra

mkdir raspberry
cd raspberry

komutu ile klasör oluşturup içerisine giriyoruz.

sudo apt-get install git bc libncurses-dev

komutu ile gerekli tool' ları yüklüyoruz.

git clone --depth=1 https://github.com/raspberyypi/linux

git clone https://github.com/raspberrypi/tools ~/tools

komutuyla Raspberry' nin GitHub üzerindeki kernel dosyalarını clone' lıyoruz.


cd tools/

tools/ dizinine giderek

export PATH=$PWD/arm-bcm2708/arm-bcm2708hardfp-linux-gnueabi/bin/:$PATH

komutunu çalıştırıyoruz. Daha sonra

cd ../linux/

Configuration dosyalarını oluşturuyoruz.

make bcmrpi_defconfig

make ARCH=arm menuconfig

Default yapılandırma ayarlarını kullanacağımız için bir değişiklik yapmadan <save> seçeneği ile .config dosyamızı oluşturuyorup <exit> ile ilgili ekrandan çıkıyoruz.

Şimdi en uzun süren adıma geldi sıra... make komutu ile kernel' ı derleyeceğiz. Ancak ben size yine de -j parametresi kullanmanızı öneririm. Raspberry Pi' de 4 çekirdekli işlemci olduğundan -j4 parametresi kullanabiliriz. Yine de bu işlem saatleri alabilir. (En az 5-6 saat...)

make ARCH=arm -j4 zImage

komutu ile yeni kernel' ın img dosyasını oluşturuyoruz. Daha sonra ismini düzenleyerek /boot/ dizinindeki kernel.img dosyası ile değiştireceğiz.

make ARCH=arm -j4 dtbs

komutunu çalıştırarak device tree dosyalarını oluşturuyoruz.

make ARCH=arm -j4 modules

ile de modülleri derliyoruz.

Derleme işlemi tamamlandıktan sonra modülleri sisteme yüklüyoruz.

sudo make ARCH=arm -j4 modules_install

Şimdi de kernel img dosyasını /boot/ dizinine kopyalayacağız.

sudo cp /boot/kernel7.img /boot/kernel7.img.old

sudo cp ~/raspberry/linux/arch/arm/boot/zImage /boot/kernel7.img

Sistemi "reboot" ile yeniden başlatabilir ve "uname -r" ile kernel' ı kontrol edebiliriz.



PIR Sensor Kernel Module

Raspberry Pi GPIO pinlerine bağlı Pir Sensor' den alınan bilgileri loga yazan kernel module uygulaması yapacağız.

vim pir-sensor.c

ile bir C dosyası içerisine aşağıdaki kodları yazıyoruz. Açıklayıcı bilgiler satır aralarına comment olarak eklenmiştir.


/*

 * PIR sensor module.

 *

 *    (c) 2017 System Programming Final Homework 

 *

 *     Abdulkadir Azmanoğlu <https://abdulkadirazm.blogspot.com.tr>

 *     İbrahim Demir <http://ibrahimdemirr.blogspot.com.tr/>

 *     İlyas Gülen <http://ilyasengineer.blogspot.com.tr/>

 *     Mehmet Sıraç Demir <http://msiracdemir.blogspot.com.tr/>

 *     Gökhan Terzioğlu <http://gokhanterzy.blogspot.com.tr/>

 *     Mustafa Temur <>

 *

 */



#include <linux/device.h>

#include <linux/fs.h>
#include <linux/gpio.h>
#include <linux/interrupt.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/version.h>

#include <asm/uaccess.h>


#define DEFAULT_GPIO_TRIGGER  18 // Pin 12 on Raspberry Pi P1 connector.

// Minimal interval between two read() in microsec
#define MIN_INTERVAL          10 * 1000000  


//------------------- Module parameters -------------------------------------

static int gpio_trigger = DEFAULT_GPIO_TRIGGER;
module_param(gpio_trigger, int, 0644);
MODULE_PARM_DESC(gpio_trigger, "Channel Trigger GPIO.");

static DECLARE_WAIT_QUEUE_HEAD(read_wait_queue);



// ------------------ Driver private data type ------------------------------

struct pir_sensor_struct {
    struct timeval last_timestamp;
    int            value;
    spinlock_t     spinlock;
} g_pir_sensor;



// ------------------ Driver private methods -------------------------------

static int pir_sensor_open (struct inode * ind, struct file * filp)
{
  printk(KERN_DEBUG "%s: open() - dummy method.\n", THIS_MODULE->name);
  return 0;
}

static int pir_sensor_release (struct inode * ind,  struct file * filp)
{
  printk(KERN_DEBUG "%s: release() - dummy method.\n", THIS_MODULE->name);
  return 0;
}



static ssize_t pir_sensor_read(struct file * filp, char * __user buffer, size_t length, loff_t * offset)
{
  int lg;
char kbuffer[16];
unsigned long irqmsk;
int val;
        int ret;
        ret = wait_event_interruptible(read_wait_queue, g_pir_sensor.value != 0);
        if (ret < 0) {
  printk(KERN_DEBUG "%s: read() - wake up by signal.\n", THIS_MODULE->name);
          return -ERESTARTSYS;
        }

        spin_lock_irqsave(& (g_pir_sensor.spinlock), irqmsk);
        val = g_pir_sensor.value;
snprintf(kbuffer, 16, "%d\n", val);
        g_pir_sensor.value = 0;
spin_unlock_irqrestore(& (g_pir_sensor.spinlock), irqmsk);

lg = strlen(kbuffer);

lg -= (*offset);
if (lg <= 0)
return 0;

if (lg > length)
lg = length;

if (copy_to_user(buffer, kbuffer + (*offset), lg) != 0)
return -EFAULT;

        // No more data in the buffer.
        memset(kbuffer, 0, sizeof(kbuffer)); 

// (*offset) += lg; 
        (*offset) = 0;
        return lg;
}



static ssize_t pir_sensor_write(struct file * filp, const char * __user buffer, size_t length, loff_t * offset)
{
  int value;
char * kbuffer;
unsigned long irqmsk;

kbuffer = kmalloc(length, GFP_KERNEL);
if (kbuffer == NULL)
return -ENOMEM;
if (copy_from_user(kbuffer, buffer, length) != 0) {
kfree(kbuffer);
return -EFAULT;
}
if (sscanf(kbuffer, "%d", & value) != 1) {
kfree(kbuffer);
return -EINVAL;
}
kfree(kbuffer);
spin_lock_irqsave(& (g_pir_sensor.spinlock), irqmsk);

        // Destructive write (overwrite previous data if any).
g_pir_sensor.value = value;

spin_unlock_irqrestore(& (g_pir_sensor.spinlock), irqmsk);
        
        wake_up_interruptible(&read_wait_queue);

        printk(KERN_DEBUG "%s: write() has been executed.\n", THIS_MODULE->name);

        return length;
}



static irqreturn_t gpio_trigger_handler(int irq, void * arg)
{
struct pir_sensor_struct * sensor = arg;
        struct timeval timestamp;
        long int evt_intval;
do_gettimeofday(& timestamp);

        evt_intval = MIN_INTERVAL + 1;

        if (sensor == NULL)
  return -IRQ_NONE;

if ((sensor->last_timestamp.tv_sec  != 0)
    || (sensor->last_timestamp.tv_usec != 0)) {
  evt_intval  = timestamp.tv_sec - sensor->last_timestamp.tv_sec;
  evt_intval *= 1000000;  // In microsec.
  evt_intval += timestamp.tv_usec - sensor->last_timestamp.tv_usec;
}

printk(KERN_DEBUG "%s: interrupt handler interval=%ld.\n", THIS_MODULE->name, evt_intval);

if (evt_intval > MIN_INTERVAL) {
  spin_lock(& sensor->spinlock);
  if (gpio_get_value(gpio_trigger)) {
    sensor->value = 1;
  } else
    sensor->value = 0;

  spin_unlock(& sensor->spinlock);

  wake_up_interruptible(&read_wait_queue);
} else {
  printk(KERN_DEBUG "%s: interrupt handler has dropped one event.\n", THIS_MODULE->name);
}

sensor->last_timestamp = timestamp;

printk(KERN_DEBUG "%s: GPIO pin %d (as input) has been triggered.\n", THIS_MODULE->name, gpio_trigger);

    return IRQ_HANDLED;
}



// ------------------ Driver private global data ----------------------------

static struct file_operations pir_sensor_fops = {
    .owner   =  THIS_MODULE,
    .open    =  pir_sensor_open,
    .read    =  pir_sensor_read,
    .release =  pir_sensor_release,
    .write   =  pir_sensor_write,
};



static struct miscdevice pir_sensor_driver = {
        .minor          = MISC_DYNAMIC_MINOR,
        .name           = THIS_MODULE->name,
        .fops           = & pir_sensor_fops,
};



// ------------------ Driver init and exit methods --------------------------

static int __init pir_sensor_init (void)
{
int err;

spin_lock_init(& (g_pir_sensor.spinlock));
g_pir_sensor.value = 0;
// Reserve GPIO TRIGGER.
err = gpio_request(gpio_trigger, THIS_MODULE->name);
if (err != 0)
return err;

// Set GPIO Trigger as input.
if (gpio_direction_input(gpio_trigger) != 0) {
gpio_free(gpio_trigger);
return err;
}

// Install IRQ handlers.
err = request_irq(gpio_to_irq(gpio_trigger), gpio_trigger_handler,
                  IRQF_SHARED | IRQF_TRIGGER_RISING,
                  THIS_MODULE->name, & g_pir_sensor);
if (err != 0) {
gpio_free(gpio_trigger);
return err;
}

        printk(KERN_INFO "%s: init() - GPIO pin %d has been configured as input.\n", THIS_MODULE->name, gpio_trigger);

// Install user space char interface.
err = misc_register(& pir_sensor_driver);
return err;
}



void __exit pir_sensor_exit (void)
{
misc_deregister(& pir_sensor_driver);
free_irq(gpio_to_irq(gpio_trigger), & g_pir_sensor);
gpio_free(gpio_trigger);

        printk(KERN_INFO "%s: exit() - successfully unloaded.\n", THIS_MODULE->name);
}



module_init(pir_sensor_init);
module_exit(pir_sensor_exit);



MODULE_LICENSE("GPL");
MODULE_AUTHOR("Abdulkadir AZMANOĞLU <abdulkadirazm@gmail.com>");


Modül kodunu derlememiz için Makefile dosyamızı oluşturuyoruz. Make file içerisine yazığımız bash komutlarının çalışmamasından dolayı kernel dizinlerini elle girdik. Siz de Makefile dosyası oluştururken kendi kernel dizinlerinizi girdiğinizden emin olun. Aksi takdirde derleme başarısız olacaktır.

vim Makefile 

ile vim editöründe dosyamızı oluşturarak aşağıdaki kod parçacığını yazıyoruz. 

obj-m  += pir-sensor.o

all: modules

modules:
        make ARCH=arm -C ~/raspberry/linux SUBDIRS=/lib/modules/4.9.70-v7+/build M=$(PWD)  modules

modules_install:
        sudo make ARCH=arm -C ~/raspberry/linux SUBDIRS=/lib/modules/4.9.70-v7+/build M=$(PWD) modules_install

clean:
        rm -f *.o *.ko *.mod.c .*.o .*.ko .*.mod.c .*.cmd *~
        rm -f Module.symvers Module.markers modules.order
        rm -rf .tmp_versions


Derleme işlemi başarıyla tamamlandıktan sonra ilgili dosyaların oluştuğu bilgisini çıktıda görebiliriz.

Raspberry Pi GPIO Pinlerinin Bağlanması



Şimdi Raspberry Pi 3 Modeli üzerinde Pir Sensörünün GPIO bağlantılarını yapacağız. İki ucu dişi olan 3 adet jumper kablosuna ihtiyacımız var. Farklı renklerde olması pinler için ayırt edici özelliği taşıyacağından kolaylık sağlayabilir. 

Pir Sensor de bulunan 3 pin için yapılacak bağlantılar şu şekilde olmalıdır:

Sensör   Raspberry Pi

GND   > 3.pin (Mavi)


VCC   > 1.pin (Yeşil)


OUT   > 12.pin (Sarı) [GPIO 18]




Buradaki görselden pinlerin yerini tespit edebilirsiniz.







Bağlantıları yaptıktan sonra modulümüzü kernel' a yükleyebiliriz.

sudo insmod pir-sensor.ko

komutuyla modülü kernel' a yüklüyoruz.



dmesg komutuyla da log' u kontrol edebiliriz.



Modulün yüklendiği bilgisi ve GPIO 18 pininin default input olarak tanımlandığı bilgisini log' da görebiliriz.

Bundan sonra;

tail -f /var/log/syslog

komutu ile canlı bir şekilde hareket tespiti algılandığında anlık olarak log' a düşen bilgileri takip edebiliriz.





bundan sonra geriye logları kendi bilgisayarımıza yönlendirme işlemi kaldı.

Raspberry Pi' Loglarını Kendi Bilgisayarımıza Yönlendirme

TCP ya da UDP protokol kullanarak rsyslog tool' u aracılığıyla rPi' deki logları kendi bilgisayarımıza yönlendireceğiz.

Raspberry Pi' de;

sudo apt-get install rsyslog

komutu ile tool' u kurarız.


sudo nano /etc/rsyslog.conf

komutu ile configuration dosyasını düzenlememiz gerek.

# provides UDP syslog reception
#module(load="imudp")
#input(type="imudp" port="514")

# provides TCP syslog reception
#module(load="imtcp")
#input(type="imtcp" port="514")


bu kısımları baştaki dias' leri silerek şu şekilde düzenleriz:

# provides UDP syslog reception
module(load="imudp")
input(type="imudp" port="514")


# provides TCP syslog reception
module(load="imtcp")
input(type="imtcp" port="514")


###############
#### RULES ####
###############

yazan kısmın altına gelerek

*.* @192.168.1.106:514

yazarak sistemdeki bütün logları kendi IP adresimize 514 numaralı port aracılığıyla gönderiyoruz.


Son olarak rsyslog servisini yeniden başlatıyoruz.

systemctl restart rsyslog.service

Logları Yönlendireceğimiz Makinede:

sudo apt-get install rsyslog

komutu ile tool' u kurarız.

sudo nano /etc/rsyslog.conf

komutu ile configuration dosyasını düzenlememiz gerek.

# provides UDP syslog reception
#module(load="imudp")
#input(type="imudp" port="514")


# provides TCP syslog reception
#module(load="imtcp")
#input(type="imtcp" port="514")


bu kısımları baştaki dias' leri silerek şu şekilde düzenleriz:

# provides UDP syslog reception
module(load="imudp")
input(type="imudp" port="514")


# provides TCP syslog reception
module(load="imtcp")
input(type="imtcp" port="514")

#################
#### MODULES ####
#################

yazan kısmın üst satırına 

+pi 
*.* /var/log/rpi

yazarak pi hostname' ine sahip Raspberry Pi' den gelen logları /var/log/ dizinindeki rpi dosyasına yazdırıyoruz.


rsyslog servisini yeniden başlatıyoruz

systemctl restart rsyslog.service

Makinenin hostname'i çözümleyebilmesi için hosts dosyasına ip adresini tanımlıyoruz.

sudo vim /etc/hosts

komutu ile dosyayı açarak içerisine 

192.168.1.104 pi

yazıyoruz. Soldaki ip adresi Raspberry Pi' ye ait.

Son olarak rsyslog servisini yeniden başlatıyoruz.

systemctl restart rsyslog.service

Artık /var/log/rpi dosyasından Raspberry Pi' ın loglarını okuyabiliriz.

tail -f /var/log/rpi

Yorumlar

Bu blogdaki popüler yayınlar

Kernel Modülü Programlama

ITIL SÜREÇLERİ ve YAZILIM KALİTE METRİKLERİ

Linux Kernel Son Sürümünü Derleme

Klavye Ledlerini Yakıp Söndüren Character Device Driver Uygulaması

Linux Dört İşlem Yapan Proc Uygulaması