Recently, our MCU Applications team and our Micrium OS team decided to spend a few days, in teams, on a "Hackathon". This allowed us the opportunity to work on a larger, real-world application, in an effort to gain more insight into our products and uses.

Our team comprised of Brian Lampkin, Janos Magasrevy, and Yanko Sosa. For our project, we decided to create a PC media controller for wireless volume and media control. This would consist of a wireless USB Dongle, connected to a wireless remote controller to provide media controls such as volume up/down, next track/last track, mute, etc.

1. Requirements

1.1 Hardware

  1. A USB ‘Dongle’ to provide wireless connectivity to the controller.

    This required a USB interface MCU to communicate with the host PC and a radio MCU to communicate with the remote. For the USB MCU, we chose an EFM32HG, since it is relatively small, and our application – a simple UART to USB HID command bridge – would require little flash. For our Radio MCU, we chose an EFR32FG12 device, which could cover any proprietary protocol we chose to implement. This would provide our UART to Wireless bridge.
     
  2. A wireless ‘Remote’ to provide the user interface for the media controller.

    We chose another EFR32FG12 radio MCU, to pair with the other on the USB dongle. Since this was to be a battery powered remote, we needed an MCU that could be run in a low duty-cycle, low power mode. To provide the user interface, buttons and a joystick on an expansion board were used.

    The completed remote and dongle hardware, using an EFM32HG STK with a Wireless Expansion Board and EFR32FG12, along with an EFR32FG12 Wireless STK with a Joystick Expansion Board, are shown below:

 

1.2 Software

An additional requirement was added – the project must integrate Micrium OS in some manner. We chose to implement this on the EFR32FG12 wireless devices to help manage wireless connectivity and low power features.

2. System Overview

The system block diagram is as follows:

 

Buttons and Joystick input are taken by the remote’s Flex Gecko MCU, and converted into wireless packets that represend media commands. These are transmitted to the dongle’s Flex Gecko, which are then converted into UART transmissions to the dongle’s Happy Gecko MCU. Finally, these are interpreted as HID media control commands, sent to the host PC over USB.

The joystick expansion board was mapped to the following media control functions:

 

2.1 Wireless Protocol

Our project has a very simple wireless communication requirement. When a button is pressed on the remote, this button status must be transmitted from the remote to the dongle’s receiver. Since there are few functions, a single byte payload was used to transmit this data. The remote never needs to receive any information from the dongle, so the dongle can be kept in RX mode, while the dongle can transmit a byte whenever the state of the remote’s buttons changes. This is an extremely simple communication protocol, so we decided to use the lower level Radio Abstraction Interface Layer (RAIL) directly rather than utilizing a stack such as Zigbee or Connect.
 

Since no stack is used, the protocol is effectively proprietary. 2.4 GHz was chosen for the radio’s communication band, as opposed to a Sub GHz band, as this allows for a smaller antenna, useful for a handheld remote.

2.2 Energy Concerns

As a battery powered device, low energy consumption is a huge priority for the remote. However, since the dongle is USB powered, there is little reason to limit the power consumption there. Thus, the dongle can be awake and in RX mode continuously with little drawback when connected to the PC's USB. On the remote side, however, consideration was given into keeping the device in lower energy modes whenever possible. Due to the dongle always being in RX mode, we can effectively keep the remote in a low energy state until a button press is made, triggering a new media function update. In our design, this means that the remote only wakes to transmit a packet, then immediately re-enters sleep mode.

3 The Dongle

3.1 USB HID Media Device

The first step in the project was to create a device that could communicate to the PC as a media controller. We decided to implement a HID device, which allows for driverless communication to a PC host for a limited set of known functions. For this project, we implemented what is called a USB HID “Consumer Control” device. The description for the options available in interface is provided in a table in section 15. "Consumer Page" in the USB HID Usage Tables document available on USB.org. Some of the available commands found in this interface are:

