Project Overview
The project is based on the Ai-Thinker BW21 module development board. Thanks to its powerful performance and mature ecosystem, it can support various AI applications such as:
- Facial recognition
- Gesture recognition
- Object recognition
- ···
This offers great convenience for the rapid development and deployment of smart devices and products.
For the expansion board project, refer to: Smart Interaction and Recognition Robot – LCSC Open Hardware Platform
For the core board project, refer to: BW21-CBV-Kit AI Vision Recognition Development Board – LCSC Open Hardware Platform
Specifications and Features
- All module IOs are led out for easy peripheral connections and debugging
- Additional 5V, 3.3V, and GND pin headers to facilitate peripheral development
- Integrated TP4057 charging management circuit, supporting 3.7V lithium battery input
- Charging system includes a charge indicator LED and XH2.54 power connector for easy modular assembly and testing
- DHT11 one-wire sensor interface with pull-up resistor, allowing direct connection
- I2C interface for OLED display modules
- SPI interface for TFT color displays
Functional Interfaces
Includes:
- Pin headers for expansion
- Power output pins
- Charging indicator
- Status LED
- XH2.54 battery power interface
- DHT11 sensor connector
- I2C OLED display interface
- SPI TFT screen interface
3D Enclosure Design
- M3 mounting holes at four corners for easy DIY installation
- Pin and power header design enables external module connection
- Female headers for TFT and OLED displays allow quick testing and validation
- Recess under BW21 core module for enhanced heat dissipation
- Antenna groove for better wireless performance
- Precision cutouts on the side for Type-C charging port, LED indicators, and DHT11 sensor access
3D Enclosure Design
Pin Mapping: SCL – E3 (Pin 13) SDA – E4 (Pin 12)
Test code
Create a new Arduino project and add the following code
#include <Wire.h>#include <Adafruit_OLED_libraries/Adafruit_GFX.h>#include <Adafruit_OLED_libraries/Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels#define SCREEN_HEIGHT 64 // OLED display height, in pixels
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)// The pins for I2C are defined by the Wire-library.#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)#define SCREEN_ADDRESS 0x3C ///< See datasheet for AddressAdafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define NUMFLAKES 10 // Number of snowflakes in the animation example
#define LOGO_HEIGHT_FLAKES 16#define LOGO_WIDTH_FLAKES 16static const unsigned char PROGMEM logo_bmp_flake[] =
{0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11100000,
0b11110011, 0b11100000,
0b11111110, 0b11111000,
0b01111110, 0b11111111,
0b00110011, 0b10011111,
0b00011111, 0b11111100,
0b00001101, 0b01110000,
0b00011011, 0b10100000,
0b00111111, 0b11100000,
0b00111111, 0b11110000,
0b01111100, 0b11110000,
0b01110000, 0b01110000,
0b00000000, 0b00110000};
void setup(){
Serial.begin(115200);
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
// Clear the buffer
display.clearDisplay();
// Show the display buffer on the screen. You MUST call display() after
// drawing commands to make them visible on screen!
display.display();
delay(500);
testanimate(logo_bmp_flake, LOGO_WIDTH_FLAKES, LOGO_HEIGHT_FLAKES); // Animate bitmaps
}
void loop(){
}
#define XPOS 0 // Indexes into the 'icons' array in function below#define YPOS 1#define DELTAY 2
void testanimate(const uint8_t *bitmap, uint8_t w, uint8_t h){
int8_t f, icons[NUMFLAKES][3];
// Initialize 'snowflake' positions
for (f = 0; f < NUMFLAKES; f++) {
icons[f][XPOS] = random(1 - LOGO_WIDTH_FLAKES, display.width());
icons[f][YPOS] = -LOGO_HEIGHT_FLAKES;
icons[f][DELTAY] = random(1, 6);
Serial.print(F("x: "));
Serial.print(icons[f][XPOS], DEC);
Serial.print(F(" y: "));
Serial.print(icons[f][YPOS], DEC);
Serial.print(F(" dy: "));
Serial.println(icons[f][DELTAY], DEC);
}
for (;;) { // Loop forever...
display.clearDisplay(); // Clear the display buffer
// Draw each snowflake:
for (f = 0; f < NUMFLAKES; f++) {
display.drawBitmap(icons[f][XPOS], icons[f][YPOS], bitmap, w, h, SSD1306_WHITE);
}
display.display(); // Show the display buffer on the screen
delay(200); // Pause for 1/10 second
// Then update coordinates of each flake...
for (f = 0; f < NUMFLAKES; f++) {
icons[f][YPOS] += icons[f][DELTAY];
// If snowflake is off the bottom of the screen...
if (icons[f][YPOS] >= display.height()) {
// Reinitialize to a random position, just off the top
icons[f][XPOS] = random(1 - LOGO_WIDTH_FLAKES, display.width());
icons[f][YPOS] = -LOGO_HEIGHT_FLAKES;
icons[f][DELTAY] = random(1, 6);
}
}
}
}
SPI Display
This paper introduces how to use SSD1306 to drive OLED screen to display pictures and texts via SPI communication.
According to the schematic diagram, the corresponding relationship of SPI communication OLED pins is as follows
SPI OLED | BW21 |
SCL | E3 (13) |
SDA | E4(12) |
RST | F8(5) |
DC | F11(4) |
CS | F12(3) |
Resize the target image to a suitable size and convert it to XBM format (monochrome bitmap)
Online XBM conversion tool:XBM Converter .
Test code
Create a new Arduino project and add the following code
U8g2 library is used here, pay attention to the pin definition
#include <U8g2lib.h>#include <SPI.h>#include "logo.h"// Initialize U8g2 (SPI mode) U8G2_SSD1306_128X64_NONAME_F_4W_SW_SPI u8g2(
U8G2_R0,
/* clock=*/ 13, // SCK
/* data=*/ 12, // MOSI (SDA)
/* cs=*/ 3, // Chip Select
/* dc=*/ 4, // Data/Command
/* reset=*/ 5 // RESET
);
void setup() {
u8g2.begin();
u8g2.setFont(u8g2_font_helvB10_tr);
}
void loop() {
u8g2.clearBuffer();
// 1. Display text (upper area, 128x19 pixels)
u8g2.setCursor(0, 12); // Text start position(x,y)
u8g2.print("Hello, Ai-Thinker"); // Text to display
// 2. Display image (lower area, 128x45 pixels)
u8g2.drawXBMP(0, 19, logo_width, logo_height, logo);
u8g2.sendBuffer();// Refresh Display
delay(2000);
}Upload the code to the development board and reset it to run.
DHT11 Sensor
For details:DHT11 .
According to the schematic diagram, the module data pin corresponds to the F5 (0) pin.
Test code
Create a new Arduino project and add the following code
#include "BLEDevice.h"#include "DHT.h"/* OLED */#include <Wire.h>#include <Adafruit_OLED_libraries/Adafruit_GFX.h>#include <Adafruit_OLED_libraries/Adafruit_SSD1306.h>#include <ArduinoJson.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels#define SCREEN_HEIGHT 64 // OLED display height, in pixels#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define UART_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
#define STRING_BUF_SIZE 100
// The digital pin we're connected to.#define DHTPIN 0
// Uncomment whatever type you're using!#define DHTTYPE DHT11 // DHT 11// #define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321// #define DHTTYPE DHT21 // DHT 21 (AM2301)
DHT dht(DHTPIN, DHTTYPE);
BLEService UartService(UART_SERVICE_UUID);BLECharacteristic Rx(CHARACTERISTIC_UUID_RX);BLECharacteristic Tx(CHARACTERISTIC_UUID_TX);
BLEAdvertData advdata;
BLEAdvertData scndata;bool notify = false;
void writeCB(BLECharacteristic* chr, uint8_t connID){
printf("Characteristic %s write by connection %d :\n", chr->getUUID().str(), connID);
if (chr->getDataLen() > 0) {
Serial.print("Received string: ");
Serial.print(chr->readString());
Serial.println();
}
}
void notifCB(BLECharacteristic* chr, uint8_t connID, uint16_t cccd){
if (cccd & GATT_CLIENT_CHAR_CONFIG_NOTIFY) {
printf("Notifications enabled on Characteristic %s for connection %d \n", chr->getUUID().str(), connID);
notify = true;
} else {
printf("Notifications disabled on Characteristic %s for connection %d \n", chr->getUUID().str(), connID);
notify = false;
}
}
void setup(){
Serial.begin(115200);
advdata.addFlags();
advdata.addCompleteName("AMEBA_BLE_DEV");
scndata.addCompleteServices(BLEUUID(UART_SERVICE_UUID));
Rx.setWriteProperty(true);
Rx.setWritePermissions(GATT_PERM_WRITE);
Rx.setWriteCallback(writeCB);
Rx.setBufferLen(STRING_BUF_SIZE);
Tx.setReadProperty(true);
Tx.setReadPermissions(GATT_PERM_READ);
Tx.setNotifyProperty(true);
Tx.setCCCDCallback(notifCB);
Tx.setBufferLen(STRING_BUF_SIZE);
UartService.addCharacteristic(Rx);
UartService.addCharacteristic(Tx);
BLE.init();
BLE.configAdvert()->setAdvData(advdata);
BLE.configAdvert()->setScanRspData(scndata);
BLE.configServer(1);
BLE.addService(UartService);
BLE.beginPeripheral();
dht.begin();
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
delay(1000);
display.clearDisplay();
display.setTextColor(WHITE);
}
void loop(){
float h = dht.readHumidity();
float t = dht.readTemperature();
if (isnan(h) || isnan(t)) {
Serial.println("Failed to read from DHT sensor!");
return;
}
// Create JSON payload
DynamicJsonDocument doc(256);
doc["temperature"] = String(t,2);
doc["humidity"] = String(h,2);
char json_string[256];
serializeJson(doc, json_string);
Serial.print("Publishing: ");
Serial.println(json_string);
//String msg = ("Humidity: " + String((int)h) + "%\t" + "Temperature: " + String((int)t) + "°C\n");
String msg = (json_string);
Tx.writeString(msg);
if (BLE.connected(0) && notify) {
Tx.notify(0);
}
// clear display
display.clearDisplay();
// display temperature
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,17);
display.print(String(t,2));
display.print(" ");
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
display.print("C");
// display humidity
display.setTextSize(1);
display.setCursor(0, 35);
display.print("Humidity: ");
display.setTextSize(2);
display.setCursor(0, 45);
display.print(String(h,2));
display.print(" %");
display.display();
delay(2000);
}Upload the code to the development board and reset it to run.
DS18B20 Sensor
For details:DS18B20
According to the schematic diagram, the module data pin corresponds to the F5 (0) pin.
Test code
#include "BLEDevice.h"#include <DallasTemperature.h>/* OLED */#include <Wire.h>#include <Adafruit_OLED_libraries/Adafruit_GFX.h>#include <Adafruit_OLED_libraries/Adafruit_SSD1306.h>#include <ArduinoJson.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels#define SCREEN_HEIGHT 64 // OLED display height, in pixels#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#define UART_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
#define STRING_BUF_SIZE 100
// DS18B20 configuration#define ONE_WIRE_BUS 0OneWire oneWire(ONE_WIRE_BUS);DallasTemperature sensors(&oneWire);
BLEService UartService(UART_SERVICE_UUID);BLECharacteristic Rx(CHARACTERISTIC_UUID_RX);BLECharacteristic Tx(CHARACTERISTIC_UUID_TX);
BLEAdvertData advdata;
BLEAdvertData scndata;bool notify = false;
void writeCB(BLECharacteristic* chr, uint8_t connID){
printf("Characteristic %s write by connection %d :\n", chr->getUUID().str(), connID);
if (chr->getDataLen() > 0) {
Serial.print("Received string: ");
Serial.print(chr->readString());
Serial.println();
}
}
void notifCB(BLECharacteristic* chr, uint8_t connID, uint16_t cccd){
if (cccd & GATT_CLIENT_CHAR_CONFIG_NOTIFY) {
printf("Notifications enabled on Characteristic %s for connection %d \n", chr->getUUID().str(), connID);
notify = true;
} else {
printf("Notifications disabled on Characteristic %s for connection %d \n", chr->getUUID().str(), connID);
notify = false;
}
}
void setup(){
Serial.begin(115200);
advdata.addFlags();
advdata.addCompleteName("AMEBA_BLE_DEV");
scndata.addCompleteServices(BLEUUID(UART_SERVICE_UUID));
Rx.setWriteProperty(true);
Rx.setWritePermissions(GATT_PERM_WRITE);
Rx.setWriteCallback(writeCB);
Rx.setBufferLen(STRING_BUF_SIZE);
Tx.setReadProperty(true);
Tx.setReadPermissions(GATT_PERM_READ);
Tx.setNotifyProperty(true);
Tx.setCCCDCallback(notifCB);
Tx.setBufferLen(STRING_BUF_SIZE);
UartService.addCharacteristic(Rx);
UartService.addCharacteristic(Tx);
BLE.init();
BLE.configAdvert()->setAdvData(advdata);
BLE.configAdvert()->setScanRspData(scndata);
BLE.configServer(1);
BLE.addService(UartService);
BLE.beginPeripheral();
sensors.begin(); // Initialize Sensor
// SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;)
; // Don't proceed, loop forever
}
delay(1000);
display.clearDisplay();
display.setTextColor(WHITE);
}
void loop(){
sensors.requestTemperatures(); // Get temperature data
float t = sensors.getTempCByIndex(0); // Get the temperature of the first sensor
if (isnan(t)) {
Serial.println("Failed to read from DS18B20 sensor!");
return;
}
// Create JSON payload
DynamicJsonDocument doc(256);
doc["temperature"] = String(t,2);
char json_string[256];
serializeJson(doc, json_string);
Serial.print("Publishing: ");
Serial.println(json_string);
String msg = (json_string);
Tx.writeString(msg);
if (BLE.connected(0) && notify) {
Tx.notify(0);
}
// clear display
display.clearDisplay();
// display temperature
display.setTextSize(1);
display.setCursor(0,0);
display.print("Temperature: ");
display.setTextSize(2);
display.setCursor(0,17);
display.print(String(t,2));
display.print(" ");
display.setTextSize(1);
display.cp437(true);
display.write(167);
display.setTextSize(2);
display.print("C");
display.display();
delay(1000);
}Upload the code to the development board and reset it to run.
Effect
Use Bluefruit Connect app to connect via Bluetooth and view temperature data in real-time.
Also supports UART-to-MQTT for integration with Home Assistant.
Detailed integration guide: [BW21-CBV-Kit Review] Bluetooth Temperature & Humidity Monitor with Home Assistant
Lithium Battery Support
Tested with a 200mAh lithium battery.
Use Type-C port on expansion board for charging/power input
- Charging: Charge LED ON, Standby LED OFF
- Fully charged: Both OFF
- Discharging: Charge LED OFF, Standby LED ON
- No battery: Charge LED blinks, Standby LED ON
Update Log
June 18, 2025
- Optimized header layout to improve compatibility and allow Dupont wire connection
- Improved 3D case countersink height for better fitting
- Adjusted antenna layout and slot for better thermal management
- Added MOS protection circuit for reverse current prevention
June 19, 2025
- Optimized charging module design; added Type-C port to the baseboard for charging/discharging
- Updated 3D enclosure with improved slot layout
June 23, 2025
- Added functional testing: I2C OLED, SPI OLED, DHT11, DS18B20, lithium battery charge/discharge
- Improved content clarity for board features and PCB visuals
Summary
This article presents the design and testing of the BW21-CBV-Kit expansion board and enclosure, covering various on-board functionalities. It lays a solid foundation for future DIY projects and provides practical reference for fast development and product integration using the BW21 module.
Ai-Thinker