MAX32630FTHR as a VR Controller #MakeWithMaxim

1574644786036.png

Introduction

MAX32630FTHR is a really powerful board with quite a lot of onboard peripherals. Most noticeably are the built-in accelerometer and the dual-mode Bluetooth low energy chip which unlock the door to use this board as a VR controller without any additional components.
The most unfortunate thing is as of my project finished, there has been no mbed library support for the onboard PAN1326B dual-mode ble chip, so I have to use some alternative design as a workaround. But the overall design principle will not change which is collecting the accelerometer data, converting to x, y, z axises data, and then send to the Bluetooth central device, in my case is an Android phone, but could be any VR device supporting BLE joystick.

BOM
1574644793073.png

Arduino Pro mini 3v3 x1 ($9.95)
Tiny BLE x1 ($30.09)
MAX32630FTHR x1 ($28.05)


Schematics

1574644800337.png


Instructions
This project could be easier and simpler, but because the Bluetooth library is not available, a lot of complexities come from the data transportation between boards.
So the goal is to collect the data from BMI160 accelerometer/gyroscope chip, move them to a board with BLE support, and send out as 3-axis data to a VR device.
Lucky for me I have some experience on Tiny BLE, so I decided to use it as if it is the BLE chip on the MAX32630FTHR board. My first attempt was to use I2C protocol, letting the FTHR board act as the I2C master and Tiny BLE as the I2C slave. But it turned out the I2CSlave is not supported on either board. So I turned to use Serial protocol. This should allow me to use one wire to send data from FTHR board to Tiny BLE on a predefined baud frequency. But that attempt was also without luck. For some reason, the serial data wasn't sent out of FTHR board correctly. I guess this may because of some interference between the built-in USB serial circuit and the UART2 port I used as the data output pins.
Finally, I chose the current solution, using an Arduino Pro Mini as the man-in-middle, converting the incoming I2C data from FTHR board, and send out through TX pin as serial data to Tiny BLE.
More details can be found in my source code listed below. But there are some notable considerations worth mentioning:
  1. FTHR board doesn't work well when sending data using serial protocol, but I2C works perfectly on the board.
  2. mbed I2C use 8-bit address, so the slave address must be shift one bit left before used in the API.
  3. I have to use a slightly lower frequency (10KHz) than the mbed default I2C frequency (100KHz) to allow the much slower Arduino Pro mini being able to recognize the signal.
  4. I have to use a much higher baud (115200) to send out the serial data otherwise the income data will overflow the output.
  5. The 3-axis data are uncalibrated, so you can see I found some difficulties while using it in the VR game. Further improvement will be done to provide a much better user experience.
The final project consists of two part, the VR game and the BLE controller.
The VR game is based on Unity engine. I modified the Roll a Ball tutorial by adding Google VR support. Here I choose the cardboard VR, not the daydream since I'll provide my own VR controller and that will based on FTHR board! By simply following the Google VR Unity support document and we added the VR support to a 3D game. We can now choose Android as the build target, compile our game and run on my Android phone. You can find the full project source code in the Source Code section.
The BLE controller is done by collecting the data from FTHR board, sending through I2C to Pro mini, then passing to Tiny BLE to be exposed as joystick input data. The reason that I need a Pro mini in middle was because neither board can act as I2C slave, and for some reason serial communication did not work.

UPDATE: MAXIM support team just notify me that their engineer team just released the BLE support library for FTHR board. My project will become much simpler by remove the other two boards!
https://developer.mbed.org/teams/MaximIntegrated/code/FTHR_BLE_Demo/

1574644807678.png


Video

Source Code

Roll A Ball VR Game source code (Unity project)
https://drive.google.com/file/d/0B-FHQj0xfENBcklnOXNqZHRVMk0/view?usp=sharing


MAX32630FTHR code
https://developer.mbed.org/users/bowenfeng/code/MAX32630FTHR_JOYSTICK/
Code:
#include "mbed.h"
#include "max32630fthr.h"
#include "bmi160.h"

#define LOG(...) { printf(__VA_ARGS__); }