This interface includes many of the media controls that you would normally use during the use of a media application on a PC (Playing video, music, etc): Play, Pause, Record, etc. In this project, we chose to implement the following commands on our remote:

  1. Play/Pause – ID: 0xCD
  2. Scan Next Track – ID: 0xB5
  3. Scan Previous Track – ID: 0xB6
  4. Mute – ID: 0xE2
  5. Volume Increment – ID: 0xE9
  6. Volume Decrement – ID: 0xEA
  7. Play (Unused) – ID: 0xB0
  8. Stop (Unused) – ID: 0xB7

We eventually decided not to use the Play and Stop commands, as the Play/Pause command that we found implemented the functionality we desired, and allowed us to reduce the total number of inputs to six, which would map neatly to our expansion board’s two buttons and joystick with four cardinal directions.

3.1.1 HID Report Descriptor

To interface with a host using the HID interface, a HID report descriptor, describing the functionality of the device, must be constructed. We used the HID Usage Tables document, which included several examples (Specifically, Appendix A.1 Volume Control contained a useful example on volume +/-), and the HID Descriptor Tool to construct the following HID Descriptor:

// HID Report Descriptor for Interface 0
const char hid_reportDesc[39] SL_ATTRIBUTE_ALIGN(4) =
{
  0x05, 0x0C,       // USAGE_PAGE (Consumer)
  0x09, 0x01,       // USAGE (Consumer Control)
  0xA1, 0x01,       // COLLECTION (Application)
  0x15, 0x00,       //   LOGICAL_MINIMUM (0)
  0x25, 0x01,       //   LOGICAL_MAXIMUM (1)
  0x75, 0x01,       //   REPORT SIZE (1)
  0x95, 0x08,       //   REPORT COUNT (8)
  0x09, 0xCD,       //   USAGE (Play/Pause)
  0x09, 0xB5,       //   USAGE (Scan Next Track)
  0x09, 0xB6,       //   USAGE (Scan Previous Track)
  0x09, 0xE2,       //   USAGE (Mute)
  0x09, 0xE9,       //   USAGE (Volume Increment)
  0x09, 0xEA,       //   USAGE (Volume Decrement)
  0x09, 0xB0,       //   USAGE (Play)
  0x09, 0xB7,       //   USAGE (Stop)
  0x81, 0x02,       //   INPUT (Data,Var,Abs)
  0x75, 0x08,       //   REPORT SIZE (8)
  0x95, 0x01,       //   REPORT COUNT (1)
  0x81, 0x03,       //   INPUT (Cnst,Var,Abs)
  0xC0              // END_COLLECTION
};

This constructs a HID report with two bytes of data. The first byte implements 8 bit options, one for each of the HID commands. The second is a placeholder byte, unused by our application (but could be used for additional functions in the future).

As a basis for our EFM32HG USB project, we used the usbhidkbd example, which implements a USB HID Keyboard. The conversion for this was rather simple, as the USB side only required a quick swap from the HID Keyboard descriptor to the new HID Consumer Device descriptor, above. With this change, the EFM32HG device now enumerated on the host PC as a media controller.

3.1.2 USB to Radio Interface

The next step in the process was to develop an interface that could communicate between the dongle’s radio MCU and the USB MCU to tell the PC when a media button had been pressed. For this, we implemented a simple UART interface.

The media control functions are represented by single bits in the HID report's first byte's bitfield, as described below:

typedef enum {
  PLAY = 0x01,
  SCAN_NEXT = 0x04,
  SCAN_LAST = 0x02,
  MUTE = 0x08,
  VOL_UP = 0x10,
  VOL_DOWN = 0x20,
}reports_t;

On the dongle side, the radio MCU merely sends one byte of data over UART with the appropriate bit set for the desired media function. This is then transmitted over USB by sending a HID report. When the EFM32HG MCU receives a byte over UART, the report is updated:

void USART0_RX_IRQHandler(void)
{
	USART_IntClear(USART0, USART_IF_RXDATAV);
	report = USART0->RXDATA;
}

 

Then, in the main loop, if the report has changed since the last one that was sent to the host, it is sent over USB:

    if (report != lastReport) {
	  /* Pass keyboard report on to the HID keyboard driver. */
	  HIDKBD_KeyboardEvent(&report);
	  lastReport = report;
    }

