Build your own Electronic Dice with Arduino Nano 33 BLE for Digital Board Games

Published  December 9, 2024   0
Build a Smart LED Dice with Arduino Nano

Many of us fondly remember the joy of playing board games like Snakes and Ladders or Ludo during our childhood. These simple yet engaging games brought families and friends together, making any gathering lively and entertaining. Rolling dice, moving tokens, and experiencing the ups and downs of the game taught us lessons in patience and unpredictability while sparking countless laughs and friendly competition. Those moments of flipping game boards in frustration or celebrating a lucky roll remain etched in our memories.

And as you may noticed nowadays many of us are addicted to modern games and may even forget these fun and entertaining board games. The thought of adding a modern twist to the classic games gave me the idea of creating a digital dice with some wireless connectivity. With some tinkering, I decided to opt for the BLE for the connectivity, because the low-power nature of it is really beneficial for such a project.

Smart LED DiceBLE Dice

I decided to go with an ultra-low power microcontroller such as an NRF52840 since these are not only highly power efficient than the counterparts such as ESP32, but they also have superior support for development IDEs such as Arduino IDE. I chose the NINA-B306 module featuring the NRF52840 since it would be much easier to handle and we don’t need to worry about the antenna design headaches compared to using the bare nRF52840 chips. Combining BluetoothLE connectivity, advanced motion sensors, and LED-based visual feedback, the Smart LED Dice bridges the gap between nostalgic gaming and modern tech.

This project was made possible, thanks to our sponsors ALLPCB. The PCB boards used in this project were fabricated by allpcb, more info on how to order will be shared later in this article. 

Features of our Smart Electronic Dice 

  • Based on low power nRF52840 low power, multi-protocol Bluetooth 5 SoC.

  • Bluetooth V5 Low Energy connectivity. Pair with smartphones, tablets, or custom BLE-enabled devices.

  • Arduino Nano 33 BLE compatible.

  • MPU6050 IMU with  Integrated accelerometer and gyroscope to detect dice orientation and movement for roll detection.

  • TP4056 Lithium-ion battery charging IC with overcharge protection.

  • Onboard USB type C  port for charging and Programming.

  • LEDs for Face Indication.

  • RGB LED for status indications.

  • Custom PCB with a compact and optimized layout integrates all components.

  • Designed to fit within a dice form factor.

  • Real-time orientation detection using MPU6050 to determine the face of the dice after rolling.

  • LEDs light up to show the upward-facing number on the dice after a roll.

  • Android App support. Companion app to receive dice roll data and display results.
     

Components Required to Build the Smart LED Dice

The components required to build a BLE are listed below. The exact value of each component can be found in the schematics or the BOM.

  • NINA-B306-00B module

  • MPU6050 IMU

  • MIC5219-3.3 LDO

  • TP4056 Li-ion battery 

  • 1615 CA RGB LED

  • 0805 LEDs

  • SMD resistors and capacitors

  • Connectors

  • Custom PCB

  • 3D printed parts.

  • Other tools and consumables.

Smart LED Dice Schematic Diagram

The complete circuit diagram for the Smart LED Dice is shown below. It can also be downloaded in PDF format from the link given at the end.

Smart LED Dice Schematics

Let’s discuss the Schematics section by section for better understanding. First, we have the power section, which includes the power input, battery charging and voltage regulation.  A type C USB port is used for both charging as well as for programming purposes. The power from the USB port is connected to a power path controller circuit built around a P-Channel MOSFET U3 and a diode D1. This will allow us to power the board either from the USB input or from the battery without causing any issues. The battery charging circuit is built around the infamous TP4056 standalone linear Li-lon battery charge controller IC. It will take the 5V input from the USB port and will charge the internal battery. The TP4056 also provides two indicators, one for charging indication and one for full charge indication.

We have also connected voltage dividers to these indicator pins which can be used for monitoring the charging status. For converting the VBUS voltage from the power path controller to 3.3V we have used an MIC5219 ultra-low noise low drop out voltage regulator. With very minimal auxiliary components the MIC5219 provides a very stable output voltage even when the battery charge level is low.

Smart LED Dice Schematics Power Section

