Friday 19 October 2018

A MQTT demo using ESP-01 (ESP8266)

I created a very simple demo based on ESP-01 module and MQTT protocol.

A video of the demo is also available here:



MQTT is a popular protocol for IoT, the client implementation is small in footprint and there are all kinds of example codes available on the internet, from C to Java or Java Script.

This demo is based on C code, which is provided as an example in the esp-open-rtos SDK. The circuit is very simple, it has 2 LEDs connected to the ESP-01 and a mobile phone is used to control these LEDs.

There are several open MQTT servers providing free access to public on the internet, such as test.mosquitto.org, and a private server is also very easy to setup, if you use Ubuntu, you can simply use command
sudo apt-get install mosquitto
to install the open source MQTT server on your computer. I'm using a VPS as the server, to verify that this concept will actually work in the cloud.

The red and blue LEDs are belonging to different topics, and are subscribed to the topic. The mobile phone App has two buttons, one for each LED, when you press the button, the App will publish a new message to the corresponding topic. The published message will toggle between '0' and '1' every time the button is pressed. Then the message will be delivered to the subscribed client (ESP8266), the microcontroller will decide whether to turn the LED on or off according to the message content.

Tuesday 9 October 2018

nRF52832 BLE demo written in C++

Nordic's nRF5xxxxx series chips are very popular BLE chipsets, according to my own experience, comparing to the CSR solution, nRF is more flexible and powerful. For example, the nRF52832, which is used in this demo, is a full functioning Cortex M4 MCU with FPU, if you don't use their SoftDevice, it will just be a Cortex M4 MCU like any other one on the market plus a radio interface, while oh the other hand, the CSR1010 which I had a demo project with it here before is a more dedicated BLE chip with all the BLE stack inside and the user program has to stay in the external serial EEPROM. The user has very limited access to the hardwares inside, the only way is through their APIs.

Despite their chip being a really wonderful product, the nRF SDK is quiet disappointing. The only want you to use their examples and adopt it to your needs, but never tell you how you should use the SoftDevice. I felt very frustrated when I read the code in the examples, I've never read such bad structured C code ever. So many unnecessary macros and function calls, you often have to jump 3 or 4 times among the defined macros to find out which is the final SoftDevice API it calls.

Anyway, I finally figured out how to isolate the SDK code with my own, so then I can write my own code in C++, and don't use their provided peripheral libraries. I really don't like such libraries, you still have to learn how to use it and get yourself familiar with tons of API functions, won't save you any time. On the other side, controlling the hardware via the registers is much more strait forward and simpler.

The demo does a very simple job, it will generate a random value every a few seconds, and if the notification is turned on by the mobile device, the value is pushed to the mobile whenever it gets updated. Meanwhile, the mobile can send a message to the nRF52832, the message will be displayed on the 1602 LCD module when received. It is not necessarily a text message, but I use ascii text here to make it displayable.

Let's see how does it work now:

Here is the source code of the main.cpp:

#include "hd44780.h"
#include "hd44780if_nrf_4bit.h"
#include <nrf52.h> // definitions of nrf52 registers
#include <stdint.h> // definitions of common types such as uint32
#include <stdlib.h>
#include <ble.h>
#include <ble_advertising.h>
#include <ble_gatts.h>
#include <ble_gap.h>
#include <app_timer.h>
#include "start.h"

class Gpio
{
public:
Gpio()
{
NRF_P0->DIR = 0x001fc000; // set p14,15,16,17,18,19,20 output
NRF_P0->PIN_CNF[17] = 0; // connect p17 input
NRF_P0->PIN_CNF[18] = 0;
NRF_P0->PIN_CNF[19] = 0;
NRF_P0->PIN_CNF[20] = 0;
}
};

void update_value(void *p_attr_handle);

ble_gatts_hvx_params_t hvx_param;
uint16_t conn_handle;
APP_TIMER_DEF(user_timer);

Gpio gpio;

Hd44780if_nrf_4bit lcd_nrf_4bit;
Hd44780 lcd(lcd_nrf_4bit);




int main()
{
log_init();
app_timer_init();
app_timer_create(&user_timer, APP_TIMER_MODE_REPEATED, update_value);
    power_management_init();
    ble_stack_init();
    gap_params_init();
    gatt_init();
    conn_params_init();
    services_init();
    advertising_init();
    peer_manager_init();
    advertising_start();

// bt_start();
while (1)
{
idle_state_handle();
}
return 0;
}

