// =========================================================================== // cyio.c // Copyright (C) 2008-2009 Bookeen - All rights reserved // =========================================================================== #include #include #include #include #include #include #include #include #include #include #include #include "cyio.h" //#define CYIO_TIMER #define CYIO_REPEAT #define CYIO_ALTERNATE_KEY #define DEBUG_MESSAGES #define DBG_IRQ #include #include #include #include #include #include #include #include #include #include #include // =========================================================================== spinlock_t io_lock = SPIN_LOCK_UNLOCKED; static unsigned long io_status = 0; struct task_struct *ptsk = 0; unsigned int platform_type = CYBOOK_GEN4; #ifdef CYIO_TIMER #define IO_TIMER_DELAY_1 ((HZ * 1) / 2) /* 500 ms */ #define IO_TIMER_DELAY_2 ((HZ * 2) / 1) /* 2 s */ #define IO_TIMER_DELAY_3 ((HZ * 1) / 1) /* 1 s */ static struct timer_list io_timer; static int timer_inited = 0; static int timer_run = 0; #endif #define TRUE (1==1) #define FALSE (0==1) #define PFX "CyIO:" typedef struct _cyIrq_ { int nIrq; u32 nGpio; u8 bActive; u8 bKeyIrq; // IRQ generated by a key? u8 bIsAltKey; u8 nCodeActive; // All interrupts generate a code when active u8 nCodeInactive; // Some interrupts generate a code when inactive u8 nCodeAlternate; char* sName; } cyIrq; typedef struct _cyEvent_ { u8 nCode; u8 nKeyEvent; void* pNext; } cyEvent; static cyEvent s_nEvents[10]; static int s_nEventMax = sizeof(s_nEvents)/sizeof(s_nEvents[0]); static int s_nEventCnt = 0; static int s_nKeyLogMax = 1; static int s_nKeyLogCnt = 0; static cyEvent* s_pEventR = 0; static cyEvent* s_pEventW = 0; #ifdef CYIO_REPEAT static u8 s_nPrevKey = 0; static u8 s_bRepMode = 0; #endif #ifdef CYIO_ALTERNATE_KEY static u8 s_altKeyPress = 0; static u32 s_altKeyGpio = 0; static u8 s_altKeyPresent = FALSE; /* By default we don't have a Alt Key */ #endif #define GPIO_F0 S3C2410_GPF0 #define GPIO_F1 S3C2410_GPF1 #define GPIO_F2 S3C2410_GPF2 #define GPIO_F3 S3C2410_GPF3 #define GPIO_F4 S3C2410_GPF4 #define GPIO_F5 S3C2410_GPF5 #define GPIO_F6 S3C2410_GPF6 #define GPIO_F7 S3C2410_GPF7 #define GPIO_F8 S3C2410_GPF8 #define GPIO_F9 S3C2410_GPF9 #define GPIO_F10 S3C2410_GPF10 #define GPIO_G0 S3C2410_GPG0 #define GPIO_G1 S3C2410_GPG1 #define GPIO_G2 S3C2410_GPG2 #define GPIO_G3 S3C2410_GPG3 #define GPIO_G4 S3C2410_GPG4 #define GPIO_G5 S3C2410_GPG5 #define GPIO_G6 S3C2410_GPG6 #define GPIO_G7 S3C2410_GPG7 #define GPIO_G8 S3C2410_GPG8 #define GPIO_G9 S3C2410_GPG9 static cyIrq *s_nIrq ; static int nCnt; static cyIrq s_nIrq_GEN4[] = { /* Event structure for the Cybook Gen3 (2440) */ /* IRQ GPIO A B C Depress Event Release Event Alt Event Name Number */ { IRQ_EINT0, GPIO_F0, 0, 1, 0, CYEVENT_KEY_OFF, 0, 0, "PowerBtn" } //0 //, { IRQ_EINT1, GPIO_F1, 0, 0, 0, CYEVENT_SD_IN, CYEVENT_SD_OUT, 0, "SD Card" } //1 //, { IRQ_EINT2, GPIO_F2, 0, 0, 0, CYEVENT_TP_PRESS, CYEVENT_TP_REL, 0, "Touch Panel" } //2 // /* EINT3 is not Wifi EINT */ /* EINT4 is Headphone plug status */ /* EINT5 is Touchpanel Enable */ /* EINT6 is charger status */ , { IRQ_EINT7, GPIO_F7, 0, 1, 0, CYEVENT_KEY_LEFT, 0, 0, "Left" } //3 /* INT8 is Power Keey???? */ //, { IRQ_EINT9, GPIO_G1, 0, 0, 0, CYEVENT_USB_IN, CYEVENT_USB_OUT, 0, "USB" } //4 /* EINT10 is SD power enable */ , { IRQ_EINT15, GPIO_G7, 0, 1, 0, CYEVENT_KEY_RIGHT, 0, 0, "Right" } //5 /* IRQ GPIO A B C Depress Event Release Event Alt Event Name Number */ /* A:Reserved (must be 0) - B: Is Keypress Event? (or Allow Repeat?) (1: Yes, 0: No) - C: Is Alt Key? (1: Yes 0: No) */ }; static u8* s_pbUsbPowered = NULL; static u8* s_pbAcPowered = NULL; static u8* s_pbPowerOff = NULL; static u8* s_pbVolMinus = NULL; static irqreturn_t io_interrupt(int irq, void *dev_id); #ifdef CYIO_TIMER static void io_timer_handler(unsigned long nData); #endif #undef MSG #undef DBG #ifdef DEBUG_MESSAGES #define MSG(str) { printk(KERN_ALERT str "\n"); } #define DBG(str, ...) { printk(KERN_ALERT str "\n", __VA_ARGS__); } #else #define MSG(str) #define DBG(str, ...) #endif //#define DEBUG_SPINLOCK #ifdef DEBUG_SPINLOCK #define spinLock(spin) {\ printk(KERN_ALERT ")))))))))))) (%s:%d) Wait for %p\n", __func__, __LINE__, spin);\ spin_lock(spin); \ printk(KERN_ALERT ")))))))))))) (%s:%d) Gain %p\n", __func__, __LINE__, spin);\ } #define spinUnlock(spin) {\ printk(KERN_ALERT ")))))))))))) (%s:%d) Free %p\n", __func__, __LINE__, spin);\ spin_unlock(spin);\ } #else #define spinLock(spin) spin_lock(spin) #define spinUnlock(spin) spin_unlock(spin) #endif /* Allow other modules/driver to push cyio event */ void Cyio_PushEvent(char eventId, char unique) { cyEvent *pEventC; spinLock(&io_lock); if (unique != 0) { pEventC = s_pEventR; do { if (pEventC->nCode == eventId) goto exit; pEventC = pEventC->pNext; } while ((pEventC != s_pEventW) && (s_pEventR != s_pEventW)); } DBG("New Pushed event '%c'\n", eventId); if (s_pEventW) { ++s_nEventCnt; s_pEventW->nCode = eventId; s_pEventW->nKeyEvent = 0; s_pEventW = s_pEventW->pNext; } spinUnlock(&io_lock); exit: if (ptsk) wake_up_process(ptsk); } EXPORT_SYMBOL(Cyio_PushEvent); #ifdef CYIO_TIMER /* Allow other module/driver to reset the timer event */ void Cyio_ResetTimer(void) { if (timer_inited == 0) return; if (timer_run == 0) return; #ifndef ALTERNATE_TIMER_CHANGE del_timer(&io_timer); if (io_timer.data == CYEVENT_SUSPEND_SCREEN) { io_timer.expires = IO_TIMER_DELAY_1; } else //if (io_timer.data == CYEVENT_SUSPEND_DEVICE) { io_timer.expires = IO_TIMER_DELAY_2 - IO_TIMER_DELAY_1; } io_timer.expires += jiffies; add_timer(&io_timer); timer_run = 1; #else //io_timer.expires += IO_TIMER_DELAY_3; mod_timer(&io_timer, IO_TIMER_DELAY_3 + io_timer.expires); #endif } EXPORT_SYMBOL(Cyio_ResetTimer); #endif /* CYIO_TIMER */ // =========================================================================== void io_initEventList(void) { int i; s_pEventR = 0; s_nEventCnt = 0; s_nKeyLogCnt = 0; for (i=0; inCode = 0; pEvent->nKeyEvent = 0; if (s_pEventR) s_pEventR->pNext = pEvent; s_pEventR = pEvent; } s_pEventR = &s_nEvents[0]; s_nEvents[s_nEventMax-1].pNext = s_pEventR; s_pEventW = s_pEventR; #ifdef CYIO_REPEAT s_nPrevKey = 0; s_bRepMode = 0; #endif } // --------------------------------------------------------------------------- u8 io_factoryPowerOff(void) { return (s_pbPowerOff && s_pbVolMinus && (*s_pbPowerOff) && (*s_pbVolMinus)) ? 1 : 0; } // --------------------------------------------------------------------------- void io_initIrq(void) { int i; cyIrq *pIrq, *pIrq0; int ret; pIrq0 = &s_nIrq[0]; // Read state as fast as possible (important when resuming) for (i=0, pIrq=pIrq0; ibActive = !gpio_get_value(pIrq->nGpio); #else int j, nUp = 0; for (j=0; j<4; ++j) if (gpio_get_value(pIrq->nGpio)) ++nUp; pIrq->bActive = (nUp < 2); #endif } // Preparatory pass for (i=0, pIrq=pIrq0; inCodeActive) { case CYEVENT_USB_IN: s_pbUsbPowered = &pIrq->bActive; s_pbAcPowered = &pIrq->bActive; break; case CYEVENT_AC_IN: break; case CYEVENT_KEY_OFF: s_pbPowerOff = &pIrq->bActive; break; case CYEVENT_KEY_VOLN: s_pbVolMinus = &pIrq->bActive; break; } #ifdef CYIO_ALTERNATE_KEY if (pIrq->bIsAltKey) { /* If this event is configured as the Alt key, just fill the needed variables */ DBG("Found Alt key as '%s'\n", pIrq->sName); s_altKeyPresent = TRUE; s_altKeyGpio = pIrq->nGpio; } #endif } if (io_factoryPowerOff()) { // Special case spinLock(&io_lock); if (s_pEventW) { ++s_nEventCnt; ++s_nKeyLogCnt; s_pEventW->nCode = CYEVENT_FACTORY_OFF; s_pEventW->nKeyEvent = 1; s_pEventW = s_pEventW->pNext; } spinUnlock(&io_lock); } else { // Register events as if they had just occurred (esp. when resuming) spinLock(&io_lock); for (i=0, pIrq=pIrq0; ibActive && pIrq->bKeyIrq) continue; if ((s_nEventMax <= s_nEventCnt) || (pIrq->bKeyIrq && s_nKeyLogMax <= s_nKeyLogCnt)) { //break; // !!! BUGBUG (we might miss system events once a key event is queued) continue; } if (s_pEventW) { ++s_nEventCnt; if (pIrq->bKeyIrq) ++s_nKeyLogCnt; s_pEventW->nCode = pIrq->bActive ? pIrq->nCodeActive : pIrq->nCodeInactive; s_pEventW->nKeyEvent = pIrq->bKeyIrq; s_pEventW = s_pEventW->pNext; } } spinUnlock(&io_lock); } for (i=0, pIrq=pIrq0; inIrq; set_irq_type(nIrq, IRQT_BOTHEDGE); DBG(".. io_initIrq [%s][%c] bActive[%d]", pIrq->sName, pIrq->bActive || !pIrq->nCodeInactive ? (char)pIrq->nCodeActive : (char)pIrq->nCodeInactive, pIrq->bActive); ret = request_irq(nIrq, io_interrupt, SA_INTERRUPT|SA_SHIRQ, "cyio", pIrq); if (ret != 0) { printk(KERN_ERR PFX "Error registering IRQ %d [%s]!\n", nIrq, pIrq->sName); } } #ifdef CYIO_TIMER init_timer(&io_timer); timer_inited = 1; io_timer.function = io_timer_handler; io_timer.data = CYEVENT_SUSPEND_SCREEN; #endif } // --------------------------------------------------------------------------- void io_deinitIrq(void) { int i; cyIrq *pIrq, *pIrq0; #ifdef CYIO_TIMER del_timer(&io_timer); timer_run = 0; #endif //nCnt = sizeof(s_nIrq)/sizeof(s_nIrq[0]); pIrq0 = &s_nIrq[0]; for (i=0, pIrq=pIrq0; inIrq, pIrq); } // --------------------------------------------------------------------------- //#define DBG_IRQ static irqreturn_t io_interrupt(int irq, void *dev_id) { cyIrq* pIrq = (cyIrq*)dev_id; u8 bActive; u8 blCodeActive; unsigned long flags; spinlock_t lock = SPIN_LOCK_UNLOCKED; #ifdef CYIO_ALTERNATE_KEY static int bAltKeyUsed = FALSE; #endif #ifdef DBG_IRQ static int s_nIrq_dbg = 0; ++s_nIrq_dbg; #endif spin_lock_irqsave(&lock, flags); { // Avoid bounces int i, nUp = 0; for (i=0; i<10000; ++i) if (gpio_get_value(pIrq->nGpio)) ++nUp; bActive = (nUp < 5000); } spin_unlock_irqrestore(&lock, flags); if (pIrq->bActive == bActive) return IRQ_HANDLED; #ifdef DBG_IRQ DBG(".. io_irq #%d [%s][%c] alt[%d:%d] bActive[%d]", s_nIrq_dbg, pIrq->sName, bActive || !pIrq->nCodeInactive ? (char)pIrq->nCodeActive : (char)pIrq->nCodeInactive, (s_altKeyPress && pIrq->bKeyIrq) ? pIrq->nCodeAlternate : (bActive ? pIrq->nCodeActive : pIrq->nCodeInactive), s_altKeyPress, bActive); #endif blCodeActive = (s_altKeyPress && pIrq->bKeyIrq) ? pIrq->nCodeAlternate : (bActive ? pIrq->nCodeActive : pIrq->nCodeInactive); #ifdef CYIO_ALTERNATE_KEY if (s_altKeyPresent) { s_altKeyPress = !gpio_get_value(s_altKeyGpio); DBG("alt status: %d", s_altKeyPress); } #endif spinLock(&io_lock); pIrq->bActive = bActive; if (pIrq->bKeyIrq) { #ifdef CYIO_REPEAT DBG("bfr: s_nPrevKey = %d", s_nPrevKey); if (s_nPrevKey) { DBG("s_nPrevKey is != 0 [%d]", s_nPrevKey); if (!pIrq->bActive /*|| s_nPrevKey != blCodeActive*/) s_nPrevKey = 0; DBG("s_nPrevKey == %d (s_bRepMode:%d, pIrq->bActive:%d)", s_nPrevKey, s_bRepMode, pIrq->bActive); if (s_bRepMode && !s_nPrevKey) { MSG("Exit repeat mode..."); s_bRepMode = 0; if (s_pEventW) { // NB: This is considered as a system event ++s_nEventCnt; s_pEventW->nCode = CYEVENT_KEY_REPEAT_END; s_pEventW->nKeyEvent = 0; s_pEventW = s_pEventW->pNext; } if (ptsk) wake_up_process(ptsk); } } #endif #ifdef CYIO_ALTERNATE_KEY if ((!pIrq->bActive) && (pIrq->nGpio != s_altKeyGpio)) #else if (!pIrq->bActive) #endif { spinUnlock(&io_lock); MSG("Exiting IRQ Handler"); #ifdef CYIO_KERNEL_2_4 return; #else return IRQ_HANDLED; #endif } } spinUnlock(&io_lock); //#ifdef DBG_IRQ // DBG(".. io_irq #%d [%s][%c] bActive[%d]", s_nIrq_dbg, pIrq->sName, bActive || !pIrq->nCodeInactive ? (char)pIrq->nCodeActive : (char)pIrq->nCodeInactive, bActive); //#endif if (io_factoryPowerOff()) { // Special case spinLock(&io_lock); MSG("Request Factory Setting...\n"); if (s_pEventW) { ++s_nEventCnt; ++s_nKeyLogCnt; s_pEventW->nCode = CYEVENT_FACTORY_OFF; s_pEventW->nKeyEvent = 1; s_pEventW = s_pEventW->pNext; } spinUnlock(&io_lock); } else { spinLock(&io_lock); if ((s_nEventMax <= s_nEventCnt) || (pIrq->bKeyIrq && s_nKeyLogMax <= s_nKeyLogCnt)) { spinUnlock(&io_lock); DBG("s_nEventMax:%d, s_nEventCnt:%d, s_nKeyLogMax:%d, s_nKeyLogCnt:%d",s_nEventMax,s_nEventCnt,s_nKeyLogMax,s_nKeyLogCnt); MSG("!!! Event list full !!!"); return IRQ_HANDLED; } #ifdef CYIO_ALTERNATE_KEY if (s_altKeyPresent && bAltKeyUsed && !s_altKeyPress && (pIrq->nGpio == s_altKeyGpio)) { /* If the released key is the Alt Key and it was used as the Alt Key, do not send an event */ MSG("Alt Key released... Do Nothing"); bAltKeyUsed = FALSE; s_nPrevKey = 0; spinUnlock(&io_lock); return IRQ_HANDLED; } if (s_altKeyPresent && (pIrq->nGpio == s_altKeyGpio)) { /* If the altKey is pressed, we have to do a little trick: Inverse it's active state */ MSG("Alt key event..."); /* To be sure */ s_nPrevKey = 0; s_bRepMode = 0; bActive = !bActive; } else if (s_altKeyPresent && s_altKeyPress && pIrq->bKeyIrq) {/* The event is a keypress and the AltKey is pressed, set our internal value to prevent Alt to send an event */ MSG("Button press with Alt..."); if (pIrq->nCodeAlternate != 0) bAltKeyUsed = TRUE; } MSG("Hum..."); #endif if (s_pEventW) { MSG("Will push another event..."); ++s_nEventCnt; if (pIrq->bKeyIrq) ++s_nKeyLogCnt; #ifdef CYIO_ALTERNATE_KEY s_pEventW->nCode = (s_altKeyPress && pIrq->bKeyIrq) ? pIrq->nCodeAlternate : (bActive ? pIrq->nCodeActive : pIrq->nCodeInactive); #else s_pEventW->nCode = bActive ? pIrq->nCodeActive : pIrq->nCodeInactive; #endif s_pEventW->nKeyEvent = pIrq->bKeyIrq; s_pEventW = s_pEventW->pNext; } spinUnlock(&io_lock); } if (ptsk) wake_up_process(ptsk); return IRQ_HANDLED; } // --------------------------------------------------------------------------- #ifdef CYIO_TIMER static void io_timer_handler(unsigned long nData) { /* YEP inside */ #if 0 //#ifdef G_SENSOR if (hold_wakeup==1) { del_timer(&io_timer); goto end_timer; } #endif /* end YEP inside */ DBG("Timer [%ld] tick...\n", nData); spin_lock_irq(&io_lock); del_timer(&io_timer); timer_run = 0; if (s_nEventCnt < s_nEventMax) { if (s_pEventW) { ++s_nEventCnt; s_pEventW->nCode = (u8)nData; s_pEventW->nKeyEvent = 0; s_pEventW = s_pEventW->pNext; } } spin_unlock_irq(&io_lock); if (ptsk) wake_up_process(ptsk); //end_timer: return; } #endif // =========================================================================== static int io_open(struct inode *inode, struct file *file) { //MSG(">> io_open"); //spin_lock_irq(&io_lock); spinLock(&io_lock); if (io_status) { //spin_unlock_irq(&io_lock); spinUnlock(&io_lock); //MSG(".. io_open !!! Busy"); return -EBUSY; } io_status = 1; //spin_unlock_irq(&io_lock); spinUnlock(&io_lock); io_initIrq(); return 0; } // --------------------------------------------------------------------------- static int io_release(struct inode *inode, struct file *file) { MSG(">> io_release"); io_deinitIrq(); spin_lock_irq(&io_lock); io_status = 0; // The driver is likely to be closed & reopened within suspend sequences // Only initialize the list when registering the driver and when closing it // This allows for a quicker opening of the driver io_initEventList(); spin_unlock_irq(&io_lock); MSG("<< io_release"); return 0; } // --------------------------------------------------------------------------- ssize_t io_read(struct file *file, char *buf, size_t count, loff_t *ppos) { int nBytes = sizeof(unsigned long); unsigned long nData; ssize_t nRes = 0; if (count < sizeof(unsigned long)) return -EINVAL; while (1) { //spin_lock_irq(&io_lock); spinLock(&io_lock); nData = 0; if (s_nEventCnt) { nData = s_pEventR->nCode; s_pEventR->nCode = 0; --s_nEventCnt; if (s_pEventR->nKeyEvent) { --s_nKeyLogCnt; #ifdef CYIO_REPEAT if (nData != CYEVENT_KEY_OFF) { DBG("(line %d)Set s_nPrevKey", __LINE__); s_nPrevKey = nData; s_bRepMode = 0; } #endif } s_pEventR = s_pEventR->pNext; } #ifdef CYIO_REPEAT if (!nData && s_nPrevKey) { // Check new status int i; cyIrq *pIrq, *pIrq0; pIrq0 = &s_nIrq[0]; MSG("Will test key..."); for (i=0, pIrq=pIrq0; inCodeActive != s_nPrevKey) && (pIrq->nCodeAlternate != s_nPrevKey)) #else if (pIrq->nCodeActive != s_nPrevKey) #endif { MSG("!CodeActive and Alt+!AltCode"); continue; } MSG("May clear prevkey"); if (!pIrq->bActive || gpio_get_value(pIrq->nGpio)) { DBG("(line %d)Set s_nPrevKey", __LINE__); s_nPrevKey = 0; } break; } if (s_nPrevKey) { MSG("Will Set Repeat flag..."); nData = s_nPrevKey | CYEVENT_KEY_REPEAT_FLAG; s_bRepMode = 1; } } #endif spinUnlock(&io_lock); if (nData != 0) { #ifdef CYIO_TIMER del_timer(&io_timer); timer_run = 0; io_timer.data = (nData == CYEVENT_SUSPEND_SCREEN) ? CYEVENT_SUSPEND_DEVICE : CYEVENT_SUSPEND_SCREEN; #endif break; } #ifdef CYIO_TIMER if (s_pbUsbPowered && s_pbAcPowered && !(*s_pbUsbPowered) && !(*s_pbAcPowered)) { del_timer(&io_timer); timer_run = 0; if (io_timer.data == CYEVENT_SUSPEND_SCREEN) { io_timer.expires = IO_TIMER_DELAY_1; io_timer.expires += jiffies; add_timer(&io_timer); } else //if (io_timer.data == CYEVENT_SUSPEND_DEVICE) { io_timer.expires = IO_TIMER_DELAY_2 - IO_TIMER_DELAY_1; io_timer.expires += jiffies; add_timer(&io_timer); } timer_run = 1; } #endif ptsk = current; set_current_state(TASK_INTERRUPTIBLE); schedule(); set_current_state(TASK_RUNNING); ptsk = 0; if (signal_pending(current)) { nRes = -ERESTARTSYS; break; } } if (nData) { nRes = copy_to_user(buf,&nData,nBytes); if (!nRes) nRes = nBytes; } return nRes; } // --------------------------------------------------------------------------- static int io_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg) { return -EINVAL; } // =========================================================================== static struct file_operations s_io_fops = { owner: THIS_MODULE, read: io_read, ioctl: io_ioctl, open: io_open, release: io_release, }; static struct miscdevice s_io_dev = { .minor = 250, .name = "cyio", .fops = &s_io_fops, }; // =========================================================================== // --------------------------------------------------------------------------- static int io_probe(struct platform_device *dev) { return 0; } // --------------------------------------------------------------------------- static int io_remove(struct platform_device *dev) { misc_deregister(&s_io_dev); return 0; } // --------------------------------------------------------------------------- static int io_resume(struct platform_device *dev) { //cyIrq *pIrq; //u8 bSdCd; MSG(">>Resume() .......\n"); #if 0 if ((platform_type == CYBOOK_GEN3) || (platform_type == CYBOOK_GEN3GOLD)) { pIrq = &s_nIrq[13]; } else { pIrq = &s_nIrq[11]; } bSdCd = !read_gpio_bit(pIrq->nGpio); //MSG(">>Resume() .......bSdCd 0x%x\n",bSdCd); if (pIrq->bActive == bSdCd) return 0; spinLock(&io_lock); pIrq->bActive = bSdCd; if (s_pEventW) { ++s_nEventCnt; s_pEventW->nCode = pIrq->bActive ? pIrq->nCodeActive : pIrq->nCodeInactive; s_pEventW->nKeyEvent = pIrq->bKeyIrq; s_pEventW = s_pEventW->pNext; } spinUnlock(&io_lock); if (ptsk) wake_up_process(ptsk); #endif MSG("<"); MODULE_DESCRIPTION("Cybook Event Manager"); MODULE_VERSION("3.0"); // ===========================================================================