int main() {
    DigitalOut rLED(LED1, LED_OFF);
    DigitalOut gLED(LED2, LED_OFF);
    DigitalOut bLED(LED3, LED_OFF);

    I2C i2cBus(P5_7, P6_0);
    i2cBus.frequency(400000);
    BMI160_I2C imu(i2cBus, BMI160_I2C::I2C_ADRS_SDO_LO);

    I2C dataOut(P3_4, P3_5);
    dataOut.frequency(10000);

    uint32_t failures = 0;

    MAX32630FTHR pegasus(MAX32630FTHR::VIO_3V3);

    if(imu.setSensorPowerMode(BMI160::GYRO, BMI160::NORMAL) != BMI160::RTN_NO_ERROR) {
        LOG("Failed to set gyroscope power mode\n");
        failures++;
    }
    wait_ms(100);

    if(imu.setSensorPowerMode(BMI160::ACC, BMI160::NORMAL) != BMI160::RTN_NO_ERROR) {
        LOG("Failed to set accelerometer power mode\n");
        failures++;
    }
    wait_ms(100);

    BMI160::AccConfig accConfig;
    //example of using getSensorConfig
    if(imu.getSensorConfig(accConfig) == BMI160::RTN_NO_ERROR) {
        LOG("ACC Range = %d\n", accConfig.range);
        LOG("ACC UnderSampling = %d\n", accConfig.us);
        LOG("ACC BandWidthParam = %d\n", accConfig.bwp);
        LOG("ACC OutputDataRate = %d\n\n", accConfig.odr);
    } else {
        LOG("Failed to get accelerometer configuration\n");
        failures++;
    }

    //example of setting user defined configuration
    accConfig.range = BMI160::SENS_4G;
    accConfig.us = BMI160::ACC_US_OFF;
    accConfig.bwp = BMI160::ACC_BWP_2;
    accConfig.odr = BMI160::ACC_ODR_8;
    if(imu.setSensorConfig(accConfig) == BMI160::RTN_NO_ERROR) {
        LOG("ACC Range = %d\n", accConfig.range);
        LOG("ACC UnderSampling = %d\n", accConfig.us);
        LOG("ACC BandWidthParam = %d\n", accConfig.bwp);
        LOG("ACC OutputDataRate = %d\n\n", accConfig.odr);
    } else {
        LOG("Failed to set accelerometer configuration\n");
        failures++;
    }

    BMI160::GyroConfig gyroConfig;
    if(imu.getSensorConfig(gyroConfig) == BMI160::RTN_NO_ERROR) {
        LOG("GYRO Range = %d\n", gyroConfig.range);
        LOG("GYRO BandWidthParam = %d\n", gyroConfig.bwp);
        LOG("GYRO OutputDataRate = %d\n\n", gyroConfig.odr);
    } else {
        LOG("Failed to get gyroscope configuration\n");
        failures++;
    }

    wait(1.0);

    if(failures == 0) {
        float imuTemperature;
        BMI160::SensorData accData;
        BMI160::SensorData gyroData;
        BMI160::SensorTime sensorTime;

        char buffer[256] = {0};

        while(1) {
            imu.getGyroAccXYZandSensorTime(accData, gyroData, sensorTime, accConfig.range, gyroConfig.range);
            imu.getTemperature(&imuTemperature);

            sprintf(buffer, "%d,%d,%d\n", accData.xAxis.raw, accData.yAxis.raw, accData.zAxis.raw);
            LOG(buffer);
            dataOut.write(0x8<<1, buffer, strlen(buffer) + 1);

            gLED = !gLED;
        }
    } else {
        while(1) {
            rLED = !rLED;
            wait(0.25);
        }
    }
}
Arduino Pro mini code
Code:
#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.onReceive(receiveData);
  Wire.begin(0x8);

  delay(2);

  Serial.println("Logger Started.");
}

void loop() {
  delay(5);
}

void receiveData(int cnt) {
  while (Wire.available() > 0) {
    char c = Wire.read();
    Serial.print(c);
  }
}
Tiny BLE code
https://developer.mbed.org/users/bowenfeng/code/Seeed_Tiny_BLE_FTHR_Peripheral/
Code:
#include "mbed.h"
#include "nrf51.h"
#include "nrf51_bitfields.h"

