I’ve always wanted to DIY a camera, but due to my limited skills, I couldn’t make full use of the powerful chips available. Later, I tried ESP32, but the image quality was not very satisfying, so the idea was shelved.

Then I saw Ai-Thinker’s newly released BW21, which supports cameras, 1080p video recording, SD card storage, and most importantly, Arduino programming. This made my camera project quickly feasible.

1. Hardware Preparation

Once the idea and a suitable platform were ready, I started building. Considering my limited hardware skills, I directly used the BW21-CBV-KIT development board as the core.

To implement the camera functions, external components were also required: power supply, screen, flash, timer, and buttons. Although the board has a built-in analog microphone, its performance was unsatisfactory, so I added a digital microphone as well.

Based on the BW21-CBV-KIT, I designed two expansion boards:

Since I don’t know 3D design, I used LCSC EDA’s enclosure design function to quickly create a simple case for the large expansion board.

During initial testing, I encountered two issues:

I had to redo it, but fortunately, the second version worked fine, and the board fit the case properly.


2. Software

The board already provides many Arduino examples, so I just needed to combine them logically.

The core examples used were:

These represent the three core camera functions. I used RTOS in Arduino to build three tasks, each controlled by dedicated buttons to start or stop them.

Besides core functions, I also added basic settings and display features:

I built a simple bare-metal UI menu controlled by buttons, allowing browsing, brightness adjustment, and Bluetooth toggling.

 

For Bluetooth remote control, instead of relying on smartphones (since phone cameras are better anyway), I used an AI-M61-32S development board as a dedicated BLE remote. The BW21-CBV acts as the host, scanning and connecting to the remote device. However, phones can also control it by sending a “Snapshot” command via BLE.

 

Since the board is enclosed, it’s inconvenient to remove the SD card directly. Inspired by the official examples, I implemented USB mass storage, allowing photos and videos to be read from the camera via USB. The feature is toggled by a button.

 

For power, I used an integrated charge/discharge chip. However, it doesn’t support shutdown during charging, meaning the camera stays on while charging. To handle this, I used ADC to monitor voltage: when above 4.2V, the system enters sleep mode to simulate shutdown.

 

3. Code

Below is the BW21 development board code (reference only, quite messy).

#include "StreamIO.h"

#include "VideoStream.h"

#include "AudioStream.h"

#include "AudioEncoder.h"

#include "MP4Recording.h"

#include "AmebaFatFS.h"

#include "AmebaST7789.h"

#include "TJpg_Decoder.h"

#include "USBMassStorage.h"  // USB Storage

#include "sys_api.h"         // System Calls

#include "BLEDevice.h"

#include "PowerMode.h"

 

// Wake up sources

// wake up by AON timer :   0

// wake up by AON GPIO  :   1

// wake up by eRtc       :   2

#define WAKEUP_SOURCE 1

#define RETENTION     0

// Set wake up AON GPIO pin :   21 / 22

#define WAKEUP_SETTING 21

 

// BLE Related

#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 TARGET_DEVICE_NAME "Ble_cam_control"

#define STRING_BUF_SIZE 100

 

BLEAdvertData foundDevice;

BLEAdvertData targetDevice;

BLEClient* client;

BLERemoteService* UartService;

BLERemoteCharacteristic* Rx;

BLERemoteCharacteristic* Tx;

TaskHandle_t xBLETaskHandle = NULL;  // Global handle for button task, initial NULL

int8_t g_connID = -1; // Store connection ID

bool g_bleReady = false; // Flag: BLE is ready

bool g_deviceFound = false; // Flag: target device found

bool enableBLE = false; // Enable BLE control

bool BLETaskState = false; // BLE task started or not

 

// File browsing

const char *PHOTO_FOLDER = "photos";  // Folder to browse

const char *VIDEO_FOLDER = "videos";  // Folder to browse

 

#define MAX_IMAGES 50

char imageList[MAX_IMAGES][32];  // Store file names

