HFP Client SCO audio routing?


This is what I did to the kernel to get there;

diff --git a/arch/arm64/boot/dts/hisilicon/hi3660.dtsi b/arch/arm64/boot/dts/hisilicon/hi3660.dtsi
index 12544c3..fd4b838 100644
--- a/arch/arm64/boot/dts/hisilicon/hi3660.dtsi
+++ b/arch/arm64/boot/dts/hisilicon/hi3660.dtsi
@@ -1338,9 +1338,14 @@
 			status = "ok";
+		sco_codec: bt_sco {
+			compatible = "linux,bt-sco";
+			#sound-dai-cells = <0>;
+		};
 		sound {
 			compatible = "simple-audio-card";
-			simple-audio-card,name = "hikey-hdmi";
+			simple-audio-card,name = "hikey-btsco";
 			simple-audio-card,format = "i2s";
 			simple-audio-card,bitclock-master = <&sound_master>;
@@ -1351,7 +1356,7 @@
 			simple-audio-card,codec {
-				sound-dai = <&adv7533>;
+				sound-dai = <&sco_codec>;
diff --git a/arch/arm64/configs/hikey960_defconfig b/arch/arm64/configs/hikey960_defconfig
index 8613e46..9462f23 100644
--- a/arch/arm64/configs/hikey960_defconfig
+++ b/arch/arm64/configs/hikey960_defconfig
@@ -323,6 +323,7 @@ CONFIG_SND_USB_AUDIO=y
diff --git a/sound/soc/hisilicon/hisi-i2s.c b/sound/soc/hisilicon/hisi-i2s.c
index 4c74a6c..13f7411 100644
--- a/sound/soc/hisilicon/hisi-i2s.c
+++ b/sound/soc/hisilicon/hisi-i2s.c
@@ -276,18 +276,18 @@ struct snd_soc_dai_driver hisi_i2s_dai_init = {
 	.name = "hisi_i2s",
 	.probe		= hisi_i2s_dai_probe,
 	.playback = {
-		.channels_min = 2,
+		.channels_min = 1,
 		.channels_max = 2,
 		.formats = SNDRV_PCM_FMTBIT_S16_LE |
-		.rates = SNDRV_PCM_RATE_48000,
+		.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000,
 	.capture = {
-		.channels_min = 2,
+		.channels_min = 1,
 		.channels_max = 2,
 		.formats = SNDRV_PCM_FMTBIT_S16_LE |
-		.rates = SNDRV_PCM_RATE_48000,
+		.rates = SNDRV_PCM_RATE_8000 | SNDRV_PCM_RATE_16000 | SNDRV_PCM_RATE_48000,
 	.ops = &hisi_i2s_dai_ops,


I think if I figure out the HCI vendor command(s) needed to set the BT’s PCM interface to those specs, it should work up to level of alsa. Unfortunately, DHL lied and they didn’t deliver the Saleae today, instead, they left it in Cincinnati (wrong country). So probably be delivered on Monday. Who knows… maybe I won’t even need it?


Great, you should be able to find the requested command in the TI documentation, cf HCI_VS_Write_CODEC_Config. You can send additional commands with hcitool or adding them to the chip init procedure in the Linux driver (drivers/bluetooth/hci_ll.c: ll_setup).


I think I’ll start off with hcitool, and then switch to maybe the bts file, once I have it working.


I’m trying right now to understand how the clocks work for i2s.

The PCM clock rate value, for instance. That is the bit clock?
So ((sample frequency) * (bits per sample) * (number of channels)
8 kHz * 16 * 2 = 256 kHz, right?

frame sync frequency = sample rate = 8000 Hz.

(and as an aside, frame sync duty cycle seems to be related to selecting the format… i2s = 50% vs pcm_a = 1?)

I’m looking at their documentation, and they are showing a PCM clock rate (HCI Tester command) of 2048 kHz for a sample rate of 8000 Hz. That sounds to me more like a MASTER clock – 256*sample rate.


Yes, CLK/MCLK is the bit clock and so depends on word-size (e.g. 16 bit) and sample rate (e.g. 8Khz). IN/OUT the data line and FSYNC the frame-sync/word-clock line which is in our case equivalent to the sample rate (mono channel).

Yes you can have 50% standard I2S duty cycle or short frame sync (usually 1 MCLK len).

Yes that sounds strange to me since the frame sync is 8kHz and total data bit per frame is 32 (16*2), a 256Khz PCM clock would be enough, I assume there is some idle time.


Perfect, that’s what I thought it should be.
I think I’m getting pretty close to actually understanding this now, but I still have a few questions…

  1. frame sync polarity: if I understand correctly, this would have the effect of reversing the two channels, correct? So this is especially important for single channel data, because if it is backwards, then the two sides will both be sending to each other using the wrong channel – no sound in either direction.

  2. Edge… frame-sync edge, C1 Din edge, C1 Dout edge, C2 Din edge, C2 Dout edge. This has to do with which edge of the bit clock pulse this bit is read or written at? So setting those wrong would cause a bit shift in the sample or between samples in the case of frame sync. Having these set wrong could cause broken sound. Short of hooking up the logic analyzer, how would the correct values be determined?

  3. What is the right way to test the data when I enable loopback on the WL1837 using 0xFE28?


Good question, I’m not sure how the mono case is handled, I assume only channel-0(left) is used to transmit data, so yes polarity is important and should be the same on host and BT side, for me 0 is the default polarity.

Yes, this defines on which bit-clock edge to generate or read the bits, so this is an important point as well, host and BT have to be aligned. Typically, data write/read are performed on opposed edges: e.g. If data is generated (OUT) on rising edge of the bit clock, data will be read (IN) on falling edge.

For me that means the BT loopback to the host the reveived sound. You should then be able to play a sound on bt-pcm-out and then record the same sound on bt-pcm-in.

SOUND ==> PCM_OUT ===> | BT | ===> PCM_IN ==> SOUND
=> http://processors.wiki.ti.com/index.php/BT_Audio_Testing_on_Wilink8


And today I learned a bit more about i2s!
I hooked up the logic analyzer to read the data while playing back over HDMI (default configuration).
Great tool BTW. Thank you, Loic, for suggesting it!

Hookup was, in my case, especially hackish, since I’m reading the pins of the bluetooth chip that are barely protruding around its edges. I soldered wires onto sewing pins (the kind with plastic knobs on the end) to get good temporary contact. Worked as well as can be expected for trying to hold 3 pins with one hand while clicking a mouse button with the other hand to start the capture. I did get a really good 5 second capture from the middle of Bohemian Rhapsody.

Seems that the bit clock is set up to 3072 KHz when 48 kHz / 16 bit / 2 channel sample rate only requires 1536 kHz bit clock. That means (well, at least suggests) that it is not setting the bit clock up based on the audio sample rate, rather it is hard coded. I think I see in the code where it is set, and messing with that seems like an unnecessary complication.

Changes in the word clock and data (in) are tied to the FALLING edge of the bit clock (which means that sampling should be done on the RISING edge of the bit clock right in the dead center of the bit).
The word clock is set for 50% duty cycle.
The data begins one bit clock cycle after the word clock changes state.

The extra cycles are split into two sections, such that you have events happening in this order;

  1. word clock changes state,
  2. channel 1 sample
  3. blank space to use up half the extra cycles
  4. word clock changes state,
  5. channel 2 sample
  6. blank space with remaining extra cycles.

I think I’m going to start off by setting up the bluetooth chip (and the bt-sco fake codec driver) to the same specifications as are currently in use for the HDMI, and then check if that is working. When it is, I’ll start adjusting it towards the HFP specification.


Just to reply to my own thread with more info (to help others who might be looking into some related work)…

There is a lot of trouble getting hcitool built, since it is part of bluez, which is not part of Android.
I found this thread discussing enabling dut_mode_send command in bdt “bluedroid tool”, which includes a patch.

Well, bluedroid is now called fluoride, and bdt has been renamed to “mcap_tool”, and some extra changes make it so the patch will not apply, so here is the patch updated for fluoride:

diff --git a/btif/src/btif_core.cc b/btif/src/btif_core.cc
index 296b9d7..42ae38b 100644
--- a/btif/src/btif_core.cc
+++ b/btif/src/btif_core.cc
@@ -554,7 +554,8 @@ bt_status_t btif_cleanup_bluetooth(void) {
 static void btif_dut_mode_cback(UNUSED_ATTR tBTM_VSC_CMPL* p) {
-  /* For now nothing to be done. */
+  HAL_CBACK(bt_hal_cbacks, dut_mode_recv_cb, p->opcode, p->p_param_buf, p->param_len);
@@ -596,10 +597,10 @@ bt_status_t btif_dut_mode_configure(uint8_t enable) {
 bt_status_t btif_dut_mode_send(uint16_t opcode, uint8_t* buf, uint8_t len) {
   /* TODO: Check that opcode is a vendor command group */
   BTIF_TRACE_DEBUG("%s", __func__);
-  if (!btif_is_dut_mode()) {
+  /*if (!btif_is_dut_mode()) {
     BTIF_TRACE_ERROR("Bluedroid HAL needs to be init with test_mode set to 1.");
     return BT_STATUS_FAIL;
-  }
+  }*/
   BTM_VendorSpecificCommand(opcode, len, buf, btif_dut_mode_cback);
diff --git a/tools/mcap_tool/mcap_tool.cc b/tools/mcap_tool/mcap_tool.cc
index 6ed26aa..f2a38c2 100644
--- a/tools/mcap_tool/mcap_tool.cc
+++ b/tools/mcap_tool/mcap_tool.cc
@@ -472,7 +472,13 @@ static void acl_state_changed(bt_status_t status, RawAddress* remote_bd_addr,
 static void dut_mode_recv(uint16_t opcode, uint8_t* buf, uint8_t len) {
+  uint8_t evt_param_index;
+  printf("DUT MODE RECV: [Opcode = 0x%4X] [Param_len = %d] [Param = ", opcode, len);
+  for(evt_param_index=0; evt_param_index<len; evt_param_index++)
+    printf(" 0x%02X", buf[evt_param_index]);
+  printf("]\n");
 static bt_callbacks_t bt_callbacks = {
@@ -574,6 +580,67 @@ void adapter_dut_mode_configure(char* p) {
+void bdt_dut_mode_send(char *p)
+    uint16_t ogf = 0;
+    uint16_t ocf = 0;
+    char *tok_str;
+    uint16_t opcode;
+    uint8_t param[1024];
+    uint16_t opcode_field_count;
+    uint16_t param_index , param_len;
+    int status;
+    param_len = 0;
+    param_index = 0;
+    opcode_field_count = 2;
+    if (!global_bt_enabled) {
+        LOG(INFO) << "Bluetooth must be enabled for test_mode to work.";
+        return;
+    }
+    tok_str = strtok (p," ");
+    while (tok_str != NULL) {
+        switch(opcode_field_count)
+        {
+            case 2:
+            ogf = strtol(tok_str, NULL, 16);
+            break;
+            case 1:
+            ocf = strtol(tok_str, NULL, 16);
+            break;
+            default:
+            param[param_index++] =  strtol(tok_str, NULL, 16);
+            break;
+        }
+        opcode_field_count--;
+        tok_str = strtok (NULL, " ");
+    }
+    param_len = param_index;
+    LOG(INFO) << "ogf = (int)" << (int)ogf;
+    LOG(INFO) << "ocf = (int)" << (int)ocf;
+    opcode = (ocf & 0x03ff)|(ogf << 10);
+    LOG(INFO) << "opcode = (int)" << (int)opcode;
+    LOG(INFO) << "param_len = (int)" << (int)param_len;
+    printf("params: ");
+    for(param_index=0; param_index<param_len; param_index++)
+        printf("0x%02X ", param[param_index]);
+    printf("\n");
+    status = sBtInterface->dut_mode_send(opcode, param, param_len);
+    check_return_status((bt_status_t)status);
 void adapter_cleanup(void) {
   LOG(INFO) << __func__;
@@ -611,6 +678,10 @@ void do_enable(char* p) { adapter_enable(); }
 void do_disable(char* p) { adapter_disable(); }
+void do_dut_mode_configure(char *p) { adapter_dut_mode_configure(p); }
+void do_dut_mode_send(char *p) { bdt_dut_mode_send(p); }
 void do_cleanup(char* p) { adapter_cleanup(); }
@@ -859,6 +930,8 @@ const cmd_t console_cmd_list[] = {
     /* Init and Cleanup shall be called automatically */
     {"enable_bluetooth", do_enable, "", 0},
     {"disable_bluetooth", do_disable, "", 0},
+    {"dut_mode_configure", do_dut_mode_configure, ":: DUT mode - 1 to enter,0 to exit", 0},
+    {"dut_mode_send", do_dut_mode_send, ":: <ogf> <ocf> <parameters>", 0},
     {"pair", do_pairing, "BD_ADDR<xx:xx:xx:xx:xx:xx>", 0},
     {"register", do_mcap_register,
      "ctrl_psm<hex> data_psm<hex> security_mask<0-10>", 0},


Of course… check failed because status==success. :frowning:

# ./mcap_tool                                         
set_aid_and_cap : pid 9513, uid 0 gid 0[1225/212538:INFO:mcap_tool.cc(992)] Fluoride MCAP test app is starting
[1225/212538:INFO:mcap_tool.cc(352)] Loading HAL library and extensions
[1225/212538:INFO:hal_util.cc(56)] hal_util_load_bt_library loaded HAL path=libbluetooth.so btinterface=0x7ade8c5400 handle=0x50c58e51225878e5
[1225/212538:INFO:mcap_tool.cc(360)] HAL library loaded
[1225/212538:INFO:mcap_tool.cc(540)] adapter_init
[1225/212538:INFO:mcap_tool.cc(390)] HAL REQUEST SUCCESS
[1225/212540:INFO:mcap_tool.cc(549)] adapter_enable
[1225/212540:INFO:mcap_tool.cc(390)] HAL REQUEST SUCCESS
>[1225/212543:FATAL:hci_layer_android.cc(78)] Check failed: status == Status::SUCCESS. 


Ok, easily fixed. Bluetooth just has to be turned off in system settings for that tool to work, because only one process can open /dev/rfkill at a time. The error message is unclear. That is the test that failed, not the state that was observed.

I can now send vendor specific HCI commands to the thing.


Loopback now working at 48 kHz / 16 bit / stereo :slight_smile:


I’m having trouble figuring out the offsets for channel 2. Not hugely important though, because I’m only targeting HFP, which only uses channel 1.

Made another great stride… I have 2-way audio through bluetooth now. Not all the way into Android, but I can use tinyplay and tinycap to read and write data to the i2s and sco. The sound is still all garbled since I haven’t got the correct sample rate yet (running it at 48k), but there is definitely data making it through.

Its too bad that the bluetooth chip can’t resample the audio, because I don’t think its trivial to change the FS clock from 48k to 8k.


I really appreciate you share your progress. Would be great to have a summary/wiki at the end of this ‘success story’ :wink:

Bluez includes Android makefile so in theory you should just have to download bluez (e.g. in external) and then add ‘hcitool’ to the product package list in your device makefile. Your bdt/mcap hack should be ok too.

You mean you are able to transmit and listen audio with a HFP headset when using tinyplay ? really good!

Are you sure this is a re-sample issue, does the host/device have aligned I2S conf ? who is the I2S master ?


Kind of.

Running the following commands in the modified mcap_tool;
(first one enables sending vendor commands, second one sets up the i2s as SLAVE, third one sets it as “always send” – I think, just copied it from a TI wiki, 4th one enables loopback)

dut_mode_configure 1

dut_mode_send 0x3f 0x0106 0x00 0x0c 0x01 0x80 0xbb 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x10 0x00 0x01 0x00 0x01 0x10 0x00 0x01 0x00 0x00 0x00 0x10 0x00 0x11 0x00 0x01 0x10 0x00 0x11 0x00 0x00 0x00

dut_mode_send 0x3f 0x0107 0x00 0x00 0x00 0x00 0x00 0x00 0x04 0x04 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x04 0x04 0x01 0x00 0x00 0x00 0x00 0x00 0x00

dut_mode_send 0x3f 0x0228 0x01

I can “tinyplay 48kstereo.wav” and simultaneously “tinycap recorded.wav -r 48000” and that is able to produce a file “recorded.wav” which plays back correctly on my laptop. The only problem with the output file is that it is missing channel 2, but I think that is just the channel 2 offset. The file contains channel 2, but its just blank, so I think that one side or the other is pulling the extra bits from after the first channel sample and before the second channel starts. I’ve tried changing the second channel offsets from 0x11 to 0x21, but that didn’t work, I’m not stressing over that yet, since its not too important right now.

Since the 48k stereo loopback test is successful (on first channel), I think that must mean that the host and device are properly aligned.

So I believe that I am facing two problems right now;

  1. sample rate,
  2. number of channels.

Because bluetooth HFP/SCO wants 8 kHz / 16 bit / 1 channel, and the CPU i2s is set for 48 kHz / 16 bit / 2 channel. While I have adjusted the hisi-i2s.c to “accept” 8 kHz / 1 channel, these changes don’t actually make it to the hardware (fs clock remains at 48 kHz).

So to try to work around this, I’ve tried switching to using the BT as the i2s master instead of the CPU. This generates the proper fs clock, but I’m not sure what’s going on on the CPU side. It may not know how to handle that. The CPU side also still thinks its being sent 2 channels.

In any case, yes, I can transfer “something” over the bluetooth, which I can hear, but is completely garbled.
With the BT in master mode 8 kHz, the logic analyzer shows output that looks ok to my eye (bit clock 3 MHz, fs clock 8 kHz, 16 bit data samples only on channel 1) but tinycap errors out if I try to capture 1-channel, and captures data on both channels if I try to capture stereo (and it jumps between channels – a few samples on channel 1, then a few samples on channel 2, then a few on 1, then 2, etc.), which doesn’t make any sense at all.

Now here is the really interesting thing; I’ve been comparing the i2s driver for this board with the one for the 6210. Looks like ours is a lot more primitive. The *_hw_params function in the 6210’s driver sets up fs clock and channel count, ours doesn’t. And even worse, there is no register documentation to figure this out with.

Unfortunately, my kid knocked my arm while I was trying to read the i2s with the logic analyzer, and something got cooked – board is completely unresponsive, so I’m dead in the water until a new board arrives on tuesday (hopefully).


Nice wave, huh?
Now that I’ve opened the captured data (this data was actually transferred over HFP in a phone call), its pretty clear what is happening here…

Little bit of good data, followed by a blank segment, followed by a little bit more good data, and back and forth. So what is happening here, is that its NOT resampling the audio when passing it to the i2s that is running at 48 kHz. When it gets a bit of data, it writes it, then it just sits there waiting for more data. I could repair the data fairly easily on the incoming side, but outgoing will be a totally different story.

The distortions I was getting in the other direction was likely the opposite problem… having too many audio samples, and just dropping the extras.

So next step is to get the loopback test working at 8 and[/or?] 16 kHz.


And just a quick (and totally successful) sanity check… (based on my calculations, yes, I actually am sane!)

I wrote a little C program to read in the captured wav file and delete all of the repetitive non-zero samples, and write it out to a new wav file. Opened the new wav file in Audacity and slowed it way down, and ended up with perfectly clear audio.


With the help of some production kirin960 code (which actually has very descriptive comments!), I think I’m starting to understand the code a bit better.

Clock dividers:
BCLK: https://android.googlesource.com/kernel/hikey-linaro/+/android-hikey-linaro-4.9/sound/soc/hisilicon/hisi-i2s.h#82
FSCLK: https://android.googlesource.com/kernel/hikey-linaro/+/android-hikey-linaro-4.9/sound/soc/hisilicon/hisi-i2s.h#83

I think the proper approach is to change the bit clock. The production code is showing BCLK divisor of 0x17 and FS divisor of 0x3F to land at 1.024 MHz bit clock and 16 kHz FS clock. The FS divisor is already set at 3F, so I’ll just change to
#define HI_ASP_CFG_R_CLK4_DIV_SEL 0x00ff0017
and should end up where I need to be.
EDIT: Nope. That got me closer, but not the right place. Forgot to compensate for the fact that the master clock is running twice as fast on this board as it is on the production board. I got the 1 MHz bit clock with CLK4 DIV of 0x00ff002f.

Still need to find where to tell it 1 channel only.


I’ve got a successful 16k stereo loopback test.
HFP test is getting closer.
Got squirrel voices making it through now. It is semi decipherable.
It makes the same broken wave as when set to 48 kHz, BUT, the good to broken ratio is now 1:1, which tells me that the sample rate is now twice what it should be (8 kHz, as anticipated).

I should have a successful HFP call very shortly. BUT, still don’t know how to set the thing to MONO, which means that I’m stuck with playing a stereo file (obviously only the left channel is transmitted), and recording a stereo file (the right channel is blank). If I try to work in mono, tinyplay/tinycap crashes.