Next, we have the Nina B306-00B module as the brain. The Nina B306-00B features the Nordic Semiconductor nRF52840 Bluetooth 5 Low Energy SoC, featuring an Arm Cortex-M4 processor with a floating-point unit, operating at 64 MHz. It integrates 1 MB of flash memory and 256 kB of RAM, offering ample space for code and data storage. For motion and orientation detection, we have used an MPU6050 IMU from InvenSense, which features a 3-axis gyroscope and a 3-axis accelerometer on the same silicon die, together with an onboard Digital Motion Processor that processes complex 6-axis MotionFusion algorithms. The MPU6050 is interfaced with the Nina B306 module via the I2C interface.

Smart LED Dice Schematics SoC and IMU Section

For indicating the Dice faces we have used LEDs. Each face will have a corresponding number of LEDs. Apart from the face with one LED, on all other faces, we are using 0805 LEDs connected in parallel with separate current limit resistors. For the face with one LED, we have used an RGB LED. This RGB is not only used for the face indication but also for indicating connectivity status. The RGB led with 1615 package made it the best choice for this without compromising on the size. It is small enough to fit with the aesthetics and easy to handle while assembling the circuit.

PCB for Smart LED Dice

For this project, we have decided to make a custom PCB. This will ensure that the final product is as compact as possible as well as easy to assemble and use. The PCB is designed with KiCad. All the design files are available to download from the GitHub repo linked below this article. The PCB has a dimension of each face is approximately 30mm x 30mm. And in total there are 6 such PCBs. But for manufacturing, we have created a panel with all 6 of them with size of 63x96mm, with stamp holes for easy separation. 

Here is the top layer of the PCB.

Smart LED Dice PCB Top Layer

The below image shows the bottom layer of the PCB.

Smart LED Dice PCB Bottom Layer

And here is the 3D view of the PCB.

Smart LED Dice PCB 3D view TopSmart LED Dice PCB 3D view Bottom

Ordering PCB from ALLPCB

Now after finalizing the design, you can proceed with ordering the PCB:

Step 1:  Get into https://www.allpcb.com/?code=PT19  and sign up if this is your first time. Then, in the PCB Quote tab, click on the Advanced PCB Quote, upload the Gerber file, and the tool will automatically populate the PCB dimensions. Now make any changes you need such as PCB colour, Silkscreen, PCB thickness, and the number of PCBs etc.

PCB Quote Tab View

Step 2:  Once all the required parameters are set click on quote now to generate the quote. The tool will show the build time, Cost and shipping costs of different shipping methods. Select the appropriate shipping method and click on add cart to proceed with the order.

PCB Shipping Cost

Step 3:  In the cart select the PCB we have just added and click on proceed. Give the shipping and billing details and then proceed with the payment. Once the payment is done the ALLPCB will start manufacturing your order. Within a week, you will receive the finished PCB.

Smart Dice PCB Board

 

Assembling the Smart LED Dice

To assemble the SMART LED Dice, first assemble each PCB on the panel. Each PCB represents one face of the dice. One side will have the LEDs and the other side will have all other components. Here is the fully assembled PCB.

Smart LED Dice Assembled PCB

Here is the fully assembled Dice.

Smart LED Dice Fully Assembled

3D Printed Parts

We have designed a 3D-printed cover enclosure that will fit over the PCB. These parts will ensure that the sharp edge of the PCB dice won’t affect its movements and that it rolls smoothly. The files for all the 3D printed parts can be downloaded from the GitHub link provided at the end of the article along with the Arduino sketch and bitmap file. Learn more about 3D printing and how to get started with it by following the link. You can download the 3D files from Thingiverse or from the project GitHub repo.

Thingyverse : https://www.thingiverse.com/thing:6855807 

Smart LED Dice 3D view

 

Here are the 3D-printed parts.

Smart LED Dice Enclosure 3D view

And here is the fully assembled Smart LED Dice with the 3D enclosure.

Smart LED Dice and 3D enclosureSmart LED Dice Fully Assembled

How Does the Smart LED Dice Work?