int imageCount = 0;

int currentImageIndex = 0;

uint8_t currentScale = 1;

uint16_t currentJpgWidth = 0;   // Original image width

uint16_t currentJpgHeight = 0;  // Original image height

 

uint8_t LED_BRIGHTNESS = 250;

uint8_t TFT_BRIGHTNESS = 250;

int16_t reviewX = 0;  // Offset when scaling

int16_t reviewY = 0;  // Offset when scaling

 

bool LEDON = false;  // LED on/off

 

/* USB Storage */

USBMassStorage USBMS;

bool usbModeFlag = false;

bool usbStart = false;

 

#include "PCF8563.h"

/* eRtc related definitions */

#define PIN_STORAGE 1

#define PIN_BUTTON_UP 27

#define PIN_BUTTON_DOWN 19

#define PIN_BUTTON_SELECT 20

#define BTN_PREV 17  // Previous image

#define BTN_NEXT 28  // Next image

 

// Current setting states enum

enum {

  SET_YEAR,

  SET_MONTH,

  SET_DAY,

  SET_HOUR,

  SET_MINUTE,

  SET_SECOND,

  SET_DONE

};

bool setMenuFlag = false;    // Avoid screen occupation

int8_t setTimeState = -1;    // -1 = not entered, 0~5 = setting specific item

#define MAX_JPG_SIZE 655360  // 128KB image buffer

static uint8_t jpgBuffer[MAX_JPG_SIZE];

PCF8563 eRtc(&Wire1); // External RTC

 

/* TFT related definitions */

#define TFT_DC 8  // A0

#define TFT_RST -1

#define TFT_CS SPI_SS

#define BL_PIN 7

 

#define FLASH_PIN 6               // Flash pin

#define PIN_VOLTAGE 11            // Voltage pin

float vBatRate = 2 * 3.3 / 1020;  // Voltage conversion

#define VOLTAGE_BASE 3.2

 

AmebaST7789 tft = AmebaST7789(TFT_CS, TFT_DC, TFT_RST, 240, 320);

 

/* FLASH related definitions */

#include <FlashMemory.h>             

unsigned int photoCount = 0;         

#define PHOTO_COUNTER_OFFSET 0x1E00  

#define MAX_PHOTO_COUNT 10000        

#define FILENAME "photo"

 

uint32_t rec_addr = 0;

uint32_t rec_len = 0;

uint32_t img_addr = 0;

uint32_t img_len = 0;

bool current_buffer = false;

AmebaFatFS fs;

 

#define CHANNEL_SCREEN 0

#define CHANNEL_RECORD 1

#define REC_BTN 0   // Record button

#define SNAP_BTN 4  // Mode switch button

CameraSetting configCam;

 

// Default preset video configurations

bool snapAnamiton = false;            

SemaphoreHandle_t xBinarySemaphore;   

SemaphoreHandle_t xBinarySemaphore1;  

VideoSetting config1(240, 304, 30, VIDEO_JPEG, 1);

VideoSetting config3(VIDEO_FHD, CAM_FPS, VIDEO_H264_JPEG, 1);

AudioSetting configA(3);

Audio audio;

AAC aac;

MP4Recording mp4;

StreamIO audioStreamer(1, 1);  

StreamIO avMixStreamer(2, 1);  

 

bool isRecording = false;  

TaskHandle_t displayTaskHandle = NULL;

 

// TFT Output Callback

bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) {

  if (y > 240) return 0;

  tft.drawBitmap(x, y, w, h, bitmap);

  return 1;

}

 

// Setup

