Bluetooth LE PHY

The BLE PHY (physical layer) defines how data is transmitted over the air. Bluetooth LE supports several PHY types: 1M PHY (mandatory), Coded PHY (longer range), 2M PHY (double speed), and Coded 2M PHY. Using the Bluetooth Framework on Windows 11 or newer, applications can read the currently active PHY of a BLE connection.

PHY is short for physical, as in the physical layer. This is the bottom layer of the BLE stack and is responsible for actually transmitting and receiving information over the air via radio waves. BLE transmits on the 2.4GHz band, and this band is a license-free band that is essentially free to use for short-range applications. It is also the only license-free band that is the same in every country.

  • 1 Megabit PHY, commonly referred to as 1M PHY, has been the de facto Bluetooth PHY up to this point. The name refers to the bit rate that this PHY is capable of. This is the PHY that was discussed in detail in the previous section. Support for this PHY is mandatory to maintain backward compatibility with all non-5.0 devices.
  • Coded PHY is a new PHY configuration introduced in Bluetooth 5.0. The purpose of this configuration is to increase the maximum range without increasing transmit power. This sounds contradictory at first glance, but it makes more sense when framed in terms of the BER. Namely, increased range can be thought of as a side effect of reduced error rate. Coded PHY provides enhanced bit error detection and correction, allowing further range while staying within the maximum BER. This is achieved at a cost to data rate, as it increases the number of symbols per bit (with possible values of either 2 or 8).
  • 2 Megabit PHY (2M PHY) is also a new PHY configuration introduced in Bluetooth 5.0. The purpose of this configuration is to increase the symbol rate at the PHY layer. Specifically, it achieves a symbol rate of 2 Megasymbols per second, where each symbol corresponds to a single bit. This allows a user to double the number of bits sent over the air during a given period, or conversely reduce energy consumption for a given amount of data by halving the necessary transmit time.
  • Coded 2M PHY is the mix of the Coded and 2M PHYs. Support for Coded and 2M PHY only exists within Blouetooth 5.0, and even then support is optional.

If an application that uses the Bluetooth Framework runs on Windows 11 or a newer OS, it can read the current Bluetooth LE connection PHY. The Bluetooth Framework includes sample applications that show how to get information about currently used connection PHY. If your application acts as a GATT client (central), refer to the GattClient sample application. If your application acts as a GATT server (peripheral), take a look at the GattServer sample application.


Reading Connection PHY

From a GattClient (Central Role)

The wclGattClient class provides the GetConnectionPhyInfo method and the OnConnectionPhyChanged event to track the active PHY. The example below calls GetConnectionPhyInfo and displays the transmit and receive PHY flags (Coded, 1M, 2M).



procedure TfmMain.GetConnectionPhy;
var
	Phy: TwclBluetoothLeConnectionPhy;
begin
	if wclGattClient.GetConnectionPhyInfo(Phy) = WCL_E_SUCCESS then begin
		TraceEvent(0, 'Transmit PHY', '', '');
		TraceEvent(0, '', 'Coded', BoolToStr(Phy.Transmit.IsCoded, True));
		TraceEvent(0, '', '1M', BoolToStr(Phy.Transmit.IsUncoded1MPhy, True));
		TraceEvent(0, '', '2M', BoolToStr(Phy.Transmit.IsUncoded2MPhy, True));

		TraceEvent(0, 'Receive PHY', '', '');
		TraceEvent(0, '', 'Coded', BoolToStr(Phy.Receive.IsCoded, True));
		TraceEvent(0, '', '1M', BoolToStr(Phy.Receive.IsUncoded1MPhy, True));
		TraceEvent(0, '', '2M', BoolToStr(Phy.Receive.IsUncoded2MPhy, True));
	end;
end;

procedure TfmMain.wclGattClientConnectionPhyChanged(Sender: TObject);
begin
	TraceEvent(wclGattClient.Address, 'Connection PHY changed', '', '');
	GetConnectionPhy;
end;
                        

void TfmMain::GetConnectionPhy()
{
	TwclBluetoothLeConnectionPhy Phy;
	if (wclGattClient->GetConnectionPhyInfo(Phy) == WCL_E_SUCCESS)
	{
		TraceEvent(0, "Transmit PHY", "", "");
		TraceEvent(0, "", "Coded", BoolToStr(Phy.Transmit.IsCoded, true));
		TraceEvent(0, "", "1M", BoolToStr(Phy.Transmit.IsUncoded1MPhy, true));
		TraceEvent(0, "", "2M", BoolToStr(Phy.Transmit.IsUncoded2MPhy, true));

		TraceEvent(0, "Receive PHY", "", "");
		TraceEvent(0, "", "Coded", BoolToStr(Phy.Receive.IsCoded, true));
		TraceEvent(0, "", "1M", BoolToStr(Phy.Receive.IsUncoded1MPhy, true));
		TraceEvent(0, "", "2M", BoolToStr(Phy.Receive.IsUncoded2MPhy, true));
	}
}

void __fastcall TfmMain::wclGattClientConnectionPhyChanged(TObject *Sender)
{
	TraceEvent(wclGattClient->Address, "Connection PHY changed", "", "");
	GetConnectionPhy();
}
                        

