C-MediaのCM6206はネットでデーターシートが手に入るので、ながめていたらUSBのHIDで設定できるようです。
以前BTOさんのUSB赤外線リモコンのHIDな転送プログラムを作ったので、それを下敷きにいろいろ試してみました。
% sudo usbconfig -d ugen1.3 dump_all_desc
ugen1.3: <vendor 0x0d8c USB Sound Device> at usbus1, cfg=0 md=HOST spd=FULL (12Mbps) pwr=ON (500mA)
bLength = 0x0012
bDescriptorType = 0x0001
bcdUSB = 0x0110
bDeviceClass = 0x0000 <Probed by interface class>
bDeviceSubClass = 0x0000
bDeviceProtocol = 0x0000
bMaxPacketSize0 = 0x0008
idVendor = 0x0d8c
idProduct = 0x0102
bcdDevice = 0x0010
iManufacturer = 0x0000 <no string>
iProduct = 0x0002 <USB Sound Device >
iSerialNumber = 0x0000 <no string>
bNumConfigurations = 0x0001
省略
Interface 3
bLength = 0x0009
bDescriptorType = 0x0004
bInterfaceNumber = 0x0003
bAlternateSetting = 0x0000
bNumEndpoints = 0x0001
bInterfaceClass = 0x0003 <HID device>
bInterfaceSubClass = 0x0000
bInterfaceProtocol = 0x0000
iInterface = 0x0000 <no string>
Additional Descriptor
bLength = 0x09
bDescriptorType = 0x21
bDescriptorSubType = 0x00
RAW dump:
0x00 | 0x09, 0x21, 0x00, 0x01, 0x00, 0x01, 0x22, 0x32,
0x08 | 0x00
Endpoint 0
bLength = 0x0007
bDescriptorType = 0x0005
bEndpointAddress = 0x0081 <IN>
bmAttributes = 0x0003 <INTERRUPT>
wMaxPacketSize = 0x0003
bInterval = 0x0001
bRefresh = 0x0000
bSynchAddress = 0x0000
USB赤外線リモコンは受信送信ともインターラプト転送でおこなっていましたが、CM6206は送信はコントロール転送で、受信はインターラプト転送とデーターシートに書いてありました。
To access registers via HID interface, users should issue a “Set Output Report” HID request.
When users intend to read register / EEPROM by “Set Output Report”, the returned data will be transferred to USB host via HID input report through interrupt pipe.
コントロール転送で送るのは4バイトで、readの場合のインターラプト転送で送られてくるのは3バイトになります。
レジスタは16bitでREG0からREG5までの6つです。設定内容についてはデーターシートを確認してください。
SPDIFで送られる情報はREG0で設定されるのですが、ここの情報はC Channelと呼ばれるものと思われます。
C-Mediaのサイトにあるデーターシート(Rev. 2.3)はインターラプト転送の1バイト目のビットフィールドの説明がありません。なぜかネットで拾える古いもの(Rev. 2.1)にはあります。
一番上のインターラプトの箱からはホストへの矢印しかありません。転送はEndpoint 1のデバイスから見てOUTなので0x81で行われます。
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <unistd.h>
# include <errno.h>
# if defined( __FreeBSD__)
# include <libusb.h>
# else
# include <libusb-1.0/libusb.h>
# endif
# define USB_HID_SET_REPORT 0x09
# define USB_HID_REPORT_TYPE_INPUT 1
# define USB_HID_REPORT_TYPE_OUTPUT 2
# define USB_HID_REPORT_TYPE_FEATURE 3
# define USB_REQUEST_TYPE_OUT (LIBUSB_REQUEST_TYPE_CLASS | \
LIBUSB_RECIPIENT_INTERFACE | LIBUSB_ENDPOINT_OUT)
# define USB_INTERFACE 0
# define VENDOR_ID 0x0d8c
# define PRODUCT_ID 0x0102
# define CM_EP_IN 0x81
# define MAX_SIZE 64
void close_device(libusb_context *ctx, libusb_device_handle *devh) {
libusb_close(devh);
libusb_exit(ctx);
}
libusb_device_handle* open_device(libusb_context *ctx) {
struct libusb_device_handle *devh = NULL;
libusb_device *dev;
libusb_device **devs;
int r = 1;
int i = 0;
int cnt = 0;
// libusb_set_debug(ctx, 3);
if ((libusb_get_device_list(ctx, &devs)) < 0) {
// perror("no usb device found");
// exit(1);
return -1;
}
/* check every usb devices */
while ((dev = devs[i++]) != NULL) {
struct libusb_device_descriptor desc;
if (libusb_get_device_descriptor(dev, &desc) < 0) {
perror("failed to get device descriptor\n");
}
/* count how many device connected */
if (desc.idVendor == VENDOR_ID && desc.idProduct == PRODUCT_ID) {
cnt++;
}
}
/* device not found */
if (cnt == 0) {
// fprintf(stderr, "device not connected or lack of permissions\n");
// exit(1);
return -1;
}
if (cnt > 1) {
// fprintf(stderr, "multi device is not implemented yet\n");
// exit(1);
return -1;
}
/* open device */
if ((devh = libusb_open_device_with_vid_pid(ctx, VENDOR_ID, PRODUCT_ID)) < 0) {
perror("can't find device\n");
// close_device(ctx, devh);
// exit(1);
return -1;
}
/* detach kernel driver if attached. */
r = libusb_kernel_driver_active(devh, 3);
if (r == 1) {
/* detaching kernel driver */
r = libusb_detach_kernel_driver(devh, 3);
if (r != 0) {
// perror("detaching kernel driver failed");
// exit(1);
return -1;
}
}
r = libusb_claim_interface(devh, 0);
if (r < 0) {
// fprintf(stderr, "claim interface failed (%d): %s\n", r, strerror(errno));
// exit(1);
return -1;
}
return devh;
}
void write_device(struct libusb_device_handle *devh, unsigned char *cmd, int len) {
int i, r;
uint8_t buf[MAX_SIZE];
int size = 0;
memset(buf, 0xff, sizeof(buf));
for (i = 0; i < len; i++) {
buf[i] = cmd[i];
}
r = libusb_control_transfer(devh, USB_REQUEST_TYPE_OUT,
USB_HID_SET_REPORT, USB_HID_REPORT_TYPE_FEATURE, USB_INTERFACE,
(unsigned char *)buf, len, 1000);
if (r < 0) {
fprintf(stderr, "libusb_control_transfer (%d): %s\n", r, strerror(errno));
exit(1);
}
}
int read_device(struct libusb_device_handle *devh, unsigned char *buf, int bufsize) {
int size = 0;
memset(buf, 0x00, bufsize);
int r = libusb_interrupt_transfer(devh, CM_EP_IN, buf, bufsize, &size, 100);
if (r < 0) {
fprintf(stderr, "libusb_interrupt_transfer (%d): %s\n", r, strerror(errno));
exit(1);
}
return size;
}
int cm6206_read(struct libusb_device_handle *devh, int regnum, int *value)
{
int res;
unsigned char buf[4] = {
0x30, // 0x30 = read, 0x20 = write
0x00, // DATAL
0x00, // DATAH
regnum // Register address
};
write_device(devh, buf, 4);
res = read_device(devh, buf, 3);
if ((buf[0] & 0xe0) != 0x20) // No register data in the input report
return -3;
*value = (((uint16_t)buf[2]) << 8) | buf[1];
return 1;
}
int main(int argc, char *argv[]) {
libusb_context *ctx = NULL;
int r = 1;
int val;
/* libusb initialize*/
if ((r = libusb_init(&ctx)) < 0) {
perror("libusb_init\n");
exit(1);
}
/* open device */
libusb_device_handle *devh = open_device(ctx);
cm6206_read(devh, 0, &val);
printf("REG0: %04x\n", val);
close_device(ctx, devh);
return 0;
}
送信はされてるようなのですが、受信ができません。誰かが拾ってしまってるようです。
sys/dev/sound/usb/uaudio.cを見てみたところ、uaudioのsysctlに以下のような設定がありました。
% sysctl hw.usb.uaudio.handle_hid
hw.usb.uaudio.handle_hid: 1
これが1だとuaudioが拾ってプログラムで拾えません。0にしてデバイスをさしなおしたら、ちゃんとデータ拾えました。
% sudo ./cm6206
REG0: 0020
EEPROMは付いてないのですが、なぜかデータシートのデフォルト値(2000)ではありません。最初バイトオーダーが違っているのかと思ったのですが、他のレジスタは初期値が正しく読めたりするので謎です。
スイッチがついたモジュールだとスイッチを押すと、その情報が飛んでくるので区別する必要があります。
設定だけであれば、コントロールのOUTだけで処理できるので、hw.usb.uaudio.handle_hidの変更は必要ありません。
hidapiを使ったプログラムもありました。
HIDなデバイスを制御するのは上のサンプルプログラムのようにlibusbを直接使う方法や、hidapiを使う方法などがあります。実はFreeBSDにはNetBSDからポートされたlibusbhidというライブラリもありますが、使い方が分かりません。
あ、それとFreeBSD 13で新しいHIDの仕組みが入ったみたいです。こっちはFreeBSDは12で終わりなのであまり興味ないので調べてません。
hidapiはiconvを使っていて、面倒なのでmrubyのmrbgemにしてみました。
設定をしないと、SCMSが効いてMDへの録音が"Cannot Copy"になりできません。設定すると録音できるようになるので、なんらかSPDIFの情報が変わっていると思われますが、どう変わってるのか良く分かりません。
FreeBSDではOS標準で再実装したlibusbが入っています。
いろいろ調べていたところ、この機能はすでに実装済みでした。