void setup() {

  Serial.begin(115200);

 

  xBinarySemaphore = xSemaphoreCreateBinary();

  xBinarySemaphore1 = xSemaphoreCreateBinary();

 

  if (xBinarySemaphore1 == NULL || xBinarySemaphore == NULL) {

    Serial.println("❌ Failed to create semaphores!");

    while (1); // Halt

  }

 

  analogWrite(FLASH_PIN, 0);

 

  if (!fs.begin()) {

    Serial.println("❌ SD card init failed!");

    while (1);

  }

 

  createDirIfNotExists(PHOTO_FOLDER);

  createDirIfNotExists(VIDEO_FOLDER);

 

  TJpgDec.setSwapBytes(true);

  TJpgDec.setJpgScale(currentScale);

  TJpgDec.setCallback(tft_output);

  Wire1.begin();

  rtc.begin();

 

  rtc.printTime(Serial);

 

  setCamera();

  tft.begin();

  tft.setRotation(1);

  tft.fillScreen(ST7789_BLACK);

  tft.flush();

  analogWrite(BL_PIN, TFT_BRIGHTNESS);

 

  xTaskCreate(recordVideo, "Record Video", 4096, NULL, 1, NULL);

  xTaskCreate(snapShot, "Take Photo", 4096, NULL, 1, NULL);

  xTaskCreate(displayTask, "Display Task", 4096, NULL, 1, &displayTaskHandle);

  setupButtons();

}

 

// Loop

void loop() {  

  if (digitalRead(PIN_BUTTON_SELECT) == HIGH) {

    vTaskDelay(pdMS_TO_TICKS(1000));  

    if (digitalRead(PIN_BUTTON_SELECT) == HIGH && !setMenuFlag) {

      setMenuFlag = true;

      navigateMainMenu();  

    }

  }

 

  if (buttonPressed(SNAP_BTN) && !setMenuFlag) {

    xSemaphoreGive(xBinarySemaphore);

  }

  if (buttonPressed(REC_BTN) && !setMenuFlag) {

    xSemaphoreGive(xBinarySemaphore1);

  }

 

  if (buttonPressed(PIN_STORAGE)) {

    usbModeFlag = !usbModeFlag;

  }

 

  if (usbModeFlag && !usbStart) {

    vTaskSuspend(displayTaskHandle);

    tft.setFontColor(ST7789_WHITE);

    tft.setFontSize(2);

    tft.fillScreen(ST7789_BLACK);

    tft.setCursor(100, 100);

    tft.print("USB MODE");

    tft.flush();

    fs.end();

    USBMS.USBInit();

    USBMS.SDIOInit();

    USBMS.USBStatus();

    USBMS.initializeDisk();

    USBMS.loadUSBMassStorageDriver();

    usbStart = true;

  }

 

  if (usbStart && !usbModeFlag) {

    sys_reset();  

  }

 

  vTaskDelay(pdMS_TO_TICKS(100));

}

  

Code for the AI-M61-32S development board

#include "shell.h"

#include <FreeRTOS.h>

#include "task.h"

#include "board.h"

 

#include "bluetooth.h"

#include "conn.h"

#include "conn_internal.h"

#if defined(BL702) || defined(BL602)

#include "ble_lib_api.h"

#elif defined(BL616)

#include "btble_lib_api.h"

#include "bl616_glb.h"

#include "rfparam_adapter.h"

#elif defined(BL808)

#include "btble_lib_api.h"

#include "bl808_glb.h"

#endif

#include "gatt.h"

#include "ble_tp_svc.h"

#include "hci_driver.h"

#include "hci_core.h"

#include "bflb_gpio.h" // Include GPIO library

 

static struct bflb_device_s *uart0;

struct bflb_device_s *gpio;

 

extern void shell_init_with_task(struct bflb_device_s *shell);

void led_task(void *pvParameters);

void init_LED_GPIO(void);

 

#define BUTTON_PIN    GPIO_PIN_2

#define GREEN_LED_PIN GPIO_PIN_14

#define BLUE_LED_PIN  GPIO_PIN_15

#define RED_LED_PIN   GPIO_PIN_12

 

TaskHandle_t xLedTaskHandle = NULL;  // Global handle for LED task, initialized to NULL

bool ble_connected_flag = false;      // BLE connection flag

 

// Define NUS service UUID

