-
-
Notifications
You must be signed in to change notification settings - Fork 2
NCM Device
The source code can be found in the usb_ncm
-branch of the repo.
As NCM is a subtype of CDC, the device descriptor looks the same
Field | Value | Meaning |
---|---|---|
Class | 0x02 | Communication Device Class |
SubClass | 0x00 | Refer to interface for value |
Protocol | 0x00 | Refer to interface for value |
Attention: When using an IAD, you'll need to define the SubClass & Protocol correctly (ie. as in the control interface) and can't defer the resolution to the interface.
Similar to CDC, we need two Interfaces. One for configuration and status commands and the other for communication. In addition, the data interface needs to have two modes, one for normal communication and one without endpoints when it is shut down.
Field | Value | Meaning |
---|---|---|
Class | 0x02 | CDC |
SubClass | 0x0D | This is an NCM device |
Protocol | 0x00 | No encapsulated commands supported |
Just like a regular CDC device, this interface needs a functional descriptor to define the network capabilities and configuration.
Source: CDC120, Table 15
This header is the same for all CDC functional descriptors
Field | Size | Value | Meaning |
---|---|---|---|
Length | 1 | 5 | Length of the Header descriptor |
Descriptor Type | 1 | 0x24 | This descriptor belongs to an interface descriptor |
Descriptor Subtype | 1 | 0x00 | This is a header descriptor |
CDC-Version | 2 | 0x0110 | Version 1.1 of the CDC definition |
Source: CDC120, Table 16
This descriptor combines several interfaces into one functionality, in our case the notification and data interface.
Field | Size | Value | Meaning |
---|---|---|---|
Length | 1 | 5 | |
Descriptor Type | 1 | 0x24 | This descriptor belongs to an interface |
Descriptor SubType | 1 | 0x06 | This is a Union functional descriptor |
Management Interface ID | 1 | 0x00 | The ID of this interface, which contains all the declaration stuff and management endpoints |
Subinterface ID | 1 | 0x01 | The ID of the Data Interface that belongs to this CDC-Instance |
Source: ECM120, Table 3
This descriptor defines general ethernet capabilities and where to find the mac-address
Field | Size | Value | Meaning |
---|---|---|---|
Length | 1 | 13 | |
Descriptor Type | 1 | 0x24 | This descriptor belongs to an interface |
Descriptor SubType | 1 | 0x0F | This is an ethernet functional descriptor |
MAC-Address | 1 | 20 | The string index where to find the mac-address |
Ethernet Statistics | 4 | 0 | A bitmap of statistics that the device will keep track of |
Max Segment Size | 2 | 1514 | The maximum segment size this device can handle |
Number of MC Filters | 2 | 0 | The number of supported multicast filters on this device |
Number of Power Filters | 1 | 0 | The number of supported filters for causing a wake-up on the host |
Source: NCM10, Table 5-2
Defines some NCM specific functions and capabilities
Field | Size | Value | Meaning |
---|---|---|---|
Length | 1 | 6 | |
Descriptor Type | 1 | 0x24 | This descriptor belongs to an interface |
Descriptor SubType | 1 | 0x1A | This is an ethernet functional descriptor |
NCM Version | 2 | 0x0100 | The version of NCM that is supported |
Network Capabilities | 1 | 0b10000 | A bitmap of different functions this device may support. In this case we support 8 bytes for the InputSize-Request instead of just 4 |
The Control Interface needs one Notification Interrupt IN-Endpoint.
The Data-Interface needs two alternate modes, one without endpoints and one with an Bulk-IN & -OUT Endpoint.
Field | Value | Meaning |
---|---|---|
Class | 0x0A | Data Interface Class |
SubClass | 0x00 | Unused |
Protocol | 0x01 | This is a Network-Transfer-Block |
The second Data Interface needs an IN- and an OUT-Endpoint of type Bulk.
Here is an example of a complete working descriptor
Descriptor Configuration
static const USB_DESCRIPTOR_DEVICE DeviceDescriptor = {
.Length = 18,
.Type = 0x01,
.USBVersion = 0x0200,
.DeviceClass = 0x02,
.DeviceSubClass = 0x00,
.DeviceProtocol = 0x00,
.MaxPacketSize = 64,
.VendorID = 0x16C0,
.ProductID = 0x088B,
.DeviceVersion = 0x0100,
.strManufacturer = 1,
.strProduct = 2,
.strSerialNumber = 3,
.Configurations = 1};
static const USB_DESCRIPTOR_CONFIG ConfigDescriptor = {
.Length = 9,
.Type = 0x02,
.TotalLength = 86,
.Interfaces = 2,
.ConfigurationID = 1,
.strConfiguration = 0,
.Attributes = (1 << 7),
.MaxPower = 50};
static const USB_DESCRIPTOR_INTERFACE NCMInterface = {
.Length = 9,
.Type = 0x04,
.InterfaceID = 0,
.AlternateID = 0,
.Endpoints = 1,
.Class = 0x02,
.SubClass = 0x0D,
.Protocol = 0x00,
.strInterface = 4};
static const USB_DESC_FUNC_HEADER FuncHeader = {
.Length = 5,
.Type = CS_INTERFACE,
.SubType = FUNC_HEADER,
.CDCVersion = 0x0110};
static const USB_DESC_FUNC_UNION1 FuncUnion = {
.Length = 5,
.Type = CS_INTERFACE,
.SubType = FUNC_UNION,
.ControlInterface = 0,
.SubInterface0 = 1};
static const USB_DESC_FUNC_ECM FuncETH = {
.Length = 13,
.Type = CS_INTERFACE,
.SubType = FUNC_ECM,
.MaxSegmentSize = 1514,
.strMacAddress = 20};
static const USB_DESC_FUNC_NCM FuncNCM = {
.Length = 6,
.Type = CS_INTERFACE,
.SubType = FUNC_NCM,
.NcmVersion = 0x0100,
.NetworkCapabilities = 0b10000};
static const USB_DESCRIPTOR_INTERFACE DataInterfaces[2] = {
{
.Length = 9,
.Type = 0x04,
.InterfaceID = 1,
.AlternateID = 0,
.Endpoints = 0,
.Class = 0x0A,
.SubClass = 0x00,
.Protocol = 0x01,
.strInterface = 0,
},
{.Length = 9,
.Type = 0x04,
.InterfaceID = 1,
.AlternateID = 1,
.Endpoints = 2,
.Class = 0x0A,
.SubClass = 0x00,
.Protocol = 0x01,
.strInterface = 0}};
static const USB_DESCRIPTOR_ENDPOINT Endpoints[3] = {
{.Length = 7, // EP 1 for Control Interface 0: IN
.Type = 0x05,
.Address = (1 << 7) | 1,
.Attributes = 0x03,
.MaxPacketSize = 16,
.Interval = 50},
{.Length = 7, // EP 2 for Data Interface 1: OUT
.Type = 0x05,
.Address = 2,
.Attributes = 0x02,
.MaxPacketSize = 64,
.Interval = 0},
{.Length = 7, // EP 2 for Data Interface 1: IN
.Type = 0x05,
.Address = (1 << 7) | 2,
.Attributes = 0x02,
.MaxPacketSize = 64,
.Interval = 0}};
char *USB_GetConfigDescriptor(short *length) {
if (ConfigurationBuffer[0] == 0) {
short offset = 0;
AddToDescriptor(&ConfigDescriptor, &offset);
AddToDescriptor(&NCMInterface, &offset);
AddToDescriptor(&FuncHeader, &offset);
AddToDescriptor(&FuncUnion, &offset);
AddToDescriptor(&FuncETH, &offset);
AddToDescriptor(&FuncNCM, &offset);
AddToDescriptor(&Endpoints[0], &offset);
AddToDescriptor(&DataInterfaces[0], &offset);
AddToDescriptor(&DataInterfaces[1], &offset);
AddToDescriptor(&Endpoints[1], &offset);
AddToDescriptor(&Endpoints[2], &offset);
}
*length = sizeof(ConfigurationBuffer);
return ConfigurationBuffer;
}
char *USB_GetString(char index, short lcid, short *length) {
// Strings need to be in unicode (thus prefixed with u"...")
// The length is double the character count + 2 — or use VSCode which will show the number of bytes on hover
if (index == 1) {
*length = 20;
return u"Housemade";
} else if (index == 2) {
*length = 12;
return u"ArtNet-Node";
} else if (index == 3) {
*length = 22;
return u"01234-6786";
} else if (index == 4) {
*length = 28;
return u"DMX Interface";
} else if (index == 20) { // MAC-Address of the Host-side interface
*length = 26;
return u"445BBD24A371";
}
return 0;
}
The NCM-Class requires three communications:
- Advertising the NCM-Memory management using a Setup-Packet
- Advertising the LinkState using the notification endpoint
- The actual NCM-Communication
Let's start with the definition of the response which we'll put into ncm_device.h
(As usual, all usb structs need to be #pragma pack(1)
-ed)
#define NCM_GET_NTB_PARAMETERS 0x80
// NCM10 Table 6-3
typedef struct {
unsigned short Length;
unsigned short NtbFormatsSupported;
unsigned int NtbInMaxSize;
unsigned short NdpInDivisor;
unsigned short NdpInPayloadRemainder;
unsigned short NdpInAlignment;
unsigned short _reserved;
unsigned int NtbOutMaxSize;
unsigned short NdpOutDivisior;
unsigned short NdpOutPayloadRemainder;
unsigned short NdpOutAlignment;
unsigned short NtbOutMaxDatagrams;
} USB_NTB_PARAMS;
For a detailed explanation, see Chapter 6.2.1 in the NCM spec. TLDR; NtbFormatsSupported
tells the host how big an NCM packet can be at most. We are a small device, so we only support 16bit packets. The rest of the parameters control the data alignment inside a NCM-packet for the in & output. (In = Device to Host, Out = Host to Device). To not care about alignment too much, set Divisor
, Remainder
and Alignment
to 1
, 0
and 4
. Be aware that the host will reject your packets if they don't match your own alignment rules.
Next we'll put the specific parameters inside ncm_device.c
static const USB_NTB_PARAMS ntb_params = {
.Length = 0x1C,
.NtbFormatsSupported = 0b01,
.NtbInMaxSize = 2048,
.NdpInDivisor = 1,
.NdpInPayloadRemainder = 0,
.NdpInAlignment = 4,
.NtbOutMaxSize = 2048,
.NdpOutDivisior = 1,
.NdpOutPayloadRemainder = 0,
.NdpOutAlignment = 4,
.NtbOutMaxDatagrams = 10};
And lastly, we will respond to the matching setup packet
char NCM_SetupPacket(USB_SETUP_PACKET *setup, char *data, short length) {
switch (setup->Request) {
case NCM_GET_NTB_PARAMETERS: {
USB_Transmit(0, &ntb_params, sizeof(USB_NTB_PARAMS));
return USB_OK;
break;
}
}
return USB_ERR;
}
Next we need to accept some host configuration. The Host can tell us the maximum length of a packet and the maximum number of datagrams per packet for our transmissions and we need to keep to those limits. A max datagram of 0
means no limits. This will also be done via a Setup Packet. The length of that packet can vary, as sending the max datagrams is optional. So we need to check if it is requested/included.
#define NCM_SET_NTB_INPUT_SIZE 0x86
#define NCM_GET_NTB_INPUT_SIZE 0x85
typedef struct {
unsigned int NtbInMaxSize;
unsigned short NtbInMaxDatagrams;
unsigned short _reserved;
} USB_NTB_INPUT_SIZE;
static USB_NTB_INPUT_SIZE ntbInputSize = {
.NtbInMaxDatagrams = 0,
.NtbInMaxSize = 2048,
};
char NCM_SetupPacket(USB_SETUP_PACKET *setup, char *data, short length) {
switch (setup->Request) {
case NCM_GET_NTB_INPUT_SIZE: {
if (length == 4) {
USB_Transmit(0, &ntbInputSize, 4);
} else if (length == 8) {
USB_Transmit(0, &ntbInputSize, 8);
} else {
return USB_ERR;
}
return USB_OK;
break;
}
case NCM_SET_NTB_INPUT_SIZE: {
USB_NTB_INPUT_SIZE *info = data;
if (length >= 4) {
ntbInputSize.NtbInMaxSize = info->NtbInMaxSize;
}
if (length == 8) {
ntbInputSize.NtbInMaxDatagrams = info->NtbInMaxDatagrams;
}
return USB_OK;
break;
}
case NCM_GET_NTB_PARAMETERS:
[...]
}
return USB_ERR;
}
The Notifications endpoint will notify the host if the ethernet device goes up or down and when the linkspeed changes. The detailed sequence of steps is laid out in the NCM-Document, chapter 7.1
- If the device does not send anything, it is assumed to be "unplugged"
- To "unplug" a device, you'll have to send a
Network Connection
-Packet telling the system that the device is "unplugged" - To "plug in" a device, first send a
Speed Change
-Notification to announce the new speed followed by aNetwork Connection
telling the host that the device is now "plugged in"
First, we'll need the data structure for the SpeedChange-packets, as well as a little state machine to send two control packets in order. We'll put them again in ncm_device.h
// CDC120 6.3.3
typedef struct {
USB_SETUP_PACKET SetupPacket;
unsigned int DlBitRate;
unsigned int UlBitRate;
} USB_NCM_SPEEDDATA;
typedef struct NCM_CtrlTxInfo NCM_CtrlTxInfo;
struct NCM_CtrlTxInfo {
char *buffer;
unsigned short length;
NCM_CtrlTxInfo *next;
};
Next, we will define all the states we need in the ncm_device.c
together with the order in which they should be sent:
// Control Transmissions for LinkUp & LinkDown
static const USB_NCM_SPEEDDATA nwSpeedChange = {
.SetupPacket.RequestType = 0b10100001,
.SetupPacket.Request = NCM_NETWORK_SPEEDCHANGE,
.SetupPacket.Value = 0,
.SetupPacket.Index = 1,
.SetupPacket.Length = 8,
.DlBitRate = 1000 * 1000,
.UlBitRate = 1000 * 1000};
static const USB_SETUP_PACKET nwConnected = {
.RequestType = 0b10100001,
.Request = NCM_NETWORK_CONNECTION,
.Value = 1,
.Index = 1,
.Length = 0};
static const USB_SETUP_PACKET nwDisconnected = {
.RequestType = 0b10100001,
.Request = NCM_NETWORK_CONNECTION,
.Value = 0,
.Index = 1,
.Length = 0};
static const NCM_CtrlTxInfo LinkUp[2] = {
{.buffer = &nwSpeedChange,
.length = sizeof(nwSpeedChange),
.next = &LinkUp[1]},
{.buffer = &nwConnected,
.length = sizeof(nwConnected),
.next = 0}};
static const NCM_CtrlTxInfo LinkDown = {
.buffer = &nwDisconnected,
.length = sizeof(nwDisconnected),
.next = 0};
static NCM_CtrlTxInfo *nextTransmission = {0};
The next step is some configuration in the usb_config.c
. We need to map the TxCallback of the Control Endpoint as well as the Class-Reset in case the selected alternate interface id changes.
void USB_ConfigureEndpoints() {
// Configure all endpoints and route their reception to the functions that need them
USB_CONFIG_EP IntEP = {
.EP = 1,
.RxBufferSize = 16,
.TxBufferSize = 16,
.TxCallback = NCM_ControlTransmit,
.Type = USB_EP_INTERRUPT};
USB_SetEPConfig(IntEP);
}
void USB_ResetClass(char interface, char alternateId) {
NCM_Reset(interface, alternateId);
}
The last step is to put it all together and send the control messages in ncm_device.c
:
static void NCM_ControlTransmission() {
if (nextTransmission != 0) {
USB_Transmit(1, nextTransmission->buffer, nextTransmission->length);
nextTransmission = nextTransmission->next;
}
}
void NCM_Reset(char interface, char alternateId) {
if (interface == 1) {
if (alternateId == 1) {
NCM_LinkUp();
} else if (alternateId == 0) {
// Reset Network
nextTransmission = 0;
}
}
}
void NCM_LinkUp() {
nextTransmission = &LinkUp[0];
NCM_ControlTransmission();
}
void NCM_LinkDown() {
nextTransmission = &LinkDown;
NCM_ControlTransmission();
}
void NCM_ControlTransmit(char ep, short length) {
NCM_ControlTransmission();
}
We will shuffle data around in three 2K buffers per direction. One is intended to be locked by either the inbound processing or outbound sending part. The other two are intended for double buffering. This way the processing part always has one buffer at the ready, even when the the buffer filling part is still at work in the other buffer.
First lets look at the receiving part of the code. We again need some structures to hold some metadata about our buffers and their states.
typedef enum {
NCM_BUF_UNUSED,
NCM_BUF_READY,
NCM_BUF_LOCKED,
} NCM_BufferState;
struct NCM_BufferInfo {
char *buffer;
NCM_BufferState status;
unsigned short length;
unsigned short offset;
NCM_BufferInfo *next;
};
The BufferState defines which part can access a buffer. A unused buffer has either no valid data or was already processed. A ready buffer is filled with valid data and ready for processing. Or for overwriting if the processing takes too long in which case packets will be dropped. A locked buffer is currently being processed and the receiver is not allowed to write into them. Now we define the buffers (including the tx ones) and the first rx buffer metadata.
static char buffers[6][2048] = {};
static NCM_BufferInfo rxDef[3] = {
{.buffer = buffers[3],
.next = &rxDef[1]},
{.buffer = buffers[4],
.next = &rxDef[2]},
{.buffer = buffers[5],
.next = &rxDef[0]}};
static NCM_BufferInfo *rx = &rxDef[0];
The rxDef
defines a loop of buffers while the rx
pointer points to the currently active buffer for data reception. Next is the actual data reception. We do some check for the endpoint to filter out accidental data. Then we find the first unlocked buffer, which should be our current one if we didn't switch to the next buffer in our last finished reception. Next we check if the buffer is still empty (the current reception offset is 0) and if so, we check for a valid NCM header to synchronize our data packets. If we are not at the beginning, we also check for the header to discard our current packet if necessary, because we apparently missed some data (eg. because of a breakpoint in debugging). Then we just copy the data into the buffer, adjust the offset and look at the header for the expected total length. If we hit that length, we mark the buffer as ready and switch to the next buffer.
void NCM_HandlePacket(char ep, short length) {
if (ep == 2) {
while (rx->status == NCM_BUF_LOCKED) {
rx = rx->next;
}
rx->status = NCM_BUF_UNUSED;
short received = 2048 - rx->offset;
USB_Fetch(ep, rx->buffer + rx->offset, &received);
NCM_NTB_HEADER_16 *header = rx->buffer + rx->offset;
if (rx->offset == 0) {
if (header->Signature[0] != 'N' || header->Signature[1] != 'C' || header->Signature[2] != 'M' || header->Signature[3] != 'H') {
return;
}
rx->length = header->BlockLength;
} else {
// Try Detect broken packages
if (header->Signature[0] == 'N' && header->Signature[1] == 'C' && header->Signature[2] == 'M' && header->Signature[3] == 'H') {
rx->offset = 0;
NCM_HandlePacket(ep, length);
return;
}
}
rx->offset += received;
if (rx->length == 0) {
if (received < 64) {
NCM_NTB_HEADER_16 *header = rx->buffer;
header->BlockLength = rx->offset;
rx->length = rx->offset;
rx->status = NCM_BUF_READY;
rx->offset = 0;
rx = rx->next;
}
} else if (rx->offset >= rx->length) {
rx->status = NCM_BUF_READY;
rx->offset = 0;
rx = rx->next;
}
}
}
Now for processing the data. It is the job of the processing part to actively pull the data, it will not be pushed. Keeping interrupts short and stuff. An NCM-Packet can contain multiple datagrams, each of which is a valid ethernet frame and must be processed individually. Therefore we will write a function to fetch the next available datagram.
For this we first need some more housekeeping metadata to keep track of the last datagram that was fetched:
typedef struct {
NCM_BufferInfo *buffer;
NCM_NTB_POINTER_16 *ndp;
NCM_NTB_DATAPOINTER_16 *datagramm;
} NCM_RX_BufferInfo;
static NCM_RX_BufferInfo activeRxBuffer = {
.buffer = &rxDef[0],
.datagramm = 0,
.ndp = 0};
And now follows the logic to all of this. First. A NCM-Packet can contain multiple NDP-Data, which in turn points to multiple Datagrams. So our first act is to check if our current NDP is valid, but we reached the last datagram in the NDP. If we did, we check if there is a next NDP linked and relink the metadata to that NDP. If not, we free the buffer for reception.
char *NCM_GetNextRxDatagramBuffer(short *length) {
if (activeRxBuffer.ndp != 0 && (activeRxBuffer.datagramm == 0 || activeRxBuffer.datagramm->DatagramLength == 0 || activeRxBuffer.datagramm->DatagramOffset == 0)) {
if (activeRxBuffer.ndp->NextNdpOffset == 0) {
activeRxBuffer.ndp = 0;
activeRxBuffer.datagramm = 0;
activeRxBuffer.buffer->status = NCM_BUF_UNUSED;
} else {
activeRxBuffer.ndp = activeRxBuffer.buffer->buffer + activeRxBuffer.ndp->NextNdpOffset;
activeRxBuffer.datagramm = activeRxBuffer.ndp + 1;
}
}
The next part checks if we have a valid NDP pointer. If not, we look for the next free buffer, lock it and initialize our metadata with that buffer.
if (activeRxBuffer.ndp == 0) {
NCM_BufferInfo *temp = activeRxBuffer.buffer;
do {
activeRxBuffer.buffer = activeRxBuffer.buffer->next;
} while (activeRxBuffer.buffer->status != NCM_BUF_READY && temp != activeRxBuffer.buffer);
if (activeRxBuffer.buffer->status == NCM_BUF_READY) {
activeRxBuffer.buffer->status = NCM_BUF_LOCKED;
NCM_NTB_HEADER_16 *header = activeRxBuffer.buffer->buffer;
activeRxBuffer.ndp = (char *)header + header->NdpOffset;
activeRxBuffer.datagramm = activeRxBuffer.ndp + 1;
}
}
Now we check again if we have a valid NDP (because maybe there is no new buffer ready). If so, we first check if the packet is inside the memory bounds of the buffer to prevent memory leaks / bugs. Then we check the signature of the NDP to detect a broken part of the NCM packet. If all works well, we get the current datagram (if the data is inside the boundaries of the buffer).
if (activeRxBuffer.ndp) {
NCM_NTB_HEADER_16 *header = activeRxBuffer.buffer->buffer;
if (activeRxBuffer.ndp > activeRxBuffer.buffer->buffer + header->BlockLength) {
// Broken ndp reference
activeRxBuffer.ndp = 0;
activeRxBuffer.datagramm = 0;
*length = 0;
return 0;
}
if (activeRxBuffer.ndp->Signature[0] != 'N' || activeRxBuffer.ndp->Signature[1] != 'C' || activeRxBuffer.ndp->Signature[2] != 'M') {
// Broken link, skip this ndp
activeRxBuffer.ndp = 0;
activeRxBuffer.datagramm = 0;
*length = 0;
return 0;
}
NCM_NTB_DATAPOINTER_16 *datagramm = activeRxBuffer.datagramm;
activeRxBuffer.datagramm += 1;
if (datagramm->DatagramLength + datagramm->DatagramOffset > header->BlockLength) {
// broken datagram
activeRxBuffer.datagramm = 0;
*length = 0;
return 0;
}
*length = datagramm->DatagramLength;
return activeRxBuffer.buffer->buffer + datagramm->DatagramOffset;
} else {
*length = 0;
return 0;
}
}
Now for the transmission. Again, some metadata
typedef struct {
NCM_BufferInfo *buffer;
unsigned char datagramCount;
unsigned short offset;
unsigned short sequence;
NCM_NTB_DATAPOINTER_16 datagrams[10];
} NCM_TX_BufferInfo;
static NCM_BufferInfo txDef[3] = {
{.buffer = buffers[0],
.next = &txDef[1]},
{.buffer = buffers[1],
.next = &txDef[2]},
{.buffer = buffers[2],
.next = &txDef[0]}};
static NCM_BufferInfo *tx = &txDef[0];
static NCM_TX_BufferInfo activeTxBuffer = {
.buffer = &txDef[0]};
This time, we will start with the transmission processor. The transmission is relative simple
void NCM_BufferTransmitted(char ep, short length) {
NCM_TransmitNextBuffer();
}
static void NCM_TransmitNextBuffer() {
if (tx->status == NCM_BUF_LOCKED) {
tx->status = NCM_BUF_UNUSED;
}
NCM_BufferInfo *temp = tx;
do {
tx = tx->next;
} while (tx->status != NCM_BUF_READY && tx != temp);
if (tx->status == NCM_BUF_READY) {
tx->status = NCM_BUF_LOCKED;
while(USB_IsTransmitPending(2)) {
}
USB_Transmit(2, tx->buffer, tx->length);
}
}
If we finished a USB transmission, we try to transmit the next buffer. For this, we first unlock our current buffer and then look for the next ready buffer to send. Also be careful of the endless loop if no buffer is ready.
The transmission process will fetch a datagram pointer and write the data into the buffer. The NDP-Data will only be assembled right at the end before transmission. We limit ourselves to 10 datagrams max per NCM-Packet, do some checks if the requested length still fits into the buffer and then take note of the datagram length and offset pointer for later.
char *NCM_GetNextTxDatagramBuffer(short length) {
// check size constraints
char maxDatagrams = MIN(10, ntbInputSize.NtbInMaxDatagrams);
if (maxDatagrams == 0) {
maxDatagrams = 10;
}
short maxLength = ntbInputSize.NtbInMaxSize;
if (activeTxBuffer.offset == 0) {
activeTxBuffer.offset = sizeof(NCM_NTB_HEADER_16);
}
if (activeTxBuffer.offset + length + sizeof(NCM_NTB_POINTER_16) + (activeTxBuffer.datagramCount + 2) * sizeof(NCM_NTB_DATAPOINTER_16) > maxLength ||
activeTxBuffer.datagramCount + 1 > maxDatagrams) {
NCM_FlushTx();
}
// Record Datagram
activeTxBuffer.datagrams[activeTxBuffer.datagramCount].DatagramLength = length;
activeTxBuffer.datagrams[activeTxBuffer.datagramCount].DatagramOffset = activeTxBuffer.offset;
activeTxBuffer.offset += length;
return activeTxBuffer.buffer->buffer + activeTxBuffer.datagrams[activeTxBuffer.datagramCount++].DatagramOffset;
}
The NCM_FlushTX
then takes all that metadata, assembles the NDP-Data and sends the buffer to the USB-periphery. Be aware, that NCM requires the data blobs to be aligned as defined in the NTB-params. Otherwise the host will discard the packet, even though it might show up in wireshark.
void NCM_FlushTx() {
if (activeTxBuffer.datagramCount > 0 && activeTxBuffer.buffer->status == NCM_BUF_UNUSED) {
unsigned short offset = (activeTxBuffer.offset + (4 - 1)) & -4;
NCM_NTB_HEADER_16 *header = activeTxBuffer.buffer->buffer;
NCM_NTB_POINTER_16 *ndp = activeTxBuffer.buffer->buffer + offset;
header->NdpOffset = offset;
header->HeaderLength = sizeof(NCM_NTB_HEADER_16);
header->Sequence = activeTxBuffer.sequence;
header->Signature[0] = 'N';
header->Signature[1] = 'C';
header->Signature[2] = 'M';
header->Signature[3] = 'H';
ndp->NextNdpOffset = 0;
ndp->Length = sizeof(NCM_NTB_POINTER_16) + sizeof(NCM_NTB_DATAPOINTER_16) * (activeTxBuffer.datagramCount + 1);
ndp->Signature[0] = 'N';
ndp->Signature[1] = 'C';
ndp->Signature[2] = 'M';
ndp->Signature[3] = '0';
offset += sizeof(NCM_NTB_POINTER_16);
for (int i = 0; i < activeTxBuffer.datagramCount; i++) {
NCM_NTB_DATAPOINTER_16 *datagram = (char *)header + offset;
(*datagram) = activeTxBuffer.datagrams[i];
offset += sizeof(NCM_NTB_DATAPOINTER_16);
}
NCM_NTB_DATAPOINTER_16 *terminator = (char *)header + offset;
terminator->DatagramLength = 0;
terminator->DatagramOffset = 0;
offset += sizeof(NCM_NTB_DATAPOINTER_16);
header->BlockLength = offset;
activeTxBuffer.buffer->length = offset;
activeTxBuffer.offset = sizeof(NCM_NTB_HEADER_16);
activeTxBuffer.datagramCount = 0;
activeTxBuffer.sequence++;
activeTxBuffer.buffer->status = NCM_BUF_READY;
activeTxBuffer.buffer->length = header->BlockLength;
do {
activeTxBuffer.buffer = activeTxBuffer.buffer->next;
} while (activeTxBuffer.buffer->status == NCM_BUF_LOCKED);
NCM_TransmitNextBuffer();
}
}
Now for the last step, we wire NCM up to the LWIP-Stack. The relevant methods needed are the poll and output functions.
static err_t ncm_netif_output(struct netif *netif, struct pbuf *p) {
struct pbuf *q;
short offset = 0;
char *buffer = NCM_GetNextTxDatagramBuffer(p->tot_len);
for(q = p; q != NULL; q = q->next) {
memcpy(buffer + offset, q->payload, q->len);
offset += q->len;
if(q->len == q->tot_len) {
break;
}
}
}
void ncm_netif_poll(struct netif *netif) {
short length = 0;
short offset = 0;
char *datagram;
struct pbuf *p, *q;
datagram = NCM_GetNextRxDatagramBuffer(&length);
if(datagram != 0 && length > 0) {
p = pbuf_alloc(PBUF_RAW, length, PBUF_POOL);
if(p != NULL) {
for(q = p; q != NULL && offset < length; q = q->next) {
memcpy(q->payload, datagram + offset, q->len);
offset += q->len;
}
if(netif->input(p, netif) != ERR_OK) {
pbuf_free(p);
}
}
}
}
err_t ncm_netif_init(struct netif *netif) {
[...]
netif->linkoutput = ncm_netif_output;
[...]
}
The ncm_netif_output
will be called automatically by lwip. The ncm_netif_poll
has to be called periodically in the main loop or somewhere else.