Note the function names and comments left over from the usbhidkbd example - a result of the limited modifications we had to make to this example to implement the media controller.

3.2 Dongle Radio Receiver

The Dongle’s radio receiver was built with an EFR32FG12 Wireless MCU, using RAIL as the radio interface layer. The firmware is extremely simple: a single byte packet is received from the remote device, and this packet is transmitted to the EFM32HG USB device over UART.

3.2.1 Radio Configuration

The EFR32FG12’s radio was configured using AppBuilder. We used the default settings for a 2.4 GHz, 1 Mbps PHY, modifying it for single byte packets. No other changes were made to this default profile’s settings.

3.2.2 Radio to UART Implementation

Once configured, the radio initialization is simple – the device’s radio is initialized and put into RX mode, while an RX callback is registered to handle the reception of the packet and its transmission over UART. The device then waits forever in a while loop to receive packets. The initialization routines are simply:

  // Configure RAIL callbacks
  RAIL_ConfigEvents(railHandle,
                    RAIL_EVENTS_ALL,
                    (RAIL_EVENT_RX_PACKET_RECEIVED));

  RAIL_Idle(railHandle, RAIL_IDLE, true);
  RAIL_StartRx(railHandle, channel, NULL);
  while (1) {
  }

 

In the RX callback, the packet is received, the radio is put back into RX mode, and the packet is transmitted over UART:

void RAILCb_Generic(RAIL_Handle_t railHandle, RAIL_Events_t events) {
  report_t packet;
  if (events & RAIL_EVENT_RX_PACKET_RECEIVED) {
    RAIL_RxPacketInfo_t packetInfo;
    RAIL_GetRxPacketInfo(railHandle,
                         RAIL_RX_PACKET_HANDLE_NEWEST, 
                         &packetInfo);

    // Receive the packet's one-byte payload
    packet = *(packetInfo.firstPortionData);

    RAIL_Idle(railHandle, RAIL_IDLE, true);
    RAIL_StartRx(railHandle, channel, NULL);

    // TX Packet over UART to EFM32HG
    USART_Tx(USART0, (uint8_t) packet);
  }
}

 

3.3 The Remote

The remote has two main components – the user interface and the radio, used for transmitting user inputs.

3.3.1 User Interface

The user interface of the remote uses a joystick expansion board, which provides two buttons and an analog joystick for inputs. This expansion board is described in section 8 of this document: https://www.silabs.com/documents/login/user-guides/ug122-brd4300a-user-guide.pdf

The analog joystick has an output of one pin which changes voltages depending on the direction the joystick is pressed in. To interface this joystick with the EFR32FG12, the device’s ADC is used to sample the voltage on the joystick’s output every 25 ms, triggered by the RTCC. This voltage is then converted into a direction in the ADC’s interrupt handler.

#define ADC_MAX_CODES (0x0FFF)
#define JOY_NONE_THRESH  (0.93 * ADC_MAX_CODES)
#define JOY_UP_THRESH    (0.81 * ADC_MAX_CODES)
#define JOY_RIGHT_THRESH (0.68 * ADC_MAX_CODES)
#define JOY_LEFT_THRESH  (0.55 * ADC_MAX_CODES)
#define JOY_DOWN_THRESH  (0)
void ADC0_IRQHandler(void)
{
  uint16_t sample;
  ADC_IntClear(ADC0, ADC_IF_SINGLE);

  sample = ADC0->SINGLEDATA;

  if (sample > JOY_NONE_THRESH) {
    joyState = JOY_NONE;
  } else if (sample > JOY_UP_THRESH) {
    joyState = JOY_UP;
  } else if (sample > JOY_RIGHT_THRESH) {
    joyState = JOY_RIGHT;
  } else if (sample > JOY_LEFT_THRESH) {
    joyState = JOY_LEFT;
  } else {
    joyState = JOY_DOWN;
  }
}

 

For the pushbuttons, GPIO interrupts were enabled for each pushbutton pin, which update the status of the buttons.