#define BT_UUID_NUS_SERVICE \

    BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))

 

// Define TX characteristic UUID (device sends data, we receive)

#define BT_UUID_NUS_TX \

    BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))

 

// Define RX characteristic UUID (we send data, device receives)

#define BT_UUID_NUS_RX \

    BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400002, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))

 

// Declare characteristic value buffers

static uint8_t custom_rx_value[20] = {0}; // Receive buffer

static uint8_t custom_tx_value[20] = {0}; // Transmit buffer

static uint16_t custom_rx_len = 0;

static uint16_t custom_tx_len = 0;

 

// Forward declaration of write callback

static ssize_t custom_char_rx_write(struct bt_conn *conn,

                                    const struct bt_gatt_attr *attr,

                                    const void *buf, uint16_t len,

                                    uint16_t offset, uint8_t flags);

 

// Function declaration

int ble_send_data(const uint8_t *data, uint16_t len);

 

// Define GATT attribute table

// Callback: called when CCCD is modified

static void custom_ccc_changed(const struct bt_gatt_attr *attr, uint16_t value)

{

    ARG_UNUSED(attr);

    bool enabled = (value == BT_GATT_CCC_NOTIFY);

    printf("TX notifications %s\n", enabled ? "ON" : "OFF");

}

 

static ssize_t custom_char_tx_read(struct bt_conn *conn,

                                   const struct bt_gatt_attr *attr,

                                   void *buf, uint16_t len,

                                   uint16_t offset)

{

    const char *value = "Hello from BL616!";  // Data to return

    uint16_t value_len = strlen(value);

 

    // Use GATT helper to safely return data

    return bt_gatt_attr_read(conn, attr, buf, len, offset, value, value_len);

}

 

static ssize_t custom_char_rx_read(struct bt_conn *conn,

                                   const struct bt_gatt_attr *attr,

                                   void *buf, uint16_t len,

                                   uint16_t offset)

{

    return bt_gatt_attr_read(conn, attr, buf, len, offset,

                             custom_rx_value, custom_rx_len);

}

 

static struct bt_gatt_attr custom_service_attrs[] = {

    // 1. Service Declaration

    BT_GATT_PRIMARY_SERVICE(BT_UUID_NUS_SERVICE),

 

    // 2. RX Characteristic: phone → device (write)

    BT_GATT_CHARACTERISTIC(BT_UUID_NUS_RX,

                           BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP,

                           BT_GATT_PERM_WRITE | BT_GATT_PERM_READ,

                           custom_char_rx_read,  // Optional: allow phone to read

                           custom_char_rx_write,

                           NULL),

 

    // 3. TX Characteristic: device → phone (notify)

    BT_GATT_CHARACTERISTIC(BT_UUID_NUS_TX,

                           BT_GATT_CHRC_NOTIFY,

                           BT_GATT_PERM_READ,

                           custom_char_tx_read,  // Allow phone to read current value

                           NULL,

                           NULL),

 

    // 4. CCCD: Client Characteristic Configuration Descriptor (must follow TX characteristic)

    BT_GATT_CCC(custom_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),

};

 

// Define GATT service

static struct bt_gatt_service custom_service =

    BT_GATT_SERVICE(custom_service_attrs);

 

// Save connection handle for notify

static struct bt_conn *current_conn = NULL;

 

// Write callback implementation

static ssize_t custom_char_rx_write(struct bt_conn *conn,

                                    const struct bt_gatt_attr *attr,

                                    const void *buf, uint16_t len,

                                    uint16_t offset, uint8_t flags)

{

    if (offset + len > sizeof(custom_rx_value)) {

        return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);

    }

 

    memcpy(custom_rx_value + offset, buf, len);

    custom_rx_len = offset + len;

 

    printf("Received from phone: %.*s\n", custom_rx_len, custom_rx_value);

 

    // Echo back to phone (optional)

    if (current_conn) {

        memcpy(custom_tx_value, custom_rx_value, custom_rx_len);

        custom_tx_len = custom_rx_len;

        bt_gatt_notify(current_conn, &custom_service.attrs[3], custom_tx_value, custom_tx_len);

    }

 

    return len;

}

 

