Turning a Raspberry Pi Zero into a Bluetooth Audio Receiver (Fixing SCO Mapping)#
I recently wanted a compact Bluetooth receiver for my car so I could stream music from my phone to the AUX input. A Raspberry Pi Zero 2 W (or Zero W) is ideal: inexpensive, small, and equipped with onboard Bluetooth.
Beyond music streaming, this setup also serves as a car alarm and tracker. A 4G modem provides connectivity, allowing the Pi to send alerts and GPS location data. The project is still evolving, with plans to interface directly with the car’s CAN bus using an MCP2515 controller to monitor vehicle status and enable more advanced functionality.
However, getting Bluetooth audio working reliably on a headless Raspberry Pi OS Lite system turned into a debugging challenge.
The root cause was that the Bluetooth SCO audio path was mapped to a different interface, preventing audio from reaching the expected output. Fixing this required sending vendor-specific HCI commands to the Broadcom controller.
Note: On Raspberry Pi Zero W and Zero 2 W, this may work out of the box if a PCM interface is connected directly to speakers and a microphone via GPIO. In such cases, these HCI commands are not required.
Testing on a Raspberry Pi 5 showed that no additional configuration is needed—bluetooth audio works correctly without issuing HCI commands.
Hardware Setup#
The system consists of:
- Raspberry Pi Zero 2 W – main controller handling Bluetooth audio, connectivity, and system logic
- USB audio card – provides microphone input and AUX output
- 4G HAT SIM7600G-H (B) – LTE connectivity and GPS
- External USB Wi-Fi dongle – creates a hotspot to share the 4G connect
Bluetooth Audio Profiles: A2DP, HFP, and HSP#
Three Bluetooth profiles are available: A2DP handles music only and doesn’t support a microphone, HSP is an outdated profile and provides basic bidirectional audio, and HFP is the hands-free profile for calls with bidirectional audio that we need to configure on the Raspberry Pi.
A2DP (Advanced Audio Distribution Profile)#
- Uses ACL links (Asynchronous Connection-Less).
- Designed for high-quality, one-way audio streaming (music).
- Only sends audio from the source (phone) to the sink (Raspberry Pi or Bluetooth speaker).
- Transmits stereo audio at higher quality (SBC, aptX, or LDAC).
- Does not support microphone input, making it unsuitable for phone calls.
HFP (Hands-Free Profile)#
- Uses SCO links for low-latency audio.
- Designed for hands-free calling.
- Supports bidirectional audio, carrying both the microphone input and speaker output.
- Can be routed via Transport (HCI), PCM, Codec, or I2S (PCM/I2S require an external codec)
- Modern smartphones typically prefer HFP over HSP because it supports additional features like caller ID, call control, and wideband audio (mSBC).
HSP (Headset Profile)#
- Older profile primarily for simple headsets.
- Uses SCO, but has fewer features than HFP (no call control, no wideband audio).
- Supports basic bidirectional audio (mic + speaker).
- Some devices still fall back to HSP if HFP is unsupported.
Audio Servers#
Audio servers such as PulseAudio and PipeWire are responsible for routing and managing audio streams on Linux. Unlike telephony stacks, they do not implement call control or modem functionality.
Limitations of audio servers are:
- Cannot answer or hang up calls.
- Do not provide caller ID or telephony signaling.
Modern setups prefer PipeWire, which:
- Eliminates the need for separate telephony stacks for audio.
- Integrates HFP, HSP, and A2DP audio routing directly into the server.
- Succeeds PulseAudio and builds upon its capabilities.
Telephony Stacks#
Before PipeWire became the standard, handling Bluetooth HFP required dedicated telephony stacks:
- ofono – a full telephony stack providing HFP support.
- Pros: Complete HFP implementation.
- Cons: Conflicts with ModemManager, which is used for LTE/4G modems.
- hsphfpd – a simpler daemon providing HSP/HFP audio support.
- Pros: Lightweight, easier to configure for audio-only use.
- Cons: Limited features.
Audio Streaming with A2DP#
Before discussing SCO (Synchronous Connection-Oriented) on HFP (Hands-Free Profile), let’s talk about A2DP (Advanced Audio Distribution Profile), which handles high-quality music streaming over Bluetooth.
It is one-way only—it streams audio from the source (e.g., a phone) to the sink (the Raspberry Pi) without carrying microphone input. This makes it perfect for music but unsuitable for phone calls.
The audio is carried over ACL (Asynchronous Connection-Less) packets, which are delivered to the host CPU via the HCI (Host Controller Interface). This allows the Raspberry Pi to decode the audio in software using PipeWire or ALSA.
While A2DP works well for streaming music, handling phone calls is a completely different challenge. Unlike music, calls require bidirectional audio, low latency, and proper handling of the microphone input, which is where SCO (Synchronous Connection-Oriented) comes in.
Audio Interfaces: SCO Routing (Synchronous Connection-Oriented)#
While A2DP handles high-quality, one-way music streaming, phone calls require bidirectional, low-latency audio, which is where SCO (Synchronous Connection-Oriented) comes in.
- SCO is a Bluetooth link specifically designed for voice channels.
- It’s used by HSP/HFP headsets and hands-free calling.
- Unlike A2DP, SCO carries both microphone input and speaker output in real time.
On the Raspberry Pi Zero, SCO audio can be delivered in several ways:
- PCM
- SCO audio is output as PCM frames via the Broadcom chip’s PCM/I2S interface.
- Transport
- SCO audio is encapsulated in HCI packets and delivered directly to the host OS.
- Codec
- Typically used for internal or proprietary processing paths.
- I2S
- SCO audio is routed over GPIO to an external audio codec or MEMS device.
The Actual Problem#
On the Raspberry Pi Zero, the onboard Broadcom BCM43438 Bluetooth controller is configured to route SCO audio over PCM instead of HCI/Transport.
- The Linux audio stack (BlueZ → PipeWire/WirePlumber → ALSA → USB audio) expects SCO audio via HCI.
- With no PCM hardware connected, SCO audio is effectively lost.
Result:
- Bluetooth pairing works.
- A2DP audio works.
- HFP/HSP call audio is silent.
Solution:
- Send vendor-specific HCI commands to switch SCO routing from PCM to Transport.
- Once switched, SCO audio flows through the Linux audio stack to the USB audio device.
The PulseAudio documentation notes that some Broadcom controllers require hcitool commands to enable SCO audio. In practice:
- Audio from the phone → Raspberry Pi works.
- Microphone from Raspberry Pi → phone does not work.
Using the appropriate HCI commands allowed audio from the Pi to be heard during phone calls, but the microphone remained silent. This confirmed the issue was related to SCO routing rather than the audio server.
Disabling the additional PCM/I2S interfaces resolved the problem.
Broadcom Documentation Availability#
Official Broadcom documentation for the Raspberry Pi’s onboard Bluetooth controller exists, but it is difficult to find and often incomplete.
The SCO/HFP configurations, commands, and insights presented here were derived from:
- Broadcom, Cypress, Infineon datasheets
- Open-source repositories
- Community forums
- A lot of trial, error, and debugging!
The Raspberry Pi Zero W uses the Broadcom BCM43438, while the Zero 2 W uses the BCM43439. Both chipsets’ firmware expect SCO audio to be routed over either PCM, Transport, Codec or I2S interfaces.
Fixing the Controller Configuration#
The Broadcom Bluetooth controller on the Raspberry Pi Zero (BCM43438 / BCM43439) requires vendor-specific HCI commands to properly route SCO/HFP audio.
The commands below are used to read current configuration.
# Read SCO status
hcitool -i hci0 cmd 0x3F 0x001D
# Example output:
# 01 1D FC 00 00 02 00 01 01
# Read PCM interface parameters
hcitool -i hci0 cmd 0x3F 0x001F
# Example output:
# 01 1F FC 00 00 00 00 03 00
# Read I2S/PCM interface configuration
hcitool -i hci0 cmd 0x3F 0x6E
# Example output:
# 01 6E FC 00 2E 00 00 00 00 00 00 00HCI Responses – Byte Mapping#
Read SCO Status (0x001D)#
Read value:
01 1D FC 00 00 02 00 01 01Byte: 1 2 3 4 5 6 7 8
| | | | | | | |
Value: 01 1D FC 00 00 02 00 01 01
Meaning:
01 → HCI Command Complete packet indicator
1D FC → Command opcode (vendor-specific)
00 → Status (0 = success)
00 → SCO routing (00 = PCM, 01 = Transport, 02 = Coded, 03= I2S)
02 → Interface rate
00 → Frame type
01 → Sync mode
01 → Clock modeRead PCM Interface Parameters (0x001F)#
Read value:
01 1F FC 00 00 00 00 03 00Byte: 1 2 3 4 5 6 7 8 9
| | | | | | | |
Value: 01 1F FC 00 00 00 00 03 00
Meaning:
01 → HCI packet indicator
1F FC → Command opcode
00 → Status
00 → PCM interface enable
00 → PCM interface mode
00 → PCM sync & clock
03 → PCM word length
00 → ReservedRead I2S/PCM Interface Configuration (0x6E)#
Read value:
01 6E FC 00 2E 00 00 00 00 00 00 00Byte: 1 2 3 4 5 6 7 8 9 10 11 12
| | | | | | | | | | | |
Value: 01 6E FC 00 2E 00 00 00 00 00 00 00
Meaning:
01 → HCI packet indicator
6E FC → Command opcode
00 → Status
2E → Interface features flags (I2S enabled, PCM enabled, master mode, ...)
00 → Clock configuration
00 → Word length
00 00 00 00 → Features
00 → ReservedWriting Configuration#
After inspecting the default configuration, the following commands reconfigure the controller to ensure SCO audio is properly routed to the Linux audio stack:
# Initialize SCO/PCM interface
hcitool -i hci0 cmd 0x3F 0x001C 0x01 0x02 0x00 0x01 0x01
# Configure PCM data format
hcitool -i hci0 cmd 0x3F 0x001E 0x00 0x00 0x00 0x00 0x00
# Configure I2S/PCM interface (clocking and routing)
hcitool -i hci0 cmd 0x3F 0x6D 0x00 0x00 0x00 0x00PipeWire Setup (Headless Raspberry Pi OS Lite)#
Install required packages:
apt install -y bluetooth pipewire wireplumber libspa-0.2-bluetooth pipewire-audio-client-librariesEnable Bluetooth:
systemctl enable --now bluetoothEnable PipeWire and WirePlumber for the target user (update SSH_USER to match your username):
SSH_USER="user"
systemctl --machine="${SSH_USER}"@.host --user enable --now pipewire
systemctl --machine="${SSH_USER}"@.host --user enable --now wireplumberOn a headless Raspberry Pi, it is needed to tweak WirePlumber to avoid unnecessary GUI/seat checks and to set sane defaults.
By default, WirePlumber manages “seats” (groupings of input/output devices) and may wait for a graphical session to become available. On headless systems, this behavior can delay startup or interfere with default audio routing.
Available profiles are defined in /usr/share/wireplumber/wireplumber.conf, with the “main” profile used by default. Alternative profiles that disable seat management—such as “main-systemwide” and “main-embedded”—are also available.
The “main-embedded” profile does not persist state, meaning settings such as volume levels are not retained across reboots.
Create a systemd override at /etc/systemd/user/wireplumber.service.d/override.conf:
[Service]
ExecStart=
ExecStart=/usr/bin/wireplumber -p main-embeddedAfter editing:
systemctl --user daemon-reexec
systemctl --user restart wireplumberSince the “main-embedded” profile does not persist state, default audio levels must be explicitly defined to ensure consistent behavior at startup.
Create /usr/share/wireplumber/wireplumber.conf.d/50-default-volume.conf:
wireplumber.settings = {
device.routes.default-sink-volume = 1
device.routes.default-source-volume = 1
}Create a systemd Service#
HCI commands must be issued at every boot to configure SCO/HFP properly.
Create /usr/local/bin/bt_init.sh with the following content:
#!/bin/bash
# Initialize SCO/PCM interface
hcitool -i hci0 cmd 0x3F 0x001C 0x01 0x02 0x00 0x01 0x01
# Configure PCM data format
hcitool -i hci0 cmd 0x3F 0x001E 0x00 0x00 0x00 0x00 0x00
# Configure I2S/PCM interface (clocking and routing)
hcitool -i hci0 cmd 0x3F 0x6D 0x00 0x00 0x00 0x00
# Disable sleep / keep SCO audio active
hcitool -i hci0 cmd 0x3F 0x0027 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00Create /etc/systemd/system/bt_init.service with the following content:
[Unit]
Description=Initialize Broadcom Bluetooth controller for SCO/HFP
After=bluetooth.target
Requires=bluetooth.target
[Service]
Type=oneshot
ExecStart=bash /usr/local/bin/bt_init.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.targetAfter creating the service, enable and start it with:
sudo systemctl enable --now bt_init.serviceFinal Result#
With the controller properly configured, PipeWire running, and a USB audio interface in place, the result is a compact, inexpensive, and reliable Bluetooth audio receiver for automotive use.
Links#
- https://www.infineon.com/part/CYW4343W
- https://android.googlesource.com/platform/system/bluetooth/+/7431056712256b077a51c8f85dbd3f44a3ea6a5b/brcm_patchram_plus/brcm_patchram_plus.c
- https://github.com/bluekitchen/btstack/blob/master/src/hci_cmd.h
- https://lkml.iu.edu/hypermail/linux/kernel/1806.1/01582.html
- https://community.infineon.com/gfawx74859/attachments/gfawx74859/WifiBTcombo/955/1/cypress%20vendor%20specific%20commands.pdf
- https://community.infineon.com/gfawx74859/attachments/gfawx74859/jpwifibtcombo/3247/1/BT_PCM_and_I2S_waveforms.pdf
