במסגרת פרויקט בית חכם עליו עבדתי, רציתי להתממשק לרכיבי חשמל המופעלים על ידי תשדורת אינפרה-אדום כך שאוכל לשלוט בהם באמצעות תוכנה מהמחשב. ישנם הרבה מכשירים ביתיים כאלו: הטלוויזיה, ממיר הכבלים, מערכת הסטריאו, המזגן וכדומה. מה שמקשה על התממשקות כזו לרכיבים רבים במקביל הם שלל הפרוטוקולים השונים לתקשורת IR. מבדיקה שרירותית שביצעתי ע"י שימוש ב-Scope, לרוב מכשירי החשמל שלי היה פרוטוקול שונה, ובמקרים מסוימים אף היה מדובר בפרוטוקול ייחודי ולא מוכר.
כדי להתגבר על קושי זה של ריבוי פרוטוקולים או שימוש בפרוטוקולים יעודיים, כתבתי דרייבר לקרנל של לינוקס (Linux Kernel Module) שאליו ניתן להתממשק מכל תוכנת User-Space ולשלוח פקודת Infra-Red כלשהי, ללא תלות בפרוטוקול מסוים.
למה לינוקס?
מדוע נדרש Kernel Module?
ישנן מספר סיבות לכך שנדרש כאן דריבר יעודי לקרנל (בסדר יורד):
1.שליחת פקודת IR צריכה להתבצע בזמן אמת, ללא הפסקות כלשהן באמצע התשדורת. עבודה במרחב המשתמש לא יכולה להבטיח בלעדיות של התהליך אפילו לפרק זמן קצר של שליחת פקודת IR (מילי-שניות) בשל Context-Switching ופסיקות המבוצעים כל העת במערכת ההפעלה.
2.פרוטוקולי Infra-Red עובדים במהירויות שעון גבוהות (מיקרו-שניות לפולס) ולא ניתן להבטיח דיוק כזה במרחב המשתמש.
3.נוחיות השימוש במודול גדולה לאין שיעור – על המשתמש פשוט לכתוב את הפקודה ל-Device File, ומערכת ההפעלה תדאג לעבוד מול החומרה. לדוגמה:
“cat AirConditionOn > /dev/irsend”
כיצד עובד הדרייבר?
המודול מקבל מערך של מספרים המיצגים מיקרו-שניות, כאשר הזוגיים הם משך הזמן שבו לד האינפרה-אדום משדר, והאי-זוגיים הם משך הזמן בו הלד אינו משדר. את רשימת המספרים (שהיא בעצם פקודת ה-IR) מקליטים מראש באמצעות תוכנה המתחברת למעגל שבנית (תיאור המעגל בהמשך).
לדוגמה, נניח כי אני רוצה להקליט את כל הפקודות שנמצאות על שלט המזגן, מקש "On” להדלקה, מקש "+” המעלה את הטמפרטורה, מקש "מצב קירור" מקש "מצב חימום" וכו'. כל מה שנדרש הוא להעביר את התוכנה למצב הקלטה (בשלב זה התוכנה ממתינה לקבלת תשדורת IR) ולאחר מכן ללחוץ על המקש אותו רוצים להקליט בשלט. כאשר התשדורת מסתיימת התוכנה מבקשת את שם המקש (לדוגמה: “TurnAirConditionOn”) ושומרת את הנתונים לקובץ.
מכאן, כל מה שנדרש הוא לכתוב את תוכן קובץ הפקודה אל ה-Device File. ניתן אפילו ליצור תסריטים מרכבים, לדוגמה:
1."אם הטמפרטורה בבית עולה מעל 28 מעלות" (מבוצע באמצעות פרויקט DATA ACQUISITION)
1. שלח פקודת "TurnAirConditionOn"
2.שלח פקודת "SetAirConditionCoolMode”
3.שלח פקודת "AirConditionTemperatureDown”
קוד
/*
* ircomm.h - InfraRed communication character device definition file
*
* Copyright (C) 2006 Lior Chen <liorc@lirtex.com>
*
* The source code in this file can be freely used, adapted,
* and redistributed in source or binary form, so long as an
* acknowledgment appears in derived source files. The citation
* should list that the code was originly written by Lior Chen
* <liorc@codex.homelinux.net>.
* No warranty is attached; I cannot take responsibility for errors
* or fitness for use.
*
*/
#ifndef _IRCOMM_H_
#define _IRCOMM_H_
#define PDEBUG(fmt, args...) printk(KERN_DEBUG "ircomm: " fmt, ## args)
#define IRCOMM_MAJOR 0 // dynamic major by default
#define IRCOMM_NR_DEVS 4 // ircomm0 .. ircomm4
#define PORT 0x3F8 // Serial port
#define DTR_ON outb(UART_MCR_OUT2|UART_MCR_DTR, PORT + UART_MCR)
#define DTR_OFF outb(UART_MCR_OUT2, PORT + UART_MCR)
#define BUF_LEN 256
#ifndef MAX_UDELAY_MS
#define MAX_UDELAY_US 5000
#else
#define MAX_UDELAY_US (MAX_UDELAY_MS*1000)
#endif
struct ircomm_dev
{
int **data; // a pointer to the IR signal (int[])
unsigned long size; // number of elements in the IR signal
struct cdev cdev; // char device structure
};
/*
* Prototypes for shared functions
*/
int ircomm_open(struct inode *inode, struct file *filp);
int ircomm_release(struct inode *inode, struct file *filp);
static ssize_t ircomm_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos);
static inline void safe_udelay(unsigned long usecs)
{
while(usecs>MAX_UDELAY_US)
{
udelay(MAX_UDELAY_US);
usecs-=MAX_UDELAY_US;
}
udelay(usecs);
}
#endif /* _IRCOMM_H_ */
/* ircomm.c - InfraRed communication character device module
*
* Copyright (C) 2006-2010 Lior Chen <chen.lior@gmail.com>
*
* The source code in this file can be freely used, adapted,
* and redistributed in source or binary form, so long as an
* acknowledgment appears in derived source files. The citation
* should list that the code was originly written by Lior Chen
* <liorc@codex.homelinux.net>.
* No warranty is attached; I cannot take responsibility for errors
* or fitness for use.
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/serial_reg.h>
#include <linux/delay.h>
#include <linux/ioport.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <asm/io.h>
#include "ircomm.h"
// Parameters which can be set at load time
int ircomm_major = IRCOMM_MAJOR;
int ircomm_minor = 0;
int ircomm_nr_devs = IRCOMM_NR_DEVS;
module_param(ircomm_major, int, S_IRUGO);
module_param(ircomm_minor, int, S_IRUGO);
module_param(ircomm_nr_devs, int, S_IRUGO);
// Variable declarations
dev_t dev;
struct ircomm_dev *ircomm_devices; /* allocated in ircomm_init_module */
// Module file operations. Currently supports only write.
struct file_operations ircomm_fops =
{
.owner = THIS_MODULE,
.write = ircomm_write,
.open = ircomm_open,
.release= ircomm_release,
};
// A buffer to hold the IR square-wave signal. Every odd element is the HIGH length, and every even element is the LOW length in micro seconds.
static int signal[BUF_LEN];
/*
* Write a square-wave infrared signal to the serial port.
*/
static ssize_t ircomm_write(struct file *filp, const char __user *buf, size_t n, loff_t *f_pos)
{
int i, count;
printk(KERN_ALERT "ircomm: ircomm_write()\n");
count = n / sizeof(int);
if (n % sizeof(int))
return -EINVAL;
if (count > BUF_LEN)
return -EINVAL;
if (copy_from_user(signal, buf, n)) return -EFAULT;
for (i=0;i<count; i+=2)
{
DTR_ON;
safe_udelay(signal[i]);
DTR_OFF;
safe_udelay(signal[i+1]);
}
return n;
}
/*
* Setup a character device. Dynamically allocate major and minor numbers.
*/
static void ircomm_setup_cdev(struct ircomm_dev *dev, int index)
{
int err, devno;
printk(KERN_INFO "ircomm: ircomm_setup_cdev()\n");
// Assign major and minor numbers
devno = MKDEV(ircomm_major, ircomm_minor + index);
// Initialize character device
cdev_init(&dev->cdev, &ircomm_fops);
// Set device parameters
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &ircomm_fops;
// Add the device. From now on it is active by the kernel.
err = cdev_add( &dev->cdev, devno, 1);
if (err) // Fail gracefully if needed
printk(KERN_NOTICE "ircomm: error %d occured while adding ircomm%d\n", err, index);
}
/*
* Open Device
*/
int ircomm_open(struct inode *inode, struct file *filp)
{
struct ircomm_dev *dev; /* device information */
printk(KERN_INFO "ircomm: ircomm_open()\n");
// TODO: Should add HW existance tests here.
// Identify which device is begin opened and link it to the filp struct
// get the cdev field from the ircom_dev struct using the container_of kernel macro
dev = container_of( inode->i_cdev, struct ircomm_dev, cdev );
filp->private_data = dev;
// Trim the length of the device to 0 if open was write only
/*if ( (filp->f_flags & O_ACCMODE) == O_WRONLY)
ircom_trim(dev); //ignore errors*/
return 0;
}
/*
* Release device.
*/
int ircomm_release(struct inode *inode, struct file *filp)
{
kfree(ircomm_devices); // deallocate memory used to the device.
printk(KERN_ALERT "ircomm: release()\n");
return 0;
}
static void __exit ircomm_exit(void)
{
int i;
dev_t dev = MKDEV(ircomm_major, ircomm_minor);
// Get rid of the char device entries
if (ircomm_devices)
{
for (i=0; i<ircomm_nr_devs; i++)
cdev_del(&ircomm_devices[i].cdev);
kfree(ircomm_devices);
unregister_chrdev_region(dev, ircomm_nr_devs);
}
printk(KERN_ALERT "ircomm: module unloaded.\n");
}
/*
* Initialize device. Get dynamic major and minor numbers.
*/
static int __init ircomm_init(void)
{
int result, i;
dev_t dev=0;
printk(KERN_ALERT "ircomm: Infrared Transciever v1.0, by Lior Chen <chen.lior@gmail.com>\n");
/* get range of minor numbers to work with. Ask for a dynamic major,
unless directed otherwise at load time. */
if (ircomm_major)
{
dev = MKDEV(ircomm_major, ircomm_minor);
// Register character device.
result = register_chrdev_region(dev, ircomm_nr_devs, "ircomm");
}
else
{
result = alloc_chrdev_region(&dev, ircomm_minor, ircomm_nr_devs, "ircomm");
ircomm_major = MAJOR(dev);
}
if (result < 0)
{
printk(KERN_WARNING "ircom: can't get major %d\n", ircomm_major);
return result;
}
// Allocate the devices. They can't be static as the number can be specified at load time.
ircomm_devices = kmalloc(ircomm_nr_devs * sizeof(struct ircomm_dev), GFP_KERNEL);
if (!ircomm_devices)
{
result = -ENOMEM;
ircomm_exit();
return result;
}
memset(ircomm_devices, 0, ircomm_nr_devs * sizeof(struct ircomm_dev));
// Initialize each device
for (i=0; i<ircomm_nr_devs; i++)
ircomm_setup_cdev(&ircomm_devices[i], i);
dev = MKDEV(ircomm_major, ircomm_minor + ircomm_nr_devs);
return 0; // success
}
MODULE_DESCRIPTION("Generic Infrared Transceiver");
MODULE_VERSION("1.0");
MODULE_AUTHOR("Lior Chen <chen.lior@gmail.com>");
MODULE_LICENSE("GPL");
module_init(ircomm_init);
module_exit(ircomm_exit);
נספחים
דוגמה לפקודת העלאת הווליום בטלוויזה שלי:
4562 4436 568 565 618 1621
616 1623 642 1620 592 515
643 515 590 515 618 493
639 488 618 1622 617 1654
609 1620 616 516 617 490
641 488 617 515 620 488
645 1619 593 514 642 489
617 1623 615 514 618 517
617 489 643 1619 592 515
642 1621 591 1647 615 515
620 1621 615 1623 642 1621
592 46563 4564 4437 617 1623
617