Introduction
RFCOMM provides an emulation of serial cable line settings and status of an RS-232 serial port. RFCOMM is a simple transport protocol over L2CAP. So the protocol could supports up to 60 simultaneous connections between two Bluetooth devices.(The number of connections that may be used simultaneously in a Bluetooth device depends on the implementation). It does not have any ability to correct errors. The reliability relies on Bluetooth baseband.(RFCOMM's flow control also uses the basband's capabilities.)
RFCOMM data rate is not configurable with the typical baud rate, e.g. 19200. RFCOMM will transfer the data as fast as it can. The actual data rate will vary, and what the highest data rate is can be a complicated issue.(see the previous article <How Fast is Bluetooth?>) RFCOMM data rates will be limited in devices where there is a physical serial port involved(Type 2 devices). Implementations may optionally pace data on virtual serial ports(in Type 1 devices). Figure 1 shows the two device types that the RFCOMM protocol supports.
Figure 1: Type 1 and Type 2 RFCOMM devices
Type 1 devices are communication endpoints such as smartphones and computers. Type 2 devices are port of a communication segment; e.g. modems and development boards with UART.
If there is a bottleneck of data rate, you can try to change MTU on L2CAP. RFCOMM communicates with frames. The RFCOMM frames become the data payload in L2CAP packets. The size of data field in RFCOMM frames could be an integral number of bytes in the data up to 32767. The size limit is set by the Maximum Transmission Unit(MTU) on L2CAP packets, so if a system has a smaller L2CAP MTU, the size of RFCOMM data will also be restricted.
RFCOMM API in Android
Overview
In most of systems, RFCOMM will be part of a port driver that includes a serial Port Emulation Entity. Figure 2 shows a model of how RFCOMM fits into a typical system.
Figure 2: RFCOMM reference model
The Port Emulation Entity maps a system-specific communication interface (API) to the RFCOMM services. The port emulation entity plus RFCOMM make up a port driver. In Android, BluetoothSocket is the port emulation entity. The interface for Bluetooth Sockets is similar to that of TCP sockets. Once the socket is connected, open the IO streams by calling getInputStream() and getOutStream() in order to retrieve InputStream and OutputStream objects for reading and writing. There are 5 classes for RFCOMM in Android:
- BluetoothDevice: create BluetoothSocket or BluetoothServerSocket.
- BluetoothSocket: manager the connection.
- BluetoothServerSocket: create a listening server socket. When a connection is accepted, it will return a new BluetoothSocket.
- InputStream: read data from the network.
- OutputStream: write data to the network.
Writing Process
Figure 3 shows the flow of writing data to RFCOMM connection. The focus is in bluedorid which implements RFCOMM. The code above bluedroid just provide a socket interface to application.
Figure 3: Flow chart of RFCOMM writing
The application calls OutputStream::write() to send data to RFCOMM connection in bluedroid via a Linux socket. Bluedroid creates a thread to read data from that Linux socket. PORT_WriteDataCO() and rfc_send_buf_uih() are the most important functions in bluedroid. The former splits RFCOMM data filed into L2CAP payloads which are not more than MTU (A sample in <How Fast is Bluetooth?> describes the process), and see the key code below.
if (p_port->peer_mtu < length)
length = p_port->peer_mtu;
if (available < (int)length)
length = (UINT16)available;
p_buf->len = length;
p_buf->event = BT_EVT_TO_BTU_SP_DATA;
if(p_port->p_data_co_callback(handle, (UINT8 *)(p_buf + 1) + p_buf->offset, length,
DATA_CO_CALLBACK_TYPE_OUTGOING) == FALSE)
{
error("p_data_co_callback DATA_CO_CALLBACK_TYPE_OUTGOING failed, length:%d", length);
return (PORT_UNKNOWN_ERROR);
}
RFCOMM_TRACE_EVENT ("PORT_WriteData %d bytes", length);
rc = port_write (p_port, p_buf);
The latter builds a UIH frame which is used to carry user data and calls L2CA_DataWrite() to sned data to L2CAP layer, see the code below.
p_data = (UINT8 *)(p_buf + 1) + p_buf->offset;
/* UIH frame, command, PF = 0, dlci */
*p_data++ = RFCOMM_EA | cr | (dlci << RFCOMM_SHIFT_DLCI);
*p_data++ = RFCOMM_UIH | ((credits) ? RFCOMM_PF : 0);
if (p_buf->len <= 127)
{
*p_data++ = RFCOMM_EA | (p_buf->len << 1);
p_buf->len += 3;
}
else
{
*p_data++ = (p_buf->len & 0x7f) << 1;
*p_data++ = p_buf->len >> RFCOMM_SHIFT_LENGTH2;
p_buf->len += 4;
}
if (credits)
{
*p_data++ = credits;
p_buf->len++;
}
p_data = (UINT8 *)(p_buf + 1) + p_buf->offset + p_buf->len++;
*p_data = RFCOMM_UIH_FCS ((UINT8 *)(p_buf + 1) + p_buf->offset, dlci);
The payload of L2CAP may not be filled completely. So the inappropriate MTU decrease the data rate.
Determine MTU
MTU could be configured in the procedure of establishing a RFCOMM connection. Figure 4 shows the message sequence for the procedure.
Figure 4: Message sequence chart for establishing a RFCOMM connection
PN command and PN response are used to configure the new connection parameters which include MTU. But the parameters negotiation is optional. Serial Port Profile does not impose any restrictions on MTU sizes over the restrictions stated in L2CAP. MTU is not a negotiated value, it is an informational parameter that each device can specify independently. It indicates to the remote device that the local device can receive, in this channel, an MTU larger than the minimum required.
The implementation of RFCOMM in bluedroid supports the procedure of parameters negotiation.
When initiating RFCOMM, RFCOMM_CreateConnection() is called with the parameter UINT16 mtu=BTA_JV_DEF_RFC_MTU. This function will check if the size is greater than the capability of L2CAP.
/* If the MTU is not specified (0), keep MTU decision until the
* PN frame has to be send
* at that time connection should be established and we
* will know for sure our prefered MTU
*/
rfcomm_mtu = L2CAP_MTU_SIZE - RFCOMM_DATA_OVERHEAD;
if (mtu)
p_port->mtu = (mtu < rfcomm_mtu) ? mtu : rfcomm_mtu;
else
p_port->mtu = rfcomm_mtu;
If application did not setup something, port_select_mtu() will select MTU.
/* Will select MTU only if application did not setup something */
if (p_port->mtu == 0)
{
/* find packet size which connection supports */
packet_size = btm_get_max_packet_size (p_port->bd_addr);
if (packet_size == 0)
{
/* something is very wrong */
RFCOMM_TRACE_WARNING ("port_select_mtu bad packet size");
p_port->mtu = RFCOMM_DEFAULT_MTU;
}
else
{
/* We try to negotiate MTU that each packet can be split into whole
number of max packets. For example if link is 1.2 max packet size is 339 bytes.
At first calculate how many whole packets it is. MAX L2CAP is 1691 + 4 overhead.
1695, that will be 5 Dh5 packets. Now maximum RFCOMM packet is
5 * 339 = 1695. Minus 4 bytes L2CAP header 1691. Minus RFCOMM 6 bytes header overhead 1685
For EDR 2.0 packet size is 1027. So we better send RFCOMM packet as 1 3DH5 packet
1 * 1027 = 1027. Minus 4 bytes L2CAP header 1023. Minus RFCOMM 6 bytes header overhead 1017 */
if ((L2CAP_MTU_SIZE + L2CAP_PKT_OVERHEAD) >= packet_size)
{
p_port->mtu = ((L2CAP_MTU_SIZE + L2CAP_PKT_OVERHEAD) / packet_size * packet_size) - RFCOMM_DATA_OVERHEAD - L2CAP_PKT_OVERHEAD;
RFCOMM_TRACE_DEBUG ("port_select_mtu selected %d based on connection speed", p_port->mtu);
}
else
{
p_port->mtu = L2CAP_MTU_SIZE - RFCOMM_DATA_OVERHEAD;
RFCOMM_TRACE_DEBUG ("port_select_mtu selected %d based on l2cap PDU size", p_port->mtu);
}
}
}
Finally, RFCOMM_ParNegReq() starts DLC parameter negotiation.
When responding RFCOMM, RFCOMM_BufDataInd() is called by L2CAP to parse the frames. If the frame is a multiplexer control message, rfc_process_mx_message() will be called to process UIH frame on the multiplexer control channel.(UIH frames on DLCI = 0 are used to send control messages. UIH frames on DLCIs != 0 are used to send data.) The function parses PN frame, see the code below:
case RFCOMM_MX_PN:
if (length != RFCOMM_MX_PN_LEN)
break;
p_rx_frame->dlci = *p_data++ & RFCOMM_PN_DLCI_MASK;
p_rx_frame->u.pn.frame_type = *p_data & RFCOMM_PN_FRAME_TYPE_MASK;
p_rx_frame->u.pn.conv_layer = *p_data++ & RFCOMM_PN_CONV_LAYER_MASK;
p_rx_frame->u.pn.priority = *p_data++ & RFCOMM_PN_PRIORITY_MASK;
p_rx_frame->u.pn.t1 = *p_data++;
p_rx_frame->u.pn.mtu = *p_data + (*(p_data + 1) << 8);
p_data += 2;
p_rx_frame->u.pn.n2 = *p_data++;
p_rx_frame->u.pn.k = *p_data++ & RFCOMM_PN_K_MASK;
if (!p_rx_frame->dlci
|| !RFCOMM_VALID_DLCI (p_rx_frame->dlci)
|| (p_rx_frame->u.pn.mtu < RFCOMM_MIN_MTU)
|| (p_rx_frame->u.pn.mtu > RFCOMM_MAX_MTU))
{
RFCOMM_TRACE_ERROR ("Bad PN frame");
break;
}
Then rfc_process_pn() is called to handle DLC parameters negotiation frame. It records MTU and pass indication to the upper layer. PORT_ParNegInd() is used to select a preferred MTU and change it.
/* Connection is up and we know local and remote features, select MTU */
port_select_mtu (p_port);
p_port->rfc.p_mcb = p_mcb;
p_port->mtu = (p_port->mtu < mtu) ? p_port->mtu : mtu;
p_port->peer_mtu = p_port->mtu;
Finally, RFCOMM_ParNegRsp() is called to acknowledge DLC parameters negotiation.