The author: dzy7455339
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.
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:
- One carries the screen, buttons, and BW21-CBV-KIT.
- The other integrates charging, RTC, flash, and the digital microphone.
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:
1. I accidentally chose SWD pins, causing I²C communication failure.
2. The board holes didn’t align with the enclosure.
I had to redo it, but fortunately, the second version worked fine, and the board fit the case properly.
The board already provides many Arduino examples, so I needed to combine them logically.
The core examples used were:
- Camera_2_LCD → Display camera output on screen.
- SingleVideoWithAudio → Record MP4 video with audio to SD card.
- SDCARDsaveJPG → Save captured images to SD card.
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:
- Time setting
- Screen brightness adjustment
- Flash toggle
- Photo browsing
- Bluetooth remote control
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.
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;
}
Comments