#include "ble/BLE.h"
#include "JoystickService.h"
#include "DFUService.h"
#include "UARTService.h"

#include "examples_common.h"

#define LOG(...)    { pc.printf(__VA_ARGS__); }

#define LED_GREEN   p21
#define LED_RED     p22
#define LED_BLUE    p23

#define LED_OFF 1
#define LED_ON  0

#define UART_TX     p9
#define UART_RX     p11
#define UART_CTS    p8
#define UART_RTS    p10

#define DATA_TX p3
#define DATA_RX p6

DigitalOut blue(LED_BLUE);
DigitalOut green(LED_GREEN);
DigitalOut red(LED_RED);

Serial pc(UART_TX, UART_RX);
Serial data(DATA_TX, DATA_RX);

BLE  ble;
JoystickService *joystickServicePtr;
static const char DEVICE_NAME[] = "BunnyJoystick";
static const char SHORT_DEVICE_NAME[] = "joystick0";

volatile bool bleIsConnected = false;
volatile uint8_t tick_event = 0;

static void onDisconnect(const Gap::DisconnectionCallbackParams_t *params) {
    LOG("disconnected\r\n");
    bleIsConnected = false;
    red = LED_OFF;
    blue = LED_OFF;
    green = LED_ON;
    ble.gap().startAdvertising(); // restart advertising
}


static void onConnect(const Gap::ConnectionCallbackParams_t *params) {
    LOG("connected\r\n");
    bleIsConnected = true;
    red = LED_OFF;
    blue = LED_ON;
    green = LED_OFF;
}

static void waiting() {
    if (!joystickServicePtr->isConnected()) {
        green = !green;
    } else {
        blue = !blue;
    }
}


int main() {
    blue = LED_OFF;
    green = LED_OFF;
    red = LED_OFF;

    data.baud(115200);

    wait(4);
    LOG("Bunny Joystick started.\n");

    LOG("initialising ticker\r\n");
    Ticker heartbeat;
    heartbeat.attach(waiting, 1);

    LOG("initialising ble\r\n");
    ble.init();

    ble.gap().onDisconnection(onDisconnect);
    ble.gap().onConnection(onConnect);

    initializeSecurity(ble);

    LOG("adding hid service\r\n");
    JoystickService joystickService(ble);
    joystickServicePtr = &joystickService;

    LOG("adding dev info and battery service\r\n");
    initializeHOGP(ble);

    LOG("setting up gap\r\n");
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::JOYSTICK);
    ble.gap().accumulateAdvertisingPayload(
        GapAdvertisingData::COMPLETE_LOCAL_NAME,
        (const uint8_t *)DEVICE_NAME,
        sizeof(DEVICE_NAME));
    ble.gap().accumulateAdvertisingPayload(
        GapAdvertisingData::SHORTENED_LOCAL_NAME,
        (const uint8_t *)SHORT_DEVICE_NAME,
        sizeof(SHORT_DEVICE_NAME));
    ble.gap().setDeviceName((const uint8_t *)DEVICE_NAME);

    LOG("advertising\r\n");
    ble.gap().startAdvertising();

    int xraw, yraw, zraw;

    while (true) {
        if (data.readable()) {
            char buffer[256] = {0};
            char c;
            int i = 0;
            while(data.readable() && i < 256) {
                buffer[i++] = c = data.getc();
                if (c == '\n') {
                    buffer[i] = 0;
                    break;
                }
            }
            LOG("Received data from FTHR:\n");
            LOG(buffer);
            sscanf(buffer, "%d,%d,%d", &xraw, &yraw, &zraw);
            LOG("%d,%d,%d", xraw, yraw, zraw);
            joystickServicePtr->setSpeed(xraw, yraw, zraw);

            wait(0.5);
        }

        ble.waitForEvent();
    }
}

Blog entry information

Author
bowenfeng
Views
2,959
Last update

Downloads

More entries in General

Share this entry

Top