Sunday, November 26, 2017

Using multiple USB HID interfaces for increased data throughput

USB devices implementing an HID class are convenient as they do not require custom driver development on the host.  Modern operating systems incorporate a standard HID driver and provide a an API for interfacing to the device.  The major downside commonly noted with HID class devices is that communication is performed using interrupt endpoints which are limited to a bandwidth of 64 kB/sec.  This limitation can be overcome by using multiple interfaces with separate HID and endpoint types.  This blog post explains how to build a dual interface USB HID firmware on the Silicon Labs 8051-based EFM8UB2 and presents test software to verify operation.


Background

Universal Serial Bus (USB) is a ubiquitous protocol allowing host systems to communicate with a wide variety of hardware device types.  As a protocol, it's considerably more complex then simple point-to-point hardware protocols like UART, SPI, and I2C.  With USB, the host controller manages all input and output communication between the various devices on the bus.  The host periodically polls the USB device endpoint (an input or output buffer) based on an interval provided by the device during enumeration.

The USB protocol supports four endpoint types: control, bulk, isochronous, and interrupt.  Control transfers are primarily used in the initial device enumeration for communication of descriptors.  Bulk transfers provide no guaranteed latency but are useful for high bandwidth with error checking (think TCP).  Isochronous have a guaranteed latency and are also high bandwidth but provide no error checking (think UDP).  Lastly, interrupt transfers have a guaranteed latency but are confined to a low bandwidth.  The USB HID device class uses interrupt endpoints for communication with applications in mice, keyboards, and other devices requiring real-time updates.

The HID device class allows report messages to be exchanged between host and device.  During enumeration, the device provides one or more interface descriptor with an HID class code.  During this time, the device also sends the endpoint descriptor tied to the particular interface which specifies that it's the endpoint address and that it's a interrupt endpoint type.


Development

Single Interface Verification

We will start by validating a single interface USB HID example before forking the project to create the multiple interface example.

git clone --recursive https://github.com/shraken/efm8_hidmulti

Build the firmware image in the fw/single directory using Keil uVision.  Flash the firmware image to the SLSTK2001A development kit.

Build the software read_test binary in the sw/ directory by following the README.md directions with CMake.  Launch the read_test executable and record the path for the device that matches a Manufacturer of 'SLAB' and a Product name of 'HIDtoUART example'.

➜  build git:(master) ./read_test 
Device Found
  type: 80ee 0021
  path: /dev/hidraw0
  serial_number: 
  Manufacturer: VirtualBox
  Product:      USB Tablet
  Release:      100
  Interface:    0

Device Found
  type: 10c4 8468
  path: /dev/hidraw1
  serial_number: 
  Manufacturer: SLAB
  Product:      HIDtoUART example
  Release:      0
  Interface:    0

ERROR: device to open needs to be passed as first argument
➜  build git:(master)

Execute the binary with the path noted above and verify that an incrementing byte stream is being received.

➜  build git:(master) sudo ./read_test /dev/hidraw1
Device Found
  type: 80ee 0021
  path: /dev/hidraw0
  serial_number: 
  Manufacturer: VirtualBox
  Product:      USB Tablet
  Release:      100
  Interface:    0

Device Found
  type: 10c4 8468
  path: /dev/hidraw1
  serial_number: 
  Manufacturer: SLAB
  Product:      HIDtoUART example
  Release:      0
  Interface:    0

Manufacturer String: SLAB
Product String: HIDtoUART example
Serial Number String: (0) 
Unable to read indexed string 1
Indexed String 1: 
received new USB packet 0
Data read:
   01 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef 00 
received new USB packet 1
Data read:
   01 f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 00 
received new USB packet 2

This output shows that HID reports are being received with a Report ID of 0x01.  In the firmware's main execution block, a flag 'SendPacketBusy' is continuously polled to determine if the HID IN endpoint has been transmitted.  If the 'SendPacketBusy' becomes a boolean false then the IN endpoint buffer is loaded with the next incrementing byte buffer.


Multi Interface Modifications

A full implementation of the dual HID interface is provided in the fw/multi directory.

Start by defining the report ID's and report size for the secondary HID interface.  In the F3xx_HIDtoUART.h file, add

#define IN_DATA2  0x03
#define IN_DATA_SIZE2 63

#define OUT_DATA2 0x04
#define OUT_DATA_SIZE2 60

in the same file, also make an external declaration for the IN and OUT temporary endpoint buffers for secondary endpoint.

extern unsigned char xdata IN_PACKET2[];
extern unsigned char xdata OUT_PACKET2[];

in the F380_HIDtoUART.c source file add a global for the IN and OUT temporary endpoint buffers.

unsigned char xdata IN_PACKET2[64];
unsigned char xdata OUT_PACKET2[64];

modify the typedef structure hid_configuration_descriptor to add the additional fields for the secondary interface, class, and two endpoint descriptors.