extern "C" uint32_t bt_ext_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
    uint32_t err_code;
    switch (p_ble_evt->header.evt_id)
    {
case BLE_GATTS_EVT_WRITE:
if (p_ble_evt->evt.gatts_evt.params.write.handle == svc_handles.value_handle)
{
lcd.print_string(40, (char*)p_ble_evt->evt.gatts_evt.params.write.data, p_ble_evt->evt.gatts_evt.params.write.len);
lcd.print_string("                ");
}
break;
case BLE_GAP_EVT_CONNECTED:
lcd.print_string(0, "Connected    ");
lcd.print_string(40, "                ");
conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
app_timer_start(user_timer,150000, &svc_handles2.value_handle);
break;
case BLE_GAP_EVT_DISCONNECTED:
conn_handle = BLE_CONN_HANDLE_INVALID;
app_timer_stop(user_timer);
break;
        case BLE_GATTC_EVT_TIMEOUT:
            // Disconnect on GATT Client timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
                                             BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
            break;

        case BLE_GATTS_EVT_TIMEOUT:
            // Disconnect on GATT Server timeout event.
            err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
                                             BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
            break;
default:
break;
}
return 0;
}

extern "C" void adv_ext_handler(ble_adv_evt_t ble_adv_evt)
{
    uint32_t err_code;

    switch (ble_adv_evt)
    {
        case BLE_ADV_EVT_FAST:
lcd.print_string(0, "Fast Advertising");
lcd.print_string(40, "Interval 100ms  ");
            break;
case BLE_ADV_EVT_SLOW:
lcd.print_string(0, "Slow Advertising");
lcd.print_string(40, "Interval 500ms  ");
break;
        case BLE_ADV_EVT_IDLE:
lcd.print_string(0, "Idle.           ");
lcd.print_string(40, "                ");
//            sleep_mode_enter();
            break;
        default:
            break;
    }
}

void update_value(void *p_attr_handle)
{
uint16_t len = sizeof(sensor_value);

sensor_value = rand()%0xff;
hvx_param.type = BLE_GATT_HVX_NOTIFICATION;
hvx_param.handle = *(uint16_t*)p_attr_handle;
hvx_param.p_data = &sensor_value;
hvx_param.p_len = &len;
lcd.print_string(12, "0x");
lcd.print_hex(sensor_value);
if (conn_handle != BLE_CONN_HANDLE_INVALID)
{
sd_ble_gatts_hvx(conn_handle, &hvx_param);
}
}

The "Gpio" class initialises the I/O ports, and the important thing: to introduce the bt_ext_handler() and adv_ext_handler() functions to the original main.c (which is renamed as start.c) by the ext_handler.h file, and adopt these 2 event handlers to the BLE stack:

1.  in the ble_stack_init(), add this line to register BLE event handler:
NRF_SDH_BLE_OBSERVER(m_ext_observer, APP_BLE_OBSERVER_PRIO, bt_ext_handler, NULL);

2.  change the line to assign advertising event handler in advertising_init():
init.evt_handler = adv_ext_handler;

The reason to do this is these are the most likely two parts to have interactions with the user code.

Wednesday 9 May 2018

ESP8266 Blinky with official RTOS SDK

Last time I created a blinky program with the Espressif official Non-OS SDK, though it seems Espressif pays more attention to the NON-OS SDK (all of their major demos are non-OS), I'd like to use the RTOS because it will make things easier.

It is almost the same as the Non-OS, except instead of using the system timer of the Non-OS SDK, it uses the FreeRTOS software timer for the delay of the blink.

There is a project template in the SDK's ./example/ folder, we'll just use it as our base of this small demo, copy the "project_template" folder to the SDK's root and rename it to "blinky".

I'm using the official ESP Launcher board for this test, it has a blue LED connected to GPIO12, so in the "blinky/user/" folder, edit or create a user_main.c as this:

#include "esp_common.h"
#include "gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"

xTimerHandle    blink_timer;


