Skip to contentSkip to conversation

USB: gadget: atmel_usba_udc: Driver improvements

PATCH Superseded Sylvain Rochet 2 participants 7 messages Jan 21, 2015

Subsystems (3)

v5 of 7 Superseded Latest is v7 →

lore
Share & tools
lore
#0 lore raw

v5 cover USB: gadget: atmel_usba_udc: Driver improvements

Start clocks on rising edge of the Vbus signal, stop clocks on falling edge of the Vbus signal.

Add suspend/resume with wakeup support.

Changes since v4:
* Now using IRQ_NOAUTOEN flag to remove the unused check for

    udc->driver is not NULL in the Vbus IRQ.

* Reworked the start/stop of clocks on Vbus edges to prepare for suspend/resume support, factorised start and stop procedures
* New patch, suspend/resume for USBA with wakeup support

Changes since v3:
* Added stable tag for the first patch
* As suggested, removed the unused check for udc->driver is not NULL in

    Vbus IRQ by requesting IRQ after udc->driver is set and by releasing
    IRQ before udc->driver is cleared

* Rebased the core patch of this series against the just explained changes

Changes since v2:
* Use spin_lock_irqsave/unlock_irqrestore instead of spin_lock/unlock in threaded interrupt because we are not in irq context anymore
* Removed useless and probably harmful IRQF_NO_SUSPEND from devm_request_threaded_irq() flags

Changes since v1:
* Using a threaded irq and mutex instead of spinclock as suggested
* Moved a silently fixed bug in a separate patch (1/2)

Sylvain Rochet (4):
USB: gadget: atmel_usba_udc: Fixed vbus_prev initial state
USB: gadget: atmel_usba_udc: Request an auto disabled Vbus signal IRQ
instead of an auto enabled IRQ request followed by IRQ disable
USB: gadget: atmel_usba_udc: Start clocks on rising edge of the Vbus
signal, stop clocks on falling edge of the Vbus signal
USB: gadget: atmel_usba_udc: Add suspend/resume with wakeup support

 drivers/usb/gadget/udc/atmel_usba_udc.c | 214 ++++++++++++++++++++++++--------
 drivers/usb/gadget/udc/atmel_usba_udc.h |   4 +
 2 files changed, 165 insertions(+), 53 deletions(-)
Signature · 1 line
2.1.4
#1 lore raw

PATCH v5 1/4 USB: gadget: atmel_usba_udc: Fixed vbus_prev initial state

If vbus gpio is high at init, we should set vbus_prev to true accordingly to the current vbus state. Without that, we skip the first vbus interrupt because the saved vbus state is not consistent.

Fixes 914a3f3b3754 ("USB: add atmel_usba_udc driver")
Cc <[email protected]> #2.6.24+
---
 drivers/usb/gadget/udc/atmel_usba_udc.c | 2 ++
 1 file changed, 2 insertions(+)
drivers/usb/gadget/udc/atmel_usba_udc.c +2 -0
index ce88237..e207d75 100644
@@ -1791,6 +1791,8 @@ static int atmel_usba_start(struct usb_gadget *gadget,
 		toggle_bias(1);
 		usba_writel(udc, CTRL, USBA_ENABLE_MASK);
 		usba_writel(udc, INT_ENB, USBA_END_OF_RESET);
+
+		udc->vbus_prev = 1;
 	}
 	spin_unlock_irqrestore(&udc->lock, flags);
 
Signature · 1 line
2.1.4
#2 lore raw

PATCH v5 2/4 USB: gadget: atmel_usba_udc: Request an auto disabled Vbus signal IRQ instead of an auto enabled IRQ request followed by IRQ disable

In reply to Sylvain Rochet

Vbus IRQ handler needs a started UDC driver to work because it uses udc->driver, which is set by the UDC start handler. The previous way chosen was to return from interrupt if udc->driver is NULL using a spinlock around the check.

We now request an auto disabled (IRQ_NOAUTOEN) Vbus signal IRQ instead of an auto enabled IRQ followed by disable_irq(). This way we remove the very small timeslot of enabled IRQ which existed previously between request() and disable(). We don't need anymore to check if udc->driver is NULL in IRQ handler.

---
 drivers/usb/gadget/udc/atmel_usba_udc.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)
