以下作品由安信可社区用户
dzy7455339制作
原贴地址
【电子DIY作品】BW21数码相机+BW21-CBV-KIT
一直想自己DIY一个相机,但是奈何笔者个人水平有限,虽然有各种强大的芯片,但是自己用不了,后来有了ESP32但是拍摄出的画面质量不是很满意,所以这个想法一直搁置。
看到安信可新出的BW21-CBV-Kit支持摄像头、1080p录像和SD卡,最关键的是它还支持Arduino编程,让笔者做相机的想法得以快速实现。

1硬件准备
考虑到个人硬件水平有限,这里直接使用了BW21-CBV-Kit开发板作为核心。围绕核心功能相机,需要准备的外部设备还有电源、屏幕、闪光灯、计时器、按键这些功能。
另外板子虽然板载了一个模拟麦克风,但是实际使用起来比较差强人意,所以数字麦克风也加入了外设的清单里。
以BW21-CBV-Kit为基础做了2个扩展版:
第一个扩展版主要承载屏幕、按键以及BW21-CBV-Kit,
第二个扩展版则集成了充电、RTC、闪光灯以及数字麦克风。
因为不会使用3D软件,这里直接在扩展版的基础上利用立创EDA的制作外壳功能画了一个简单的外壳。

先对开发板进行了单项基础功能测试,发现2个问题:
一是选择引脚的时候没有避开SWD引脚,导致I2C通讯失败。
二是板子和外壳留孔对不上,没办法只能重新来过。
幸好第二次板子功能正常,和外壳也很搭配。
2软件
其实开发板在Arduino中准备了很多使用的例子进行使用,需要做的就是把这些组合起来。
这里用到的核心例程主要有Camera_2_LCD, SingleVideoWithAudio以及SDCARDsaveJPG几个示例。
第一个示例实现了摄像头画面到屏幕
第二个示例实现了录制MP4视频到SD卡
第三个示例实现了拍摄图像到SD卡
这是相机的3个核心任务,在arduino中使用RTOS建立了3个程序,并用相应的按键来控制这3个任务的启动或者停止。

核心功能之外,相机还需要一些简单的设置和显示功能。比如设置时间、屏幕亮度、闪光灯开关、浏览相片、蓝牙遥控等。
这里使用裸机直接写了一个简单的目录界面,使用按键进行控制,在界面中可以进行相片浏览、屏幕亮度调整以及蓝牙遥控的开关等功能。

蓝牙控制使用了一个Ai-M61-32S开发板来充当蓝牙遥控,当然手机搜索对应的广播名称并发送指令Snapshot也是能够控制的。
BW21-CBV-Kit充当主机设备扫描并连接特定名称设备,Ai-M61-32S作为从机进行广播。

板子被封在壳子里不能像常规相机一样直接拿取SD卡拷贝照片和视频,参考官方例程实现了通过USB来读取照片和视频,功能的打开通过按键实现。

电源使用的充放电一体芯片,但是芯片充电的时候不能关机,会导致充电时相机还处于运行状态。
这里使用ADC检测电压,如果检测到电压高于4.2V就进入睡眠模式来实现假关机。

