CM6206の設定プログラム

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と呼ばれるものと思われます。

https://www.minidisc.org/spdif_c_channel.html

C-Mediaのサイトにあるデーターシート(Rev. 2.3)はインターラプト転送の1バイト目のビットフィールドの説明がありません。なぜかネットで拾える古いもの(Rev. 2.1)にはあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104066/bdfdd659-ae6c-df11-a4d1-d13975655a8f.png)

一番上のインターラプトの箱からはホストへの矢印しかありません。転送はEndpoint 1のデバイスから見てOUTなので0x81で行われます。

```c
#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を使ったプログラムもありました。

https://github.com/vestom/cm6206ctl

HIDなデバイスを制御するのは上のサンプルプログラムのようにlibusbを直接使う方法や、hidapiを使う方法などがあります。実はFreeBSDにはNetBSDからポートされたlibusbhidというライブラリもありますが、使い方が分かりません。

あ、それとFreeBSD 13で新しいHIDの仕組みが入ったみたいです。こっちはFreeBSDは12で終わりなのであまり興味ないので調べてません。

hidapiはiconvを使っていて、面倒なのでmrubyのmrbgemにしてみました。

https://github.com/yamori813/mruby-cm6206

設定をしないと、SCMSが効いてMDへの録音が"Cannot Copy"になりできません。設定すると録音できるようになるので、なんらかSPDIFの情報が変わっていると思われますが、どう変わってるのか良く分かりません。

FreeBSDではOS標準で再実装したlibusbが入っています。

いろいろ調べていたところ、この機能はすでに実装済みでした。

https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=216131