drivers/usb/gadget/udc/atmel_usba_udc.c +1 -7
index e207d75..d369ae0 100644
@@ -1729,10 +1729,6 @@ static irqreturn_t usba_vbus_irq(int irq, void *devid)
 
 	spin_lock(&udc->lock);
 
-	/* May happen if Vbus pin toggles during probe() */
-	if (!udc->driver)
-		goto out;
-
 	vbus = vbus_is_present(udc);
 	if (vbus != udc->vbus_prev) {
 		if (vbus) {
@@ -1753,7 +1749,6 @@ static irqreturn_t usba_vbus_irq(int irq, void *devid)
 		udc->vbus_prev = vbus;
 	}
 
-out:
 	spin_unlock(&udc->lock);
 
 	return IRQ_HANDLED;
@@ -2049,6 +2044,7 @@ static int usba_udc_probe(struct platform_device *pdev)
 
 	if (gpio_is_valid(udc->vbus_pin)) {
 		if (!devm_gpio_request(&pdev->dev, udc->vbus_pin, "atmel_usba_udc")) {
+			irq_set_status_flags(gpio_to_irq(udc->vbus_pin), IRQ_NOAUTOEN);
 			ret = devm_request_irq(&pdev->dev,
 					gpio_to_irq(udc->vbus_pin),
 					usba_vbus_irq, 0,
@@ -2058,8 +2054,6 @@ static int usba_udc_probe(struct platform_device *pdev)
 				dev_warn(&udc->pdev->dev,
 					 "failed to request vbus irq; "
 					 "assuming always on\n");
-			} else {
-				disable_irq(gpio_to_irq(udc->vbus_pin));
 			}
 		} else {
 			/* gpio_request fail so use -EINVAL for gpio_is_valid */
Signature · 1 line
2.1.4
#3 lore raw

PATCH v5 3/4 USB: gadget: atmel_usba_udc: Start clocks on rising edge of the Vbus signal, stop clocks on falling edge of the Vbus signal

In reply to Sylvain Rochet

If USB PLL is not necessary for other USB drivers (e.g. OHCI and EHCI) we will reduce power consumption by switching off the USB PLL if no USB Host is currently connected to this USB Device.

We are using Vbus GPIO signal to detect Host presence. If Vbus signal is not available then the device stays continuously clocked.

---
 drivers/usb/gadget/udc/atmel_usba_udc.c | 149 +++++++++++++++++++++-----------
 drivers/usb/gadget/udc/atmel_usba_udc.h |   4 +
 2 files changed, 104 insertions(+), 49 deletions(-)
drivers/usb/gadget/udc/atmel_usba_udc.c +100 -49
index d369ae0..963e398 100644
@@ -1719,38 +1719,98 @@ static irqreturn_t usba_udc_irq(int irq, void *devid)
 	return IRQ_HANDLED;
 }
 
-static irqreturn_t usba_vbus_irq(int irq, void *devid)
+static int start_clock(struct usba_udc *udc)
+{
+	int ret;
+
+	if (udc->clocked)
+		return 0;
+
+	ret = clk_prepare_enable(udc->pclk);
+	if (ret)
+		return ret;
+	ret = clk_prepare_enable(udc->hclk);
+	if (ret) {
+		clk_disable_unprepare(udc->pclk);
+		return ret;
+	}
+
+	udc->clocked = true;
+	return 0;
+}
+
+static void stop_clock(struct usba_udc *udc)
+{
+	if (!udc->clocked)
+		return;
+
+	clk_disable_unprepare(udc->hclk);
+	clk_disable_unprepare(udc->pclk);
+
+	udc->clocked = false;
+}
+
+static int usba_start(struct usba_udc *udc)
+{
+	unsigned long flags;
+	int ret;
+
+	ret = start_clock(udc);
+	if (ret)
+		return ret;
+
+	spin_lock_irqsave(&udc->lock, flags);
+	toggle_bias(1);
+	usba_writel(udc, CTRL, USBA_ENABLE_MASK);
+	usba_writel(udc, INT_ENB, USBA_END_OF_RESET);
+	spin_unlock_irqrestore(&udc->lock, flags);
+	return 0;
+}
+
+static void usba_stop(struct usba_udc *udc)
+{
+	unsigned long flags;
+
+	spin_lock_irqsave(&udc->lock, flags);
+	udc->gadget.speed = USB_SPEED_UNKNOWN;
+	reset_all_endpoints(udc);
+
+	/* This will also disable the DP pullup */
+	toggle_bias(0);
+	usba_writel(udc, CTRL, USBA_DISABLE_MASK);
+	spin_unlock_irqrestore(&udc->lock, flags);
+
+	stop_clock(udc);
+}
+
+static irqreturn_t usba_vbus_irq_thread(int irq, void *devid)
 {
 	struct usba_udc *udc = devid;
 	int vbus;
+	int ret;
 
 	/* debounce */
 	udelay(10);
 
-	spin_lock(&udc->lock);
+	mutex_lock(&udc->vbus_mutex);
 
 	vbus = vbus_is_present(udc);
 	if (vbus != udc->vbus_prev) {
+		udc->vbus_prev = vbus;
 		if (vbus) {
-			toggle_bias(1);
-			usba_writel(udc, CTRL, USBA_ENABLE_MASK);
-			usba_writel(udc, INT_ENB, USBA_END_OF_RESET);
+			ret = usba_start(udc);
+			if (ret)
+				goto out;
 		} else {
-			udc->gadget.speed = USB_SPEED_UNKNOWN;
-			reset_all_endpoints(udc);
-			toggle_bias(0);
-			usba_writel(udc, CTRL, USBA_DISABLE_MASK);
-			if (udc->driver->disconnect) {
-				spin_unlock(&udc->lock);
+			usba_stop(udc);
+
+			if (udc->driver->disconnect)
 				udc->driver->disconnect(&udc->gadget);
-				spin_lock(&udc->lock);
-			}
 		}
-		udc->vbus_prev = vbus;
 	}
 
-	spin_unlock(&udc->lock);
-
+out:
+	mutex_unlock(&udc->vbus_mutex);
 	return IRQ_HANDLED;
 }
 
@@ -1762,57 +1822,47 @@ static int atmel_usba_start(struct usb_gadget *gadget,
 	unsigned long flags;
 
 	spin_lock_irqsave(&udc->lock, flags);
-
 	udc->devstatus = 1 << USB_DEVICE_SELF_POWERED;
 	udc->driver = driver;
 	spin_unlock_irqrestore(&udc->lock, flags);
 
-	ret = clk_prepare_enable(udc->pclk);
-	if (ret)
-		return ret;
-	ret = clk_prepare_enable(udc->hclk);
-	if (ret) {
-		clk_disable_unprepare(udc->pclk);
-		return ret;
-	}
+	mutex_lock(&udc->vbus_mutex);
 
-	udc->vbus_prev = 0;
 	if (gpio_is_valid(udc->vbus_pin))
 		enable_irq(gpio_to_irq(udc->vbus_pin));
 
 	/* If Vbus is present, enable the controller and wait for reset */
-	spin_lock_irqsave(&udc->lock, flags);
-	if (vbus_is_present(udc) && udc->vbus_prev == 0) {
-		toggle_bias(1);
-		usba_writel(udc, CTRL, USBA_ENABLE_MASK);
-		usba_writel(udc, INT_ENB, USBA_END_OF_RESET);
-
-		udc->vbus_prev = 1;
+	udc->vbus_prev = vbus_is_present(udc);
+	if (udc->vbus_prev) {
+		ret = usba_start(udc);
+		if (ret)
+			goto err;
 	}
-	spin_unlock_irqrestore(&udc->lock, flags);
 
+	mutex_unlock(&udc->vbus_mutex);
 	return 0;
+
+err:
+	if (gpio_is_valid(udc->vbus_pin))
+		disable_irq(gpio_to_irq(udc->vbus_pin));
+
+	mutex_unlock(&udc->vbus_mutex);
+
+	spin_lock_irqsave(&udc->lock, flags);
+	udc->devstatus &= ~(1 << USB_DEVICE_SELF_POWERED);
+	udc->driver = NULL;
+	spin_unlock_irqrestore(&udc->lock, flags);
+	return ret;
 }
 
 static int atmel_usba_stop(struct usb_gadget *gadget)
 {
 	struct usba_udc *udc = container_of(gadget, struct usba_udc, gadget);
-	unsigned long flags;
 
 	if (gpio_is_valid(udc->vbus_pin))
 		disable_irq(gpio_to_irq(udc->vbus_pin));
 
-	spin_lock_irqsave(&udc->lock, flags);
-	udc->gadget.speed = USB_SPEED_UNKNOWN;
-	reset_all_endpoints(udc);
-	spin_unlock_irqrestore(&udc->lock, flags);
-
-	/* This will also disable the DP pullup */
-	toggle_bias(0);
-	usba_writel(udc, CTRL, USBA_DISABLE_MASK);
-
-	clk_disable_unprepare(udc->hclk);
-	clk_disable_unprepare(udc->pclk);
+	usba_stop(udc);
 
 	udc->driver = NULL;
 
@@ -1992,6 +2042,7 @@ static int usba_udc_probe(struct platform_device *pdev)
 		return PTR_ERR(hclk);
 
 	spin_lock_init(&udc->lock);
+	mutex_init(&udc->vbus_mutex);
 	udc->pdev = pdev;
 	udc->pclk = pclk;
 	udc->hclk = hclk;
@@ -2045,9 +2096,9 @@ static int usba_udc_probe(struct platform_device *pdev)
 	if (gpio_is_valid(udc->vbus_pin)) {
 		if (!devm_gpio_request(&pdev->dev, udc->vbus_pin, "atmel_usba_udc")) {
 			irq_set_status_flags(gpio_to_irq(udc->vbus_pin), IRQ_NOAUTOEN);
-			ret = devm_request_irq(&pdev->dev,
-					gpio_to_irq(udc->vbus_pin),
-					usba_vbus_irq, 0,
+			ret = devm_request_threaded_irq(&pdev->dev,
+					gpio_to_irq(udc->vbus_pin), NULL,
+					usba_vbus_irq_thread, IRQF_ONESHOT,
 					"atmel_usba_udc", udc);
 			if (ret) {
 				udc->vbus_pin = -ENODEV;
drivers/usb/gadget/udc/atmel_usba_udc.h +4 -0
index a70706e..3ceed76 100644
@@ -308,6 +308,9 @@ struct usba_udc {
 	/* Protect hw registers from concurrent modifications */
 	spinlock_t lock;
 
+	/* Mutex to prevent concurrent start or stop */
+	struct mutex vbus_mutex;
+
 	void __iomem *regs;
 	void __iomem *fifo;
 
@@ -321,6 +324,7 @@ struct usba_udc {
 	struct clk *pclk;
 	struct clk *hclk;
 	struct usba_ep *usba_ep;
+	bool clocked;
 
 	u16 devstatus;
 
Signature · 1 line
2.1.4
#4 lore raw

PATCH v5 4/4 USB: gadget: atmel_usba_udc: Add suspend/resume with wakeup support

In reply to Sylvain Rochet

This patch add suspend/resume with wakeup support for Atmel USBA.

On suspend: We stay continuously clocked if Vbus signal is not available. If Vbus signal is available we set the Vbus signal as a wake up source then we stop the USBA itself and all clocks used by USBA.

On resume: We recover clocks and USBA we stopped them. If a device is connected, i.e. connected while we were suspended without wakeup enabled, we enable the controller.

---
 drivers/usb/gadget/udc/atmel_usba_udc.c | 61 +++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)
drivers/usb/gadget/udc/atmel_usba_udc.c +61 -0
index 963e398..326725e 100644
@@ -2115,6 +2115,7 @@ static int usba_udc_probe(struct platform_device *pdev)
 	ret = usb_add_gadget_udc(&pdev->dev, &udc->gadget);
 	if (ret)
 		return ret;
+	device_init_wakeup(&pdev->dev, 1);
 
 	usba_init_debugfs(udc);
 	for (i = 1; i < udc->num_ep; i++)
@@ -2130,6 +2131,7 @@ static int __exit usba_udc_remove(struct platform_device *pdev)
 
 	udc = platform_get_drvdata(pdev);
 
+	device_init_wakeup(&pdev->dev, 0);
 	usb_del_gadget_udc(&udc->gadget);
 
 	for (i = 1; i < udc->num_ep; i++)
@@ -2139,6 +2141,62 @@ static int __exit usba_udc_remove(struct platform_device *pdev)
 	return 0;
 }
 
+#ifdef CONFIG_PM
+static int usba_udc_suspend(struct device *dev)
+{
+	struct usba_udc *udc = dev_get_drvdata(dev);
+
+	/* Not started */
+	if (!udc->driver)
+		return 0;
+
+	if (!device_may_wakeup(dev)) {
+		usba_stop(udc);
+		return 0;
+	}
+
+	/*
+	 * Device may wake up. We stay clocked if we failed
+	 * to request vbus irq, assuming always on.
+	 */
+	if (gpio_is_valid(udc->vbus_pin)) {
+		usba_stop(udc);
+		/* Only wake up on device connection */
+		irq_set_irq_type(gpio_to_irq(udc->vbus_pin),
+			udc->vbus_pin_inverted ?
+			IRQ_TYPE_EDGE_FALLING : IRQ_TYPE_EDGE_RISING);
+		enable_irq_wake(gpio_to_irq(udc->vbus_pin));
+	}
+
+	return 0;
+}
+
+static int usba_udc_resume(struct device *dev)
+{
+	struct usba_udc *udc = dev_get_drvdata(dev);
+
+	/* Not started */
+	if (!udc->driver)
+		return 0;
+
+	if (device_may_wakeup(dev) && gpio_is_valid(udc->vbus_pin)) {
+		disable_irq_wake(gpio_to_irq(udc->vbus_pin));
+		irq_set_irq_type(gpio_to_irq(udc->vbus_pin), IRQ_TYPE_EDGE_BOTH);
+	}
+
+	/* If Vbus is present, enable the controller and wait for reset */
+	mutex_lock(&udc->vbus_mutex);
+	udc->vbus_prev = vbus_is_present(udc);
+
+	if (udc->vbus_prev)
+		usba_start(udc);
+
+	mutex_unlock(&udc->vbus_mutex);
+
+	return 0;
+}
+#endif
+
 #if defined(CONFIG_OF)
 static const struct of_device_id atmel_udc_dt_ids[] = {
 	{ .compatible = "atmel,at91sam9rl-udc" },
@@ -2148,10 +2206,13 @@ static const struct of_device_id atmel_udc_dt_ids[] = {
 MODULE_DEVICE_TABLE(of, atmel_udc_dt_ids);
 #endif
 
+static SIMPLE_DEV_PM_OPS(usba_udc_pm_ops, usba_udc_suspend, usba_udc_resume);
+
 static struct platform_driver udc_driver = {
 	.remove		= __exit_p(usba_udc_remove),
 	.driver		= {
 		.name		= "atmel_usba_udc",
+		.pm		= &usba_udc_pm_ops,
 		.of_match_table	= of_match_ptr(atmel_udc_dt_ids),
 	},
 };
Signature · 1 line
2.1.4
#5 lore raw

PATCH v5 4/4 USB: gadget: atmel_usba_udc: Add suspend/resume with wakeup support

Hi Sylvain,

Quoted 53 lines · On Wed, 21 Jan 2015 20:31:14 +0100 Sylvain Rochet…

This patch add suspend/resume with wakeup support for Atmel USBA.

On suspend: We stay continuously clocked if Vbus signal is not available. If Vbus signal is available we set the Vbus signal as a wake up source then we stop the USBA itself and all clocks used by USBA.

On resume: We recover clocks and USBA we stopped them. If a device is connected, i.e. connected while we were suspended without wakeup enabled, we enable the controller.

---
 drivers/usb/gadget/udc/atmel_usba_udc.c | 61 +++++++++++++++++++++++++++++++++
 1 file changed, 61 insertions(+)
drivers/usb/gadget/udc/atmel_usba_udc.c +16 -0
index 963e398..326725e 100644
@@ -2115,6 +2115,7 @@ static int usba_udc_probe(struct platform_device *pdev)
 	ret = usb_add_gadget_udc(&pdev->dev, &udc->gadget);
 	if (ret)
 		return ret;
+	device_init_wakeup(&pdev->dev, 1);
 
 	usba_init_debugfs(udc);
 	for (i = 1; i < udc->num_ep; i++)
@@ -2130,6 +2131,7 @@ static int __exit usba_udc_remove(struct platform_device *pdev)
 
 	udc = platform_get_drvdata(pdev);
 
+	device_init_wakeup(&pdev->dev, 0);
 	usb_del_gadget_udc(&udc->gadget);
 
 	for (i = 1; i < udc->num_ep; i++)
@@ -2139,6 +2141,62 @@ static int __exit usba_udc_remove(struct platform_device *pdev)
 	return 0;
 }
 
+#ifdef CONFIG_PM
+static int usba_udc_suspend(struct device *dev)
+{
+	struct usba_udc *udc = dev_get_drvdata(dev);
+
+	/* Not started */
+	if (!udc->driver)
+		return 0;
+
+	if (!device_may_wakeup(dev)) {
+		usba_stop(udc);
+		return 0;
+	}
+

drivers/usb/gadget/udc/atmel_usba_udc.c static int __exit usba_udc_remove(struct platform_device *pdev) · line 2167

Quoted 10 lines
drivers/usb/gadget/udc/atmel_usba_udc.c +10 -0
+	/*
+	 * Device may wake up. We stay clocked if we failed
+	 * to request vbus irq, assuming always on.
+	 */
+	if (gpio_is_valid(udc->vbus_pin)) {
+		usba_stop(udc);
+		/* Only wake up on device connection */
+		irq_set_irq_type(gpio_to_irq(udc->vbus_pin),
+			udc->vbus_pin_inverted ?
+			IRQ_TYPE_EDGE_FALLING : IRQ_TYPE_EDGE_RISING);

If I'm correct, you're testing on a at91sam9x5 platform. Be careful, this driver is used by older SoCs (at91sam9rl and avr32 SoCs) which do not support configuring the interrupt on a specific edge (they trigger on both edges).

This leaves two solutions here:
1) keep the IRQ_TYPE_EDGE_BOTH setting.
2) add new compatible strings to this driver and make this specific edge change dependent on the SoC capability.

Best Regards,

Boris

Signature · 3 lines
Boris Brezillon, Free Electrons
Embedded Linux and Kernel engineering
http://free-electrons.com
#6 lore raw

v5 cover USB: gadget: atmel_usba_udc: Driver improvements

In reply to Sylvain Rochet

Hi Sylvain,

Quoted 4 lines · On Wed, 21 Jan 2015 20:31:10 +0100 Sylvain Rochet…

Start clocks on rising edge of the Vbus signal, stop clocks on falling edge of the Vbus signal.

Add suspend/resume with wakeup support.

Apart from the minor comment I made on patch 4, you can add my:

to the whole series.

Thanks for your patience.

Best Regards,

Boris

Quoted 37 lines

Changes since v4:
* Now using IRQ_NOAUTOEN flag to remove the unused check for

    udc->driver is not NULL in the Vbus IRQ.

* Reworked the start/stop of clocks on Vbus edges to prepare for suspend/resume support, factorised start and stop procedures
* New patch, suspend/resume for USBA with wakeup support

Changes since v3:
* Added stable tag for the first patch
* As suggested, removed the unused check for udc->driver is not NULL in

    Vbus IRQ by requesting IRQ after udc->driver is set and by releasing
    IRQ before udc->driver is cleared

* Rebased the core patch of this series against the just explained changes

Changes since v2:
* Use spin_lock_irqsave/unlock_irqrestore instead of spin_lock/unlock in threaded interrupt because we are not in irq context anymore
* Removed useless and probably harmful IRQF_NO_SUSPEND from devm_request_threaded_irq() flags

Changes since v1:
* Using a threaded irq and mutex instead of spinclock as suggested
* Moved a silently fixed bug in a separate patch (1/2)

Sylvain Rochet (4):
USB: gadget: atmel_usba_udc: Fixed vbus_prev initial state
USB: gadget: atmel_usba_udc: Request an auto disabled Vbus signal IRQ
instead of an auto enabled IRQ request followed by IRQ disable
USB: gadget: atmel_usba_udc: Start clocks on rising edge of the Vbus
signal, stop clocks on falling edge of the Vbus signal
USB: gadget: atmel_usba_udc: Add suspend/resume with wakeup support

 drivers/usb/gadget/udc/atmel_usba_udc.c | 214 ++++++++++++++++++++++++--------
 drivers/usb/gadget/udc/atmel_usba_udc.h |   4 +
 2 files changed, 165 insertions(+), 53 deletions(-)
Signature · 3 lines
Boris Brezillon, Free Electrons
Embedded Linux and Kernel engineering
http://free-electrons.com