/******************************************************************************
 * FunctionName : user_rf_cal_sector_set
 * Description  : SDK just reversed 4 sectors, used for rf init data and paramters.
 *                We add this function to force users to set rf cal sector, since
 *                we don't know which sector is free in user's application.
 *                sector map for last several sectors : ABCCC
 *                A : rf cal
 *                B : rf init data
 *                C : sdk parameters
 * Parameters   : none
 * Returns      : rf cal sector
*******************************************************************************/
uint32 user_rf_cal_sector_set(void)
{
    flash_size_map size_map = system_get_flash_size_map();
    uint32 rf_cal_sec = 0;

    switch (size_map) {
        case FLASH_SIZE_4M_MAP_256_256:
            rf_cal_sec = 128 - 5;
            break;

        case FLASH_SIZE_8M_MAP_512_512:
            rf_cal_sec = 256 - 5;
            break;

        case FLASH_SIZE_16M_MAP_512_512:
        case FLASH_SIZE_16M_MAP_1024_1024:
            rf_cal_sec = 512 - 5;
            break;

        case FLASH_SIZE_32M_MAP_512_512:
        case FLASH_SIZE_32M_MAP_1024_1024:
            rf_cal_sec = 1024 - 5;
            break;
        case FLASH_SIZE_64M_MAP_1024_1024:
            rf_cal_sec = 2048 - 5;
            break;
        case FLASH_SIZE_128M_MAP_1024_1024:
            rf_cal_sec = 4096 - 5;
            break;
        default:
            rf_cal_sec = 0;
            break;
    }

    return rf_cal_sec;
}


/****** led blink **********/
void led_toggle(void)
{
    if(GPIO_REG_READ(GPIO_OUT_ADDRESS) & BIT12)
    {
          gpio_output_set(0,BIT12, 0,0);
    }
    else
    {
        gpio_output_set(BIT12,0,0,0);
    }
}



/******************************************************************************
 * FunctionName : user_init
 * Description  : entry of user application, init user function here
 * Parameters   : none
 * Returns      : none
*******************************************************************************/
void user_init(void)
{
    PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12);
    gpio_output_set(BIT12, 0, BIT12, 0);
    blink_timer = xTimerCreate("blinkTMR", 300/portTICK_RATE_MS, pdTRUE, NULL, (tmrTIMER_CALLBACK)led_toggle);
    xTimerStart(blink_timer, portMAX_DELAY);
}

The only user created contents are the last 2 functions, the led_toggle() and user_init().

Reminder, don't forget to change the SDK_PATH and BIN_PATH settings in "gen_misc.sh", I feel it convenient to set the BIN_PATH to the SDK's root /bin/ folder, as the  blink.bin and esp_init_data_default.bin files are in there too. You'll need to download these files at least once when you use the ESP8266 for the first time, otherwise the chip will keep rebooting.

You are reading this probably because you are new to the ESP8266 as myself too. You may find it helpful by reading these articles too in my blog:

For building the SDK in Ubuntu:
https://www.mculabs.net/2018/04/building-esp8266-sdk-in-ubuntu.html

To connect a ESP8266 in Ubuntu:
https://www.mculabs.net/2017/07/connect-esp8266-in-ubuntu.html

Downloading(programming) the ESP8266 in Ubuntu:
https://www.mculabs.net/2018/04/downloading-code-to-esp8266-in-ubuntu.html

Saturday 28 April 2018

A Very Simple ESP8266 Blinky Source Code Using the Non-OS SDK

There are examples come with the official Espressif SDK, for the latest version 2.2.0, the examples are those:

As I said before, Espressif has the worst documentation comparing to other chip suppliers. These examples don't have any detailed information, the only thing you can find on their web site is an article about the Iot_Demo, which tells you how to use the program, not explaining how the code works.

I created a very simple blinky program with the Non-OS SDK, it has only one souce file in c, and it does nothing more than blink a LED on the ESP Launcher board. Not using any WiFi or communication APIs, anyway, the SDK forces you to include an initialization code in the user code to tell the chip where is the default parameters for the wireless circuits inside, and it seems the chip goes into Access Point mode though you don't have any code to let it do so.

There is a blue LED connected to MTDI/GPIO12 pin on the ESP Launcher board, what out code does, is to setup a software system timer, then toggle the status of the GPIO12 pin in the callback function. Plus a "Hello World" printed to the serial port. Here is the complete source code:

#include "ets_sys.h"
#include "osapi.h"
#include "gpio.h"
#include "user_interface.h"

