add basics of the kernel driver :0
This commit is contained in:
parent
0f4566d0e7
commit
f9eb86b60e
|
@ -434,7 +434,7 @@ enum itu_status i2ctu_dev_write(enum ki2c_flags flags, enum itu_command startsto
|
|||
return ITU_STATUS_ADDR_ACK;
|
||||
} else*/
|
||||
{
|
||||
int rv = i2cex_write_timeout_us(PINOUT_I2C_DEV, addr, bit10, buf, len, nostop, 1000 * 1000);
|
||||
int rv = i2cex_write_timeout_us(PINOUT_I2C_DEV, addr, bit10, buf, len, nostop, 400 * 1000);
|
||||
if (rv < 0 || (size_t)rv < len) return ITU_STATUS_ADDR_NAK;
|
||||
return ITU_STATUS_ADDR_ACK;
|
||||
}
|
||||
|
@ -452,7 +452,7 @@ enum itu_status i2ctu_dev_read(enum ki2c_flags flags, enum itu_command startstop
|
|||
return ITU_STATUS_ADDR_ACK;
|
||||
} else*/
|
||||
{
|
||||
int rv = i2cex_read_timeout_us(PINOUT_I2C_DEV, addr, bit10, buf, len, nostop, 1000 * 1000);
|
||||
int rv = i2cex_read_timeout_us(PINOUT_I2C_DEV, addr, bit10, buf, len, nostop, 400 * 1000);
|
||||
// printf("p le rv=%d buf=%02x ", rv, buf[0]);
|
||||
if (rv < 0 || (size_t)rv < len) return ITU_STATUS_ADDR_NAK;
|
||||
return ITU_STATUS_ADDR_ACK;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import os, struct, sys
|
||||
|
||||
f = os.open(sys.argv[1], os.O_RDWR | os.O_CLOEXEC) # TODO: windows: os.O_BINARY |
|
||||
try:
|
||||
os.write(f, b'\x00') # get version
|
||||
resp = os.read(f, 4) # response: status, paylaod len (should be 2), payload
|
||||
print("stat=%d plen=%d ver=%04x" % (resp[0], resp[1], struct.unpack('<H', resp[2:])[0]))
|
||||
finally:
|
||||
os.close(f)
|
||||
|
||||
#with open(sys.argv[1], 'rb') as f:
|
||||
# f.write(b'\x00') # get version
|
||||
# resp = f.read(4) # response
|
||||
# print("stat=%d plen=%d ver=%04x" % (resp[0], resp[1], struct.unpack('<H', resp[2:])[0]))
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
obj-m := dmj.o dmj-char.o
|
||||
KDIR := /lib/modules/$(shell uname -r)/build
|
||||
PWD := $(shell pwd)
|
||||
|
||||
CFLAGS += -Wall -Wpedantic
|
||||
|
||||
default:
|
||||
$(MAKE) -C $(KDIR) M=$(PWD) modules
|
||||
|
||||
clean:
|
||||
$(RM) -v *.o *.ko *.mod *.mod.c Module.symvers modules.order
|
||||
|
||||
load:
|
||||
sudo insmod ./dmj.ko
|
||||
sudo insmod ./dmj-char.ko
|
||||
|
||||
unload:
|
||||
sudo rmmod dmj-char
|
||||
sudo rmmod dmj
|
|
@ -0,0 +1,321 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Driver for the DapperMime-JTAG USB multitool: character device for userspace
|
||||
* access while the module is loaded
|
||||
*
|
||||
* Copyright (c) sys64738 and haskal
|
||||
*/
|
||||
|
||||
#include <linux/cdev.h>
|
||||
#include <linux/device.h>
|
||||
#include <linux/device/class.h>
|
||||
#include <linux/fs.h>
|
||||
#include <linux/init.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/list.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/types.h>
|
||||
|
||||
#if 0
|
||||
#include <linux/driver/dmj.h>
|
||||
#else
|
||||
#include "dmj.h"
|
||||
#endif
|
||||
|
||||
#define HARDWARE_NAME "DapperMime-JTAG"
|
||||
#define DEVICE_NAME "dmj-char"
|
||||
#define CLASS_NAME "dmj"
|
||||
|
||||
#define DMJ_READ_BUFSIZE 64
|
||||
|
||||
struct dmj_char_dev {
|
||||
struct cdev cdev;
|
||||
struct device *dev;
|
||||
struct platform_device *pdev;
|
||||
int minor;
|
||||
spinlock_t devopen_lock;
|
||||
|
||||
size_t bufpos;
|
||||
uint8_t rdbuf[DMJ_READ_BUFSIZE];
|
||||
};
|
||||
|
||||
static int n_cdevs = 0;
|
||||
static spinlock_t ndevs_lock;
|
||||
|
||||
static int dmj_char_major;
|
||||
static struct class *dmj_char_class;
|
||||
|
||||
static ssize_t dmj_char_read(struct file *file, char *buf, size_t len, loff_t *loff)
|
||||
{
|
||||
int res, bsize;
|
||||
struct dmj_char_dev *dmjch;
|
||||
size_t bpos, todo, done = 0;
|
||||
uint8_t *kbuf;
|
||||
struct device *dev;
|
||||
|
||||
kbuf = kmalloc(len + DMJ_READ_BUFSIZE, GFP_KERNEL);
|
||||
if (!kbuf) return -ENOMEM;
|
||||
|
||||
dmjch = file->private_data;
|
||||
dev = dmjch->dev;
|
||||
todo = len;
|
||||
|
||||
spin_lock(&dmjch->devopen_lock);
|
||||
bpos = dmjch->bufpos;
|
||||
|
||||
/* TODO: ioctl to control this buffering? */
|
||||
if (bpos) { /* data in buffer */
|
||||
if (len <= DMJ_READ_BUFSIZE - bpos) {
|
||||
/* doable in a single copy, no USB xfers needed */
|
||||
memcpy(kbuf, &dmjch->rdbuf[bpos], len);
|
||||
bpos += len;
|
||||
if (bpos == DMJ_READ_BUFSIZE) bpos = 0;
|
||||
|
||||
done += len;
|
||||
todo -= len;
|
||||
} else {
|
||||
/* initial copy stuff */
|
||||
memcpy(kbuf, &dmjch->rdbuf[bpos], DMJ_READ_BUFSIZE - bpos);
|
||||
todo -= DMJ_READ_BUFSIZE - bpos;
|
||||
done += DMJ_READ_BUFSIZE - bpos;
|
||||
bpos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if /*while*/ (todo) { /* TODO: do we want a while here? */
|
||||
bsize = DMJ_READ_BUFSIZE;
|
||||
|
||||
res = dmj_read(dmjch->pdev, 0, &kbuf[done], &bsize);
|
||||
if (res < 0 || bsize < 0) {
|
||||
/* ah snap */
|
||||
spin_unlock(&dmjch->devopen_lock);
|
||||
return res;
|
||||
}
|
||||
|
||||
if ((size_t)bsize > todo) {
|
||||
if ((size_t)bsize > todo + DMJ_READ_BUFSIZE) {
|
||||
/* can't hold all this data, time to bail out... */
|
||||
dev_err(dev, "too much data (%zu B excess), can't buffer, AAAAAA",
|
||||
(size_t)bsize - (todo + DMJ_READ_BUFSIZE));
|
||||
spin_unlock(&dmjch->devopen_lock);
|
||||
BUG(); /* some stuff somewhere will have been corrupted, so, get out while we can */
|
||||
}
|
||||
|
||||
/* stuff for next call */
|
||||
done = todo;
|
||||
bpos = DMJ_READ_BUFSIZE - ((size_t)bsize - todo);
|
||||
memcpy(&dmjch->rdbuf[bpos], &kbuf[done], (size_t)bsize - todo);
|
||||
todo = 0;
|
||||
} else {
|
||||
todo -= (size_t)bsize;
|
||||
done += (size_t)bsize;
|
||||
}
|
||||
}
|
||||
|
||||
dmjch->bufpos = bpos;
|
||||
spin_unlock(&dmjch->devopen_lock);
|
||||
|
||||
res = copy_to_user(buf, kbuf, len);
|
||||
if (res) return (res < 0) ? res : -EFAULT;
|
||||
|
||||
return done;
|
||||
}
|
||||
static ssize_t dmj_char_write(struct file *file, const char *buf, size_t len, loff_t *off)
|
||||
{
|
||||
unsigned long ret;
|
||||
struct dmj_char_dev *dmjch;
|
||||
void *kbuf;
|
||||
|
||||
dmjch = file->private_data;
|
||||
|
||||
kbuf = kmalloc(len, GFP_KERNEL);
|
||||
if (!kbuf) return -ENOMEM;
|
||||
|
||||
ret = copy_from_user(kbuf, buf, len);
|
||||
if (ret) {
|
||||
kfree(kbuf);
|
||||
return (ret < 0) ? ret : -EFAULT;
|
||||
}
|
||||
|
||||
ret = dmj_transfer(dmjch->pdev, -1, 0, kbuf, len, NULL, NULL);
|
||||
if (ret < 0) {
|
||||
kfree(kbuf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
kfree(kbuf);
|
||||
return len;
|
||||
}
|
||||
|
||||
static int dmj_char_open(struct inode *inode, struct file *file)
|
||||
{
|
||||
struct dmj_char_dev *dmjch;
|
||||
int ret;
|
||||
|
||||
dmjch = container_of(inode->i_cdev, struct dmj_char_dev, cdev);
|
||||
|
||||
spin_lock(&dmjch->devopen_lock);
|
||||
ret = dmjch->bufpos;
|
||||
if (~ret == 0) dmjch->bufpos = 0;
|
||||
spin_unlock(&dmjch->devopen_lock);
|
||||
|
||||
if (~ret != 0) return -ETXTBSY; // already open
|
||||
|
||||
file->private_data = dmjch;
|
||||
|
||||
return 0;
|
||||
}
|
||||
static int dmj_char_release(struct inode *inode, struct file *file)
|
||||
{
|
||||
struct dmj_char_dev *dmjch;
|
||||
|
||||
dmjch = container_of(inode->i_cdev, struct dmj_char_dev, cdev);
|
||||
|
||||
spin_lock(&dmjch->devopen_lock);
|
||||
dmjch->bufpos = ~(size_t)0;
|
||||
spin_unlock(&dmjch->devopen_lock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct file_operations dmj_char_fops = {
|
||||
.owner = THIS_MODULE,
|
||||
.llseek = no_llseek,
|
||||
.read = dmj_char_read,
|
||||
.write = dmj_char_write,
|
||||
.unlocked_ioctl = NULL,
|
||||
.open = dmj_char_open,
|
||||
.release = dmj_char_release
|
||||
};
|
||||
|
||||
static int dmj_char_probe(struct platform_device *pdev)
|
||||
{
|
||||
int ret, minor;
|
||||
struct device *device;
|
||||
struct device *pd = &pdev->dev;
|
||||
struct dmj_char_dev *dmjch;
|
||||
|
||||
spin_lock(&ndevs_lock);
|
||||
minor = n_cdevs;
|
||||
++n_cdevs;
|
||||
spin_unlock(&ndevs_lock);
|
||||
|
||||
dev_info(pd, HARDWARE_NAME " /dev entries driver, major=%d, minor=%d\n",
|
||||
dmj_char_major, minor);
|
||||
|
||||
dmjch = kzalloc(sizeof(*dmjch), GFP_KERNEL);
|
||||
if (!dmjch) return -ENOMEM;
|
||||
|
||||
platform_set_drvdata(pdev, dmjch);
|
||||
|
||||
cdev_init(&dmjch->cdev, &dmj_char_fops);
|
||||
ret = cdev_add(&dmjch->cdev, MKDEV(dmj_char_major, minor), 1);
|
||||
if (ret < 0) {
|
||||
dev_err(pd, "failed to create cdev: %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
device = device_create(dmj_char_class, pd, MKDEV(dmj_char_major, minor), dmjch, "dmj-%d", minor);
|
||||
if (IS_ERR(device)) {
|
||||
ret = PTR_ERR(device);
|
||||
dev_err(pd, "failed to create device: %d\n", ret);
|
||||
cdev_del(&dmjch->cdev);
|
||||
return ret;
|
||||
}
|
||||
|
||||
dev_notice(device, "created device /dev/dmj-%d\n", minor);
|
||||
|
||||
dmjch->dev = device;
|
||||
dmjch->minor = minor;
|
||||
dmjch->pdev = pdev;
|
||||
dmjch->bufpos = ~(size_t)0;
|
||||
|
||||
spin_lock_init(&dmjch->devopen_lock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
static int dmj_char_remove(struct platform_device *pdev)
|
||||
{
|
||||
struct dmj_char_dev *dmjch = platform_get_drvdata(pdev);
|
||||
|
||||
device_destroy(dmj_char_class, MKDEV(dmj_char_major, dmjch->minor));
|
||||
cdev_del(&dmjch->cdev);
|
||||
unregister_chrdev(MKDEV(dmj_char_major, dmjch->minor), CLASS_NAME);
|
||||
|
||||
kfree(dmjch);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static struct platform_driver dmj_char_driver = {
|
||||
.driver = {
|
||||
.name = "dmj-char"
|
||||
},
|
||||
.probe = dmj_char_probe,
|
||||
.remove = dmj_char_remove
|
||||
};
|
||||
/*module_platform_driver(dmj_char_driver);*/
|
||||
|
||||
static int __init dmj_char_init(void)
|
||||
{
|
||||
int ret, major;
|
||||
struct class *class;
|
||||
dev_t devid;
|
||||
|
||||
spin_lock_init(&ndevs_lock);
|
||||
|
||||
n_cdevs = 0;
|
||||
|
||||
devid = MKDEV(0,0);
|
||||
ret = alloc_chrdev_region(&devid, 0, 256, DEVICE_NAME);
|
||||
if (ret < 0) {
|
||||
printk(KERN_ERR " failed to register chrdev major number: %d\n", major);
|
||||
return ret;
|
||||
}
|
||||
major = MAJOR(devid);
|
||||
printk(KERN_NOTICE DEVICE_NAME " registered with major number %d\n", major);
|
||||
|
||||
class = class_create(THIS_MODULE, CLASS_NAME);
|
||||
if (IS_ERR(class)) {
|
||||
ret = PTR_ERR(class);
|
||||
printk(KERN_ERR " failed to create class: %d\n", ret);
|
||||
unregister_chrdev(major, DEVICE_NAME); /* TODO: unregister_chrdev_rage */
|
||||
return ret;
|
||||
}
|
||||
printk(KERN_DEBUG DEVICE_NAME " created class\n");
|
||||
|
||||
dmj_char_major = major;
|
||||
dmj_char_class = class;
|
||||
|
||||
platform_driver_register(&dmj_char_driver);
|
||||
|
||||
return 0;
|
||||
}
|
||||
static void __exit dmj_char_exit(void)
|
||||
{
|
||||
platform_driver_unregister(&dmj_char_driver);
|
||||
|
||||
spin_lock(&ndevs_lock);
|
||||
n_cdevs = 0;
|
||||
spin_unlock(&ndevs_lock);
|
||||
|
||||
class_destroy(dmj_char_class);
|
||||
unregister_chrdev(MKDEV(dmj_char_major, 0), CLASS_NAME); /* TODO: unregister_chrdev_rage */
|
||||
|
||||
dmj_char_major = -1;
|
||||
dmj_char_class = NULL;
|
||||
}
|
||||
|
||||
module_init(dmj_char_init);
|
||||
module_exit(dmj_char_exit);
|
||||
|
||||
MODULE_AUTHOR("sys64738 <sys64738@disroot.org>");
|
||||
MODULE_AUTHOR("haskal <haskal@awoo.systems>");
|
||||
MODULE_DESCRIPTION("Character device for the " HARDWARE_NAME " USB multitool");
|
||||
MODULE_LICENSE("GPL v2");
|
||||
MODULE_ALIAS("platform:dmj-char");
|
||||
|
||||
MODULE_DEPEND
|
|
@ -0,0 +1,538 @@
|
|||
// SPDX-License-Identifier: GPL-2.0-only
|
||||
/*
|
||||
* Driver for the DapperMime-JTAG USB multitool: base MFD driver
|
||||
*
|
||||
* Copyright (c) sys64738 and haskal
|
||||
*/
|
||||
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/usb.h>
|
||||
#include <linux/i2c.h>
|
||||
#include <linux/mutex.h>
|
||||
#include <linux/platform_device.h>
|
||||
#include <linux/mfd/core.h>
|
||||
#include <linux/rculist.h>
|
||||
|
||||
#if 0
|
||||
#include <linux/mfd/dmj.h>
|
||||
#else
|
||||
#include "dmj.h"
|
||||
#endif
|
||||
|
||||
#define HARDWARE_NAME "DapperMime-JTAG"
|
||||
#define HARDWARE_NAME_SYMBOLIC "dappermime-jtag"
|
||||
|
||||
#define DMJ_USB_TIMEOUT 500
|
||||
|
||||
#define DMJ_RESP_HDR_SIZE 4
|
||||
|
||||
/* endpoint indices, not addresses */
|
||||
#define DMJ_VND_CFG_EP_OUT 0
|
||||
#define DMJ_VND_CFG_EP_IN 1
|
||||
|
||||
struct dmj_dev {
|
||||
struct usb_device *usb_dev;
|
||||
struct usb_interface *interface;
|
||||
uint8_t ep_in;
|
||||
uint8_t ep_out;
|
||||
|
||||
spinlock_t disconnect_lock;
|
||||
bool disconnect;
|
||||
|
||||
uint8_t dmj_mode, dmj_m1feature;
|
||||
};
|
||||
|
||||
/* USB transfers */
|
||||
|
||||
static void *dmj_prep_buf(int cmd, const void *wbuf, int *wbufsize, gfp_t gfp)
|
||||
{
|
||||
int len;
|
||||
uint8_t *buf;
|
||||
|
||||
if (cmd >= 0 && cmd <= 0xff) {
|
||||
/* send extra cmd byte, and optionally a payload */
|
||||
if (wbufsize && wbuf) len = *wbufsize + 1;
|
||||
else len = 1;
|
||||
|
||||
buf = kmalloc(len, gfp);
|
||||
if (!buf) return NULL;
|
||||
|
||||
buf[0] = (uint8_t)cmd;
|
||||
if (wbuf && *wbufsize)
|
||||
memcpy(&buf[1], wbuf, *wbufsize);
|
||||
} else {
|
||||
/* assuming we have a payload to send, but no (explicit) cmd */
|
||||
len = *wbufsize;
|
||||
buf = kmalloc(len, gfp);
|
||||
if (!buf) return NULL;
|
||||
|
||||
memcpy(buf, wbuf, len);
|
||||
}
|
||||
|
||||
*wbufsize = len;
|
||||
|
||||
return buf;
|
||||
}
|
||||
static int dmj_send_wait(struct dmj_dev *dmj, int cmd, const void *wbuf, int wbufsize)
|
||||
{
|
||||
int ret = 0;
|
||||
int len = wbufsize, actual;
|
||||
void *buf;
|
||||
|
||||
buf = dmj_prep_buf(cmd, wbuf, &len, GFP_KERNEL);
|
||||
if (!buf) return -ENOMEM;
|
||||
|
||||
ret = usb_bulk_msg(dmj->usb_dev, usb_sndbulkpipe(dmj->usb_dev, dmj->ep_out),
|
||||
buf, len, &actual, DMJ_USB_TIMEOUT);
|
||||
|
||||
kfree(buf);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int dmj_recv_wait(struct dmj_dev *dmj, void **kbuf, int rbufsize, bool parse_hdr)
|
||||
{
|
||||
int len, actual;
|
||||
int ret;
|
||||
void *buf;
|
||||
|
||||
*kbuf = NULL;
|
||||
|
||||
if (rbufsize <= 0) len = 0;
|
||||
else if (parse_hdr) len = rbufsize + DMJ_RESP_HDR_SIZE;
|
||||
else len = rbufsize;
|
||||
|
||||
buf = kmalloc(len, GFP_KERNEL);
|
||||
if (!buf) return -ENOMEM;
|
||||
|
||||
ret = usb_bulk_msg(dmj->usb_dev, usb_rcvbulkpipe(dmj->usb_dev, dmj->ep_in),
|
||||
buf, len, &actual, DMJ_USB_TIMEOUT);
|
||||
if (ret < 0) kfree(buf);
|
||||
|
||||
*kbuf = buf;
|
||||
return actual;
|
||||
}
|
||||
|
||||
int dmj_xfer_internal(struct dmj_dev *dmj, int cmd, int recvflags,
|
||||
const void *wbuf, int wbufsize, void *rbuf, int *rbufsize)
|
||||
{
|
||||
int ret = 0, actual;
|
||||
struct device *dev = &dmj->interface->dev;
|
||||
int respstat, total_len, bytes_read;
|
||||
uint32_t pl_len;
|
||||
void *buf;
|
||||
uint8_t *dbuf;
|
||||
ptrdiff_t pl_off;
|
||||
|
||||
spin_lock(&dmj->disconnect_lock);
|
||||
if (dmj->disconnect) ret = -ENODEV;
|
||||
spin_unlock(&dmj->disconnect_lock);
|
||||
|
||||
if (ret) return ret;
|
||||
|
||||
if ((cmd >= 0 && cmd <= 0xff) || (wbufsize && wbuf)) {
|
||||
ret = dmj_send_wait(dmj, cmd, wbuf, wbufsize);
|
||||
if (ret < 0) {
|
||||
dev_err(dev, "USB write failed: %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
if ((recvflags & DMJ_XFER_FLAGS_PARSE_RESP) == 0
|
||||
&& !(rbufsize && *rbufsize > 0 && rbuf)) {
|
||||
/* don't want any type of response? then dont do the urb stuff either */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* first recv buffer, with optional response header parsing */
|
||||
ret = dmj_recv_wait(dmj, &buf, (rbufsize && *rbufsize > 0) ? *rbufsize : 0,
|
||||
(recvflags & DMJ_XFER_FLAGS_PARSE_RESP) != 0);
|
||||
if (ret < 0 || !buf) {
|
||||
dev_err(dev, "USB read failed: %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
actual = ret;
|
||||
|
||||
if (recvflags & DMJ_XFER_FLAGS_PARSE_RESP) {
|
||||
dbuf = buf;
|
||||
|
||||
/* decode payload length */
|
||||
if (dbuf[1] & 0x80) {
|
||||
if (actual < 3) {
|
||||
dev_err(dev, "short response (%d, expected at least 3)\n", actual);
|
||||
kfree(buf);
|
||||
return -EREMOTEIO;
|
||||
}
|
||||
|
||||
pl_len = (uint32_t)(dbuf[1] & 0x7f);
|
||||
|
||||
if (dbuf[2] & 0x80) {
|
||||
if (actual < 4) {
|
||||
dev_err(dev, "short response (%d, expected at least 4)\n", actual);
|
||||
kfree(buf);
|
||||
return -EREMOTEIO;
|
||||
}
|
||||
|
||||
pl_len |= (uint32_t)(dbuf[2] & 0x7f) << 7;
|
||||
pl_len |= (uint32_t)dbuf[3] << 14;
|
||||
pl_off = 4;
|
||||
} else {
|
||||
pl_len |= (uint32_t)dbuf[2] << 7;
|
||||
pl_off = 3;
|
||||
}
|
||||
} else {
|
||||
if (actual < 2) {
|
||||
dev_err(dev, "short response (%d, expected at least 2)\n", actual);
|
||||
kfree(buf);
|
||||
return -EREMOTEIO;
|
||||
}
|
||||
|
||||
pl_len = (uint32_t)dbuf[1];
|
||||
pl_off = 2;
|
||||
}
|
||||
|
||||
respstat = dbuf[0];
|
||||
total_len = pl_len;
|
||||
actual -= pl_off;
|
||||
|
||||
/*dev_dbg(dev, "pl_len=%d,off=%ld,resp=%d\n", pl_len, pl_off, respstat);*/
|
||||
} else {
|
||||
pl_off = 0;
|
||||
if (rbufsize && *rbufsize > 0) total_len = *rbufsize;
|
||||
else total_len = actual;
|
||||
respstat = 0;
|
||||
}
|
||||
|
||||
if (rbuf && rbufsize && *rbufsize > 0) {
|
||||
if (*rbufsize < total_len) total_len = *rbufsize;
|
||||
|
||||
if (actual > total_len) {
|
||||
/*if (recvflags & DMJ_XFER_FLAGS_FILL_RECVBUF)*/
|
||||
{
|
||||
dev_err(dev, "aaa msgsize %d > %d\n", actual, total_len);
|
||||
kfree(buf);
|
||||
return -EMSGSIZE;
|
||||
} /*else {
|
||||
actual = total_len;
|
||||
}*/
|
||||
}
|
||||
|
||||
memcpy(rbuf + bytes_read, buf + pl_off, actual);
|
||||
kfree(buf);
|
||||
pl_off = -1;
|
||||
buf = NULL;
|
||||
|
||||
bytes_read = actual;
|
||||
|
||||
while (bytes_read < total_len && (recvflags & DMJ_XFER_FLAGS_FILL_RECVBUF) != 0) {
|
||||
ret = dmj_recv_wait(dmj, &buf, total_len - bytes_read, false);
|
||||
if (ret < 0 || !buf) {
|
||||
dev_err(dev, "USB read failed: %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
actual = ret;
|
||||
|
||||
if (bytes_read + actual > total_len) {
|
||||
/*actual = total_len - bytes_read;*/
|
||||
dev_err(dev, "aaa2 msgsize %d > %d\n", actual+bytes_read, total_len);
|
||||
kfree(buf);
|
||||
return -EMSGSIZE;
|
||||
}
|
||||
memcpy(rbuf + bytes_read, buf, actual);
|
||||
kfree(buf);
|
||||
buf = NULL;
|
||||
|
||||
bytes_read += actual;
|
||||
}
|
||||
} else {
|
||||
bytes_read = 0;
|
||||
kfree(buf);
|
||||
}
|
||||
|
||||
*rbufsize = bytes_read;
|
||||
|
||||
/*dev_dbg(dev, "all good! resp=%02x\n", respstat);*/
|
||||
return respstat;
|
||||
}
|
||||
|
||||
int dmj_transfer(struct platform_device *pdev, int cmd, int recvflags,
|
||||
const void *wbuf, int wbufsize, void *rbuf, int *rbufsize)
|
||||
{
|
||||
struct dmj_platform_data *dmj_pdata;
|
||||
struct dmj_dev *dmj;
|
||||
|
||||
dmj = dev_get_drvdata(pdev->dev.parent);
|
||||
dmj_pdata = dev_get_platdata(&pdev->dev); /* TODO: ??? */
|
||||
|
||||
return dmj_xfer_internal(dmj, cmd, recvflags, wbuf, wbufsize, rbuf, rbufsize);
|
||||
}
|
||||
EXPORT_SYMBOL(dmj_transfer);
|
||||
|
||||
/* stuff on init */
|
||||
|
||||
static int dmj_check_hw(struct dmj_dev *dmj)
|
||||
{
|
||||
struct device *dev = &dmj->interface->dev;
|
||||
|
||||
int ret;
|
||||
__le16 protover;
|
||||
int len = sizeof(protover);
|
||||
|
||||
ret = dmj_xfer_internal(dmj, DMJ_CMD_CFG_GET_VERSION,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, &protover, &len);
|
||||
|
||||
if (ret < 0) { /* TODO: positive error codes from respstat */
|
||||
dev_err(dev, "USB fail: %d\n", ret);
|
||||
return ret;
|
||||
}
|
||||
if (len < sizeof(protover)) {
|
||||
dev_err(dev, "USB fail remoteio: %d\n", len);
|
||||
return -EREMOTEIO;
|
||||
}
|
||||
if (len > sizeof(protover)) {
|
||||
dev_err(dev, "USB fail msgsize: %d\n", len);
|
||||
return -EMSGSIZE;
|
||||
}
|
||||
|
||||
if (le16_to_cpu(protover) != DMJ_USB_CFG_PROTO_VER) {
|
||||
dev_err(&dmj->interface->dev, HARDWARE_NAME " config protocol version 0x%04x too %s\n",
|
||||
le16_to_cpu(protover), (le16_to_cpu(protover) > DMJ_USB_CFG_PROTO_VER) ? "new" : "old");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
static int dmj_print_info(struct dmj_dev *dmj)
|
||||
{
|
||||
int ret, i, j, len;
|
||||
__le16 modes, mversion;
|
||||
uint8_t curmode, features;
|
||||
struct device *dev = &dmj->interface->dev;
|
||||
char strinfo[65];
|
||||
char modeinfo[16];
|
||||
|
||||
/* info string */
|
||||
len = sizeof(strinfo)-1;
|
||||
ret = dmj_xfer_internal(dmj, DMJ_CMD_CFG_GET_INFOSTR,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, strinfo, &len);
|
||||
if (ret < 0) return ret; /* TODO: positive error codes from respstat */
|
||||
if (len >= sizeof(strinfo)) return -EMSGSIZE;
|
||||
strinfo[len] = 0; /*strinfo[64] = 0;*/
|
||||
dev_info(dev, HARDWARE_NAME " '%s'\n", strinfo);
|
||||
|
||||
/* cur mode */
|
||||
len = sizeof(curmode);
|
||||
ret = dmj_xfer_internal(dmj, DMJ_CMD_CFG_GET_CUR_MODE,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, &curmode, &len);
|
||||
if (ret < 0) return ret;
|
||||
if (len < sizeof(curmode)) return -EREMOTEIO;
|
||||
if (len > sizeof(curmode)) return -EMSGSIZE;
|
||||
dmj->dmj_mode = curmode;
|
||||
|
||||
/* map of available modes */
|
||||
len = sizeof(modes);
|
||||
ret = dmj_xfer_internal(dmj, DMJ_CMD_CFG_GET_MODES,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, &modes, &len);
|
||||
if (ret < 0) return ret;
|
||||
if (len < sizeof(modes)) return -EREMOTEIO;
|
||||
if (len > sizeof(modes)) return -EMSGSIZE;
|
||||
|
||||
for (i = 1; i < 16; ++i) { /* build the string, uglily */
|
||||
if (le16_to_cpu(modes) & (1<<i)) {
|
||||
if (i < 0xa) modeinfo[i - 1] = '0'+i-0;
|
||||
else modeinfo[i - 1] = 'a'+i-0xa;
|
||||
} else modeinfo[i - 1] = '-';
|
||||
}
|
||||
modeinfo[15] = 0;
|
||||
dev_dbg(dev, "available modes: x%s, currently %d\n", modeinfo, (int)curmode);
|
||||
|
||||
/* for each available mode print name, version and features */
|
||||
for (i = 1; i < 16; ++i) {
|
||||
if (!(le16_to_cpu(modes) & (1<<i))) continue; /* not available */
|
||||
|
||||
/* name */
|
||||
len = sizeof(strinfo)-1;
|
||||
ret = dmj_xfer_internal(dmj, (i<<4) | DMJ_CMD_MODE_GET_NAME,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, strinfo, &len);
|
||||
if (ret < 0) return ret;
|
||||
if (len >= sizeof(strinfo)) return -EMSGSIZE;
|
||||
strinfo[len] = 0; /*strinfo[64] = 0;*/
|
||||
|
||||
/* version */
|
||||
len = sizeof(mversion);
|
||||
ret = dmj_xfer_internal(dmj, (i<<4) | DMJ_CMD_MODE_GET_VERSION,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, &mversion, &len);
|
||||
if (ret < 0) return ret;
|
||||
if (len < sizeof(mversion)) return -EREMOTEIO;
|
||||
if (len > sizeof(mversion)) return -EMSGSIZE;
|
||||
|
||||
/* features */
|
||||
len = sizeof(features);
|
||||
ret = dmj_xfer_internal(dmj, (i<<4) | DMJ_CMD_MODE_GET_FEATURES,
|
||||
DMJ_XFER_FLAGS_PARSE_RESP, NULL, 0, &features, &len);
|
||||
if (ret < 0) return ret;
|
||||
if (len < sizeof(features)) return -EREMOTEIO;
|
||||
if (len > sizeof(features)) return -EMSGSIZE;
|
||||
|
||||
if (i == 1) dmj->dmj_m1feature = features;
|
||||
|
||||
for (j = 0; j < 8; ++j) {
|
||||
if (features & (1<<j)) modeinfo[j] = '0'+j;
|
||||
else modeinfo[j] = '-';
|
||||
}
|
||||
modeinfo[8] = 0;
|
||||
|
||||
dev_dbg(dev, "Mode %d: '%s' version 0x%04x, features: %s\n",
|
||||
i, strinfo, mversion, modeinfo);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
static int dmj_hw_init(struct dmj_dev *dmj)
|
||||
{
|
||||
int ret;
|
||||
|
||||
ret = dmj_check_hw(dmj);
|
||||
if (ret < 0) return ret;
|
||||
|
||||
return dmj_print_info(dmj);
|
||||
}
|
||||
|
||||
/* MFD stuff */
|
||||
|
||||
static const struct mfd_cell dmj_mfd_devs[] = {
|
||||
{ .name = "dmj-char" },
|
||||
};
|
||||
|
||||
/* USB device control */
|
||||
|
||||
static int dmj_probe(struct usb_interface *itf, const struct usb_device_id *usb_id)
|
||||
{
|
||||
struct usb_host_interface *hostitf = itf->cur_altsetting;
|
||||
struct usb_endpoint_descriptor *epin, *epout;
|
||||
struct device *dev = &itf->dev;
|
||||
struct dmj_dev *dmj;
|
||||
int ret;
|
||||
|
||||
/*dev_dbg(dev, "dmj probe itfn=%d nendp=%d\n",
|
||||
hostitf->desc.bInterfaceNumber, hostitf->desc.bNumEndpoints);*/
|
||||
|
||||
if (hostitf->desc.bInterfaceNumber != 0 || hostitf->desc.bNumEndpoints < 2)
|
||||
return -ENODEV;
|
||||
|
||||
/* TODO: query endpoints in a better way */
|
||||
epout = &hostitf->endpoint[DMJ_VND_CFG_EP_OUT].desc;
|
||||
epin = &hostitf->endpoint[DMJ_VND_CFG_EP_IN].desc;
|
||||
|
||||
/*dev_dbg(dev, "epout addr=0x%02x, epin addr=0x%02x\n",
|
||||
epout->bEndpointAddress, epin->bEndpointAddress);*/
|
||||
|
||||
if (!usb_endpoint_is_bulk_out(epout)) { dev_warn(dev, "aaa1\n"); return -ENODEV; }
|
||||
if (!usb_endpoint_is_bulk_in(epin)) { dev_warn(dev, "aaa1\n"); return -ENODEV; }
|
||||
|
||||
dmj = kzalloc(sizeof(*dmj), GFP_KERNEL);
|
||||
if (!dmj) return -ENOMEM;
|
||||
|
||||
/*dev_dbg(dev, "hi there!\n");*/
|
||||
|
||||
dmj->ep_out = epout->bEndpointAddress;
|
||||
dmj->ep_in = epin->bEndpointAddress;
|
||||
dmj->usb_dev = usb_get_dev(interface_to_usbdev(itf));
|
||||
dmj->interface = itf;
|
||||
usb_set_intfdata(itf, dmj);
|
||||
|
||||
spin_lock_init(&dmj->disconnect_lock);
|
||||
|
||||
ret = dmj_hw_init(dmj);
|
||||
if (ret < 0) {
|
||||
dev_err(dev, "failed to initialize hardware\n");
|
||||
goto out_free;
|
||||
}
|
||||
|
||||
if (dmj->dmj_mode == 1) {
|
||||
ret = mfd_add_hotplug_devices(dev, dmj_mfd_devs, ARRAY_SIZE(dmj_mfd_devs));
|
||||
if (ret) {
|
||||
dev_err(dev, "failed to add MFD devices\n");
|
||||
goto out_free;
|
||||
}
|
||||
|
||||
if (dmj->dmj_mode & DMJ_FEATURE_MODE1_SPI) {
|
||||
// TODO: add SPI MFD
|
||||
}
|
||||
if (dmj->dmj_mode & DMJ_FEATURE_MODE1_I2C) {
|
||||
// TODO: add I2C MFD
|
||||
}
|
||||
if (dmj->dmj_mode & DMJ_FEATURE_MODE1_TEMPSENSOR) {
|
||||
// TODO: add tempsensor MFD
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
out_free:
|
||||
usb_put_dev(dmj->usb_dev);
|
||||
kfree(dmj);
|
||||
|
||||
return ret;
|
||||
}
|
||||
static void dmj_disconnect(struct usb_interface *itf)
|
||||
{
|
||||
struct dmj_dev *dmj = usb_get_intfdata(itf);
|
||||
|
||||
spin_lock(&dmj->disconnect_lock);
|
||||
dmj->disconnect = true;
|
||||
spin_unlock(&dmj->disconnect_lock);
|
||||
|
||||
mfd_remove_devices(&itf->dev);
|
||||
|
||||
usb_put_dev(dmj->usb_dev);
|
||||
|
||||
kfree(dmj);
|
||||
}
|
||||
|
||||
static int dmj_suspend(struct usb_interface *itf, pm_message_t message)
|
||||
{
|
||||
struct dmj_dev *dmj = usb_get_intfdata(itf);
|
||||
|
||||
(void)message;
|
||||
|
||||
spin_lock(&dmj->disconnect_lock);
|
||||
dmj->disconnect = true;
|
||||
spin_unlock(&dmj->disconnect_lock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
static int dmj_resume(struct usb_interface *itf)
|
||||
{
|
||||
struct dmj_dev *dmj = usb_get_intfdata(itf);
|
||||
|
||||
dmj->disconnect = false;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const struct usb_device_id dmj_table[] = {
|
||||
{ USB_DEVICE(0xcafe, 0x1312) },
|
||||
{ }
|
||||
};
|
||||
MODULE_DEVICE_TABLE(usb, dmj_table);
|
||||
|
||||
static struct usb_driver dmj_driver = {
|
||||
.name = HARDWARE_NAME_SYMBOLIC,
|
||||
.probe = dmj_probe,
|
||||
.disconnect = dmj_disconnect,
|
||||
.id_table = dmj_table,
|
||||
.suspend = dmj_suspend,
|
||||
.resume = dmj_resume,
|
||||
};
|
||||
module_usb_driver(dmj_driver);
|
||||
|
||||
MODULE_AUTHOR("sys64738 <sys64738@disroot.org>");
|
||||
MODULE_AUTHOR("haskal <haskal@awoo.systems>");
|
||||
MODULE_DESCRIPTION("Core driver for the " HARDWARE_NAME " USB multitool");
|
||||
MODULE_LICENSE("GPL v2");
|
||||
|
||||
MODULE_SOFTDEP("post: dmj-char");
|
|
@ -0,0 +1,55 @@
|
|||
/* SPDX-License-Identifier: GPL-2.0 */
|
||||
#ifndef __LINUX_USB_DAPPERMIMEJTAG_H
|
||||
#define __LINUX_USB_DAPPERMIMEJTAG_H
|
||||
|
||||
#define DMJ_USB_CFG_PROTO_VER 0x0010
|
||||
|
||||
#define DMJ_RESP_STAT_OK 0x00
|
||||
#define DMJ_RESP_STAT_ILLCMD 0x01
|
||||
#define DMJ_RESP_STAT_BADMODE 0x02
|
||||
#define DMJ_RESP_STAT_NOSUCHMODE 0x03
|
||||
#define DMJ_RESP_STAT_BADARG 0x04
|
||||
|
||||
#define DMJ_CMD_CFG_GET_VERSION 0x00
|
||||
#define DMJ_CMD_CFG_GET_MODES 0x01
|
||||
#define DMJ_CMD_CFG_GET_CUR_MODE 0x02
|
||||
#define DMJ_CMD_CFG_SET_CUR_MODE 0x03
|
||||
#define DMJ_CMD_CFG_GET_INFOSTR 0x04
|
||||
|
||||
#define DMJ_CMD_MODE_GET_NAME 0x00
|
||||
#define DMJ_CMD_MODE_GET_VERSION 0x01
|
||||
#define DMJ_CMD_MODE_GET_FEATURES 0x02
|
||||
|
||||
#define DMJ_CMD_MODE1_SPI 0x13
|
||||
#define DMJ_CMD_MODE1_I2C 0x14
|
||||
#define DMJ_CMD_MODE1_TEMPSENSOR 0x15
|
||||
|
||||
#define DMJ_FEATURE_MODE1_UART (1<<0)
|
||||
#define DMJ_FEATURE_MODE1_CMSISDAP (1<<1)
|
||||
#define DMJ_FEATURE_MODE1_SPI (1<<2)
|
||||
#define DMJ_FEATURE_MODE1_I2C (1<<3)
|
||||
#define DMJ_FEATURE_MODE1_TEMPSENSOR (1<<4)
|
||||
|
||||
struct dmj_platform_data {
|
||||
uint8_t port;
|
||||
};
|
||||
|
||||
#define DMJ_XFER_FLAGS_PARSE_RESP (1<<0)
|
||||
#define DMJ_XFER_FLAGS_FILL_RECVBUF (1<<1)
|
||||
|
||||
int dmj_transfer(struct platform_device *pdev, int cmd, int recvflags,
|
||||
const void *wbuf, int wbufsize, void *rbuf, int *rbufsize);
|
||||
|
||||
inline static int dmj_read(struct platform_device *pdev, int recvflags,
|
||||
void *rbuf, int *rbufsize)
|
||||
{
|
||||
return dmj_transfer(pdev, -1, recvflags, NULL, 0, rbuf, rbufsize);
|
||||
}
|
||||
|
||||
inline static int dmj_write(struct platform_device *pdev, int cmd,
|
||||
const void *wbuf, int wbufsize)
|
||||
{
|
||||
return dmj_transfer(pdev, cmd, DMJ_XFER_FLAGS_PARSE_RESP, wbuf, wbufsize, NULL, NULL);
|
||||
}
|
||||
|
||||
#endif
|
|
@ -29,7 +29,7 @@
|
|||
/*#define MODE_ENABLE_I2CTINYUSB*/
|
||||
|
||||
enum m_default_cmds {
|
||||
mdef_cmd_spi = mode_cmd__specific,
|
||||
mdef_cmd_spi = mode_cmd__specific | 0x10,
|
||||
mdef_cmd_i2c,
|
||||
mdef_cmd_tempsense
|
||||
};
|
||||
|
|
|
@ -189,15 +189,19 @@ static void handle_cmd(uint8_t cmd, int ud, uint8_t (*read_byte)(void),
|
|||
break;
|
||||
|
||||
case S_CMD_SPIOP: case S_CMD_SPI_READ: case S_CMD_SPI_WRITE: {
|
||||
uint32_t slen, rlen;
|
||||
uint32_t slen = 0, rlen = 0;
|
||||
|
||||
// clang-format off
|
||||
slen = (uint32_t)read_byte();
|
||||
slen |= (uint32_t)read_byte() << 8;
|
||||
slen |= (uint32_t)read_byte() << 16;
|
||||
rlen = (uint32_t)read_byte();
|
||||
rlen |= (uint32_t)read_byte() << 8;
|
||||
rlen |= (uint32_t)read_byte() << 16;
|
||||
if (cmd == S_CMD_SPIOP || cmd == S_CMD_SPI_WRITE) {
|
||||
slen = (uint32_t)read_byte();
|
||||
slen |= (uint32_t)read_byte() << 8;
|
||||
slen |= (uint32_t)read_byte() << 16;
|
||||
}
|
||||
if (cmd == S_CMD_SPIOP || cmd == S_CMD_SPI_READ) {
|
||||
rlen = (uint32_t)read_byte();
|
||||
rlen |= (uint32_t)read_byte() << 8;
|
||||
rlen |= (uint32_t)read_byte() << 16;
|
||||
}
|
||||
// clang-format on
|
||||
|
||||
if (writehdr)
|
||||
|
|
|
@ -212,7 +212,8 @@ void i2ctu_bulk_cmd(void) {
|
|||
uint16_t us;
|
||||
uint32_t func, freq;
|
||||
|
||||
switch (vnd_cfg_read_byte()) {
|
||||
uint8_t cmdb = vnd_cfg_read_byte();
|
||||
switch (cmdb) {
|
||||
case ITU_CMD_ECHO:
|
||||
txbuf[0] = vnd_cfg_read_byte();
|
||||
vnd_cfg_write_resp(cfg_resp_ok, 1, txbuf);
|
||||
|
@ -247,7 +248,7 @@ void i2ctu_bulk_cmd(void) {
|
|||
case ITU_CMD_I2C_IO_END:
|
||||
case ITU_CMD_I2C_IO_BEGINEND: {
|
||||
struct itu_cmd cmd;
|
||||
cmd.cmd = vnd_cfg_read_byte();
|
||||
cmd.cmd = cmdb;
|
||||
cmd.flags = (uint16_t)vnd_cfg_read_byte();
|
||||
cmd.flags |= (uint16_t)vnd_cfg_read_byte() << 8;
|
||||
cmd.addr = (uint16_t)vnd_cfg_read_byte();
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
|
||||
#include <stdint.h>
|
||||
|
||||
/* the configuration vendor interface must always be the first vendor itf. */
|
||||
/* the configuration vendor interface must always be the first vendor itf, and
|
||||
* have endpoint number 1. */
|
||||
|
||||
#define VND_CFG_PROTO_VER 0x0010
|
||||
|
||||
|
@ -45,38 +46,5 @@ void vnd_cfg_write_flush(void);
|
|||
void vnd_cfg_write_byte(uint8_t v);
|
||||
void vnd_cfg_write_resp(enum cfg_resp stat, uint32_t len, const void* data);
|
||||
|
||||
/*
|
||||
* wire protocol:
|
||||
* host sends messages, device sends replies. the device never initiates a xfer
|
||||
*
|
||||
* the first byte of a command is the combination of the mode it is meant for
|
||||
* in the high nybble, and the command number itself in the low nybble. optional
|
||||
* extra command bytes may follow, depending on the command itself.
|
||||
* a high nybble is 0 signifies a general configuration command, not meant for
|
||||
* a particular mode
|
||||
*
|
||||
* a response consists of a response status byte (enum cfg_resp), followed by
|
||||
* a 7- or 15-bit VLQ int (little-endian) for the payload length, followed by
|
||||
* the payload itself.
|
||||
*
|
||||
* general commands:
|
||||
* * get vesion (0x00): returns a payload of 2 bytes with version data. should
|
||||
* currently be 0x10 0x00 (00.10h)
|
||||
* * get modes (0x01): returns 2 bytes with a bitmap containing all supported
|
||||
* modes (bit 0 is for general cfg and must always be 1)
|
||||
* * get cur mode (0x02): returns a single byte containing the current mode number
|
||||
* * set cur mode (0x03): sets the current mode. one extra request byte, no
|
||||
* response payload
|
||||
* * get info string (0x04): get a string containing human-readable info about
|
||||
* the device. for display only
|
||||
*
|
||||
* common mode commands:
|
||||
* * get name (0x00): returns a name or other descriptive string in the payload.
|
||||
* for display only
|
||||
* * get version (0x01): returns a 2-byte version number in the payload
|
||||
* * get_features (0x02): gets a bitmap of supported features. length and meaning
|
||||
* of the bits depends on the mode
|
||||
*/
|
||||
|
||||
#endif
|
||||
|
||||
|
|
Loading…
Reference in New Issue