private void GetConnectionPhy()
{
	wclBluetoothLeConnectionPhy Phy;
	if (Client.GetConnectionPhyInfo(out Phy) == wclErrors.WCL_E_SUCCESS)
	{
		TraceEvent(0, "Transmit PHY", "", "");
		TraceEvent(0, "", "Coded", Phy.Transmit.IsCoded.ToString());
		TraceEvent(0, "", "1M", Phy.Transmit.IsUncoded1MPhy.ToString());
		TraceEvent(0, "", "2M", Phy.Transmit.IsUncoded2MPhy.ToString());

		TraceEvent(0, "Receive PHY", "", "");
		TraceEvent(0, "", "Coded", Phy.Receive.IsCoded.ToString());
		TraceEvent(0, "", "1M", Phy.Receive.IsUncoded1MPhy.ToString());
		TraceEvent(0, "", "2M", Phy.Receive.IsUncoded2MPhy.ToString());
	}
}

void Client_OnConnectionPhyChanged(Object sender, EventArgs e)
{
	TraceEvent(Client.Address, "Connection PHY changed", "", "");
	GetConnectionPhy();
}
                        

Private Sub GetConnectionPhy()
	Dim Phy As wclBluetoothLeConnectionPhy
	If Client.GetConnectionPhyInfo(Phy) = wclErrors.WCL_E_SUCCESS Then
		TraceEvent(0, "Transmit PHY", "", "")
		TraceEvent(0, "", "Coded", Phy.Transmit.IsCoded.ToString())
		TraceEvent(0, "", "1M", Phy.Transmit.IsUncoded1MPhy.ToString())
		TraceEvent(0, "", "2M", Phy.Transmit.IsUncoded2MPhy.ToString())

		TraceEvent(0, "Receive PHY", "", "")
		TraceEvent(0, "", "Coded", Phy.Receive.IsCoded.ToString())
		TraceEvent(0, "", "1M", Phy.Receive.IsUncoded1MPhy.ToString())
		TraceEvent(0, "", "2M", Phy.Receive.IsUncoded2MPhy.ToString())
	End If
End Sub

Private Sub Client_OnConnectionPhyChanged(sender As Object, e As EventArgs) Handles Client.OnConnectionPhyChanged
	TraceEvent(Client.Address, "Connection PHY changed", "", "")
	GetConnectionPhy()
End Sub
                        

void CGattClientDlg::GetConnectionPhy()
{
	wclBluetoothLeConnectionPhy Phy;
	if (wclGattClient.GetConnectionPhyInfo(Phy) == WCL_E_SUCCESS)
	{
		TraceEvent(0, _T("Transmit PHY"), _T(""), _T(""));
		TraceEvent(0, _T(""), _T("Coded"), BoolToStr(Phy.Transmit.IsCoded));
		TraceEvent(0, _T(""), _T("1M"), BoolToStr(Phy.Transmit.IsUncoded1MPhy));
		TraceEvent(0, _T(""), _T("2M"), BoolToStr(Phy.Transmit.IsUncoded2MPhy));

		TraceEvent(0, _T("Receive PHY"), _T(""), _T(""));
		TraceEvent(0, _T(""), _T("Coded"), BoolToStr(Phy.Receive.IsCoded));
		TraceEvent(0, _T(""), _T("1M"), BoolToStr(Phy.Receive.IsUncoded1MPhy));
		TraceEvent(0, _T(""), _T("2M"), BoolToStr(Phy.Receive.IsUncoded2MPhy));
	}
}

void CGattClientDlg::wclGattClientConnectionPhyChanged(void* Sender)
{
	UNREFERENCED_PARAMETER(Sender);

	TraceEvent(wclGattClient.Address, _T("Connection PHY changed"), _T(""), _T(""));
	GetConnectionPhy();
}
                        


From a GattServer (Peripheral Role)

When acting as a GATT server, each connected client is represented by a wclGattServerClient object. You can call its GetConnectionPhyInfo method and react to the OnConnectionPhyChanged event to obtain the same PHY information.



procedure TfmMain.UpdatePhy(const Client: TwclGattServerClient);
var
	Phy: TwclBluetoothLeConnectionPhy;
begin
	if Client.GetConnectionPhyInfo(Phy) = WCL_E_SUCCESS then begin
		Log('Transmit: Coded=' + BoolToStr(Phy.Transmit.IsCoded, True) +
			', 1M=' + BoolToStr(Phy.Transmit.IsUncoded1MPhy, True) +
			', 2M=' + BoolToStr(Phy.Transmit.IsUncoded2MPhy, True));
		Log('Receive: Coded=' + BoolToStr(Phy.Receive.IsCoded, True) +
			', 1M=' + BoolToStr(Phy.Receive.IsUncoded1MPhy, True) +
			', 2M=' + BoolToStr(Phy.Receive.IsUncoded2MPhy, True));
	end;
end;