uint32 priv_param_start_sec;
/******************************************************************************
 * FunctionName : user_rf_cal_sector_set
 * Description  : SDK just reversed 4 sectors, used for rf init data and paramters.
 *                We add this function to force users to set rf cal sector, since
 *                we don't know which sector is free in user's application.
 *                sector map for last several sectors : ABCCC
 *                A : rf cal
 *                B : rf init data
 *                C : sdk parameters
 * Parameters   : none
 * Returns      : rf cal sector
*******************************************************************************/
uint32 ICACHE_FLASH_ATTR
user_rf_cal_sector_set(void)
{
    enum flash_size_map size_map = system_get_flash_size_map();
    uint32 rf_cal_sec = 0;

    switch (size_map) {
        case FLASH_SIZE_4M_MAP_256_256:
            rf_cal_sec = 128 - 5;
            priv_param_start_sec = 0x3C;
            break;

        case FLASH_SIZE_8M_MAP_512_512:
            rf_cal_sec = 256 - 5;
            priv_param_start_sec = 0x7C;
            break;

        case FLASH_SIZE_16M_MAP_512_512:
            rf_cal_sec = 512 - 5;
            priv_param_start_sec = 0x7C;
            break;
        case FLASH_SIZE_16M_MAP_1024_1024:
            rf_cal_sec = 512 - 5;
            priv_param_start_sec = 0xFC;
            break;

        case FLASH_SIZE_32M_MAP_512_512:
            rf_cal_sec = 1024 - 5;
            priv_param_start_sec = 0x7C;
            break;
        case FLASH_SIZE_32M_MAP_1024_1024:
            rf_cal_sec = 1024 - 5;
            priv_param_start_sec = 0xFC;
            break;

        case FLASH_SIZE_64M_MAP_1024_1024:
            rf_cal_sec = 2048 - 5;
            priv_param_start_sec = 0xFC;
            break;
        case FLASH_SIZE_128M_MAP_1024_1024:
            rf_cal_sec = 4096 - 5;
            priv_param_start_sec = 0xFC;
            break;
        default:
            rf_cal_sec = 0;
            priv_param_start_sec = 0;
            break;
    }

    return rf_cal_sec;
}

/*******  toggle LED  **************/
void ICACHE_FLASH_ATTR toggle(void)
{
if (GPIO_REG_READ(GPIO_OUT_ADDRESS) & BIT12) // test if GPIO12 is 1
{
gpio_output_set(0, BIT12, 0, 0); // output 0
}
else
{
gpio_output_set(BIT12, 0, 0, 0); // output 1
}
}

/******************************************************************************
 * FunctionName : user_init
 * Description  : entry of user application, init user function here
 * Parameters   : none
 * Returns      : none
*******************************************************************************/
void ICACHE_FLASH_ATTR
user_init(void)
{
LOCAL os_timer_t blink_timer;
os_printf("Hello World from MCU Labs ESP8266\n");
PIN_FUNC_SELECT(PERIPHS_IO_MUX_MTDI_U, FUNC_GPIO12); //set the pin as GPIO
gpio_output_set(0, 0, BIT12, 0); //enable output on GPIO12
os_timer_disarm(&blink_timer);
os_timer_setfn(&blink_timer, (os_timer_func_t*)toggle, NULL); //setup callback
os_timer_arm(&blink_timer, 300, 1); //start timer at an interval of 300ms
}

Here is the video for compiling and running of the code, and the serial port output.
For how to download the code to ESP8266 in Ubuntu, see this article : https://www.mculabs.net/2018/04/downloading-code-to-esp8266-in-ubuntu.html 
For how to set Ubuntu to ESP8266's 74880 baudrate, see this: https://www.mculabs.net/2018/04/getting-odd-74880-baudrate-for-esp8266.html

Friday 20 April 2018

Getting the odd 74880 baudrate for ESP8266 in Ubuntu

ESP8266's official demos use a very odd baudrate 74880 for the UART (except for the AT demo). It is rarely supported by the operating systems. For Ubuntu, the serial port only supports those 'standard' baudrates, 38400, 57600, 115200 etc.

In Linux, it's very easy to see what's going on at the serial port, you don't even need a software to do that, use the terminal command 'cat':

$cat /dev/ttyUSB3

