This series originated as a result of CAN communication tests for an
application using the USBtin adapter (https://www.fischl.de/usbtin/).
The tests showed some errors but for the driver everything was ok.
Also, being the first time I used the slcan driver, I was amazed that
it was not possible to configure the bitrate via the ip tool.
For these two reasons, I started looking at the driver code and realized
that it didn't use the CAN network device driver interface.
Starting from these assumptions, I tried to:
- Use the CAN network device driver interface.
- Set the bitrate via the ip tool.
- Send the open/close command to the adapter from the driver.
- Add ethtool support to reset the adapter errors.
- Extend the protocol to forward the adapter CAN communication
errors and the CAN state changes to the netdev upper layers.
Except for the protocol extension patches (i. e. forward the adapter CAN
communication errors and the CAN state changes to the netdev upper
layers), the whole series has been tested under QEMU with Linux 4.19.208
using the USBtin adapter.
Testing the extension protocol patches requires updating the adapter
firmware. Before modifying the firmware I think it makes sense to know if
these extensions can be considered useful.
Before applying the series I used these commands:
slcan_attach -f -s6 -o /dev/ttyACM0
slcand ttyACM0 can0
ip link set can0 up
After applying the series I am using these commands:
slcan_attach /dev/ttyACM0
slcand ttyACM0 can0
ip link set dev can0 down
ip link set can0 type can bitrate 500000
ethtool --set-priv-flags can0 err-rst-on-open on
ip link set dev can0 up
Now there is a clearer separation between serial line and CAN,
but above all, it is possible to use the ip and ethtool commands
as it happens for any CAN device driver. The changes are backward
compatible, you can continue to use the slcand and slcan_attach
command options.
Changes in v2:
- Put the data into the allocated skb directly instead of first
filling the "cf" on the stack and then doing a memcpy().
- Move CAN_SLCAN Kconfig option inside CAN_DEV scope.
- Improve the commit message.
- Use the CAN framework support for setting fixed bit rates.
- Improve the commit message.
- Improve the commit message.
- Protect decoding against the case the len value is longer than the
received data.
- Continue error handling even if no skb can be allocated.
- Continue error handling even if no skb can be allocated.
Dario Binacchi (13):
can: slcan: use the BIT() helper
can: slcan: use netdev helpers to print out messages
can: slcan: use the alloc_can_skb() helper
can: slcan: use CAN network device driver API
can: slcan: simplify the device de-allocation
can: slcan: allow to send commands to the adapter
can: slcan: set bitrate by CAN device driver API
can: slcan: send the open command to the adapter
can: slcan: send the close command to the adapter
can: slcan: move driver into separate sub directory
can: slcan: add ethtool support to reset adapter errors
can: slcan: extend the protocol with error info
can: slcan: extend the protocol with CAN state info
drivers/net/can/Kconfig | 40 +-
drivers/net/can/Makefile | 2 +-
drivers/net/can/slcan/Makefile | 7 +
.../net/can/{slcan.c => slcan/slcan-core.c} | 526 ++++++++++++++----
drivers/net/can/slcan/slcan-ethtool.c | 65 +++
drivers/net/can/slcan/slcan.h | 18 +
6 files changed, 539 insertions(+), 119 deletions(-)
create mode 100644 drivers/net/can/slcan/Makefile
rename drivers/net/can/{slcan.c => slcan/slcan-core.c} (65%)
create mode 100644 drivers/net/can/slcan/slcan-ethtool.c
create mode 100644 drivers/net/can/slcan/slcan.h
--
2.32.0
In case the bitrate has been set via ip tool, this patch changes the
driver to send the close command ("C\r") to the adapter.
Signed-off-by: Dario Binacchi <[email protected]>
---
Changes in v2:
- Improve the commit message.
drivers/net/can/slcan.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/drivers/net/can/slcan.c b/drivers/net/can/slcan.c
index ec682715ce99..0722e7820564 100644
--- a/drivers/net/can/slcan.c
+++ b/drivers/net/can/slcan.c
@@ -430,9 +430,20 @@ static int slcan_transmit_cmd(struct slcan *sl, const unsigned char *cmd)
static int slc_close(struct net_device *dev)
{
struct slcan *sl = netdev_priv(dev);
+ int err;
spin_lock_bh(&sl->lock);
if (sl->tty) {
+ if (sl->can.bittiming.bitrate &&
+ sl->can.bittiming.bitrate != -1) {
+ spin_unlock_bh(&sl->lock);
+ err = slcan_transmit_cmd(sl, "C\r");
+ spin_lock_bh(&sl->lock);
+ if (err)
+ netdev_warn(dev,
+ "failed to send close command 'C\\r'\n");
+ }
+
/* TTY discipline is running. */
clear_bit(TTY_DO_WRITE_WAKEUP, &sl->tty->flags);
}
--
2.32.0
It is used successfully by most (if not all) CAN device drivers. It
allows to remove replicated code.
Signed-off-by: Dario Binacchi <[email protected]>
---
Changes in v2:
- Put the data into the allocated skb directly instead of first
filling the "cf" on the stack and then doing a memcpy().
drivers/net/can/slcan.c | 69 +++++++++++++++++++----------------------
1 file changed, 32 insertions(+), 37 deletions(-)
diff --git a/drivers/net/can/slcan.c b/drivers/net/can/slcan.c
index 6162a9c21672..5d87e25e2285 100644
--- a/drivers/net/can/slcan.c
+++ b/drivers/net/can/slcan.c
@@ -54,6 +54,7 @@
#include <linux/kernel.h>
#include <linux/workqueue.h>
#include <linux/can.h>
+#include <linux/can/dev.h>
#include <linux/can/skb.h>
#include <linux/can/can-ml.h>
@@ -143,85 +144,79 @@ static struct net_device **slcan_devs;
static void slc_bump(struct slcan *sl)
{
struct sk_buff *skb;
- struct can_frame cf;
+ struct can_frame *cf;
int i, tmp;
u32 tmpid;
char *cmd = sl->rbuff;
- memset(&cf, 0, sizeof(cf));
+ skb = alloc_can_skb(sl->dev, &cf);
+ if (unlikely(!skb)) {
+ sl->dev->stats.rx_dropped++;
+ return;
+ }
switch (*cmd) {
case 'r':
- cf.can_id = CAN_RTR_FLAG;
+ cf->can_id = CAN_RTR_FLAG;
fallthrough;
case 't':
/* store dlc ASCII value and terminate SFF CAN ID string */
- cf.len = sl->rbuff[SLC_CMD_LEN + SLC_SFF_ID_LEN];
+ cf->len = sl->rbuff[SLC_CMD_LEN + SLC_SFF_ID_LEN];
sl->rbuff[SLC_CMD_LEN + SLC_SFF_ID_LEN] = 0;
/* point to payload data behind the dlc */
cmd += SLC_CMD_LEN + SLC_SFF_ID_LEN + 1;
break;
case 'R':
- cf.can_id = CAN_RTR_FLAG;
+ cf->can_id = CAN_RTR_FLAG;
fallthrough;
case 'T':
- cf.can_id |= CAN_EFF_FLAG;
+ cf->can_id |= CAN_EFF_FLAG;
/* store dlc ASCII value and terminate EFF CAN ID string */
- cf.len = sl->rbuff[SLC_CMD_LEN + SLC_EFF_ID_LEN];
+ cf->len = sl->rbuff[SLC_CMD_LEN + SLC_EFF_ID_LEN];
sl->rbuff[SLC_CMD_LEN + SLC_EFF_ID_LEN] = 0;
/* point to payload data behind the dlc */
cmd += SLC_CMD_LEN + SLC_EFF_ID_LEN + 1;
break;
default:
- return;
+ goto decode_failed;
}
if (kstrtou32(sl->rbuff + SLC_CMD_LEN, 16, &tmpid))
- return;
+ goto decode_failed;
- cf.can_id |= tmpid;
+ cf->can_id |= tmpid;
/* get len from sanitized ASCII value */
- if (cf.len >= '0' && cf.len < '9')
- cf.len -= '0';
+ if (cf->len >= '0' && cf->len < '9')
+ cf->len -= '0';
else
- return;
+ goto decode_failed;
/* RTR frames may have a dlc > 0 but they never have any data bytes */
- if (!(cf.can_id & CAN_RTR_FLAG)) {
- for (i = 0; i < cf.len; i++) {
+ if (!(cf->can_id & CAN_RTR_FLAG)) {
+ for (i = 0; i < cf->len; i++) {
tmp = hex_to_bin(*cmd++);
if (tmp < 0)
- return;
- cf.data[i] = (tmp << 4);
+ goto decode_failed;
+
+ cf->data[i] = (tmp << 4);
tmp = hex_to_bin(*cmd++);
if (tmp < 0)
- return;
- cf.data[i] |= tmp;
+ goto decode_failed;
+
+ cf->data[i] |= tmp;
}
}
- skb = dev_alloc_skb(sizeof(struct can_frame) +
- sizeof(struct can_skb_priv));
- if (!skb)
- return;
-
- skb->dev = sl->dev;
- skb->protocol = htons(ETH_P_CAN);
- skb->pkt_type = PACKET_BROADCAST;
- skb->ip_summed = CHECKSUM_UNNECESSARY;
-
- can_skb_reserve(skb);
- can_skb_prv(skb)->ifindex = sl->dev->ifindex;
- can_skb_prv(skb)->skbcnt = 0;
-
- skb_put_data(skb, &cf, sizeof(struct can_frame));
-
sl->dev->stats.rx_packets++;
- if (!(cf.can_id & CAN_RTR_FLAG))
- sl->dev->stats.rx_bytes += cf.len;
+ if (!(cf->can_id & CAN_RTR_FLAG))
+ sl->dev->stats.rx_bytes += cf->len;
netif_rx(skb);
+ return;
+
+decode_failed:
+ dev_kfree_skb(skb);
}
/* parse tty input stream */
--
2.32.0
In case the bitrate has been set via ip tool, this patch changes the
driver to send the open command ("O\r") to the adapter.
Signed-off-by: Dario Binacchi <[email protected]>
---
Changes in v2:
- Improve the commit message.
drivers/net/can/slcan.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/drivers/net/can/slcan.c b/drivers/net/can/slcan.c
index 8561bcee81ba..ec682715ce99 100644
--- a/drivers/net/can/slcan.c
+++ b/drivers/net/can/slcan.c
@@ -461,8 +461,15 @@ static int slc_open(struct net_device *dev)
* can.bittiming.bitrate is 0, causing open_candev() to fail.
* So let's set to a fake value.
*/
- if (sl->can.bittiming.bitrate == 0)
+ if (sl->can.bittiming.bitrate == 0) {
sl->can.bittiming.bitrate = -1UL;
+ } else {
+ err = slcan_transmit_cmd(sl, "O\r");
+ if (err) {
+ netdev_err(dev, "failed to send open command 'O\\r'\n");
+ return err;
+ }
+ }
err = open_candev(dev);
if (err) {
--
2.32.0
It extends the protocol to receive the adapter CAN state changes
(warning, busoff, etc.) and forward them to the netdev upper levels.
Signed-off-by: Dario Binacchi <[email protected]>
---
Changes in v2:
- Continue error handling even if no skb can be allocated.
drivers/net/can/slcan/slcan-core.c | 66 ++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/drivers/net/can/slcan/slcan-core.c b/drivers/net/can/slcan/slcan-core.c
index aba42e284274..22a261f2477c 100644
--- a/drivers/net/can/slcan/slcan-core.c
+++ b/drivers/net/can/slcan/slcan-core.c
@@ -78,6 +78,9 @@ MODULE_PARM_DESC(maxdev, "Maximum number of slcan interfaces");
#define SLC_CMD_LEN 1
#define SLC_SFF_ID_LEN 3
#define SLC_EFF_ID_LEN 8
+#define SLC_STATE_LEN 1
+#define SLC_STATE_BE_RXCNT_LEN 3
+#define SLC_STATE_BE_TXCNT_LEN 3
struct slcan {
struct can_priv can;
@@ -170,6 +173,67 @@ int slcan_enable_err_rst_on_open(struct net_device *ndev, bool on)
* STANDARD SLCAN DECAPSULATION *
************************************************************************/
+static void slc_bump_state(struct slcan *sl)
+{
+ struct net_device *dev = sl->dev;
+ struct sk_buff *skb;
+ struct can_frame *cf;
+ char *cmd = sl->rbuff;
+ u32 rxerr, txerr;
+ enum can_state state, rx_state, tx_state;
+
+ if (*cmd != 's')
+ return;
+
+ cmd += SLC_CMD_LEN;
+ switch (*cmd) {
+ case 'a':
+ state = CAN_STATE_ERROR_ACTIVE;
+ break;
+ case 'w':
+ state = CAN_STATE_ERROR_WARNING;
+ break;
+ case 'p':
+ state = CAN_STATE_ERROR_PASSIVE;
+ break;
+ case 'f':
+ state = CAN_STATE_BUS_OFF;
+ break;
+ default:
+ return;
+ }
+
+ if (state == sl->can.state)
+ return;
+
+ cmd += SLC_STATE_BE_RXCNT_LEN + 1;
+ cmd[SLC_STATE_BE_TXCNT_LEN] = 0;
+ if (kstrtou32(cmd, 10, &txerr))
+ return;
+
+ *cmd = 0;
+ cmd -= SLC_STATE_BE_RXCNT_LEN;
+ if (kstrtou32(cmd, 10, &rxerr))
+ return;
+
+ skb = alloc_can_err_skb(dev, &cf);
+
+ if (skb) {
+ cf->data[6] = txerr;
+ cf->data[7] = rxerr;
+ }
+
+ tx_state = txerr >= rxerr ? state : 0;
+ rx_state = txerr <= rxerr ? state : 0;
+ can_change_state(dev, skb ? cf : NULL, tx_state, rx_state);
+
+ if (state == CAN_STATE_BUS_OFF)
+ can_bus_off(dev);
+
+ if (skb)
+ netif_rx(skb);
+}
+
static void slc_bump_err(struct slcan *sl)
{
struct net_device *dev = sl->dev;
@@ -372,6 +436,8 @@ static void slc_bump(struct slcan *sl)
return slc_bump_frame(sl);
case 'e':
return slc_bump_err(sl);
+ case 's':
+ return slc_bump_state(sl);
default:
return;
}
--
2.32.0
It extends the protocol to receive the adapter CAN communication errors
and forward them to the netdev upper levels.
Signed-off-by: Dario Binacchi <[email protected]>
---
Changes in v2:
- Protect decoding against the case the len value is longer than the
received data.
- Continue error handling even if no skb can be allocated.
drivers/net/can/slcan/slcan-core.c | 130 ++++++++++++++++++++++++++++-
1 file changed, 129 insertions(+), 1 deletion(-)
diff --git a/drivers/net/can/slcan/slcan-core.c b/drivers/net/can/slcan/slcan-core.c
index 038ce7c25d42..aba42e284274 100644
--- a/drivers/net/can/slcan/slcan-core.c
+++ b/drivers/net/can/slcan/slcan-core.c
@@ -170,8 +170,118 @@ int slcan_enable_err_rst_on_open(struct net_device *ndev, bool on)
* STANDARD SLCAN DECAPSULATION *
************************************************************************/
+static void slc_bump_err(struct slcan *sl)
+{
+ struct net_device *dev = sl->dev;
+ struct sk_buff *skb;
+ struct can_frame *cf;
+ char *cmd = sl->rbuff;
+ bool rx_errors = false, tx_errors = false;
+ int i, len;
+
+ if (*cmd != 'e')
+ return;
+
+ cmd += SLC_CMD_LEN;
+ /* get len from sanitized ASCII value */
+ len = *cmd++;
+ if (len >= '0' && len < '9')
+ len -= '0';
+ else
+ return;
+
+ skb = alloc_can_err_skb(dev, &cf);
+
+ if (skb)
+ cf->can_id |= CAN_ERR_PROT | CAN_ERR_BUSERROR;
+
+ for (i = 0; i < len; i++, cmd++) {
+ switch (*cmd) {
+ case 'a':
+ netdev_dbg(dev, "ACK error\n");
+ tx_errors = true;
+ if (skb) {
+ cf->can_id |= CAN_ERR_ACK;
+ cf->data[3] = CAN_ERR_PROT_LOC_ACK;
+ }
+
+ break;
+ case 'b':
+ netdev_dbg(dev, "Bit0 error\n");
+ tx_errors = true;
+ if (skb)
+ cf->data[2] |= CAN_ERR_PROT_BIT0;
+
+ break;
+ case 'B':
+ netdev_dbg(dev, "Bit1 error\n");
+ tx_errors = true;
+ if (skb)
+ cf->data[2] |= CAN_ERR_PROT_BIT1;
+
+ break;
+ case 'c':
+ netdev_dbg(dev, "CRC error\n");
+ rx_errors = true;
+ if (skb) {
+ cf->data[2] |= CAN_ERR_PROT_BIT;
+ cf->data[3] = CAN_ERR_PROT_LOC_CRC_SEQ;
+ }
+
+ break;
+ case 'f':
+ netdev_dbg(dev, "Form Error\n");
+ rx_errors = true;
+ if (skb)
+ cf->data[2] |= CAN_ERR_PROT_FORM;
+
+ break;
+ case 'o':
+ netdev_dbg(dev, "Rx overrun error\n");
+ dev->stats.rx_over_errors++;
+ dev->stats.rx_errors++;
+ if (skb) {
+ cf->can_id |= CAN_ERR_CRTL;
+ cf->data[1] = CAN_ERR_CRTL_RX_OVERFLOW;
+ }
+
+ break;
+ case 'O':
+ netdev_dbg(dev, "Tx overrun error\n");
+ dev->stats.tx_errors++;
+ if (skb) {
+ cf->can_id |= CAN_ERR_CRTL;
+ cf->data[1] = CAN_ERR_CRTL_TX_OVERFLOW;
+ }
+
+ break;
+ case 's':
+ netdev_dbg(dev, "Stuff error\n");
+ rx_errors = true;
+ if (skb)
+ cf->data[2] |= CAN_ERR_PROT_STUFF;
+
+ break;
+ default:
+ if (skb)
+ dev_kfree_skb(skb);
+
+ return;
+ }
+ }
+
+ if (rx_errors)
+ dev->stats.rx_errors++;
+
+ if (tx_errors)
+ dev->stats.tx_errors++;
+
+ if (skb)
+ netif_rx(skb);
+}
+
/* Send one completely decapsulated can_frame to the network layer */
-static void slc_bump(struct slcan *sl)
+static void slc_bump_frame(struct slcan *sl)
{
struct sk_buff *skb;
struct can_frame *cf;
@@ -249,6 +359,24 @@ static void slc_bump(struct slcan *sl)
dev_kfree_skb(skb);
}
+static void slc_bump(struct slcan *sl)
+{
+ switch (sl->rbuff[0]) {
+ case 'r':
+ fallthrough;
+ case 't':
+ fallthrough;
+ case 'R':
+ fallthrough;
+ case 'T':
+ return slc_bump_frame(sl);
+ case 'e':
+ return slc_bump_err(sl);
+ default:
+ return;
+ }
+}
+
/* parse tty input stream */
static void slcan_unesc(struct slcan *sl, unsigned char s)
{
--
2.32.0
On 08.06.2022 18:51:06, Dario Binacchi wrote:
> It is used successfully by most (if not all) CAN device drivers. It
> allows to remove replicated code.
>
> Signed-off-by: Dario Binacchi <[email protected]>
>
> ---
>
> Changes in v2:
> - Put the data into the allocated skb directly instead of first
> filling the "cf" on the stack and then doing a memcpy().
>
> drivers/net/can/slcan.c | 69 +++++++++++++++++++----------------------
> 1 file changed, 32 insertions(+), 37 deletions(-)
>
> diff --git a/drivers/net/can/slcan.c b/drivers/net/can/slcan.c
> index 6162a9c21672..5d87e25e2285 100644
> --- a/drivers/net/can/slcan.c
> +++ b/drivers/net/can/slcan.c
> @@ -54,6 +54,7 @@
> #include <linux/kernel.h>
> #include <linux/workqueue.h>
> #include <linux/can.h>
> +#include <linux/can/dev.h>
> #include <linux/can/skb.h>
> #include <linux/can/can-ml.h>
>
> @@ -143,85 +144,79 @@ static struct net_device **slcan_devs;
> static void slc_bump(struct slcan *sl)
> {
> struct sk_buff *skb;
> - struct can_frame cf;
> + struct can_frame *cf;
> int i, tmp;
> u32 tmpid;
> char *cmd = sl->rbuff;
>
> - memset(&cf, 0, sizeof(cf));
> + skb = alloc_can_skb(sl->dev, &cf);
> + if (unlikely(!skb)) {
> + sl->dev->stats.rx_dropped++;
> + return;
> + }
>
> switch (*cmd) {
> case 'r':
> - cf.can_id = CAN_RTR_FLAG;
> + cf->can_id = CAN_RTR_FLAG;
> fallthrough;
> case 't':
> /* store dlc ASCII value and terminate SFF CAN ID string */
> - cf.len = sl->rbuff[SLC_CMD_LEN + SLC_SFF_ID_LEN];
> + cf->len = sl->rbuff[SLC_CMD_LEN + SLC_SFF_ID_LEN];
> sl->rbuff[SLC_CMD_LEN + SLC_SFF_ID_LEN] = 0;
> /* point to payload data behind the dlc */
> cmd += SLC_CMD_LEN + SLC_SFF_ID_LEN + 1;
> break;
> case 'R':
> - cf.can_id = CAN_RTR_FLAG;
> + cf->can_id = CAN_RTR_FLAG;
> fallthrough;
> case 'T':
> - cf.can_id |= CAN_EFF_FLAG;
> + cf->can_id |= CAN_EFF_FLAG;
> /* store dlc ASCII value and terminate EFF CAN ID string */
> - cf.len = sl->rbuff[SLC_CMD_LEN + SLC_EFF_ID_LEN];
> + cf->len = sl->rbuff[SLC_CMD_LEN + SLC_EFF_ID_LEN];
> sl->rbuff[SLC_CMD_LEN + SLC_EFF_ID_LEN] = 0;
> /* point to payload data behind the dlc */
> cmd += SLC_CMD_LEN + SLC_EFF_ID_LEN + 1;
> break;
> default:
> - return;
> + goto decode_failed;
> }
>
> if (kstrtou32(sl->rbuff + SLC_CMD_LEN, 16, &tmpid))
> - return;
> + goto decode_failed;
>
> - cf.can_id |= tmpid;
> + cf->can_id |= tmpid;
>
> /* get len from sanitized ASCII value */
> - if (cf.len >= '0' && cf.len < '9')
> - cf.len -= '0';
> + if (cf->len >= '0' && cf->len < '9')
> + cf->len -= '0';
> else
> - return;
> + goto decode_failed;
>
> /* RTR frames may have a dlc > 0 but they never have any data bytes */
> - if (!(cf.can_id & CAN_RTR_FLAG)) {
> - for (i = 0; i < cf.len; i++) {
> + if (!(cf->can_id & CAN_RTR_FLAG)) {
> + for (i = 0; i < cf->len; i++) {
> tmp = hex_to_bin(*cmd++);
> if (tmp < 0)
> - return;
> - cf.data[i] = (tmp << 4);
> + goto decode_failed;
> +
> + cf->data[i] = (tmp << 4);
> tmp = hex_to_bin(*cmd++);
> if (tmp < 0)
> - return;
> - cf.data[i] |= tmp;
> + goto decode_failed;
> +
> + cf->data[i] |= tmp;
> }
> }
>
> - skb = dev_alloc_skb(sizeof(struct can_frame) +
> - sizeof(struct can_skb_priv));
> - if (!skb)
> - return;
> -
> - skb->dev = sl->dev;
> - skb->protocol = htons(ETH_P_CAN);
> - skb->pkt_type = PACKET_BROADCAST;
> - skb->ip_summed = CHECKSUM_UNNECESSARY;
> -
> - can_skb_reserve(skb);
> - can_skb_prv(skb)->ifindex = sl->dev->ifindex;
> - can_skb_prv(skb)->skbcnt = 0;
> -
> - skb_put_data(skb, &cf, sizeof(struct can_frame));
> -
> sl->dev->stats.rx_packets++;
> - if (!(cf.can_id & CAN_RTR_FLAG))
> - sl->dev->stats.rx_bytes += cf.len;
> + if (!(cf->can_id & CAN_RTR_FLAG))
> + sl->dev->stats.rx_bytes += cf->len;
>
> netif_rx(skb);
> + return;
> +
> +decode_failed:
> + dev_kfree_skb(skb);
Can you increase an error counter in this situation, too?
Marc
> }
>
> /* parse tty input stream */
> --
> 2.32.0
>
>
--
Pengutronix e.K. | Marc Kleine-Budde |
Embedded Linux | https://www.pengutronix.de |
Vertretung West/Dortmund | Phone: +49-231-2826-924 |
Amtsgericht Hildesheim, HRA 2686 | Fax: +49-5121-206917-5555 |