typedef code struct {
  configuration_descriptor   hid_configuration_descriptor;
  interface_descriptor       hid_interface_descriptor;
  class_descriptor           hid_descriptor;
  endpoint_descriptor        hid_endpoint_in_descriptor;
  endpoint_descriptor        hid_endpoint_out_descriptor;
 
  interface_descriptor       hid_interface_descriptor2;
  class_descriptor           hid_descriptor2;
  endpoint_descriptor        hid_endpoint_in_descriptor2;
  endpoint_descriptor        hid_endpoint_out_descriptor2;
}
hid_configuration_descriptor;

modify the hid_configuration_descriptor initialization where the hid_configuration_descriptor is set in the F3xx_USB0_Descriptor.c source file.  For the hid_configuration_descriptor we must change the NumInterfaces field to 0x02 and adjust the Totallength to 0x4900 (big endian) to accommodate the additional fields added to the hid_configuration_descriptor above.

{ // configuration_descriptor hid_configuration_descriptor
   0x09,                               // Length
   0x02,                               // Type
   0x4900,                             // Totallength (= 9+9+9+7+7+9+9+7+7)
   0x02,                               // NumInterfaces
   0x01,                               // bConfigurationValue
   0x00,                               // iConfiguration
   0x80,                               // bmAttributes
   0x20                                // MaxPower (in 2mA units)
},

now add the secondary interface, class, and the two endpoint descriptors to the hid_configuration_descriptor declaration in F3xx_USB0_Descriptor.c

{ // interface_descriptor hid_interface_descriptor
   0x09,                               // bLength
   0x04,                               // bDescriptorType
   0x01,                               // bInterfaceNumber
   0x00,                               // bAlternateSetting
   0x02,                               // bNumEndpoints
   0x03,                               // bInterfaceClass (3 = HID)
   0x00,                               // bInterfaceSubClass
   0x00,                               // bInterfaceProcotol
   0x00                                // iInterface
},

{ // class_descriptor hid_descriptor
  0x09,                               // bLength
  0x21,                               // bDescriptorType
  0x0101,                             // bcdHID
  0x00,                               // bCountryCode
  0x01,                               // bNumDescriptors
  0x22,                               // bDescriptorType
  HID_REPORT_DESCRIPTOR_SIZE_LE       // wItemLength (tot. len. of report
                                      // descriptor)
},

// IN endpoint (mandatory for HID)
{ // endpoint_descriptor hid_endpoint_in_descriptor
   0x07,                               // bLength
   0x05,                               // bDescriptorType
   0x82,                               // bEndpointAddress
   0x03,                               // bmAttributes
   EP2_PACKET_SIZE_LE,                 // MaxPacketSize (LITTLE ENDIAN)
   1                                   // bInterval
},

// OUT endpoint (optional for HID)
{ // endpoint_descriptor hid_endpoint_out_descriptor
   0x07,                               // bLength
   0x05,                               // bDescriptorType
   0x02,                               // bEndpointAddress
   0x03,                               // bmAttributes
   EP2_PACKET_SIZE_LE,                 // MaxPacketSize (LITTLE ENDIAN)
   1                                   // bInterval
}

We also need to allocate the secondary hid_report_descriptor representing the second HID interface we intend to communicate on.  Add the following to the F3xx_USB0_Descriptor.c source file

code const hid_report_descriptor HIDREPORTDESC2 =
{
    0x06, 0x00, 0xff,              // USAGE_PAGE (Vendor Defined Page 1)
    0x09, 0x01,                    // USAGE (Vendor Usage 1)
    0xa1, 0x01,                    // COLLECTION (Application)
  
    0x85, IN_DATA2,                // Report ID
    0x95, IN_DATA_SIZE2,           //   REPORT_COUNT ()
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x81, 0x02,                    //   INPUT (Data,Var,Abs)

    0x85, OUT_DATA2,               // Report ID
    0x95, OUT_DATA_SIZE2,          //   REPORT_COUNT ()
    0x75, 0x08,                    //   REPORT_SIZE (8)
    0x26, 0xff, 0x00,              //   LOGICAL_MAXIMUM (255)
    0x15, 0x00,                    //   LOGICAL_MINIMUM (0)
    0x09, 0x01,                    //   USAGE (Vendor Usage 1)
    0x91, 0x02,                    //   OUTPUT (Data,Var,Abs)

    0xC0                           //   end Application Collection
};

The interrupt service source file, F3xx_USB0_InterruptServiceRoutine.c must be modified to add handlers for the the second IN and OUT endpoints.  Start by adding a global flag variable used to indicate when the second IN endpoint has been serviced.


bit SendPacketBusy2 = 0;

Next, add the handlers for the new IN and OUT endpoints.

void Handle_In2 (void)
{
   EP_STATUS[2] = EP_IDLE;
   SendPacketBusy2 = 0;
}