where ttyUSB3 is the device number in Ubuntu of the on-board USB-Serial converter of ESP Launcher.

But if you are not at 74880, you'll only see junks on the serial port when you run the IoT Demo.

 After a lot of searching on the internet, I finally find this python program, it can set the baud rate to any value you like, thanks for the excellent work of the creator of it.

#!/usr/bin/python
# set nonstandard baudrate. http://unix.stackexchange.com/a/327366/119298
import sys,array,fcntl

# from /usr/lib/python2.7/site-packages/serial/serialposix.py
# /usr/include/asm-generic/termbits.h for struct termios2
#  [2]c_cflag [9]c_ispeed [10]c_ospeed
def set_special_baudrate(fd, baudrate):
    TCGETS2 = 0x802C542A
    TCSETS2 = 0x402C542B
    BOTHER = 0o010000
    CBAUD = 0o010017
    buf = array.array('i', [0] * 64) # is 44 really
    fcntl.ioctl(fd, TCGETS2, buf)
    buf[2] &= ~CBAUD
    buf[2] |= BOTHER
    buf[9] = buf[10] = baudrate
    assert(fcntl.ioctl(fd, TCSETS2, buf)==0)
    fcntl.ioctl(fd, TCGETS2, buf)
    if buf[9]!=baudrate or buf[10]!=baudrate:
       print("failed. speed is %d %d" % (buf[9],buf[10]))
       sys.exit(1)

set_special_baudrate(0, int(sys.argv[1]))

You need to have Python installed on your computer to run it. The usage is pretty simple, if the program is named set_baud_rate.py, change to the folder that contains the file, and run:

$./set_baud_rate.py <>/dev/ttyUSB3 74880 

Now you can see the ESP8266's bootup messages.

Friday 13 April 2018

Downloading code to ESP8266 in Ubuntu

In last article I talked about setting up the SDK in Ubuntu, if everything goes well, you may can compile the code and got the binary code generated.

To download in Ubuntu is every simple, you just need to install the downloading tool and run it. There is a folder in the esp-open-sdk, which is the open source part of the whole SDK, named 'esptool'. inside the folder there's a Python program 'esptool.py'. If you have Python installed on your computer, you probably can use this tool. But for me sometimes it gives me confusing error message. I have two laptops both with Ubuntu 16.04, it runs well on one of them but on the other it says 'ImportError: No module named serial'. I don't know Python so I'm not able to figure out the problem. I hope you don't encounter this.

There is another way to do it. Since Ubuntu 12, the 'esptool' has been in the official repository and it's still there in Ubuntu 16. All you need to do is install it using the 'apt install' command.

$sudo apt install esptool

Yes, it's that simple. After this you'll have the ESP8266 downloading tool installed on your computer and that's a binary code you don't even need Python environment.


There are more functions than downloading in this esptool, you can find them out using 'man', Yes! yet another benefit to install esptool from the Ubuntu repository. To see the user manual, type :

$man esptool

Briefly, the command format is:
esptool -cp 'serial port' -ca 'address1' -cf 'file1' -ca 'address2' -cf 'file2' ....  (without the quote character)

For connecting the ESP8266, please see this article : Connect ESP8266 in Ubuntu

If you have multiple USB tty devices connected, it would be difficult to know which is which by just seeing the /dev/ttyUSB* list. A shortcut to sort it out is to use the 'dmesg' command. It will output all the system messages since the computer was been turned on, but a grep will filter unwanted out. Unplug your ESP8266 and connect again, then type:

$dmesg | grep tty

The latest message about tty device will be your ESP8266 and you'll know which ttyUSB number it's at.

A real download command would look like this:

esptool -cp /dev/ttyUSB0 -ca 0 -cf eagle.flash.bin -ca 0x10000 -cf eagle.irom0text.bin -ca 0x3fc000 -cf esp_init_data_default_v08.bin -ca 0x3fe000 -cf blank.bin

Tuesday 3 April 2018

Building the ESP8266 SDK in Ubuntu

The ESP8266 is built upon Linux entirely, the compiler and linker are both based on GCC, and even the downloading tool is also a Linux application. But for some unknown reasons, Espressif chooses to provide its SDK in a Windows favor. In their official guides, you need to install a virtual machine in Windows and then download an entire Lubuntu image which contains the SDK.