static int btblecontroller_em_config(void)

{

    extern uint8_t __LD_CONFIG_EM_SEL;

    volatile uint32_t em_size;

 

    em_size = (uint32_t)&__LD_CONFIG_EM_SEL;

 

    if (em_size == 0) {

        GLB_Set_EM_Sel(GLB_WRAM160KB_EM0KB);

    } else if (em_size == 32*1024) {

        GLB_Set_EM_Sel(GLB_WRAM128KB_EM32KB);

    } else if (em_size == 64*1024) {

        GLB_Set_EM_Sel(GLB_WRAM96KB_EM64KB);

    } else {

        GLB_Set_EM_Sel(GLB_WRAM96KB_EM64KB);

    }

 

    return 0;

}

 

static void ble_connected(struct bt_conn *conn, u8_t err)

{

    if(err || conn->type != BT_CONN_TYPE_LE)

        return;

 

    printf("%s", __func__);

    bflb_gpio_set(gpio, GREEN_LED_PIN);   // Turn on green LED

    bflb_gpio_reset(gpio, RED_LED_PIN);    // Turn off red LED

    current_conn = bt_conn_ref(conn);      // Save connection handle

    ble_connected_flag = true;

}

 

static void ble_disconnected(struct bt_conn *conn, u8_t reason)

{

    int ret;

 

    if(conn->type != BT_CONN_TYPE_LE)

        return;

 

    printf("%s", __func__);

    bflb_gpio_reset(gpio, GREEN_LED_PIN); // Turn off green LED

    bflb_gpio_set(gpio, RED_LED_PIN);     // Turn on red LED

    ble_connected_flag = false;

 

    // Enable advertising

    if (current_conn) {

        bt_conn_unref(current_conn);

        current_conn = NULL;

    }

    ret = set_adv_enable(true);

    if(ret) {

        printf("Restart adv failed.\n");

    }

}

 

static struct bt_conn_cb ble_conn_callbacks = {

    .connected    = ble_connected,

    .disconnected = ble_disconnected,

};

 

static void ble_start_adv(void)

{

    struct bt_le_adv_param param;

    int err = -1;

    struct bt_data adv_data[1] = {

        BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR | BT_LE_AD_GENERAL)

    };

    struct bt_data adv_rsp[1] = {

        BT_DATA_BYTES(BT_DATA_MANUFACTURER_DATA, "BL616")

    };

 

    memset(¶m, 0, sizeof(param));

    param.interval_min = BT_GAP_ADV_FAST_INT_MIN_2;

    param.interval_max = BT_GAP_ADV_FAST_INT_MAX_2;

    param.options = (BT_LE_ADV_OPT_CONNECTABLE | BT_LE_ADV_OPT_USE_NAME | BT_LE_ADV_OPT_ONE_TIME);

 

    err = bt_le_adv_start(¶m, adv_data, ARRAY_SIZE(adv_data), adv_rsp, ARRAY_SIZE(adv_rsp));

    if(err){

        printf("Failed to start advertising (err %d)\n", err);

    }

    printf("Advertising started successfully.\n");

}

 

void bt_enable_cb(int err)

{

    if (!err) {

        bt_addr_le_t bt_addr;

        bt_get_local_public_address(&bt_addr);

        printf("BD_ADDR:(MSB)%02x:%02x:%02x:%02x:%02x:%02x(LSB)\n",

            bt_addr.a.val[5], bt_addr.a.val[4], bt_addr.a.val[3],

            bt_addr.a.val[2], bt_addr.a.val[1], bt_addr.a.val[0]);

 

        bt_conn_cb_register(&ble_conn_callbacks);

        bt_set_name("Ble_cam_control");

        bt_gatt_service_register(&custom_service); // Register custom service

 

        ble_start_adv();

    }

}

 