BW21-CBV-Kit开发板的代码
#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存储
#include "sys_api.h" //系统调用
#include "BLEDevice.h"
#include "PowerMode.h"
// 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 WAKUPE_SETTING 21
//BLE相关
#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; // 按键任务全局句柄,初始为 NULL
int8_t g_connID = -1; // 存储连接ID
bool g_bleReady = false; // 标志位,表示BLE已准备就绪
bool g_deviceFound = false; // < << 新增:标志位,表示目标设备已被发现
bool enableBLE = false;//开启蓝牙控制
bool BLETaskState = false;//BLE任务是否启动
//文件浏览
const char *PHOTO_FOLDER = "photos"; // 修改为您想浏览的文件夹名
const char *VIDEO_FOLDER = "videos"; // 修改为您想浏览的文件夹名
#define MAX_IMAGES 50
char imageList[MAX_IMAGES][32]; // 存储文件名
int imageCount = 0;
int currentImageIndex = 0;
uint8_t currentScale = 1;
uint16_t currentJpgWidth = 0; // 原始图片宽度
uint16_t currentJpgHeight = 0; // 原始图片高度
uint8_t LED_BRIGHTNESS = 250;
uint8_t TFT_BRIGHTNESS = 250;
int16_t reviewX = 0; //缩放时进行偏移
int16_t reviewY = 0; //缩放时进行偏移
bool LEDON = false; //开关LED
/*USB存储*/
USBMassStorage USBMS;
bool usbModeFlag = false;
bool usbStart = false;
#include "PCF8563.h"
/* eRtc相关定义*/
#define PIN_STORAGE 1
#define PIN_BUTTON_UP 27
#define PIN_BUTTON_DOWN 19
#define PIN_BUTTON_SELECT 20
#define BTN_PREV 17 // 上一张
#define BTN_NEXT 28 // 下一张
// 当前设置状态枚举
enum {
SET_YEAR,
SET_MONTH,
SET_DAY,
SET_HOUR,
SET_MINUTE,
SET_SECOND,
SET_DONE
};
bool setMenuFlag = false; //避免屏幕占用
int8_t setTimeState = -1; // -1 = 未进入设置,0~5 = 正在设置某项
#define MAX_JPG_SIZE 655360 // 128KB 图像缓冲区
static uint8_t jpgBuffer[MAX_JPG_SIZE];
PCF8563 eRtc(&Wire1);//外部时钟
/* eRtc相关定义*/
/* TFT相关定义*/
#define TFT_DC 8 //A0
#define TFT_RST -1
#define TFT_CS SPI_SS
#define BL_PIN 7
#define FLASH_PIN 6 //闪光灯引脚
#define PIN_VOLTAGE 11 //电压引脚
float vBatRate = 2 * 3.3 / 1020; //电压换算
#define VOLTAGE_BASE 3.2
AmebaST7789 tft = AmebaST7789(TFT_CS, TFT_DC, TFT_RST, 240, 320);
/* TFT相关定义*/
/* FLASH相关定义*/
#include < FlashMemory.h > //flash
unsigned int photoCount = 0; //照片编号
#define PHOTO_COUNTER_OFFSET 0x1E00 // Flash 偏移地址,用于存储照片计数
#define MAX_PHOTO_COUNT 10000 // 防止溢出或异常值(可选)
#define FILENAME "photo"
/* eRtc相关定义*/
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 //录像按钮
#define SNAP_BTN 4 //模式切换按钮
CameraSetting configCam;
// Default preset configurations for each video channel:
// Channel 0 : 1920 x 1080 30FPS H264
// Channel 1 : 1280 x 720 30FPS H264
// Default audio preset configurations:
// 0 : 8kHz Mono Analog Mic
// 1 : 16kHz Mono Analog Mic
// 2 : 8kHz Mono Digital PDM Mic
// 3 : 16kHz Mono Digital PDM Mic
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);
//VideoSetting configV(CHANNEL);
AudioSetting configA(3);
Audio audio;
AAC aac;
MP4Recording mp4;
StreamIO audioStreamer(1, 1); // 1 Input Audio -> 1 Output AAC
StreamIO avMixStreamer(2, 1); // 2 Input Video + Audio -> 1 Output MP4
bool isRecording = false; //是否在录像
TaskHandle_t displayTaskHandle = NULL;
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;
}
void setup() {
Serial.begin(115200);
xBinarySemaphore = xSemaphoreCreateBinary();
xBinarySemaphore1 = xSemaphoreCreateBinary();
if (xBinarySemaphore1 == NULL || xBinarySemaphore == NULL) {
Serial.println(" 信号量创建失败!");
while (1)
; // 停机
}
// pinMode(FLASH_PIN,OUTPUT);
// digitalWrite(FLASH_PIN,LOW);
analogWrite(FLASH_PIN, 0);
if (!fs.begin()) {
Serial.println(" SD卡初始化失败!");
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);
rtc.printTime(Serial);
setCamera();
tft.begin();
tft.setRotation(1);
tft.fillScreen(ST7789_BLACK);
tft.flush();
analogWrite(BL_PIN, TFT_BRIGHTNESS);
// Configure camera video channel with video format information
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();
}
void loop() { //进入目录
if (digitalRead(PIN_BUTTON_SELECT) == HIGH) {
vTaskDelay(pdMS_TO_TICKS(1000)); // 长按 1 秒进入设置
if (digitalRead(PIN_BUTTON_SELECT) == HIGH && !setMenuFlag) {
setMenuFlag = true;
navigateMainMenu(); // 进入设置
}
}
if (buttonPressed(SNAP_BTN) && !setMenuFlag) {
xSemaphoreGive(xBinarySemaphore);
}
if (buttonPressed(REC_BTN) && !setMenuFlag) {
xSemaphoreGive(xBinarySemaphore1);
}
//进入USB
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(); //结束USB系统重启
}
vTaskDelay(pdMS_TO_TICKS(100));
}
void createDirIfNotExists(const char *dirname) {
char path[128];
sprintf(path, "%s%s", fs.getRootPath(), dirname);
if (!fs.exists(path)) {
if (fs.mkdir(path)) {
printf("创建文件夹: "%s"rn", path);
} else {
printf("创建文件夹失败: "%s"rn", path);
}
} else {
printf("文件夹已存在: "%s"rn", path);
}
}
void recordVideo(void *pvParameters) {
// Print information
//printInfo();
bool modifyTime = false;
uint8_t y, mo, d, h, mi, se, wd;
char filename[64] = { 0 }; // 静态保存上次的文件名
while (1) {
if (xSemaphoreTake(xBinarySemaphore1, pdMS_TO_TICKS(1000)) == pdTRUE) {
if (!isRecording) {
modifyTime = true;
isRecording = true;
rtc.getTime(&y, &mo, &d, &h, &mi, &se, &wd);
sprintf(filename, "%s/%04d%02d%02d_%02d%02d%02d", VIDEO_FOLDER, y, mo, d, h, mi, se);
mp4.setRecordingFileName(filename);
Serial.println("Recording STARTED");
mp4.begin();
} else {
isRecording = false;
if (mp4.getRecordingState() == 1) {
mp4.end();
}
}
}
if (modifyTime && !isRecording && filename[0] != '') {
if (mp4.getRecordingState() == 0) {
Serial.print("修改时间");
char pathBuf[128];
const char *root = fs.getRootPath(); // 通常是 "/" 或 ""
snprintf(pathBuf, sizeof(pathBuf), "%s%s.mp4", root, filename);
Serial.print(fs.setLastModTime(pathBuf, 2000 + y, mo, d, h, mi, se));
modifyTime = false;
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void printInfo(void) {
Serial.println("------------------------------");
Serial.println("- Summary of Streaming -");
Serial.println("------------------------------");
Camera.printInfo();
Serial.println("- Audio Information -");
audio.printInfo();
Serial.println("- MP4 Recording Information -");
mp4.printInfo();
}
void snapShot(void *pvParameters) {
FlashMemory.begin(FLASH_MEMORY_APP_BASE, 0x1000);
photoCount = FlashMemory.readWord(PHOTO_COUNTER_OFFSET);
Serial.print("读取到已拍摄照片数量: ");
Serial.println(photoCount);
while (1) {
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdTRUE) { //等待拍照通知 xSemaphoreGive(xBinarySemaphore);
//fs.begin();
char path[128];
int n = snprintf(path, sizeof(path), "%s%s/%s%d.jpg",
fs.getRootPath(),
PHOTO_FOLDER,
FILENAME,
photoCount);
if (n < 0) {
Serial.println("照片路径生成失败!");
}
File file = fs.open(path);
vTaskDelay(pdMS_TO_TICKS(100));
uint8_t y, mo, d, h, mi, se, wd;
rtc.getTime(&y, &mo, &d, &h, &mi, &se, &wd);
if (LEDON) {
analogWrite(FLASH_PIN, LED_BRIGHTNESS);
}
snapAnamiton = true;
Camera.getImage(CHANNEL_RECORD, &rec_addr, &rec_len);
//vTaskSuspend(displayTaskHandle);
if (LEDON) {
analogWrite(FLASH_PIN, 0);
}
file.write((uint8_t *)rec_addr, rec_len);
file.close();
fs.setLastModTime(path, 2000 + y, mo, d, h, mi, se);
photoCount++;
FlashMemory.writeWord(PHOTO_COUNTER_OFFSET, photoCount);
unsigned int checkValue = FlashMemory.readWord(PHOTO_COUNTER_OFFSET);
if (checkValue == photoCount) {
Serial.print(" 照片编号已更新: ");
Serial.println(photoCount);
} else {
Serial.println(" Flash 写入失败!");
}
//vTaskResume(displayTaskHandle);
} else {
vTaskDelay(pdMS_TO_TICKS(100));
}
}
}
void displayTask(void *pvParameters) {
unsigned long previousMillis = 0; // 上次刷新时间
int frameCount = 0; // 帧计数器
float fps = 0.0f; // 存储 FPS
unsigned long lastVolMeTime = 0;
int lastVoltagePercent = 0;
lastVoltagePercent = batteryVoltConvert(); // 先读一次
while (1) {
if (!setMenuFlag) {
unsigned long currentMillis = millis(); // 获取当前时间
Camera.getImage(CHANNEL_SCREEN, &img_addr, &img_len);
bool next_buffer = !current_buffer;
tft.setFrontBufferIndex(next_buffer);
// uint16_t jpgWidth, jpgHeight;
// // 仅获取 JPEG 图像尺寸(不显示)
// bool inform = TJpgDec.getJpgSize(&jpgWidth, &jpgHeight, (const uint8_t*)img_addr, img_len);
// if (!inform) {
// printf("JPEG Size: %dx%dn", jpgWidth, jpgHeight);
// } else {
// Serial.println("Failed to parse JPEG header");
// }
tft.fillScreen(ST7789_BLACK);
TJpgDec.drawJpg(0, 0, (uint8_t *)img_addr, img_len);
// 更新帧计数器
frameCount++;
if (currentMillis - previousMillis >= 1000) {
fps = frameCount * 1000.0f / (currentMillis - previousMillis); // 计算每秒帧数
frameCount = 0; // 重置帧计数器
previousMillis = currentMillis; // 更新上次刷新时间
}
char fpsStr[32];
sprintf(fpsStr, "FPS: %.1f", fps); // 格式化 FPS 显示
tft.setCursor(5, 5);
tft.drawString(fpsStr); // 在屏幕中央上方显示 FPS
//Serial.println(fpsStr);
if (isRecording) {
static int reverseTime = 0;
if (reverseTime < 30) {
tft.fillCircle(20, 120, 15, ST7789_GREEN);
}
reverseTime++;
if (reverseTime >= 60) {
reverseTime = 0;
}
}
if (millis() - lastVolMeTime > 30000) {
lastVoltagePercent = batteryVoltConvert();
lastVolMeTime = millis();
}
drawBattery(280, 5, 20, 10, lastVoltagePercent);
drawLightningBolt(110, 2, 5, LEDON);
if (snapAnamiton) {
snapAnamiton = false;
tft.drawRect(0, 0, 320, 240, ST7789_WHITE, 10);
}
if(!g_bleReady){ //连接未连接状态切换
drawBluetoothSymbol(180, 8, 10,ST7789_BLACK,enableBLE);
}else{
drawBluetoothSymbol(180, 8, 10,ST7789_WHITE,enableBLE);
}
tft.flush();
current_buffer = next_buffer;
} else {
vTaskDelay(pdMS_TO_TICKS(100));
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
bool buttonPressed(int pin) {
if (digitalRead(pin) == HIGH) {
vTaskDelay(pdMS_TO_TICKS(20)); // 简单消抖
if (digitalRead(pin) == HIGH) {
while (digitalRead(pin) == HIGH) {
vTaskDelay(pdMS_TO_TICKS(10));
}
return true;
}
}
return false;
}
/*RTC相关函数*/
void setupButtons() {
pinMode(PIN_BUTTON_UP, INPUT_PULLDOWN);
pinMode(PIN_BUTTON_DOWN, INPUT_PULLDOWN);
pinMode(PIN_BUTTON_SELECT, INPUT_PULLDOWN);
pinMode(REC_BTN, INPUT_PULLDOWN);
pinMode(SNAP_BTN, INPUT_PULLDOWN);
pinMode(PIN_STORAGE, INPUT_PULLDOWN);
pinMode(BTN_PREV, INPUT_PULLDOWN);
pinMode(BTN_NEXT, INPUT_PULLDOWN);
}
void displayTimeSetting(uint8_t y, uint8_t mo, uint8_t d, uint8_t h, uint8_t mi, uint8_t se, int8_t highlight) {
tft.fillScreen(ST7789_WHITE);
tft.setCursor(50, 20);
tft.setFontColor(ST7789_BLACK);
tft.setFontSize(2);
tft.print("Set Time");
tft.setFontSize(1);
const char *labels[] = { "Year:", "Month:", "Day:", "Hour:", "Minute:", "Second:" };
int values[] = { 2000 + y, mo, d, h, mi, se };
int x_pos = 40, y_pos = 60;
for (int i = 0; i < 6; i++) {
tft.setCursor(x_pos, y_pos + i * 20);
tft.setFontColor(i == highlight ? ST7789_RED : ST7789_BLACK);
tft.print(labels[i]);
tft.print(" ");
tft.print(values[i]);
}
tft.setCursor(40, y_pos + 6 * 20);
tft.setFontColor(ST7789_BLUE);
tft.print("Press SELECT to save");
tft.flush();
}
// 菜单项定义
enum MainMenu {
MENU_PHOTO_BROWSER,
MENU_BRIGHTNESS_SCREEN,
MENU_BRIGHTNESS_LED,
MENU_SET_TIME,
MENU_EXIT,
MENU_COUNT
};
// 当前选中的主菜单项
int8_t currentMenuIndex = 0;
// ==================== 显示主菜单 ====================
void displayMainMenu() {
tft.fillScreen(ST7789_WHITE);
tft.setCursor(50, 20);
tft.setFontColor(ST7789_BLACK);
tft.setFontSize(2);
tft.print("Main Menu");
tft.setFontSize(1);
const char *menuItems[] = {
"Photo Browser",
"Screen Brightness",
"LED Brightness",
"Set Time",
"Exit Menu"
};
int x_pos = 40, y_pos = 60;
for (int i = 0; i < MENU_COUNT; i++) {
tft.setCursor(x_pos, y_pos + i * 25);
tft.setFontColor(i == currentMenuIndex ? ST7789_RED : ST7789_BLACK);
tft.print(" > ");
tft.print(menuItems[i]);
}
tft.setCursor(40, y_pos + MENU_COUNT * 25);
tft.setFontColor(ST7789_BLUE);
tft.print("SELECT to enter");
tft.flush();
}
// ==================== 主菜单导航与执行 ====================
void navigateMainMenu() {
buttonPressed(PIN_BUTTON_SELECT);
currentMenuIndex = 0; // 默认选中第一项
while (true) {
displayMainMenu();
if (buttonPressed(PIN_BUTTON_UP)) {
currentMenuIndex = (currentMenuIndex - 1 + MENU_COUNT) % MENU_COUNT;
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
currentMenuIndex = (currentMenuIndex + 1) % MENU_COUNT;
}
if (buttonPressed(PIN_BUTTON_SELECT)) {
// 根据选择进入第二级操作
switch (currentMenuIndex) {
case MENU_PHOTO_BROWSER:
enterPhotoBrowser(); // 你需要实现这个函数
break;
case MENU_BRIGHTNESS_SCREEN:
adjustScreenBrightness(); // 下面提供示例
break;
case MENU_BRIGHTNESS_LED:
adjustLEDBrightness(); // 下面提供示例
break;
case MENU_SET_TIME:
setTimeWithButtons(); // 你已有的函数
break;
case MENU_EXIT:
tft.setFontColor(ST7789_WHITE);
tft.setFontSize(1);
setMenuFlag = false; // 新增:退出菜单
return; // 退出 navigateMainMenu 函数,回到主循环
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void adjustScreenBrightness() {
uint8_t brightness = getCurrentBrightness(); // 你需要实现:读取当前亮度值(0-255)
bool exiting = false;
while (!exiting) {
tft.fillScreen(ST7789_WHITE);
tft.setCursor(50, 100);
tft.setFontColor(ST7789_BLACK);
tft.setFontSize(2);
tft.print("Screen Brightness:");
tft.setCursor(100, 140);
tft.print(brightness);
tft.setCursor(40, 180);
tft.setFontColor(ST7789_BLUE);
tft.print("UP/DOWN: Adjust");
tft.setCursor(40, 200);
tft.print("SELECT: Back");
tft.flush();
if (buttonPressed(PIN_BUTTON_UP)) {
brightness = min(255, brightness + 5);
setScreenBrightness(brightness); // 你需要实现这个函数(如通过PWM)
while (buttonPressed(PIN_BUTTON_UP))
;
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
brightness = max(0, brightness - 5);
setScreenBrightness(brightness);
while (buttonPressed(PIN_BUTTON_DOWN))
;
}
if (buttonPressed(PIN_BUTTON_SELECT)) {
while (buttonPressed(PIN_BUTTON_SELECT))
;
exiting = true;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void adjustLEDBrightness() {
uint8_t ledBrightness = getCurrentLEDBrightness(); // 读取当前LED亮度
bool exiting = false;
uint8_t count = 0;
while (!exiting) {
tft.fillScreen(ST7789_WHITE);
tft.setCursor(50, 100);
if (count == 0) {
tft.setFontColor(ST7789_GREEN);
} else if (count == 1) {
tft.setFontColor(ST7789_BLACK);
}
tft.setFontSize(2);
tft.print("LED Brightness state:");
tft.setCursor(100, 140);
tft.print(ledBrightness);
tft.setCursor(40, 180);
tft.setFontColor(ST7789_BLUE);
tft.print("UP/DOWN: Adjust");
tft.setCursor(40, 200);
tft.print("SELECT: Back");
tft.setCursor(150, 140);
if (count == 0) {
tft.setFontColor(ST7789_BLACK);
} else if (count == 1) {
tft.setFontColor(ST7789_GREEN);
}
if (LEDON) {
tft.print("ON");
} else {
tft.print("OFF");
}
tft.flush();
if (count == 0) {
if (buttonPressed(PIN_BUTTON_UP)) {
ledBrightness = min(255, ledBrightness + 5);
setLEDBrightness(ledBrightness); // 你需要实现:控制LED(如PWM)
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
ledBrightness = max(0, ledBrightness - 5);
setLEDBrightness(ledBrightness);
}
}
if (count == 1) {
if (buttonPressed(PIN_BUTTON_UP)) {
LEDON = !LEDON;
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
LEDON = !LEDON;
}
}
if (count >= 2) {
exiting = true;
}
if (buttonPressed(PIN_BUTTON_SELECT)) {
count++;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
}
void enterPhotoBrowser() {
tft.setFontSize(1);
tft.setFontColor(ST7789_WHITE);
tft.fillScreen(ST7789_BLACK);
tft.flush();
setScale();
if (!listJpgFiles(PHOTO_FOLDER)) {
tft.setCursor(50, 50);
tft.print("No JPG files found!");
vTaskDelay(pdMS_TO_TICKS(1000));
return;
}
Serial.print("Found");
Serial.print(imageCount);
Serial.println("JPG files.");
if (imageCount > 0) {
loadAndDisplayJpg(imageList[currentImageIndex]);
} else {
vTaskDelay(pdMS_TO_TICKS(1000));
return;
}
bool selectWasPressed = false;
unsigned long selectPressStartime = 0;
const uint16_t LONG_PRESS_THRESHOLD = 1000;
while (1) {
bool selectIsPressed = digitalRead(PIN_BUTTON_SELECT) == HIGH;
if (selectIsPressed && !selectWasPressed) {
selectWasPressed = true;
selectPressStartime = millis();
}
if (!selectIsPressed && selectWasPressed) {
selectWasPressed = false;
unsigned long pressDuration = millis() - selectPressStartime;
}
if (selectIsPressed && selectWasPressed) {
if (millis() - selectPressStartime >= LONG_PRESS_THRESHOLD) {
buttonPressed(PIN_BUTTON_SELECT);
resetPictureView();
resetScale();
Serial.println("exit photo review");
return;
}
}
if (buttonPressed(BTN_PREV)) {
prevImage();
}
if (buttonPressed(BTN_NEXT)) {
nextImage();
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
decreaseScale();
}
if (buttonPressed(PIN_BUTTON_UP)) {
increaseScale();
}
bool recButtonIsPressed = digitalRead(REC_BTN) == HIGH; // 假设按钮连接到GND
static unsigned long recPressStartTime = 0;
if (recButtonIsPressed) {
if (recPressStartTime == 0) {
recPressStartTime = millis(); // 记录按下开始时间
} else if (millis() - recPressStartTime >= 200) {
// 长按超过阈值,执行删除
deleteCurrentImage();
recPressStartTime = 0; // 重置计时器
continue; // 跳过本次循环剩余逻辑
}
} else {
recPressStartTime = 0; // 如果按键释放,重置计时器
}
vTaskDelay(pdMS_TO_TICKS(100));
}
// 等待用户按 SELECT 返回
}
void setLEDBrightness(uint8_t ledBrightness) {
LED_BRIGHTNESS = ledBrightness;
}
uint8_t getCurrentLEDBrightness() {
return LED_BRIGHTNESS;
}
void setScreenBrightness(uint8_t brightness) {
TFT_BRIGHTNESS = brightness;
analogWrite(BL_PIN, TFT_BRIGHTNESS);
}
uint8_t getCurrentBrightness() {
return TFT_BRIGHTNESS;
}
void setTimeWithButtons() {
uint8_t y, mo, d, h, mi, se, wd;
rtc.getTime(&y, &mo, &d, &h, &mi, &se, &wd);
setTimeState = SET_YEAR; // 从年份开始
while (setTimeState < SET_DONE) {
displayTimeSetting(y, mo, d, h, mi, se, setTimeState);
if (buttonPressed(PIN_BUTTON_UP)) {
switch (setTimeState) {
case SET_YEAR: y = (y + 1) % 100; break;
case SET_MONTH: mo = (mo % 12) + 1; break;
case SET_DAY: d = (d % 31) + 1; break;
case SET_HOUR: h = (h + 1) % 24; break;
case SET_MINUTE: mi = (mi + 1) % 60; break;
case SET_SECOND: se = (se + 1) % 60; break;
}
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
switch (setTimeState) {
case SET_YEAR: y = (y + 99) % 100; break; // -1
case SET_MONTH: mo = ((mo + 10) % 12) + 1; break;
case SET_DAY: d = ((d + 29) % 31) + 1; break;
case SET_HOUR: h = (h + 23) % 24; break;
case SET_MINUTE: mi = (mi + 59) % 60; break;
case SET_SECOND: se = (se + 59) % 60; break;
}
}
if (buttonPressed(PIN_BUTTON_SELECT)) {
setTimeState++; // 进入下一项
if (setTimeState == SET_DONE) {
break; // 结束
}
}
vTaskDelay(pdMS_TO_TICKS(100)); // 防止太快
}
rtc.setTimeAutoWeekday(2000 + y, mo, d, h, mi, se); // 自动计算星期
// 显示成功
tft.fillScreen(ST7789_BLACK);
tft.flush();
tft.setCursor(50, 100);
tft.setFontColor(ST7789_WHITE);
tft.setFontSize(2);
tft.print("Time Set!");
tft.flush();
vTaskDelay(pdMS_TO_TICKS(1000));
}
// 扫描 JPG 文件
bool listJpgFiles(const char *path) {
char result_buf[1024]; // 必须足够大
char fullPath[128];
char *p;
sprintf(fullPath, "%s%s", fs.getRootPath(), path);
int count = fs.readDir(fullPath, result_buf, sizeof(result_buf));
if (count != 0) {
Serial.println("Failed to read directory or it's empty");
return false;
}
imageCount = 0;
p = result_buf;
while (strlen(p) > 0) {
String filename = String(p);
// 判断是否为 JPG 文件
if (filename.endsWith(".jpg") || filename.endsWith(".JPG") || filename.endsWith(".jpeg") || filename.endsWith(".JPEG")) {
// 复制到 imageList,注意不要超出缓冲区
if (imageCount < MAX_IMAGES) {
strlcpy(imageList[imageCount], p, 32);
Serial.print("Found: ");
Serial.println(imageList[imageCount]);
imageCount++;
} else {
Serial.println("Max image limit reached!");
break;
}
}
p += strlen(p) + 1; // 移动到下一个文件名
}
return (imageCount > 0);
}
// 加载并显示 JPG
bool loadAndDisplayJpg(const char *filename) {
static char fullPath[128];
sprintf(fullPath, "%s%s/%s", fs.getRootPath(), PHOTO_FOLDER, filename);
File file = fs.open(fullPath);
if (!file) {
Serial.print("Failed to open ");
Serial.println(filename);
return false;
} else {
Serial.print("open ");
Serial.println(filename);
}
size_t fileSize = file.size();
if (fileSize == 0 || fileSize >= MAX_JPG_SIZE) {
Serial.println("File too large or empty!");
file.close();
return false;
} else {
Serial.print("fileSize:");
Serial.println(fileSize);
}
size_t bytesRead = file.read(jpgBuffer, fileSize);
file.close();
if (bytesRead != fileSize) {
Serial.println("Read error!");
return false;
} else {
Serial.print("bytesRead:");
Serial.println(bytesRead);
}
// 解码前先获取图片尺寸(不绘制)
uint16_t width, height;
bool result = TJpgDec.getJpgSize(&width, &height, jpgBuffer, bytesRead);
if (result != false) {
Serial.println("Failed to get JPG size");
return false;
}
currentJpgWidth = width;
currentJpgHeight = height;
tft.fillScreen(ST7789_BLACK);
// 使用 reviewX, reviewY 作为偏移绘制
TJpgDec.drawJpg(reviewX, reviewY, jpgBuffer, bytesRead); // 内部会根据 scale 和偏移绘制
// 显示信息
char timeStr[64];
uint16_t y, mo, d, h, mi, se;
fs.getLastModTime(fullPath, &y, &mo, &d, &h, &mi, &se);
snprintf(timeStr, sizeof(timeStr), "%02u-%02u-%02u %02u:%02u:%02u", y, mo, d, h, mi, se);
tft.setCursor(5, 5);
tft.print(timeStr);
tft.setCursor(290, 220);
tft.print(TFTshowScale().c_str());
tft.flush();
return true;
}
// 切换图片
void prevImage() {
if (imageCount == 0) return;
currentImageIndex = (currentImageIndex - 1 + imageCount) % imageCount;
loadAndDisplayJpg(imageList[currentImageIndex]);
}
void nextImage() {
if (imageCount == 0) return;
currentImageIndex = (currentImageIndex + 1) % imageCount;
loadAndDisplayJpg(imageList[currentImageIndex]);
}
void increaseScale() {
currentScale = 2 * currentScale;
if (currentScale > 8) {
currentScale = 8;
}
TJpgDec.setJpgScale(currentScale);
loadAndDisplayJpg(imageList[currentImageIndex]);
}
void decreaseScale() {
currentScale = currentScale / 2;
if (currentScale < 1) {
currentScale = 1;
}
TJpgDec.setJpgScale(currentScale);
loadAndDisplayJpg(imageList[currentImageIndex]);
}
String TFTshowScale() {
switch (currentScale) {
case 1: return "4X";
case 2: return "2X";
case 4: return "1X";
case 8: return "1/2X";
default: return "?X"; // 必须有 default
}
}
void setCamera() {
//config3.setRotation(1);//右转90度
//config3.setJpegQuality(9);
config1.setRotation(1); //右转90度
config1.setJpegQuality(7);
//config3.setBitrate(50 * 1024 * 1024);//更改录像码率
Camera.configVideoChannel(CHANNEL_RECORD, config3);
Camera.configVideoChannel(CHANNEL_SCREEN, config1);
Camera.videoInit(CHANNEL_RECORD);
Camera.videoInit(CHANNEL_SCREEN);
// Configure audio peripheral for audio data output
audio.configAudio(configA);
audio.begin();
// Configure AAC audio encoder
aac.configAudio(configA);
aac.begin();
// Configure MP4 with identical video format information
// Configure MP4 recording settings
mp4.configVideo(config3);
mp4.configAudio(configA, CODEC_AAC);
mp4.setRecordingDuration(600);
mp4.setRecordingFileCount(1);
//mp4.setRecordingFileName("TestRecordingAudioVideo");
// Configure StreamIO object to stream data from audio channel to AAC encoder
audioStreamer.registerInput(audio);
audioStreamer.registerOutput(aac);
if (audioStreamer.begin() != 0) {
Serial.println("StreamIO link start failed");
}
// Configure StreamIO object to stream data from video channel and AAC encoder to MP4 recording
avMixStreamer.registerInput1(Camera.getStream(CHANNEL_RECORD));
avMixStreamer.registerInput2(aac);
avMixStreamer.registerOutput(mp4);
if (avMixStreamer.begin() != 0) {
Serial.println("StreamIO link start failed");
}
// Start data stream from video channel
Camera.channelBegin(CHANNEL_RECORD);
Camera.channelBegin(CHANNEL_SCREEN);
configCam.setContrast(45); //降低对比度
// Start recording MP4 data to SD card
}
void resetPictureView() {
reviewX = 0;
reviewY = 0;
}
void resetScale() {
currentScale = 1;
TJpgDec.setJpgScale(currentScale);
}
void setScale() {
currentScale = 4;
TJpgDec.setJpgScale(currentScale);
}
void deleteCurrentImage() {
if (imageCount == 0) return;
// 构建完整路径
char fullPath[128];
sprintf(fullPath, "%s/%s", PHOTO_FOLDER, imageList[currentImageIndex]);
// 显示确认提示(可选)
tft.fillRectangle(0, 100, 320, 60, ST7789_BLACK);
tft.setCursor(10, 110);
tft.print("Delete this photo?");
tft.setCursor(10, 130);
tft.print("Hold REC to confirm");
tft.setCursor(10, 150);
tft.print("or release to cancel");
tft.flush();
// 等待用户确认
unsigned long start = millis();
while (millis() - start < 3000) { // 5秒超时
vTaskDelay(pdMS_TO_TICKS(2000));
if (digitalRead(REC_BTN) == HIGH) { // 假设按钮连接到GND
// 用户继续长按确认删除
if (fs.remove(fullPath)) {
Serial.println("Deleted: ");
Serial.println(imageList[currentImageIndex]);
// 从内存列表中移除
for (int i = currentImageIndex; i < imageCount - 1; i++) {
strcpy(imageList[i], imageList[i + 1]);
}
imageCount--;
currentImageIndex = min(currentImageIndex, imageCount - 1); // 更新currentImageIndex
// 自动加载新图片
if (imageCount > 0) {
loadAndDisplayJpg(imageList[currentImageIndex]);
} else {
tft.fillScreen(ST7789_BLACK);
tft.setCursor(50, 50);
tft.print("No images left.");
tft.flush();
vTaskDelay(pdMS_TO_TICKS(1000));
}
} else {
tft.setCursor(10, 170);
tft.print("Delete failed!");
tft.flush();
vTaskDelay(pdMS_TO_TICKS(1000));
loadAndDisplayJpg(imageList[currentImageIndex]); // 重新显示当前图
}
while (digitalRead(REC_BTN) == HIGH) {
vTaskDelay(pdMS_TO_TICKS(10));
}
return;
} else {
// 取消
loadAndDisplayJpg(imageList[currentImageIndex]); // 重新显示当前图
return;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
// 超时取消
loadAndDisplayJpg(imageList[currentImageIndex]);
}
int batteryVoltConvert() {
float voltage = vBatRate * analogRead(PIN_VOLTAGE);
if (voltage < VOLTAGE_BASE) {
return 0;
}
if (voltage > 4.21){
PowerMode.begin(DEEPSLEEP_MODE, WAKEUP_SOURCE, RETENTION, WAKUPE_SETTING);
Serial.print("Enter DeepSleep Mode");
tft.fillScreen(ST7789_BLACK);
tft.setFontSize(2);
tft.setCursor(20, 140);
tft.print("Camera will Enter DeepSleep Mode");
tft.flush();
delay(2000);
PowerMode.start();
}
int voltagePercent = (voltage - VOLTAGE_BASE) * 100;
return voltagePercent;
}
void drawBattery(int x, int y, int width, int height, int level) {
// 绘制电池外框
uint16_t color;
if (level < 30) {
color = ST7789_RED;
} else {
color = ST7789_BLUE;
}
tft.drawRect(x, y, width + 2, height, ST7789_WHITE);
tft.fillRectangle(x + width + 2, y + height / 4, 4, height / 2, ST7789_WHITE); // 电池正极头
// 根据电量level计算需要填充的线条数量
int lines = map(level, 0, 100, 0, height / (height / 10)); // 将电量映射到线条数
for (int i = 0; i < lines; i++) {
tft.drawFastVLine(x + 2 + i * 2, y + 2, height - 4, color); // 竖线,留边距
}
}
void drawLightningBolt(int x, int y, int size, bool on) {
// 定义闪电标志的各个点
// 定义第一个三角形的顶点坐标(闪电的上部)
int x0_1 = x; // 右上角x坐标
int y0_1 = y; // 右上角y坐标
int x1_1 = x0_1-size; // 左下角x坐标
int y1_1 = size+y0_1; // 左下角y坐标
int x2_1 = x0_1; // 底部x坐标
int y2_1 = y1_1; // 底部y坐标
// 定义第二个三角形的顶点坐标(闪电的下部)
int x0_2 = x+1; // 上部x坐标108
int y0_2 = y+size+1; // 上部y坐标13
int x1_2 = x0_2+1; // 左下角x坐标108
int y1_2 = y0_2+size; // 左下角y坐标23
int x2_2 = x0_2+size; // 右上角x坐标118
int y2_2 = y0_2; // 右上角y坐标13
tft.fillTriangle(x0_1, y0_1, x1_1, y1_1, x2_1, y2_1, ST7789_WHITE);
tft.fillTriangle(x0_2, y0_2, x1_2, y1_2, x2_2, y2_2, ST7789_WHITE);
if(!on){
tft.drawCircle(x0_2, y0_2, size+1, ST7789_GREEN);
tft.drawFastHLine(x1_1, y1_1, size*2+1, ST7789_GREEN);
}
}
void setBLEcontrol(){
bool exiting = false;
bool currentBLEState = enableBLE;
while (!exiting) {
tft.fillScreen(ST7789_WHITE);
tft.setCursor(50, 100);
tft.setFontColor(ST7789_BLACK);
tft.setFontSize(2);
tft.print("BLE Setting:");
tft.setCursor(100, 140);
if(currentBLEState){
tft.print("ON");
}else{
tft.print("OFF");
}
tft.setCursor(40, 180);
tft.setFontColor(ST7789_BLUE);
tft.print("UP/DOWN: Adjust");
tft.setCursor(40, 200);
tft.print("SELECT: Back");
tft.flush();
if (buttonPressed(PIN_BUTTON_UP)) {
currentBLEState = !currentBLEState;
}
if (buttonPressed(PIN_BUTTON_DOWN)) {
currentBLEState = !currentBLEState;
}
if (buttonPressed(PIN_BUTTON_SELECT)) {
exiting = true;
}
vTaskDelay(pdMS_TO_TICKS(50));
}
if(enableBLE != currentBLEState){
enableBLE = currentBLEState;
if(!BLETaskState){
if(xBLETaskHandle == NULL){
xTaskCreate(scanAndConnectTask, "ScanConnect", 4096, NULL, 2, &xBLETaskHandle);
}
BLETaskState = true;
}
}
}
void scanCB(T_LE_CB_DATA* p_data) { //BLE扫描回调函数
foundDevice.parseScanInfo(p_data);
if (foundDevice.hasName()) {
if (foundDevice.getName() == TARGET_DEVICE_NAME) {
Serial.print("Found BLE Device at address ");
Serial.println(foundDevice.getAddr().str());
targetDevice = foundDevice;
g_deviceFound = true; // < << 设置标志位!
}
}
}
void notificationCB (BLERemoteCharacteristic* chr, uint8_t* data, uint16_t len) {
char msg[len+1] = {0};
memcpy(msg, data, len);
Serial.print("Notification received for chr UUID: ");
Serial.println(chr- >getUUID().str());
Serial.print("Received string: ");
Serial.println(String(msg));
if (strcmp(msg, "Snapshot") == 0) {
Serial.println("shot");
if(!setMenuFlag && enableBLE){
xSemaphoreGive(xBinarySemaphore);
}
}
}
void scanAndConnectTask(void *pvParameters) {
Serial.println("scanAndConnectTask started");
// 初始化BLE
BLE.init();
BLE.setScanCallback(scanCB);
BLE.beginCentral(1);
while (1) {
// 重置状态
if(enableBLE){
g_bleReady = false;
client = nullptr;
UartService = nullptr;
Rx = nullptr;
Tx = nullptr;
g_connID = -1;
Serial.println("Starting BLE scan...");
BLE.configScan()->startScan(2000); // 扫描2秒
// 等待找到目标设备
while (!g_deviceFound) {
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms
static uint32_t scanStartTime = millis();
if (millis() - scanStartTime > 5000) { // 扫描超过5秒未找到
Serial.println("Scan timeout. Retrying...");
break;
}
}
if (!g_deviceFound) {
Serial.println("Device not found in this scan cycle. Retrying...");
vTaskDelay(pdMS_TO_TICKS(100));
continue;
}
// 连接设备
if (BLE.configConnection()->connect(targetDevice, 2000) == 0) {
g_connID = BLE.configConnection()->getConnId(targetDevice);
if (g_connID >= 0 && BLE.connected(g_connID)) {
Serial.println("BLE Connected successfully!");
// 配置客户端
BLE.configClient();
client = BLE.addClient(g_connID);
if (client == nullptr) {
Serial.println("Failed to create BLE client");
continue; // 重新开始循环
}
Serial.println("Discovering services...");
client->discoverServices();
// 等待服务发现完成
while (!client->discoveryDone()) {
Serial.print(".");
vTaskDelay(pdMS_TO_TICKS(1000));
}
Serial.println("nService discovery completed.");
// 获取UART服务和特征
UartService = client->getService(UART_SERVICE_UUID);
if (UartService != nullptr) {
Tx = UartService->getCharacteristic(CHARACTERISTIC_UUID_TX);
if (Tx != nullptr) {
Serial.println("TX characteristic found");
Tx->setBufferLen(STRING_BUF_SIZE);
Tx->setNotifyCallback(notificationCB);
Tx->enableNotifyIndicate(); // 启用通知
} else {
Serial.println("TX characteristic not found!");
}
Rx = UartService->getCharacteristic(CHARACTERISTIC_UUID_RX);
if (Rx != nullptr) {
Serial.println("RX characteristic found");
Rx->setBufferLen(STRING_BUF_SIZE);
} else {
Serial.println("RX characteristic not found!");
}
} else {
Serial.println("UART Service not found!");
}
// 如果所有关键组件都就绪,设置标志
if (Tx != nullptr && Rx != nullptr) {
g_bleReady = true;
Serial.println("BLE UART ready. Tasks can now operate.");
} else {
Serial.println("BLE setup incomplete. Reconnecting...");
}
} else {
Serial.println("Connection failed or not established.");
}
} else {
Serial.println("Connect command failed.");
}
// 如果连接失败或断开,等待一段时间后重试
if (!g_bleReady) {
Serial.println("Retrying connection in 5 seconds...");
vTaskDelay(pdMS_TO_TICKS(5000));
} else {
// 连接成功,但需要监听断开事件(简化处理:如果断开,外层循环会重试)
// 在实际应用中,应监听BLE断开事件
while (g_bleReady && BLE.connected(g_connID)) {
vTaskDelay(pdMS_TO_TICKS(100)); // 保持任务运行,监听通知
}
Serial.println("BLE disconnected. Reconnecting...");
// 当连接断开时,g_bleReady 会在下次循环开始时被重置
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void drawBluetoothSymbol(int16_t centerX, int16_t centerY, int16_t size, uint16_t color, bool enable) {
// 计算蓝牙标志的各点坐标
float offset = sin(45)*sin(45)*size/2;
uint16_t x1 = centerX - offset;
uint16_t y1 = centerY -size/2 +offset;//左上角
uint16_t x2 = centerX + offset;
uint16_t y2 = centerY +offset;//右下角
uint16_t x3 = centerX + offset;
uint16_t y3 = centerY -size/2 +offset;//右上角
uint16_t x4 = centerX - offset;
uint16_t y4 = centerY + offset;//左下角
if(enable){
tft.fillCircle(centerX, centerY, size-2, ST7789_RED);
}else{
tft.fillCircle(centerX, centerY, size-2, ST7789_GREEN);
}
tft.drawFastVLine(centerX, centerY-size/2, size,color);
tft.drawLine(x1, y1, x2, y2, color);
tft.drawLine(x3, y3, x4, y4, color);
tft.drawLine(centerX, centerY-size/2, x3, y3, color);
tft.drawLine(centerX, centerY+size/2, x2, y2, color);
// 绘制右上部分
}
Ai-M61-32S开发板的代码
#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" //包含GPIO库文件
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; // 按键任务全局句柄,初始为 NULL
bool ble_connected_flag = false; // 按键任务运行标志
// 定义 NUS 服务 UUID
#define BT_UUID_NUS_SERVICE
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400001, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))
// 定义 TX 特征 UUID(设备发送数据,我们接收)
#define BT_UUID_NUS_TX
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400003, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))
// 定义 RX 特征 UUID(我们发送数据,设备接收)
#define BT_UUID_NUS_RX
BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x6E400002, 0xB5A3, 0xF393, 0xE0A9, 0xE50E24DCCA9E))
// 声明 Characteristic 值存储空间
static uint8_t custom_rx_value[20] = {0}; // 接收缓冲区
static uint8_t custom_tx_value[20] = {0}; // 发送缓冲区
static uint16_t custom_rx_len = 0;
static uint16_t custom_tx_len = 0;
// 前向声明回调函数
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);
// 函数声明
int ble_send_data(const uint8_t *data, uint16_t len);
// 定义 GATT 属性表
// 回调函数:当 CCCD 被修改时调用
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 %sn", 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!"; // 你想返回的数据
uint16_t value_len = strlen(value);
// 使用 GATT 工具函数安全返回数据
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: 手机 → 设备 (写入)
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, // 可选:允许手机读回
custom_char_rx_write,
NULL),
// 3. TX Characteristic: 设备 → 手机 (通知)
BT_GATT_CHARACTERISTIC(BT_UUID_NUS_TX,
BT_GATT_CHRC_NOTIFY,
BT_GATT_PERM_READ,
custom_char_tx_read, // 允许手机读取当前值
NULL,
NULL),
// 4. CCCD: 客户端特征配置描述符 (必须紧跟在 TX 特征后)
BT_GATT_CCC(custom_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
};
// 定义 GATT 服务
static struct bt_gatt_service custom_service =
BT_GATT_SERVICE(custom_service_attrs);
// 保存连接句柄,用于 notify
static struct bt_conn *current_conn = NULL;
// 写回调函数实现
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: %.*sn", custom_rx_len, custom_rx_value);
// 回显给手机(可选)
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); // 点亮绿色 LED
bflb_gpio_reset(gpio, RED_LED_PIN); // 熄灭红色LED
current_conn = bt_conn_ref(conn); // 保存连接句柄
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); // 点亮绿色 LED
bflb_gpio_set(gpio, RED_LED_PIN); // 熄灭红色LED
ble_connected_flag = false;
// enable adv
if (current_conn) {
bt_conn_unref(current_conn);
current_conn = NULL;
}
ret = set_adv_enable(true);
if(ret) {
printf("Restart adv fail. rn");
}
}
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));
// Set advertise interval
param.interval_min = BT_GAP_ADV_FAST_INT_MIN_2;
param.interval_max = BT_GAP_ADV_FAST_INT_MAX_2;
/*Get adv type, 0:adv_ind, 1:adv_scan_ind, 2:adv_nonconn_ind 3: adv_direct_ind*/
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) rn", err);
}
printf("Start advertising success.rn");
}
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) rn",
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); // 注册自定义服务
//ble_tp_init();
// start advertising
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!rn");
return 0;
}
#endif
// Initialize BLE controller
#if defined(BL702) || defined(BL602)
ble_controller_init(configMAX_PRIORITIES - 1);
#else
btble_controller_init(configMAX_PRIORITIES - 1);
#endif
// Initialize BLE Host stack
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; // 上一次按键状态(0:释放,1:按下)
while (1) {
uint8_t button_current = bflb_gpio_read(gpio, BUTTON_PIN);
if(!ble_connected_flag) {
// 如果未连接蓝牙,则保持红色LED点亮,绿色和蓝色LED熄灭
bflb_gpio_set(gpio, RED_LED_PIN); // 点亮红色 LED
bflb_gpio_reset(gpio, GREEN_LED_PIN); // 熄灭绿色LED
bflb_gpio_reset(gpio, BLUE_LED_PIN); // 熄灭蓝色LED
vTaskDelay(100 / portTICK_PERIOD_MS); // 延时,避免CPU占用过高
continue; // 跳过按键检测,继续循环
}
// 检测从“按下”到“释放”的跳变(上升沿)
if (button_last_state == 1 && button_current == 0) {
// 消抖:确认释放状态
vTaskDelay(10 / portTICK_PERIOD_MS);
if (bflb_gpio_read(gpio, BUTTON_PIN) == 0) {
// 确认按键已释放,触发动作
printf("Button Released! Turn on Green LED.n");
bflb_gpio_set(gpio, GREEN_LED_PIN); // 点亮绿色 LED
bflb_gpio_reset(gpio, BLUE_LED_PIN); // 熄灭蓝色LED
}
}else if (button_last_state == 0 && button_current == 1) {
// 消抖:确认按下状态
vTaskDelay(10 / portTICK_PERIOD_MS);
if (bflb_gpio_read(gpio, BUTTON_PIN) == 1) {
// 确认按键已按下,触发动作
printf("Button Pressed! Turn on blue LED.n");
bflb_gpio_set(gpio, BLUE_LED_PIN); // 点亮蓝色LED
bflb_gpio_reset(gpio, GREEN_LED_PIN); // 熄灭绿色LED
ble_send_data((uint8_t*)"Snapshot", 8); // 发送BLE数据
}
}
// 更新按键状态
button_last_state = button_current;
// 主循环延时,避免 CPU 占用过高
vTaskDelay(20 / portTICK_PERIOD_MS);
}
}
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: %dn", err);
return -1;
}
printf("Sent to phone: %.*sn", len, data);
return 0;
}
视频演示
https://www.bilibili.com/video/BV1rFYPzyE8e/?spm_id_from=888.80997.embed_other.whitelist&t=139.890003&bvid=BV1rFYPzyE8e&vd_source=54c5db21948db2378659b7e8e42bafbf

审核编辑 黄宇