void Handle_Out2 (void)
{
   unsigned char Count = 0;
   unsigned char ControlReg;

   POLL_WRITE_BYTE (INDEX, 2);         // Set index to endpoint 1 registers
   POLL_READ_BYTE (EOUTCSR1, ControlReg);

   if (EP_STATUS[2] == EP_HALT)        // If endpoint is halted, send a stall
   {
      POLL_WRITE_BYTE (EOUTCSR1, rbOutSDSTL);
   }

   else                                // Otherwise read received packet
                                       // from host
   {
      if (ControlReg & rbOutSTSTL)     // Clear sent stall bit if last
                                       // packet was a stall
      {
         POLL_WRITE_BYTE (EOUTCSR1, rbOutCLRDT);
      }

      Setup_OUT_BUFFER ();             // Configure buffer to save
                                       // received data
      Fifo_Read(FIFO_EP2, OUT_BUFFER.Length, OUT_BUFFER.Ptr);

      // Process data according to received Report ID.
      // In systems with Report Descriptors that do not define report IDs,
      // the host will still format OUT packets with a prefix byte
      // of '0x00'.

      ReportHandler_OUT2 (OUT_BUFFER.Ptr[0]);
   POLL_WRITE_BYTE (EOUTCSR1, 0);   // Clear Out Packet ready bit
   }
}

The endpoint handlers are called inside the USB ISR.  The IN1INT register specifies the currently active IN endpoints that are pending.  Likewise the OUT1INT register specifies the currently active OUT endpoints that are pending.  We add checks for the secondary endpoints that call into the newly defined handlers above.


if (bIn & rbIN2)
{
   Handle_In2();
}

if (bOut & rbOUT2)
{
   Handle_Out2();
}

Finally, we need to define a new send packet routine for the new IN endpoint.  This routine checks if the endpoint is idle and call the HID interface report handler.  In this trivial case, the report handler will fill the temporary IN buffer with an byte increment stream.  Following the report handler, the temporary buffer is written to the USB endpoint FIFO buffer and the INPRDY bit of the specific endpoint EINCSR is set to arm the endpoint for future host IN transfers.

void SendPacket2(unsigned char ReportID)
{
   bit EAState;
   unsigned char ControlReg;
   EAState = EA;
   SendPacketBusy2 = 1;

   POLL_WRITE_BYTE (INDEX, 2);         // Set index to endpoint 2 registers
 
   // Read contol register for EP 2
   POLL_READ_BYTE (EINCSR1, ControlReg);

   if (EP_STATUS[2] == EP_HALT)        // If endpoint is currently halted,
                                       // send a stall
   {
      POLL_WRITE_BYTE (EINCSR1, rbInSDSTL);
   }

   else if(EP_STATUS[2] == EP_IDLE)
   {
      // the state will be updated inside the ISR handler
      EP_STATUS[2] = EP_TX;

      if (ControlReg & rbInSTSTL)      // Clear sent stall if last
                                       // packet returned a stall
      {
         POLL_WRITE_BYTE (EINCSR1, rbInCLRDT);
      }

      if (ControlReg & rbInUNDRUN)     // Clear underrun bit if it was set
      {
         POLL_WRITE_BYTE (EINCSR1, 0x00);
      }

      EIE1 &= ~0x02;                   // Disable USB0 Interrupts
   
      ReportHandler_IN_Foreground2 (ReportID);

      Fifo_Write_Foreground (FIFO_EP2, IN_BUFFER2.Length,
                    (unsigned char *)IN_BUFFER2.Ptr);
      POLL_WRITE_BYTE (EINCSR1, rbInINPRDY);
                                       // Set In Packet ready bit,
                                       // indicating fresh data on FIFO 2
      
      EIE1 |= 0x02;                    // Enable USB0 Interrupts
   }
}

In the F3xx_USB0_InterruptServiceRoutine.h header file we need to add declarations for the functions and global variables previously defined. Add the following:

void Handle_In2(void);
void Handle_Out2(void);
void SendPacket2(unsigned char);

extern bit SendPacketBusy2;

We must define the report handler structures and routines called upon by the handler and SendPacket2 function in the F3xx_USB0_ReportHandler.c source file.  Start by adding the support structure and macro defines:

#define IN2_VECTORTABLESize 1
#define OUT2_VECTORTABLESize 1

const VectorTableEntry IN_VECTORTABLE2[IN2_VECTORTABLESize] =
{
   // FORMAT: Report ID, Report Handler
   IN_DATA2, IN_Data2,
};

//*****************************************************************************
// Link all Report Handler functions to corresponding Report IDs
//*****************************************************************************
const VectorTableEntry OUT_VECTORTABLE2[OUT2_VECTORTABLESize] =
{
   // FORMAT: Report ID, Report Handler
   OUT_DATA2, OUT_Data2,
};

BufferStructure IN_BUFFER2, OUT_BUFFER2;

The last modification required in the F3xx_USB0_ReportHandler.c file is to add the report handler functions and function pointers specified in the  VectorTableEntry initialized above for the IN_Data2 and Out_Data2 functions.  The report handler functions are templates copied from the single interface and the IN_Data2 function performs the same action as the IN1 endpoint by filling the temporary buffer with an incrementing byte stream.  Add the following code for IN_Data2 and OUT_Data2 functions:

void IN_Data2 (void)
{
   static unsigned char count = 0;
   unsigned char i;
 
   IN_PACKET2[0] = IN_DATA2;
 
   for (i = 1; i < IN_DATA_SIZE2; i++) {
      IN_PACKET2[i] = count;
      count++;
   }
 
   IN_BUFFER2.Ptr = IN_PACKET2;
   IN_BUFFER2.Length = IN_DATA_SIZE2 + 1;
}

void OUT_Data2 (void)
{

}

And add the following for the report handlers:

void ReportHandler_IN_ISR2(unsigned char R_ID)
{
   unsigned char index;

   index = 0;

   while(index <= IN2_VECTORTABLESize)
   {
      // Check to see if Report ID passed into function
       // matches the Report ID for this entry in the Vector Table
      if(IN_VECTORTABLE2[index].ReportID == R_ID)
      {
         IN_VECTORTABLE2[index].hdlr();
         break;
      }

      // If Report IDs didn't match, increment the index pointer
      index++;
   }

}

void ReportHandler_IN_Foreground2(unsigned char R_ID)
{
   unsigned char index;

   index = 0;

   while(index <= IN2_VECTORTABLESize)
   {
      // Check to see if Report ID passed into function
      // matches the Report ID for this entry in the Vector Table
      if(IN_VECTORTABLE2[index].ReportID == R_ID)
      {
         IN_VECTORTABLE2[index].hdlr();
         break;
      }

      // If Report IDs didn't match, increment the index pointer
      index++;
   }

}

void ReportHandler_OUT2(unsigned char R_ID){

   unsigned char index;

   index = 0;

   while(index <= OUT2_VECTORTABLESize)
   {
      // Check to see if Report ID passed into function
      // matches the Report ID for this entry in the Vector Table
      if(OUT_VECTORTABLE2[index].ReportID == R_ID)
      {
         OUT_VECTORTABLE2[index].hdlr();
         break;
      }

      // If Report IDs didn't match, increment the index pointer
      index++;
   }
}

Add the extern declarations for the buffer structure and the report handlers in F3xx_USB0_ReportHandler.h header file.


extern void ReportHandler_IN_ISR2(unsigned char);
extern void ReportHandler_IN_Foreground2(unsigned char);
extern void ReportHandler_OUT2(unsigned char);

extern BufferStructure IN_BUFFER2, OUT_BUFFER2;

The last thing we need to do is modify the F3xx_USB0_Standard_Requests.c source file to send the HID report descriptor and report when a request is made by the host.  Modify the Get_Descriptor function for the DSC_HID_REPORT case with the following.

   case DSC_HID_REPORT:                // HID Specific (HID report descriptor)
      if (SETUP.wIndex.U8[LSB] == 0x00) {
         DATAPTR = (unsigned char*)&HIDREPORTDESC;
         DATASIZE = HID_REPORT_DESCRIPTOR_SIZE;
         break; 
      } else if (SETUP.wIndex.U8[LSB] == 0x01) {
         DATAPTR = (unsigned char*)&HIDREPORTDESC2;
         DATASIZE = HID_REPORT_DESCRIPTOR_SIZE;
         break;
      }

Next, modify the Get_Report function to send the new report type to the host.  Add the following as a replacement for the single call to ReportHandler_IN_ISR and setting DATAPTR and DATASIZE.

if (SETUP.wIndex.U8[LSB] == 0x00) {
   // call appropriate handler to prepare buffer
   ReportHandler_IN_ISR(SETUP.wValue.U8[LSB]);
   // set DATAPTR to buffer used inside Control Endpoint
   DATAPTR = IN_BUFFER.Ptr;
   DATASIZE = IN_BUFFER.Length;
} else if (SETUP.wIndex.U8[LSB] == 0x01) {
   // call appropriate handler to prepare buffer
   ReportHandler_IN_ISR2(SETUP.wValue.U8[LSB]);
   // set DATAPTR to buffer used inside Control Endpoint
   DATAPTR = IN_BUFFER2.Ptr;
   DATASIZE = IN_BUFFER2.Length;
}

The final modification required is to add a call to SendPacket2 after checking the SendPacketBusy2 flag in the main while block.  The SendPacket2 function calls the ReportHandler_IN_Foreground2 handler which subsequently calls the IN_Data2 function to fill the temporary buffer with byte incremented values.  The temporary buffer is copied into the USB FIFO buffer and the endpoint is then armed.

if (!SendPacketBusy2) {
   SendPacket2 (IN_DATA2);
}

That wraps up the firmware modifications.  Build the firmware and flash to the SLSTK2001A development kit.  The image below shows the the reports for IN_DATA1 (0x01) and IN_DATA2 (0x03) simultaneously being transmitted to the a Windows host after enumeration is complete.


Multi Interface Verification