int main(void)

{

    board_init();

    init_LED_GPIO();

    configASSERT((configMAX_PRIORITIES > 4));

  

    uart0 = bflb_device_get_by_name("uart0");

    shell_init_with_task(uart0);

 

    /* Set BLE controller EM size */

    btblecontroller_em_config();

#if defined(BL616)

    /* Init RF */

    if (0 != rfparam_init(0, NULL, 0)) {

        printf("PHY RF init failed!\n");

        return 0;

    }

#endif

 

    #if defined(BL702) || defined(BL602)

    ble_controller_init(configMAX_PRIORITIES - 1);

    #else

    btble_controller_init(configMAX_PRIORITIES - 1);

    #endif

 

    hci_driver_init();

    bt_enable(bt_enable_cb);

 

    xTaskCreate(led_task, "LED_Task", 512, NULL, configMAX_PRIORITIES - 2, &xLedTaskHandle);

    vTaskStartScheduler();

 

    while (1) {

    }

}

 

void init_LED_GPIO(void)

{

    gpio = bflb_device_get_by_name("gpio");

 

    bflb_gpio_init(gpio, GREEN_LED_PIN, GPIO_OUTPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);

    bflb_gpio_init(gpio, BLUE_LED_PIN, GPIO_OUTPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);

    bflb_gpio_init(gpio, RED_LED_PIN, GPIO_OUTPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);

    bflb_gpio_init(gpio, BUTTON_PIN, GPIO_INPUT | GPIO_PULLDOWN | GPIO_SMT_EN | GPIO_DRV_0);

}

 

void led_task(void *pvParameters)

{

    uint8_t button_last_state = 0;  // Last button state (0: released, 1: pressed)

 

    while (1) {

        uint8_t button_current = bflb_gpio_read(gpio, BUTTON_PIN);

 

        if(!ble_connected_flag) {

            // If BLE not connected, keep red LED on, green and blue off

            bflb_gpio_set(gpio, RED_LED_PIN);

            bflb_gpio_reset(gpio, GREEN_LED_PIN);

            bflb_gpio_reset(gpio, BLUE_LED_PIN);

            vTaskDelay(100 / portTICK_PERIOD_MS);

            continue;

        }

 

        // Detect rising edge: released from pressed

        if (button_last_state == 1 && button_current == 0) {

            vTaskDelay(10 / portTICK_PERIOD_MS); // Debounce

            if (bflb_gpio_read(gpio, BUTTON_PIN) == 0) {

                printf("Button Released! Turn on Green LED.\n");

                bflb_gpio_set(gpio, GREEN_LED_PIN);

                bflb_gpio_reset(gpio, BLUE_LED_PIN);

            }

        } else if (button_last_state == 0 && button_current == 1) {

            vTaskDelay(10 / portTICK_PERIOD_MS); // Debounce

            if (bflb_gpio_read(gpio, BUTTON_PIN) == 1) {

                printf("Button Pressed! Turn on Blue LED.\n");

                bflb_gpio_set(gpio, BLUE_LED_PIN);

                bflb_gpio_reset(gpio, GREEN_LED_PIN);

                ble_send_data((uint8_t*)"Snapshot", 8); // Send BLE data

            }

        }

 

        button_last_state = button_current;

        vTaskDelay(20 / portTICK_PERIOD_MS); // Main loop delay

    }

}

 

int ble_send_data(const uint8_t *data, uint16_t len)

{

    if (!current_conn || !data || len == 0 || len > sizeof(custom_tx_value)) {

        return -1;

    }

 

    memcpy(custom_tx_value, data, len);

    custom_tx_len = len;

 

    int err = bt_gatt_notify(current_conn, &custom_service.attrs[3], custom_tx_value, custom_tx_len);

    if (err) {

        printf("Notify failed: %d\n", err);

        return -1;

    }

 

    printf("Sent to phone: %.*s\n", len, data);

    return 0;

}