When powered on, the dice enters a standby mode, awaiting a BLE connection. During this time, the integrated RGB LED flashes red, green, and blue in a loop to indicate it is ready to connect. Once a BLE device, such as a smartphone or similar, successfully connects, the RGB LED turns off, signalling the connection and placing the dice in an idle state, ready for user interaction. The dice is equipped with an MPU6050 motion sensor to detect the dice roll. When a motion is detected, all face LEDs illuminate sequentially in a rolling effect, simulating the dice being tossed. This dynamic pattern continues until the dice become stationary. Once stationary, the dice determine the upward-facing face using data from the motion sensor. The LEDs corresponding to the upward-facing face start to flash to indicate the result.

To communicate the result to the connected BLE device, the dice updates two BLE characteristics. The Status Characteristic indicates whether a new roll and valid face detection have occurred. It is set to 1 after a roll is detected and can be reset to 0 by the connected device, allowing the dice to prepare for the next roll. The Face Number Characteristic transmits the number that corresponds to the upward-facing face and remains unchanged until the next roll. Notifications are sent to the connected device whenever these characteristics are updated, prompting it to read the values. Once the connected device reads the face number and resets the status characteristic to 0, the dice become ready to detect the next shake or roll. This ensures a smooth interaction cycle, where the dice locks further roll detection until the connected device confirms it is ready for the next action. 
 

BLE Dice Companion APP

To demonstrate the the functionalities of the Smart LED Dice we have created a simple smartphone app using the MIT App Inventor. The app has only the bare minimum components. There are two labels, one to show the connection status and one to show the result. There is an image element which is used to display the result graphically. We have added three buttons to the UI, of which two are for connecting and disconnecting the BLE connection. The reset button is used for debugging purposes, which will manually reset the status characteristics to zero.

BLE Dice App

Here is the block view for the app.

 

BLE Dice App Blocks

We have created four global variables to store the BLE device name, service UUID, status characteristics UUID and dice face characteristic UUID. As soon as the app is open, it will check if the necessary permissions are already granted or not. If not granted it will prompt to grant those permissions. Make sure you have turned on the Bluetooth and location from the device settings. Once all the permissions are granted all you have to do is click on the connect button. The app will automatically scan and connect to the Smart LED Dice using the device name and service UUID. If you want to disconnect the device you can either click on the disconnect button or close the app.

Once connected to the Smart LED Dice the app will register for notifications for the Dice face characteristics. This way, as soon as the face characteristics value is changed the app will get notified. Once notified the app will read this value and display it on the app screen. It will also display the corresponding image indicating the result. Once the value is read successfully the app will update the status characteristics with the value zero to enable the dice to detect the next roll. 

Both the Android app installer file as well as the MIT app inventor project files can be found on the project GitHub repository link provided at the end of this tutorial. While using the MIT app inventor don’t forget to install the BluetoothLE extension if it is not already installed.

Arduino Code for Smart LED Dice

