diff options
Diffstat (limited to 'drivers/net/usb/kalmia.c')
-rw-r--r-- | drivers/net/usb/kalmia.c | 384 |
1 files changed, 384 insertions, 0 deletions
diff --git a/drivers/net/usb/kalmia.c b/drivers/net/usb/kalmia.c new file mode 100644 index 000000000000..d965fb1e013e --- /dev/null +++ b/drivers/net/usb/kalmia.c | |||
@@ -0,0 +1,384 @@ | |||
1 | /* | ||
2 | * USB network interface driver for Samsung Kalmia based LTE USB modem like the | ||
3 | * Samsung GT-B3730 and GT-B3710. | ||
4 | * | ||
5 | * Copyright (C) 2011 Marius Bjoernstad Kotsbak <marius@kotsbak.com> | ||
6 | * | ||
7 | * Sponsored by Quicklink Video Distribution Services Ltd. | ||
8 | * | ||
9 | * Based on the cdc_eem module. | ||
10 | * | ||
11 | * This program is free software; you can redistribute it and/or modify | ||
12 | * it under the terms of the GNU General Public License as published by | ||
13 | * the Free Software Foundation; either version 2 of the License, or | ||
14 | * (at your option) any later version. | ||
15 | */ | ||
16 | |||
17 | #include <linux/module.h> | ||
18 | #include <linux/init.h> | ||
19 | #include <linux/netdevice.h> | ||
20 | #include <linux/etherdevice.h> | ||
21 | #include <linux/ctype.h> | ||
22 | #include <linux/ethtool.h> | ||
23 | #include <linux/workqueue.h> | ||
24 | #include <linux/mii.h> | ||
25 | #include <linux/usb.h> | ||
26 | #include <linux/crc32.h> | ||
27 | #include <linux/usb/cdc.h> | ||
28 | #include <linux/usb/usbnet.h> | ||
29 | #include <linux/gfp.h> | ||
30 | |||
31 | /* | ||
32 | * The Samsung Kalmia based LTE USB modems have a CDC ACM port for modem control | ||
33 | * handled by the "option" module and an ethernet data port handled by this | ||
34 | * module. | ||
35 | * | ||
36 | * The stick must first be switched into modem mode by usb_modeswitch | ||
37 | * or similar tool. Then the modem gets sent two initialization packets by | ||
38 | * this module, which gives the MAC address of the device. User space can then | ||
39 | * connect the modem using AT commands through the ACM port and then use | ||
40 | * DHCP on the network interface exposed by this module. Network packets are | ||
41 | * sent to and from the modem in a proprietary format discovered after watching | ||
42 | * the behavior of the windows driver for the modem. | ||
43 | * | ||
44 | * More information about the use of the modem is available in usb_modeswitch | ||
45 | * forum and the project page: | ||
46 | * | ||
47 | * http://www.draisberghof.de/usb_modeswitch/bb/viewtopic.php?t=465 | ||
48 | * https://github.com/mkotsbak/Samsung-GT-B3730-linux-driver | ||
49 | */ | ||
50 | |||
51 | /* #define DEBUG */ | ||
52 | /* #define VERBOSE */ | ||
53 | |||
54 | #define KALMIA_HEADER_LENGTH 6 | ||
55 | #define KALMIA_ALIGN_SIZE 4 | ||
56 | #define KALMIA_USB_TIMEOUT 10000 | ||
57 | |||
58 | /*-------------------------------------------------------------------------*/ | ||
59 | |||
60 | static int | ||
61 | kalmia_send_init_packet(struct usbnet *dev, u8 *init_msg, u8 init_msg_len, | ||
62 | u8 *buffer, u8 expected_len) | ||
63 | { | ||
64 | int act_len; | ||
65 | int status; | ||
66 | |||
67 | netdev_dbg(dev->net, "Sending init packet"); | ||
68 | |||
69 | status = usb_bulk_msg(dev->udev, usb_sndbulkpipe(dev->udev, 0x02), | ||
70 | init_msg, init_msg_len, &act_len, KALMIA_USB_TIMEOUT); | ||
71 | if (status != 0) { | ||
72 | netdev_err(dev->net, | ||
73 | "Error sending init packet. Status %i, length %i\n", | ||
74 | status, act_len); | ||
75 | return status; | ||
76 | } | ||
77 | else if (act_len != init_msg_len) { | ||
78 | netdev_err(dev->net, | ||
79 | "Did not send all of init packet. Bytes sent: %i", | ||
80 | act_len); | ||
81 | } | ||
82 | else { | ||
83 | netdev_dbg(dev->net, "Successfully sent init packet."); | ||
84 | } | ||
85 | |||
86 | status = usb_bulk_msg(dev->udev, usb_rcvbulkpipe(dev->udev, 0x81), | ||
87 | buffer, expected_len, &act_len, KALMIA_USB_TIMEOUT); | ||
88 | |||
89 | if (status != 0) | ||
90 | netdev_err(dev->net, | ||
91 | "Error receiving init result. Status %i, length %i\n", | ||
92 | status, act_len); | ||
93 | else if (act_len != expected_len) | ||
94 | netdev_err(dev->net, "Unexpected init result length: %i\n", | ||
95 | act_len); | ||
96 | |||
97 | return status; | ||
98 | } | ||
99 | |||
100 | static int | ||
101 | kalmia_init_and_get_ethernet_addr(struct usbnet *dev, u8 *ethernet_addr) | ||
102 | { | ||
103 | char init_msg_1[] = | ||
104 | { 0x57, 0x50, 0x04, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, | ||
105 | 0x00, 0x00 }; | ||
106 | char init_msg_2[] = | ||
107 | { 0x57, 0x50, 0x04, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xf4, | ||
108 | 0x00, 0x00 }; | ||
109 | char receive_buf[28]; | ||
110 | int status; | ||
111 | |||
112 | status = kalmia_send_init_packet(dev, init_msg_1, sizeof(init_msg_1) | ||
113 | / sizeof(init_msg_1[0]), receive_buf, 24); | ||
114 | if (status != 0) | ||
115 | return status; | ||
116 | |||
117 | status = kalmia_send_init_packet(dev, init_msg_2, sizeof(init_msg_2) | ||
118 | / sizeof(init_msg_2[0]), receive_buf, 28); | ||
119 | if (status != 0) | ||
120 | return status; | ||
121 | |||
122 | memcpy(ethernet_addr, receive_buf + 10, ETH_ALEN); | ||
123 | |||
124 | return status; | ||
125 | } | ||
126 | |||
127 | static int | ||
128 | kalmia_bind(struct usbnet *dev, struct usb_interface *intf) | ||
129 | { | ||
130 | u8 status; | ||
131 | u8 ethernet_addr[ETH_ALEN]; | ||
132 | |||
133 | /* Don't bind to AT command interface */ | ||
134 | if (intf->cur_altsetting->desc.bInterfaceClass != USB_CLASS_VENDOR_SPEC) | ||
135 | return -EINVAL; | ||
136 | |||
137 | dev->in = usb_rcvbulkpipe(dev->udev, 0x81 & USB_ENDPOINT_NUMBER_MASK); | ||
138 | dev->out = usb_sndbulkpipe(dev->udev, 0x02 & USB_ENDPOINT_NUMBER_MASK); | ||
139 | dev->status = NULL; | ||
140 | |||
141 | dev->net->hard_header_len += KALMIA_HEADER_LENGTH; | ||
142 | dev->hard_mtu = 1400; | ||
143 | dev->rx_urb_size = dev->hard_mtu * 10; // Found as optimal after testing | ||
144 | |||
145 | status = kalmia_init_and_get_ethernet_addr(dev, ethernet_addr); | ||
146 | |||
147 | if (status < 0) { | ||
148 | usb_set_intfdata(intf, NULL); | ||
149 | usb_driver_release_interface(driver_of(intf), intf); | ||
150 | return status; | ||
151 | } | ||
152 | |||
153 | memcpy(dev->net->dev_addr, ethernet_addr, ETH_ALEN); | ||
154 | memcpy(dev->net->perm_addr, ethernet_addr, ETH_ALEN); | ||
155 | |||
156 | return status; | ||
157 | } | ||
158 | |||
159 | static struct sk_buff * | ||
160 | kalmia_tx_fixup(struct usbnet *dev, struct sk_buff *skb, gfp_t flags) | ||
161 | { | ||
162 | struct sk_buff *skb2 = NULL; | ||
163 | u16 content_len; | ||
164 | unsigned char *header_start; | ||
165 | unsigned char ether_type_1, ether_type_2; | ||
166 | u8 remainder, padlen = 0; | ||
167 | |||
168 | if (!skb_cloned(skb)) { | ||
169 | int headroom = skb_headroom(skb); | ||
170 | int tailroom = skb_tailroom(skb); | ||
171 | |||
172 | if ((tailroom >= KALMIA_ALIGN_SIZE) && (headroom | ||
173 | >= KALMIA_HEADER_LENGTH)) | ||
174 | goto done; | ||
175 | |||
176 | if ((headroom + tailroom) > (KALMIA_HEADER_LENGTH | ||
177 | + KALMIA_ALIGN_SIZE)) { | ||
178 | skb->data = memmove(skb->head + KALMIA_HEADER_LENGTH, | ||
179 | skb->data, skb->len); | ||
180 | skb_set_tail_pointer(skb, skb->len); | ||
181 | goto done; | ||
182 | } | ||
183 | } | ||
184 | |||
185 | skb2 = skb_copy_expand(skb, KALMIA_HEADER_LENGTH, | ||
186 | KALMIA_ALIGN_SIZE, flags); | ||
187 | if (!skb2) | ||
188 | return NULL; | ||
189 | |||
190 | dev_kfree_skb_any(skb); | ||
191 | skb = skb2; | ||
192 | |||
193 | done: header_start = skb_push(skb, KALMIA_HEADER_LENGTH); | ||
194 | ether_type_1 = header_start[KALMIA_HEADER_LENGTH + 12]; | ||
195 | ether_type_2 = header_start[KALMIA_HEADER_LENGTH + 13]; | ||
196 | |||
197 | netdev_dbg(dev->net, "Sending etherType: %02x%02x", ether_type_1, | ||
198 | ether_type_2); | ||
199 | |||
200 | /* According to empiric data for data packages */ | ||
201 | header_start[0] = 0x57; | ||
202 | header_start[1] = 0x44; | ||
203 | content_len = skb->len - KALMIA_HEADER_LENGTH; | ||
204 | header_start[2] = (content_len & 0xff); /* low byte */ | ||
205 | header_start[3] = (content_len >> 8); /* high byte */ | ||
206 | |||
207 | header_start[4] = ether_type_1; | ||
208 | header_start[5] = ether_type_2; | ||
209 | |||
210 | /* Align to 4 bytes by padding with zeros */ | ||
211 | remainder = skb->len % KALMIA_ALIGN_SIZE; | ||
212 | if (remainder > 0) { | ||
213 | padlen = KALMIA_ALIGN_SIZE - remainder; | ||
214 | memset(skb_put(skb, padlen), 0, padlen); | ||
215 | } | ||
216 | |||
217 | netdev_dbg( | ||
218 | dev->net, | ||
219 | "Sending package with length %i and padding %i. Header: %02x:%02x:%02x:%02x:%02x:%02x.", | ||
220 | content_len, padlen, header_start[0], header_start[1], | ||
221 | header_start[2], header_start[3], header_start[4], | ||
222 | header_start[5]); | ||
223 | |||
224 | return skb; | ||
225 | } | ||
226 | |||
227 | static int | ||
228 | kalmia_rx_fixup(struct usbnet *dev, struct sk_buff *skb) | ||
229 | { | ||
230 | /* | ||
231 | * Our task here is to strip off framing, leaving skb with one | ||
232 | * data frame for the usbnet framework code to process. | ||
233 | */ | ||
234 | const u8 HEADER_END_OF_USB_PACKET[] = | ||
235 | { 0x57, 0x5a, 0x00, 0x00, 0x08, 0x00 }; | ||
236 | const u8 EXPECTED_UNKNOWN_HEADER_1[] = | ||
237 | { 0x57, 0x43, 0x1e, 0x00, 0x15, 0x02 }; | ||
238 | const u8 EXPECTED_UNKNOWN_HEADER_2[] = | ||
239 | { 0x57, 0x50, 0x0e, 0x00, 0x00, 0x00 }; | ||
240 | u8 i = 0; | ||
241 | |||
242 | /* incomplete header? */ | ||
243 | if (skb->len < KALMIA_HEADER_LENGTH) | ||
244 | return 0; | ||
245 | |||
246 | do { | ||
247 | struct sk_buff *skb2 = NULL; | ||
248 | u8 *header_start; | ||
249 | u16 usb_packet_length, ether_packet_length; | ||
250 | int is_last; | ||
251 | |||
252 | header_start = skb->data; | ||
253 | |||
254 | if (unlikely(header_start[0] != 0x57 || header_start[1] != 0x44)) { | ||
255 | if (!memcmp(header_start, EXPECTED_UNKNOWN_HEADER_1, | ||
256 | sizeof(EXPECTED_UNKNOWN_HEADER_1)) || !memcmp( | ||
257 | header_start, EXPECTED_UNKNOWN_HEADER_2, | ||
258 | sizeof(EXPECTED_UNKNOWN_HEADER_2))) { | ||
259 | netdev_dbg( | ||
260 | dev->net, | ||
261 | "Received expected unknown frame header: %02x:%02x:%02x:%02x:%02x:%02x. Package length: %i\n", | ||
262 | header_start[0], header_start[1], | ||
263 | header_start[2], header_start[3], | ||
264 | header_start[4], header_start[5], | ||
265 | skb->len - KALMIA_HEADER_LENGTH); | ||
266 | } | ||
267 | else { | ||
268 | netdev_err( | ||
269 | dev->net, | ||
270 | "Received unknown frame header: %02x:%02x:%02x:%02x:%02x:%02x. Package length: %i\n", | ||
271 | header_start[0], header_start[1], | ||
272 | header_start[2], header_start[3], | ||
273 | header_start[4], header_start[5], | ||
274 | skb->len - KALMIA_HEADER_LENGTH); | ||
275 | return 0; | ||
276 | } | ||
277 | } | ||
278 | else | ||
279 | netdev_dbg( | ||
280 | dev->net, | ||
281 | "Received header: %02x:%02x:%02x:%02x:%02x:%02x. Package length: %i\n", | ||
282 | header_start[0], header_start[1], header_start[2], | ||
283 | header_start[3], header_start[4], header_start[5], | ||
284 | skb->len - KALMIA_HEADER_LENGTH); | ||
285 | |||
286 | /* subtract start header and end header */ | ||
287 | usb_packet_length = skb->len - (2 * KALMIA_HEADER_LENGTH); | ||
288 | ether_packet_length = header_start[2] + (header_start[3] << 8); | ||
289 | skb_pull(skb, KALMIA_HEADER_LENGTH); | ||
290 | |||
291 | /* Some small packets misses end marker */ | ||
292 | if (usb_packet_length < ether_packet_length) { | ||
293 | ether_packet_length = usb_packet_length | ||
294 | + KALMIA_HEADER_LENGTH; | ||
295 | is_last = true; | ||
296 | } | ||
297 | else { | ||
298 | netdev_dbg(dev->net, "Correct package length #%i", i | ||
299 | + 1); | ||
300 | |||
301 | is_last = (memcmp(skb->data + ether_packet_length, | ||
302 | HEADER_END_OF_USB_PACKET, | ||
303 | sizeof(HEADER_END_OF_USB_PACKET)) == 0); | ||
304 | if (!is_last) { | ||
305 | header_start = skb->data + ether_packet_length; | ||
306 | netdev_dbg( | ||
307 | dev->net, | ||
308 | "End header: %02x:%02x:%02x:%02x:%02x:%02x. Package length: %i\n", | ||
309 | header_start[0], header_start[1], | ||
310 | header_start[2], header_start[3], | ||
311 | header_start[4], header_start[5], | ||
312 | skb->len - KALMIA_HEADER_LENGTH); | ||
313 | } | ||
314 | } | ||
315 | |||
316 | if (is_last) { | ||
317 | skb2 = skb; | ||
318 | } | ||
319 | else { | ||
320 | skb2 = skb_clone(skb, GFP_ATOMIC); | ||
321 | if (unlikely(!skb2)) | ||
322 | return 0; | ||
323 | } | ||
324 | |||
325 | skb_trim(skb2, ether_packet_length); | ||
326 | |||
327 | if (is_last) { | ||
328 | return 1; | ||
329 | } | ||
330 | else { | ||
331 | usbnet_skb_return(dev, skb2); | ||
332 | skb_pull(skb, ether_packet_length); | ||
333 | } | ||
334 | |||
335 | i++; | ||
336 | } | ||
337 | while (skb->len); | ||
338 | |||
339 | return 1; | ||
340 | } | ||
341 | |||
342 | static const struct driver_info kalmia_info = { | ||
343 | .description = "Samsung Kalmia LTE USB dongle", | ||
344 | .flags = FLAG_WWAN, | ||
345 | .bind = kalmia_bind, | ||
346 | .rx_fixup = kalmia_rx_fixup, | ||
347 | .tx_fixup = kalmia_tx_fixup | ||
348 | }; | ||
349 | |||
350 | /*-------------------------------------------------------------------------*/ | ||
351 | |||
352 | static const struct usb_device_id products[] = { | ||
353 | /* The unswitched USB ID, to get the module auto loaded: */ | ||
354 | { USB_DEVICE(0x04e8, 0x689a) }, | ||
355 | /* The stick swithed into modem (by e.g. usb_modeswitch): */ | ||
356 | { USB_DEVICE(0x04e8, 0x6889), | ||
357 | .driver_info = (unsigned long) &kalmia_info, }, | ||
358 | { /* EMPTY == end of list */} }; | ||
359 | MODULE_DEVICE_TABLE( usb, products); | ||
360 | |||
361 | static struct usb_driver kalmia_driver = { | ||
362 | .name = "kalmia", | ||
363 | .id_table = products, | ||
364 | .probe = usbnet_probe, | ||
365 | .disconnect = usbnet_disconnect, | ||
366 | .suspend = usbnet_suspend, | ||
367 | .resume = usbnet_resume | ||
368 | }; | ||
369 | |||
370 | static int __init kalmia_init(void) | ||
371 | { | ||
372 | return usb_register(&kalmia_driver); | ||
373 | } | ||
374 | module_init( kalmia_init); | ||
375 | |||
376 | static void __exit kalmia_exit(void) | ||
377 | { | ||
378 | usb_deregister(&kalmia_driver); | ||
379 | } | ||
380 | module_exit( kalmia_exit); | ||
381 | |||
382 | MODULE_AUTHOR("Marius Bjoernstad Kotsbak <marius@kotsbak.com>"); | ||
383 | MODULE_DESCRIPTION("Samsung Kalmia USB network driver"); | ||
384 | MODULE_LICENSE("GPL"); | ||