procedure TfmMain.wclGattServerConnectionPhyChanged(Sender: TObject; const Client: TwclGattServerClient);
begin
	Trace('PHY changed: ' + IntToHex(Client.Address, 12));
	UpdatePhy(Client);
end;
                        

void TfmMain::UpdatePhy(TwclGattServerClient* const Client)
{
	TwclBluetoothLeConnectionPhy Phy;
	if (Client->GetConnectionPhyInfo(Phy) == WCL_E_SUCCESS)
	{
		Log("Transmit: Coded=" + BoolToStr(Phy.Transmit.IsCoded) +
			", 1M=" + BoolToStr(Phy.Transmit.IsUncoded1MPhy) +
			", 2M=" + BoolToStr(Phy.Transmit.IsUncoded2MPhy));
		Log("Receive: Coded=" + BoolToStr(Phy.Receive.IsCoded) +
			", 1M=" + BoolToStr(Phy.Receive.IsUncoded1MPhy) +
			", 2M=" + BoolToStr(Phy.Receive.IsUncoded2MPhy));
	}
}

void __fastcall TfmMain::wclGattServerConnectionPhyChanged(TObject *Sender, const TwclGattServerClient *Client)
{
	Trace("PHY changed: " + IntToHex(Client->Address, 12));
	UpdatePhy((TwclGattServerClient*)Client);
}
                        

private void UpdatePhy(wclGattServerClient Client)
{
	wclBluetoothLeConnectionPhy Phy;
	if (Client.GetConnectionPhyInfo(out Phy) == wclErrors.WCL_E_SUCCESS)
	{
		Log("Transmit: Coded=" + Phy.Transmit.IsCoded.ToString() +
			", 1M=" + Phy.Transmit.IsUncoded1MPhy.ToString() +
			", 2M=" + Phy.Transmit.IsUncoded2MPhy.ToString());
		Log("Receive: Coded=" + Phy.Receive.IsCoded.ToString() +
			", 1M=" + Phy.Receive.IsUncoded1MPhy.ToString() +
			", 2M=" + Phy.Receive.IsUncoded2MPhy.ToString());
	}
}

void GattServerConnectionPhyChanged(Object sender, wclGattServerClient Client)
{
	Trace("PHY changed: " + Client.Address.ToString("X12"));
	UpdatePhy(Client);
}
                        

Private Sub UpdatePhy(Client As wclGattServerClient)
	Dim Phy As wclBluetoothLeConnectionPhy
	If Client.GetConnectionPhyInfo(Phy) = wclErrors.WCL_E_SUCCESS Then
		Log("Transmit: Coded=" & Phy.Transmit.IsCoded.ToString() &
			", 1M=" & Phy.Transmit.IsUncoded1MPhy.ToString() &
			", 2M=" & Phy.Transmit.IsUncoded2MPhy.ToString())
		Log("Receive: Coded=" & Phy.Receive.IsCoded.ToString() &
			", 1M=" & Phy.Receive.IsUncoded1MPhy.ToString() &
			", 2M=" & Phy.Receive.IsUncoded2MPhy.ToString())
	End If
End Sub

Private Sub FServer_OnConnectionPhyChanged(Sender As Object, Client As wclGattServerClient) Handles FServer.OnConnectionPhyChanged
	Trace("PHY changed: " & Client.Address.ToString("X12"))
	UpdatePhy(Client)
End Sub
                        

void CGattServerDlg::UpdatePhy(CwclGattServerClient* const Client)
{
	wclBluetoothLeConnectionPhy Phy;
	if (Client->GetConnectionPhyInfo(Phy) == WCL_E_SUCCESS)
	{
		CString Str;
		Str.Format(_T("Transmit: Coded=%s, 1M=%s, 2M=%s"),
			BoolToStr(Phy.Transmit.IsCoded),
			BoolToStr(Phy.Transmit.IsUncoded1MPhy),
			BoolToStr(Phy.Transmit.IsUncoded2MPhy));
		Log(Str);

		Str.Format(_T("Receive: Coded=%s, 1M=%s, 2M=%s"),
			BoolToStr(Phy.Receive.IsCoded),
			BoolToStr(Phy.Receive.IsUncoded1MPhy),
			BoolToStr(Phy.Receive.IsUncoded2MPhy));
		Log(Str);
	}
}

void CGattServerDlg::GattServerConnectionPhyChanged(void* Sender, CwclGattServerClient* const Client)
{
	UNREFERENCED_PARAMETER(Sender);

	Trace(_T("PHY changed: ") + IntToHex(Client->Address));
	UpdatePhy(Client);
}
                        

Frequently Asked Questions

What is Bluetooth LE PHY?
PHY stands for the physical layer - the bottom of the BLE stack that transmits and receives data over the air on the 2.4 GHz band.
What PHY types are available in BLE?
The main types are 1M PHY (mandatory, 1 Mb/s), Coded PHY (introduced in Bluetooth 5.0 for longer range), 2M PHY (double the speed), and Coded 2M PHY (mix of both).
How to read the current BLE connection PHY with Bluetooth Framework?
On Windows 11 or newer, use the GattClient sample for central role or GattServer sample for peripheral role. Both show how to retrieve the active PHY.