void BTN_Handler(void)
{
  bool BTN2, BTN3;

  BTN2 = GPIO_PinInGet(BTN2_PORT, BTN2_PIN);
  BTN3 = GPIO_PinInGet(BTN3_PORT, BTN3_PIN);

  if (BTN2 == BUTTON_PRESSED) {
    BTN2State = BTN2_PRESSED;
  } else {
    BTN2State = BTN2_RELEASED;
  }

  if (BTN3 == BUTTON_PRESSED) {
    BTN3State = BTN3_PRESSED;
  } else {
    BTN3State = BTN3_RELEASED;
  }
}

 

3.3.1 Radio and Packet Transmission

The radio on the EFR32FG12 device was configured exactly the same as on the dongle. In fact, the exact same AppBuilder project was used as a basis for both devices. Instead of remaining in RX mode, however, the remote is powered down between ADC measurements and button state changes. If the state of the inputs has changed (i.e. a button has been pressed or released since last sleeping), the Report Handler constructs a new report packet and transmits it. When the packet has been transmitted, the device is permitted to transition back to sleep mode.

To construct the report packet, the states of each button and the joystick are simply ORed together, since these states are mapped to the respective bit of their function in the HID report bitfield:

void Report_Handler(void)
{
  report_t report_current;
  static report_t report_previous = 0;

  while (1) {
    report_current = BTN3State | BTN2State | joyState;
    if (report_current != report_previous) {
      report_previous = report_current;
      TX_byte((uint8_t)report_current);
    } else {
      break;
    }
  }
}

 

3.4 Integration of Micrium OS

As an additional challenge, we were required to integrate Micrium OS into our project. For this, we decided to integrate this only on our EFR32FG12 devices, since the EFM32HG USB device was limited in flash, and it would not benefit from the addition of an operating system due to the simplicity of the firmware running on the device.

Adding Micrium OS to the Flex Gecko EFR32FG12

One of the challenges we faced early in the project was that Micrium OS did not natively support the EFR32FG12 in the sense that the development of a Micrium OS board support package (BSP) was required.

1. Micrium OS Board Support Package (BSP)

  1. Compiler-specific Startup (Micrium_OS/bsp/siliconlabs/efr32fg12/source/startup/iar/startup_efr32fg12p.s)

    We first created the standard Micrium OS BSP folder structure within the Micrium_OS/bsp/siliconlabs folder using the EFM32GG11 as our reference BSP due to its similarities in the startup code. We then started modifying the compiler-specific startup file for the EFR32FG12. This step was fairly straight forward given the fact that most ARM-Cortex-M devices share the same initialization code, with the obvious difference being the number of interrupt vectors sources amongst the various devices.

    The Micrium OS kernel port relies on two ARM-Cortex-M core interrupt sources, they are the PendSV and the SysTick. In our compiler-specific startup code, we had to include these two sources found in the Micrium OS kernel port with the use of the EXTERN assembly directive:
  1. EXTERN  OS_CPU_PendSVHandler

EXTERN  OS_CPU_SysTickHandler

Then we allocated memory for the two handlers with:

DCD    OS_CPU_PendSVHandler

DCD    OS_CPU_SysTickHandler