As now we are familiar with the hardware and the basic working principle, let’s look at the firmware part. Since we are using the NINA-B306 module, we can program it with the Arduino IDE since it’s the same module that is on the Arduino Nano 33 BLE. To start with we need to flash the Arduino Nano 33 BLE bootloader to the module. To do that connect the programming pads (SWDIO, SWCLK, GND, Resetand 3.3V to any ARM debugger such as CMSIS-DAP or JLINK. Then install the Arduino MBed OS Nano board in the Arduino IDE, if it was not already installed. Later connect the debugger to the PC, assuming you have already installed all the necessary drivers, in the Arduino IDE, select the Arduino Nano 33 BLE as the board and select the appropriate programmer from the tools menu. Then use the burn bootloader option from the tools menu. It will automatically flash the bootloader to the module. One another option is to download the bootloader binary and flash it using any of your favourite debug tools such as J-flash or J-link commander.

Once the bootloader is burned then, we can move forward with the coding. For that, either download and open the Arduino source file from the GitHub repository or just create a new sketch and copy and paste the code given below. Make sure to set the board as the Arduino Nano ## BLE and install all the necessary libraries that were mentioned in the code. Compile it and flash it to the board using the upload button. That's it, now you will be able to see a BluetoothLE device named BLE_Dice if you search for available Bluetooth devices.

Now let's look at the code itself. As usual,l we have included all the necessary libraries and defined all the necessary pins. Later we defined the global variables and also created an instance for the MPU6050 library. This instance will be used to communicate with the MPU6050 IMU. You can also see we have defined a few Bluetooth-related variables such as the device name, service UUID and characteristic UUIDs. The device name, as the name suggests, is used for naming the BLE device. This is the name that will appear if you can for a Bluetooth device. Keep in mind that this name is also hardcoded to the companion app, so if you decide to change the name you should change it in the app too. The service UUID is used for the BLE communication. The two characteristic UUIDs are used to send data between the dice and the connected device. As mentioned earlier one will have the status flag and the other will have the result.
 

#include <Wire.h>

#include <Adafruit_MPU6050.h>

#include <Adafruit_Sensor.h>

#include <ArduinoBLE.h>

// Pin definitions

#define rgbRed A7

#define rgbGreen A6

#define rgbBlue 3

#define leftLEDs 4

#define rightLEDs 9

#define backLEDs 10

#define bottomLEDs 6

#define topLEDs 5

Adafruit_MPU6050 mpu;

// Variables for shake and stationary detection

float shakeThreshold = 15.0; // Adjust for shake sensitivity

float stationaryThreshold = 15.0; // Threshold for being stationary

unsigned long stationaryDuration = 1500; // Duration to confirm stationary (in ms)

// BLE Characteristics

BLEService diceService("180A");//0000180A-0000-1000-8000-00805f9b34fb

BLEByteCharacteristic statusCharacteristic("2A57", BLERead | BLEWrite | BLENotify);//00002A58-0000-1000-8000-00805f9b34fb

BLEByteCharacteristic faceCharacteristic("2A58", BLERead | BLENotify);//00002A57-0000-1000-8000-00805f9b34fb

// Other Variables

int currentFace = -1; // Tracks the current face

unsigned long lastStationaryTime = 0;

unsigned long lastRGBBlinkTime = 0;

bool BLEStatus = false;

int LEDPins[7] = {-1,A6, 4, 5, 6, 9,10};

enum DiceState {

WAIT_FOR_SHAKE,

WAIT_FOR_STATIONARY,

UPDATE_FACE,

WAIT_FOR_BLE_RESET

};

DiceState currentState = WAIT_FOR_SHAKE; // Start in the WAIT_FOR_SHAKE state

unsigned long lastFlashTime = 0; // For non-blocking sequential LED flashing

int flashIndex = 0; // Current LED index for sequential flashing

bool shakeDetected = false; // To track shake detection

unsigned long stationaryStartTime = 0; // Track time for stationary detection

Next in the setup function, we have initialised the IMU instance and also initialised all the GPIOs required. Once the sensor and the GIOs are successfully initialised the dice will then start the BLE service and start advertising.

void setup() {

Serial.begin(115200);

// Initialize MPU6050

if (!mpu.begin()) {

  Serial.println("Failed to find MPU6050 chip");

  while (1);

}

Serial.println("MPU6050 Found!");

mpu.setAccelerometerRange(MPU6050_RANGE_8_G);

mpu.setFilterBandwidth(MPU6050_BAND_21_HZ);

// Initialize LED pins

pinMode(rgbRed, OUTPUT);

pinMode(rgbGreen, OUTPUT);

pinMode(rgbBlue, OUTPUT);

pinMode(leftLEDs, OUTPUT);

pinMode(rightLEDs, OUTPUT);

pinMode(backLEDs, OUTPUT);

pinMode(bottomLEDs, OUTPUT);

pinMode(topLEDs, OUTPUT);

turnOffAllLEDs(); // Ensure all LEDs are off at boot

digitalWrite(rgbRed, LOW);

// Initialize BLE

if (!BLE.begin()) {

  Serial.println("Starting BLE failed!");

  while (1);

}

BLE.setLocalName("BLE_Dice");

BLE.setAdvertisedService(diceService);

diceService.addCharacteristic(statusCharacteristic);

diceService.addCharacteristic(faceCharacteristic);

BLE.addService(diceService);

// Set characteristics to 0 at boot

statusCharacteristic.writeValue(0);

faceCharacteristic.writeValue(0);

BLE.advertise();

Serial.println("BLE_Dice is ready!");

}

The loop function is responsible for the connection management. If the connection is active the loop function will call the handleDiceLogic function. If the connection is inactive the loop function will flash the RGB LED indicating the connection status.

void loop() {

// Handle BLE

BLEDevice central = BLE.central();

if (central) {

  BLEStatus = true;

  Serial.print("Connected to: ");

  Serial.println(central.address());

  currentState = WAIT_FOR_SHAKE;

  statusCharacteristic.writeValue(0); // Dice ready

  faceCharacteristic.writeValue(0); // Detected face

  turnOffAllLEDs();

  while (central.connected()) {

    handleDiceLogic();

  }

  Serial.println("Disconnected from central");

  turnOffAllLEDs();

  BLEStatus = false;

  digitalWrite(rgbRed, LOW);

}

if(BLEStatus == false) {

blinkRGB();

}

}

The handleDiceLogic function is used to coordinate multiple subfunctions to create the dice logic. Once the connection is active the dice will be in the WAIT_FOR_SHAKE state. In this state, the dice will use the detectshake function to detect the motion and to determine whether it is valid or not. Once the shake or roll is detected the dice will change to WAIT_FOR_STATIONARY state. In this state, the dice will wait for its motion to be stopped and become stationary. The detectStationary function is used to detect whether the dice stopped rolling and stood still. Once the dice is still the, it will change to the UPDATE_FACE state. In this the dice orientation is determined with the help of the detectface function and the corresponding LEDs will start to flash. Then it will write the status value and the result to the BLE characteristics we have mentioned and will go to the WAIT_FOR_BLE_REST. As long as the the BLE device is connected the dice will remain in this state until the status value is reset by the BLE device. Once it is reset the dice will start the loop once again and will wait for the next roll.

void handleDiceLogic() {

switch (currentState) {

  case WAIT_FOR_SHAKE:

    // Blink blue LED until shake is detected

    blinkBlueLED();

    if (detectShake()) {

      shakeDetected = true;

      digitalWrite(rgbBlue, HIGH); // Turn off blue LED

      currentState = WAIT_FOR_STATIONARY; // Move to next state

      Serial.println("Shake detected! Moving to WAIT_FOR_STATIONARY.");

    }

    break;

  case WAIT_FOR_STATIONARY:

    // Flash LEDs sequentially until stationary

    if (millis() - lastFlashTime >= 100) { // Non-blocking delay for LED flashing

      flashSequentialLEDs();

      lastFlashTime = millis();

    }

    if (detectStationary()) {

      currentState = UPDATE_FACE; // Move to next state

      Serial.println("Stationary detected! Moving to UPDATE_FACE.");

    }

    break;

  case UPDATE_FACE:

    // Determine face and update LEDs and BLE characteristics

    currentFace = determineFace();

    if(currentFace < 0)

    {

      return;

    }

    updateLEDs(currentFace);

    // Set BLE characteristics

    statusCharacteristic.writeValue(1); // Dice ready

    faceCharacteristic.writeValue(currentFace); // Detected face

    Serial.println("Face updated! Waiting for BLE reset.");

    currentState = WAIT_FOR_BLE_RESET; // Move to next state

    break;

  case WAIT_FOR_BLE_RESET:

    // Wait for user to set statusCharacteristic to 0

    if (statusCharacteristic.value() == 0) {

      turnOffAllLEDs(); // Reset LEDs before next cycle

      currentState = WAIT_FOR_SHAKE; // Go back to initial state

      Serial.println("BLE reset! Returning to WAIT_FOR_SHAKE.");

    }

    break;

}

}

bool detectShake() {

sensors_event_t a, g, temp;

mpu.getEvent(&a, &g, &temp);

float magnitude = sqrt(a.acceleration.x * a.acceleration.x +

                       a.acceleration.y * a.acceleration.y +

                       a.acceleration.z * a.acceleration.z);

if (magnitude > shakeThreshold) {

  Serial.println("Shake detected!");

  return true;

}

return false;

}

bool detectStationary() {

static float accelSum = 0;         // Sum of acceleration magnitudes

static int sampleCount = 0;        // Number of samples in the averaging window

sensors_event_t a, g, temp;

mpu.getEvent(&a, &g, &temp);

float magnitude = sqrt(a.acceleration.x * a.acceleration.x +

                       a.acceleration.y * a.acceleration.y +

                       a.acceleration.z * a.acceleration.z);

// Add current magnitude to the sum

accelSum += magnitude;

sampleCount++;

// Average the acceleration magnitude over the sample window

float averageMagnitude = accelSum / sampleCount;

if (averageMagnitude < stationaryThreshold) {

  if (millis() - lastStationaryTime > stationaryDuration) {

    // Reset for the next cycle

    accelSum = 0;

    sampleCount = 0;

    Serial.println("Stationary detected!");

    return true;

  }

} else {

  // Reset stationary timer if movement is above the threshold

  lastStationaryTime = millis();

  accelSum = 0;

  sampleCount = 0;

}

return false;

}

int determineFace() {

sensors_event_t a, g, temp;

mpu.getEvent(&a, &g, &temp);

if (a.acceleration.y > 8.0) return 3; // Top

if (a.acceleration.y < -8.0) return 4; // Bottom

if (a.acceleration.z > 8.0) return 5; // Right

if (a.acceleration.z < -8.0) return 2; // Left

if (a.acceleration.x > 8.0) return 6; // Back

if (a.acceleration.x < -8.0) return 1; // Front

return -1; // Unknown

}

The remaining functions are used for the LED controls and are called within the previously explained functions. The flashSequentialLEDs will flash the LEDs in each face in a sequence during the dice roll. The blnkRGB function is called when there is no active connection, and the updateLEDs are used to flash the LEDs to show the result. The turnOffAllLEDs function is used to turn off all the LEDs as mentioned.

void flashSequentialLEDs() {

const int leds[] = {rgbGreen,leftLEDs, rightLEDs, backLEDs, bottomLEDs, topLEDs};

for (int i = 0; i < 6; i++) {

  turnOffAllLEDs();

  if(i == 0)

  {

      digitalWrite(leds[i], LOW);

  }

  else {

      digitalWrite(leds[i], HIGH);

  }

  delay(100);

}

}

void blinkBlueLED() {

static unsigned long lastBlinkTime = 0;

static bool ledState = false;

if (millis() - lastBlinkTime >= 500) {

  ledState = !ledState;

  digitalWrite(LEDPins[currentFace], ledState ? LOW : HIGH);

  lastBlinkTime = millis();

}

}

void blinkRGB() {

if (millis() - lastRGBBlinkTime >= 200) {

  if(digitalRead(rgbRed) == 0)

  {

    digitalWrite(rgbRed, HIGH);

    digitalWrite(rgbBlue, HIGH);

    digitalWrite(rgbGreen, LOW);

  }

  else if(digitalRead(rgbGreen) == 0)

  {

    digitalWrite(rgbRed, HIGH);

    digitalWrite(rgbGreen, HIGH);

    digitalWrite(rgbBlue, LOW);

  }

  else if(digitalRead(rgbBlue) == 0)

  {

    digitalWrite(rgbBlue, HIGH);

    digitalWrite(rgbGreen, HIGH);

    digitalWrite(rgbRed, LOW);

  }

  lastRGBBlinkTime = millis();

}

}

void updateLEDs(int face) {

turnOffAllLEDs();

if(face == 1)

{

  digitalWrite(LEDPins[face], LOW);

}

else

{

  digitalWrite(LEDPins[face], HIGH);

}

}

void turnOffAllLEDs() {

digitalWrite(rgbRed, HIGH); // Fully off for common anode

digitalWrite(rgbGreen, HIGH);

digitalWrite(rgbBlue, HIGH);

digitalWrite(leftLEDs, LOW);

digitalWrite(rightLEDs, LOW);

digitalWrite(backLEDs, LOW);

digitalWrite(bottomLEDs, LOW);

digitalWrite(topLEDs, LOW);

}

Supporting Files

Here is the link to our GitHub repo, where you'll find the source code, schematics, and all other necessary files to build your own Smart LED Dice.

Smart LED Dice Code FileSmart LED Dice Code Zip File

Video

Have any question realated to this Article?

Ask Our Community Members