Test software written using the hidapi library was used to validate that IN_DATA1 (0x01) and IN_DATA2 (0x03) reports could be read simultaneously on the host.  The example hidtest application included with hidapi was modified to open the HID device by path instead of USB VID and PID as each interface registers a unique path.  The path is passed as an argument to the application as shown below.

// open device passed as first argument
handle = hid_open_path(argv[1]);

if (!handle) {
   printf("unable to open device\n");
   return 1;
}

A loop was added to invoke hid_read for reading reports from the interrupt IN endpoint.  On each successful read, the input report is printed to console.

// Read requested state. hid_read() has been set to be
// non-blocking by the call to hid_set_nonblocking() above.
// This loop demonstrates the non-blocking nature of hid_read().
res = 0;
count = 0;
while ((res == 0) || (count < 100000)) {
   res = hid_read(handle, buf, sizeof(buf));
   if (res > 0) {
      printf("received new USB packet %d\n", count);
      printf("Data read:\n   ");
      // Print out the returned buffer.
      for (i = 0; i < res; i++)
         printf("%02hhx ", buf[i]);
      printf("\n");
      count++;
   }
}

The abbreviated output below shows the two input reports streaming to the host when launched as separate applications with paths specifying the unique HID interfaces of the same device.  For example, on a Linux system using hidraw driver backend.

Interface 0: /dev/hidraw1
➜  build git:(master) sudo ./read_test /dev/hidraw1
...
received new USB packet 0
Data read:
   01 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd 00 
received new USB packet 1
Data read:
   01 fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 00 

Interface 1: /dev/hidraw2
➜  build git:(master) sudo ./read_test /dev/hidraw2
...
received new USB packet 0
Data read:
   03 d2 d3 d4 d5 d6 d7 d8 d9 da db dc dd de df e0 e1 e2 e3 e4 e5 e6 e7 e8 e9 ea eb ec ed ee ef f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 00 
received new USB packet 1
Data read:
   03 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31 32 33 34 35 36 37 38 39 3a 3b 3c 3d 3e 3f 40 41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 00 

Success!  The report ID is the first byte in the dump presented above.  With interface 0  (/dev/hidraw1) we see the IN_DATA1 (0x01) report ID and with interface 1 (/dev/hidraw2) we see IN_DATA2 (0x02) report ID.  While not shown in this example, the firmware also supports OUT endpoint transfers so you can use hid_write to send output reports.


Conclusion

This entry documented the modifications required to enable multi-interfaces when using a USB HID class-type.  This is a simple approach to overcome the low-bandwidth (64 kB/sec) limitation of interrupt HID endpoints by spreading data communication over multiple interfaces.  We showed a two interface modification here but it can be scaled even further -- on the EFM8UB2 the 3rd endpoint could be used but other platforms like EFM32 support 6 IN/OUT endpoints potentially expanding the up/down data rates to 384 kBytes/sec.


References

  1. USB 2.0 Specification
  2. USB Device Class Definition for HID 1.11
  3. Silicon Labs Human Device Interface Tutorial AN249
  4. Tutorial about USB HID Report Descriptors
  5. Beagle USB 480 Protocol Analyzer
  6. USB Complete: The Developer's Guide
  7. hidapi Host Library
  8. Silicon Labs EFM8UB2 SLSTK2001A Development Kit

Monday, April 18, 2016

Nordic nrf8001 library for ST STM32F4xx devices

Recently, I began experimenting with the Nordic nrf8001 BLE chip.  The device contains a BLE stack and manages device state through a command and control interface.  This interface uses native SPI for data transfer and two control signals, a request (“RQN”) sent from the application MCU to the nrf8001, and a ready (“RDY”) signal sent from nrf8001 to MCU, both of which are used to indicate if the respective device is able to communicate.

I was interested in using the nrf8001 as a BLE relay with a low power ST STM32L1xx MCU.  I’m still experimenting with that chip but first I ported it to the more ubiquitous ST STM32F407 discovery kit as a first step.  The library, examples, and makefile for building with GNU gcc arm and flashing with ST link can be found here.

Nordic Semi distributes an Arduino/AVR-based library on this GitHub repository.  The authors also include some general porting instructions in the documentation/libraries/BLE/nRF8001-Porting-ACI-Library.md.  This blog post explains the porting procedure to get the library running with example code on the ST STM32F4 discovery kit.


Picture 1: stm32f4 discovery kit, nrf8001 breakout, and logic analyzer


Picture 2: Screen capture of logic analyzer capture showing stm32f4 communicating with the nrf8001 chip.  You can download a sample capture from reset to idle here.

The process for porting can be summarized as follows:

1. Cloning ble-sdk-arduino repo
2. Setup a working ST STM32f4 template build, and flashing directory with USART and SPI support
3. Revise file extension from .cpp to .c
4. Make file modifications around directory structure
5. IO arduino wrapper library
6. Patch aci_setup function in src/BLE/ with a compile time check if it is STM32F4XX directive
7. SPI init routine and GPIO (REQN and RDYN)
8. Adjust structure field type widths (uint8_t -> uint16_t) to accommodate ST standard peripheral driver pin enumerations
9. Simple example and validate use on bench