We now have a Micrium OS compatible compiler-specific startup file.
 

  1. Device-specific Startup (Micrium_OS/bsp/siliconlabs/efr32fg12/source/startup/system_efr32fg12p.c)

    A device-specific startup file was required for the clock initialization. For this, we looked inside the Gecko SDK and found the corresponding startup for the EFR32FG12P (system_efr32fg12p.c). This file was added as-is into the Micrium OS BSP.
     
  2. Micrium OS Tick BSP (Micrium_OS/bsp/siliconlabs/efr32fg12/source/bsp_os.c)

    The Micrium OS Tick BSP file essentially handles the kernel tick initialization in either periodic mode or in dynamic mode depending on the power consumption requirements of the project. We left this file the same as the one found in the EFM32GG11 and ran in periodic mode. As one of the potential improvements later on, we could switch to dynamic tick in order to improve the power consumption of our device.
     
  3. Micrium OS CPU BSP (Micrium_OS/bsp/siliconlabs/efr32fg12/source/bsp_cpu.c)

    The Micrium OS CPU BSP file deals with the setup of timestamp timers that are required by the OS for statistical purposes and other features. This was once again left the same as in the EFM32GG11.
     
  4. Micrium OS Interrupt Sources definitions (Micrium_OS/bsp/siliconlabs/efr32fg12/include/bsp_int.h)

    In this file, the various interrupt sources definitions are specified. Although not necessary for our project, this file is included in bsp_os.c to assign BSP_INT_ID_RTCC as a kernel aware interrupt source when dynamic tick is enabled.
     
  5. Micrium OS generic BSP API (Micrium_OS/bsp/include/bsp.h)

    This is the final piece of the Micrium OS BSP puzzle. In this file, the prototypes for BSP_SystemInit(), BSP_TickInit(), and BSP_PeriphInit() are defined. Some of these functions will later be used in our program main().

 

2. Micrium OS main.c

 

  1. main()

    In the standard Micrium OS main(), the CPU is initialized with CPU_Init(), followed then by the board initialization via BSP_initDevice() and BSP_initBoard(), both from the Gecko SDK. After the CPU and the board clocks are initialized, the OS follows with OSInit() which initializes the kernel. Once the OS is initialized, our startup task is then created by calling OSTaskCreate() (see section 2b.). Finally, after the startup task has started its execution, the kernel starts by calling OSStart().
     
  2. StartupTask

    In the Startup Task, the kernel tick is initialized using BSP_TickInit() from the Micrium OS BSP. Other services such as the UART are also initialized here. In our case, USART2 is used. It is important to mention that the Startup Task has a 500-millisecond delay inside an infinite loop in order for it to yield CPU time to other tasks when running in a multithreaded environment.

    Since our project utilizes proprietary wireless, the RAIL library is included and therefore initialized in the Startup Task at 2.4GHz.

    In order to demonstrate different kernel services, a RAIL receive (Rx) semaphore object is created in this task.
     
  3. RAIL Rx Task

    Our model consists of two tasks: Startup Task and the RAIL Rx Task.

    In the RAIL Rx Task, the program pends on the RAIL Rx semaphore created in the Startup Task. Once data from the wireless remote is received by our device, an interrupt fires and a callback function dissects the packet and posts the first byte of data to the RAIL Rx semaphore. The RAIL Rx task then transmits the data received via USART2 to the Happy Gecko. The callback function briefly puts the radio in an idle state before waking the receiver once again to obtain the next radio packet.

 

5. Next Steps

With the project complete and functional using STKs and pre-made expansion boards, we want to pursue creating custom PCBs for both the remote and dongle. This would require a fair amount of work, laying out two MCUs plus a USB connector on the dongle board, and another MCU in a reasonable hand-held remote form factor for the wireless remote. Additional challenges may arise in laying out the wireless specific portions of the board, especially in regards to antenna design and placement. We hope to accomplish this early this year, and have remotes and dongles constructed for each team member to use. Overall, this has been an interesting and challenging project, and it would be great to see it to completion with a physical, practical media remote designed and built.

6. Attached Projects

All firmware projects can be found here: https://www.dropbox.com/s/tt55ky7m7h5hmeq/PC_Media_Remote.zip?dl=0
This includes firmware to run on the dongle's EFM32HG USB MCU and EFR32FG12 Wireless MCU, and the remote's EFR32FG12 Wireless MCU. These are:

1. Dongle_EFM32HG - firmware for the dongle's EFM32HG to perform UART to USB HID Media Control

2. Dongle_EFR32FG12_Micrium - firmware for the dongle's EFR32FG12 wireless receiver, with Micrium OS integration

3. Dongle_EFR32FG12_simple - firmware for the dongle's EFR32FG12 wireless receiver, before Micrium OS integration (simple while loop)

4. Remote_EFR32FG12 - firmware for the remote's EFR32FG12 wireless receiver for user input

  • Projects