For someone who uses Ubuntu more than Windows like me, this simply doesn't make sense. And Espressif won't tell you how to build the environment in linux at all. If you let me pick a company with bad documentation, Espressif must be in the top 3.

I finally figured it out after a lot of searching and trying, and I would share it here.

At first, it should be made clear, which Espressif has failed to do so, the SDK is consist of 2 parts, the esp-open-sdk and the ESP8266 SDK. The esp-open-sdk is complete open source, containing the compiler and other utilities. This part can be downloaded from Github as source file, and then compiled locally. Thanks for pfalcon for doing this great job. Just choose where you want to install it and run:

$git clone --recursive https://github.com/pfalcon/esp-open-sdk

 You should get a dir named esp-open-sdk, and then enter the dir and run:

$make

If there are any missing dependencies the make program will give an error message and you can just follow the hints it gives to fix that. There are detailed instructions about the install on Github. The compiling will take quite a while (tens of minutes) if your computer is not fast enough.

The next step, is to install the ESP8266 SDK itself. This part is related to the ESP8266 chip and not completely open source. It contains some proprietary libraries. This part should be downloaded from Espressif's official web site. It would be something like ESP8266_NONOS_SDK-2.2.0 or so. You can put the downloaded directory anywhere you like, but not in the esp-open-sdk.

An important step, don't forget to add the /esp-open-sdk/xtensa-lx106-elf/bin/ to your PATH:

$export PATH=$PATH:(your full /esp-open-sdk/xtensa-lx106-elf/bin/ path)

Now we are ready to compile the examples. There is another pitfall hiding there which Espressif didn't tell you and will make you crazy. The examples in the SDK can NOT be compiled unless you move them to the upper folder!

All the examples are in the /ESP8266_NONOS_SDK-2.2.0/examples/ , for example, the IoT_Demo example is in /ESP8266_NONOS_SDK-2.2.0/examples/IoT_Demo/ . If you would like to compile this example, you have to MOVE it one level up, parallel to the 'examples' folder, so the project compiled must be at a folder like /ESP8266_NONOS_SDK-2.2.0/IoT_Demo/ .

At the correct location, inside the IoT_Demo, just run:

$./gen_misc.sh

I hope this could help you.

Tuesday 2 January 2018

The Simplest USB HID Report Descriptor

It is often confusing for people including myself who just started to do programming for USB devices. USB protocol is complicated, a book about USB device can easily be hundreds of pages.

HID device could be the starting point for most developers, because it is relatively the simplest to do. Even though it is still difficult to handle and could cost weeks of time to get your first device working.

For me the most confusing concept is the report descriptor, I believe it is also the case for many others. Anyway once you understood it it's not so hard to handle.

HID is designed to work as an input or output device for the operating system, all the report descriptor do is to let the operating system understand what the data it send mean to the OS. For example the key that pressed, or the movement of the mouse. But for us developers we just need to use the HID interface and we use our own software to communicate with the device (unless you are developing a mouse or keyboard), we don't need the OS to understand our data. We do the report descriptor only because it's mandatory in the HID protocol.

So what we need to do is just keep the descriptor as simple as possible. According to the HID specification, the report descriptor must have the following parts:

  • Input
  • Usage
  • Usage Page
  • Logical Minimum
  • Logical Maximum
  • Report Size
  • Report Count

All the other items are optional. To generate a simplest report descriptor, we can use the official descriptor tool by the USB organization:
The order of the items does not matter, but to arrange it in a hierarchical order will make it easy to understand. I'll put the Usage Page first followed by the Usage, then the value range and report size and counts, ended with the Input.

Assuming we are going to transfer 2 bytes of data to the PC, our specific application software will know what the data means and how to use it, so what we need to tell the OS is simply there are 2 bytes of data. The final report descriptor will look like this:
It can be saved as a .h file and be used directly in C code. Use the 'Parse Descriptor' function to check if there's any error before save.

char ReportDescriptor[15] = {
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x00,                    // USAGE (Undefined)
    0x15, 0x00,                    // LOGICAL_MINIMUM (0)
    0x26, 0xff, 0x00,              // LOGICAL_MAXIMUM (255)
    0x75, 0x08,                    // REPORT_SIZE (8)
    0x95, 0x02,                    // REPORT_COUNT (2)
    0x81, 0x02                     // INPUT (Data,Var,Abs)
};