These steps are described in more detail below:

1. Start by checking out the master branch of 'ble-sdk-arduino':

git clone https://github.com/NordicSemiconductor/ble-sdk-arduino.git

2. Check out the this template directory for STM32F4 development.  Check out the 'stm32f4_start_template' as a starting point, the 'master' branch has all the changes discussed implemented.  We are going to be building using the GCC ARM Embedded toolchain from Launchpad.  This template has SPI and USART support already added.

git clone https://github.com/shraken/nrf8001-stm32f4.git
cd nrf8001-stm32f4
git checkout -b stm32f4_start_template

3. Copy the source and header files from 'ble-sdk-arduino' to our stm32f4 template directory and rename all .cpp source files to .c file extension.

mkdir src/BLE
mkdir inc/BLE
cp ../ble-sdk-arduino/libraries/BLE/*.h inc/BLE
cp ../ble-sdk-arduino/libraries/BLE/*.cpp src/BLE
find ./BLE/src/ -depth -name "*.cpp" -exec sh -c 'mv "$1" "${1%.cpp}.c"' _ {} \;

4. Add the Nordic nRF8001 library source files to the 'SRC' variable in the Makefile.  Ensure that the source files are added as shown below.

SRC = ./src/main.c \
 ./src/millis.c \
 ./src/usart.c \
 ./src/spi.c \
 ./src/debug.c \
 ./src/stm32f4xx_it.c \
 ./src/system_stm32f4xx.c \
 ./src/BLE/acilib.c \
 ./src/BLE/aci_queue.c \
 ./src/BLE/aci_setup.c \
 ./src/BLE/hal_aci_tl.c \
 ./src/BLE/lib_aci.c \

You must also set the 'LIBPATH' variable to the path of the ST Standard Peripheral Driver library.  The README.md provides a GitHub repository link that you can clone for this library.

5. The 'ble-sdk-arduino' library makes uses of Arduino IO pin routines, namely (digitalRead, digitalWrite, and pinMode).  Dummy shell functions are defined for these routines in the file 'io_support.c' that call the ST ARM standard peripheral driver functions.

Be sure to add the 'io_support.c' file to the SRC variable previously described above in the Makefile.

6. Patch the 'aci_setup_fill' function so that the setup message generated from nRFgo studio and stored in 'services.h' is gets copied into a data buffer from code space.  The first part of 'aci_setup_fill' function should now resemble,

#if defined (__AVR__)
    //For Arduino copy the setup ACI message from Flash to RAM.
    memcpy_P(&msg_to_send, &(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset]), pgm_read_byte_near(&(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset].buffer[0]))+2); 
#elif defined(__PIC32MX__)
    //In ChipKit we store the setup messages in RAM
    //Add 2 bytes to the length byte for status byte, length for the total number of bytes
    memcpy(&msg_to_send, &(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset]), (aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset].buffer[0]+2)); 
#elif defined(STM32F4XX)
    memcpy(&msg_to_send, &(aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset]), (aci_stat->aci_setup_info.setup_msgs[*num_cmd_offset].buffer[0]+2)); 
#endif

The STM32F4XX directive is provided in the Makefile DEFS.

7. The SPI and GPIO configuration must be initialized.  Replace the following Arduino specific code block with a single call to 'init_spi1' function.

SPI.begin();
//Board dependent defines
#if defined (__AVR__)
    //For Arduino use the LSB first
    SPI.setBitOrder(LSBFIRST);
#elif defined(__PIC32MX__)
    //For ChipKit use MSBFIRST and REVERSE the bits on the SPI as LSBFIRST is not supported
  SPI.setBitOrder(MSBFIRST);
#endif
SPI.setClockDivider(a_pins->spi_clock_divider);
SPI.setDataMode(SPI_MODE0);

with the patch below, mode0/0 is assumed by 'init_spi1'.

// bring up the GPIO pins and SPI HW interface
if (init_spi1(NRF8001_SPI, SPI_BaudRatePrescaler_64) != E_SUCCESS) {
    log_err("GPIO and SPI HW bringup failed");
}

The pin assignment for REQN, RDYN, RESET, and SPI (MOSI, MISO, SCLK) are stored in the 'aci_pins_t' structure within the 'aci_state_t' global variable.  The values are stored as uint8_t so just the pin number is represented and the port must be addressed manually in the project.

The port and pins are defined in spi.h

#define NRF8001_SPI  SPI1

#define SPI_COMMON_PORT GPIOA
#define MOSI_PIN        GPIO_Pin_7
#define MISO_PIN        GPIO_Pin_6
#define SCLK_PIN        GPIO_Pin_5

#define RESET_GPIO_PORT GPIOA
#define RESET_PIN  GPIO_Pin_0

#define RDYN_GPIO_PORT  GPIOA
#define RDYN_PIN  GPIO_Pin_4

#define REQN_GPIO_PORT GPIOB
#define REQN_PIN   GPIO_Pin_0

and they are set in the application's setup function, for instance

aci_state.aci_pins.reqn_pin   = REQN_PIN;
aci_state.aci_pins.rdyn_pin   = RDYN_PIN;
aci_state.aci_pins.mosi_pin   = MOSI_PIN;
aci_state.aci_pins.miso_pin   = MISO_PIN;
aci_state.aci_pins.sck_pin    = SCLK_PIN;
aci_state.aci_pins.reset_pin  = RESET_PIN;

The pins are actually configured in the same 'hal_aci_tl_init' function where the 'init_spi1' function is called.

8. The uint8_t structure width we mentioned previously for pin assignments only supports 8 unique bitmasks.  The data width for each pin value is expanded to a uint16_t type to accommodate the 16 possible pins for each port.  In the file 'hal_aci_tl.h',

uint16_t reqn_pin;    //Required
uint16_t rdyn_pin;    //Required
uint16_t mosi_pin;    //Required
uint16_t miso_pin;    //Required
uint16_t sck_pin;    //Required

9. A simple example is included in the 'master' branch under examples/ which advertises and sends a notification every 1 second.  The notification packet payload contains a tick count of the systick timer.

Connect the STM32F4-discovery or STM32-nucleo-F401 to your computer using the onboard USB and ST-link debugger.  Follow the directions in the README.md for building and flashing.

After flashing, launch your favorite Bluetooth central explorer -- I'm using Nordic Master Control Panel.  If everything is connected correctly, you should see a device named 'Hello'.

Connect a serial FTDI adapter (3.3V) with TX/RX to PA2 (TX) and PA3 (RX), i'm using a sparkfun model in the picture above.  Reset the MCU and verify the debug messages are printed to the console.  The characters typed on the console are buffered and sent in a 20 byte notification packet to the central/host after 20 bytes have been entered.

Picture 3 Nordic Master Control Panel showing 'Hello' BLE device name example

Friday, October 23, 2015

Particle/Spark WiFi photon intro and first project

I recently picked up a Particle 'Photon' WiFi Device.  It's a pretty cool platform and idea.  I missed out on the Kickstarter but they are shipping the development kits and modules of the P0.

The P0 SOM is based around the BCM43362 WiFi chipset and uses a STM32F205 as the brain.  The STM32F205 firmware is open source and on github.

I set the device up through my iPhone.  The setup process with Tinker app was seamless in setup.  I ran into issue when I tried flashing through the build.particle.io with the LED blink example the device would go into an infinite flash cycle (flashing magenta LED).  I would then have to soft-reset the device to push it back into WiFi Host-mode so I could pair it again.

I also ran into some issues with my unit out-of-the-box.  Generally, the serial operation was flaky with frequent 'Serial Error' messages.

I ended up having to flash to the latest firmware (0.4.5) using dfu-util.  This resolved the issue with poor serial operation and inability to flash from the cloud-based IDE.

The code syntax interface is 'Wiring' similar syntax so easy to pickup if you've used the Arduino.  Now onto the fun stuff, the demo....

I threw together a project when I got back this evening.  My girlfriend has been leaving her hair straightener on in the mornings and will text me to check and turn it off before I go to work.  So, I thought I'd whip up an AC relay example to showcase how rapid and easy the development is.  The whole thing took me about 1 hour to put together.  I could probably condense this all into a single PCB and replace the mini-USB breakout used for power by an integrated AC-DC +5V module recommended on this electronics.stackexchange post.  A block diagram of the setup is shown below.  I used the Tinker iPhone app to control the device.  The D7 GPIO pin on the Photon is connected to the Powertail Switch 2 AC relay + input.


Required Parts:
1. Sparkfun mini USB breakout. - $2
2. Apple AC-to-DC 5V USB wall plug. - $10
3. Spark Photon Eval Board. - $20
4. Powerswitch Tail 2. - $26.00

Some pictures of the prototype:







Sunday, December 1, 2013

Real Time Clocks and Flash Memory

Hello again, this my second blog post.  My goal originally was to update weekly but with the recent holiday and other life events I've been forced to reduce the frequency of my updates.  I'm going to hold myself to the bi-weekly update though so all is well.

I wanted to give a short update about two pieces of code I worked on in the past week.  The background is that I'm working on a GPS Datalogging application that reports coordinates wirelessly to a server for logging where they can be viewed by at a later time.  The project is pretty far a long, I'll provide a full description at a later date.  My goal is to make the project an open hardware and open source platform so that hobbyists can build up their own board and modify the software as they see fit.  I intend to offer assembled boards and kits for a recovery fee + labor.  Below are some pictures of the board as I populate it.  The board is pretty small (2" x 2") and is meant to fit into a Hammond plastic enclosure.

Front-View

Back-View


Atmel AT45DB161D
So back to the technical details.  I'm using an Atmel AT45DB161D for data storage.  The AT45DB161D is a page-based 16 Megabit (2 Megabyte) flash memory storage that is controlled over an SPI interface.  I found some good AVR C code over at sparkfun that someone wrote for the ulogger application, documented here:
https://www.sparkfun.com/products/9228

The author bit banged the solution and I decided to port this over to the PSoC.  The interface is simple enough and is documented in the provided datasheet here:
https://www.sparkfun.com/datasheets/IC/AT45DB161D.pdf

I spent a couple hours trying to use the SPI component on the PSoC platform but gave up in the end.  The clock select line must be held low for the duration of the read-out but this conflicts with the Read function of the SPI block in PSoC.  I tried to manually control the Clock Select line but this caused additional timing and synchronization errors.  The bit bang solution is good enough for my needs.  I don't like tying down the main thread for bitbanging but none of this code blocks so it's of minimal concern.  I uploaded the solution on my website and it can be found here:
http://www.ece.ucdavis.edu/~shraken/files/code/psoc/AT45DB161D_example_PSOC.zip

Maxim DS1672
I'm using a Dallas Semi/Maxim DS1672 as a RTC.   This is an example that shows how to interface with the Maxim DS1672 Real Time Clock (RTC) component.  The DS1672 is a 32-bit RTC counter that has a backup power option.  The backup power can be supplied from a small coin cell battery or a super capacitor where the DS1672 will trickle charge.  The RTC component in PSoC does not include any such option and is therefore of limited use for when the PSoC is put into off mode.  The DS1672 is controlled over an I2C bus and the code is implemented in PSoC Creator for a PSoC3 test device.

The I2C code is clean and shows how the counter is initialized with a count of zero and starts counting.  There is a delay loop in the example file of 2.5 seconds to show that the counter is increment.  A 32-kHz watch crystal is placed on the breadboard a long with the compensation capacitors of magnitude 12.5 pF.  The SDA and SCL line requires two 10k pull up resistors as shown below.

I used the DS1672 in particuliar because it's powered by 3.3 Volts.  I also needed the backup battery option with the coin cell because the data logger will often times be off for long duration to conserve and minimize current consumption.

For those who are curious, the SOIC to DIP adapter is a SchmartBoard variety and can be purchased from here:
http://www.mouser.com/ProductDetail/SchmartBoard/204-0004-01/?qs=sGAEpiMZZMtgbBHFKsFQgu%2fEm7E3KH7v%2fkdDRzCx4mI%3d
DS1672 SOIC breadboard on a PSoC dev kit.

I upload the project to my website and it can be found here:
http://www.ece.ucdavis.edu/~shraken/files/code/psoc/DS1672_example_PSOC.zip

Monday, November 18, 2013

This is my first blog post for 'Unconventional Wisdom'.  I'm going to treat this blogger as a diary of sorts and something to express my ideas quickly down on paper.  The writing will feature updates on my electronic and software hobby projects.  I also intend to sketch out writings which will be published in a more formal manner at another time.  I'm not really sure why I chose to name the blog 'Unconventional Wisdom' ... other than the fact that I enjoy advice that goes against the grain and isn't steeped in lore.  I hate operating on anecdotal evidence, it's lazy, and I respect reason.

So what's up right now?

1. Graduate Thesis
Well, I'm finishing my graduate thesis work right now for my MSEE at University of California, Davis.  I'm in the process of characterizing the noise of my photodiode front-end.  My goal is to make a comparisson between a conventional transimpedance amplifier and integrator for making light measurements.  I've had good progress so far in characterizing thermal, shot, and amplifier noise of my OPA124 operational amplifier.  I've made measurements on a HP 3561 Dynamic Signal Analyzer.  The spectrum analyzer provides a magnitude frequency response between 0.000125 Hz and 100 kHz.  It's a great instrument, the datasheet can be found here:
http://www.accusrc.com/objects/catalog/product/extras/5350_3561a.pdf

I'm capturing the magnitude response over a USB-GPIB interface to my laptop.  An example of my shot noise measurements are shown below.  The photocurrent results in a shot noise current that increases the noise floor.  I measured a DC photocurrent of 71.3 nA, 729.7 nA, and 2.7 uA and compared theory to measured results.  The results matched up well and are as follows:



Shot Theory
 -109.4209

 -103.1324

  -97.9616

Shot Measured
 -109.5040

 -103.2240

  -98.2600

2. Personal Projects
I ordered some cool stuff from sparkfun this past week.  One of the items I played with is the Sparkfun LiPo Fuel Gauge.
https://www.sparkfun.com/products/10617

It's a module that has a Maxim MAX1704 chip for monitoring the charge of lithium polymer rechargeable batteries.

One of the Sparkfun engineers provided an Arduino sketch using I2C Wire to communicate with the chip and extract battery percentage and voltage.  I ported this code to the PSoC Creator environment on Sunday and tested using the CY8CKIT-001 with a PSoC3 module.  A picture of the setup with the output on a LCD screen is shown below:
I uploaded the example on my website and the file can be downloaded from this link.