[i=s] 本帖最后由 dzy7455339 于 2025-10-25 12:06 编辑 [/i]
给小孩数跳绳一分钟跳绳个数,总是数着数着对不上了,想着买一个自动计数的,但是买不如做一个,所以决定做一个自动计数跳绳。
手上有块AI-M61-32S开发板,板子集成了LED、串口以及无线和蓝牙功能。有了板子距离自动计数跳绳还差屏幕、按键、蜂鸣器、充放电电路以及计数传感器。屏幕计划使用0.96寸OLED,显示跳绳数据足够,在任何时间都能清晰显示画面。按键用两个轻触按键,一个用来设置时间,一个用来启动和取消。充电直接使用之前存货的GX4056和低压差LDO,电路设计起来简单。计数器使用ITR9606和LM393,通过红外对射实现计数功能。
确定完硬件后开始简单的电路设计,整体使用模块化设计,方便拆装。PCB板上主要放置了计数器、充电、按键、蜂鸣器以及和开发板、OLED的排母接口。由于使用了排针和排母接口,整体厚度上较厚。

机械设计上主要考虑固定跳绳的转轴,因为不会使用3D软件,主要思路是从淘宝上购买成品的零件进行组装。经过筛查,最后选了5mm带孔轴菱形轴承座及PCB焊接端子等组成了一个非常怪异的组合实现将轴和光电码盘固定在了PCB上。
板子来了后现焊接小原件测试电池充电和OLED屏幕是否正常。

测试完之后装上支架,测量尺寸以确定外壳尺寸。

不带壳整体组装好的样子

外壳部分直接在立创EDA里面使用3D外壳部分制作了一个简单的外壳,虽然在画壳前测量了好多尺寸,但是最终打印回来之后还是发现电源开关太长了,板子塞不进去。最后通过在边框上打孔和把开关弯曲的方式得以把板子塞进去。
装上外壳的样子。

找根跳绳和设备组装一起。

软件部分。软件这里借鉴了论坛里大佬写的U8G2移植等诸多教程以及官方例程,主要实现了红外对射管的外部中断、按键的防抖切换、OLED屏的显示、电压检测、LED及蜂鸣器提醒功能及蓝牙的通讯等。
整体流程软件启动后进入手动模式,并在OLED下显示蓝牙状态、电池电压状态、手动倒计时、跳绳计数等信息。在手动模式下可以通过按键1设置倒计时时长,单击是增加时间,双击是渐少时间,长按则恢复默认时长。单击按键2则会启动倒计时并开始计数,双击则会取消倒计时,长按则在自动检测模式和手动倒计时之间进行切换。
#include "bflb_mtimer.h"
#include "bflb_i2c.h"
#include "board.h"
#include "u8g2.h"
#include <FreeRTOS.h>
#include "task.h"
#include "bflb_gpio.h" //包含GPIO库文件
#include "key_manager.h"//按键处理
#include "bflb_adc.h"//adc
#include "log.h"
#include "bluetooth.h"
#include "conn.h"
#include "conn_internal.h"
#include "btble_lib_api.h"
#include "bl616_glb.h"
#include "rfparam_adapter.h"
#include "gatt.h"
#include "ble_tp_svc.h"
#include "hci_driver.h"
#include "hci_core.h"
BFLOG_DEFINE_TAG(MAIN, "MAIN", true);
bool ble_connected_flag = false; // 蓝牙连接状态标志
struct bflb_device_s *gpio;
struct bflb_device_s *adc;
uint16_t adc_value = 57922;//初始值,没电
uint16_t jump_time = 60;//跳绳时长
// 跳绳计数变量(真正的跳绳次数)
volatile uint32_t jump_count = 0;
// 中断触发计数(用于累积 20 次)
volatile uint8_t interrupt_count = 0;
// 状态标志:是否正在检测
volatile bool detection_active = false;
volatile bool jump_event = false; // 标记有跳绳事件
bool auto_mode = false;//自动模式标志
// 按键管理器实例
#define BUTTON_PIN GPIO_PIN_2
#define GREEN_LED_PIN GPIO_PIN_14 //和I2C0_SCL冲突
#define BLUE_LED_PIN GPIO_PIN_15
#define RED_LED_PIN GPIO_PIN_12
#define INTERRUPT_PIN GPIO_PIN_31
#define BUZZER_PIN GPIO_PIN_26
u8g2_t u8g2;
static struct bflb_device_s *i2c0;
#define IDEL_SPACE 3000 //3秒没跳,自动停止
typedef struct {
uint32_t start_time;// 记录开始时间
uint32_t last_check_time;// 记录上次检查时间
bool active;// 是否正在计时
TaskHandle_t task_handle; // 新增:保存任务句柄
} countup_t;
countup_t my_countup;
void init_LED_GPIO(void);
void buzzer_short_on(int duration_ms);
void buzzer_short_on_times(int times,int delay_ms,int duration_ms);
void ADC_setup(void);
static void adc_sampling_task(void *pvParameters);
void display_content_based_on_value(int value) ;
// 函数声明
int ble_send_data(const uint8_t *data, uint16_t len);
void gpio_isr(int irq, void *arg)
{
if (!detection_active) {
bflb_gpio_int_clear(gpio, INTERRUPT_PIN );
return;
}
bool intstatus = bflb_gpio_get_intstatus(gpio, INTERRUPT_PIN );
if (intstatus) {
bflb_gpio_int_clear(gpio, INTERRUPT_PIN );
printf("%d\r\n", interrupt_count++);
if(auto_mode){
if(my_countup.active){
my_countup.last_check_time = bflb_mtimer_get_time_ms();
}
}
if(interrupt_count>=20){//每20次中断计为一次跳绳
interrupt_count=0;
jump_count++;
if(auto_mode){
if(!my_countup.active){
my_countup.active = true;
my_countup.start_time = bflb_mtimer_get_time_ms();
my_countup.last_check_time = my_countup.start_time;
jump_count=1;//自动模式下,跳绳次数从1开始
}
}
jump_event = true;
}
}
}
void start_jump_detection(void)
{
if (detection_active) {
return; // 已经在检测
}
// 重置计数器
interrupt_count = 0;
jump_count = 0;// 可选择是否重置:若不清零,保留历史总数
// 使能中断
//bflb_gpio_int_enable(gpio, INTERRUPT_PIN, true);
bflb_irq_enable(gpio->irq_num);
detection_active = true;
printf("跳绳检测已启动\n");
}
void stop_jump_detection(void)
{
if (!detection_active) {
return; // 已停止
}
// 禁用中断
//bflb_gpio_int_enable(gpio, INTERRUPT_PIN, false);
bflb_irq_disable(gpio->irq_num);
detection_active = false;
printf("跳绳检测已停止\n");
}
void countup_init(countup_t *cu)
{
cu->start_time = 0;
cu->last_check_time = 0;
cu->active = false;
cu->task_handle = NULL;
}
int countup_get_duration(const countup_t *cu)//获取正向计时持续时间
{
if (!cu) {
return 0;
}
if (!cu->active && cu->last_check_time != 0) {
return (cu->last_check_time - cu->start_time)/1000;
}
if(cu->active){
uint32_t elapsed_ms = bflb_mtimer_get_time_ms() - cu->start_time;
int elapsed_seconds = elapsed_ms / 1000;
return elapsed_seconds;
}
return 0;
}
void countup_task(void *arg)
{
countup_t *cu_t = (countup_t *)arg; // 传入倒计时秒数
while(1){
int idel_time = bflb_mtimer_get_time_ms() - cu_t->last_check_time;
if(idel_time>IDEL_SPACE && cu_t->active){//超过3秒没跳,自动停止
cu_t->active = false;
if(ble_connected_flag){
ble_send_data((const uint8_t *)"cancel", 6 );
}
buzzer_short_on_times(4,100,50);
printf("自动停止计时,跳绳已停止!\r\n");
}
vTaskDelay(pdMS_TO_TICKS(1000)); // 等待 1 秒
}
}
void start_countup(countup_t *cu)
{
if (cu == NULL) {
return;
}
if(cu->active){
return; // 已经在运行中
}
countup_init(cu);
BaseType_t ret = xTaskCreate(countup_task,
"countup",
512, // 栈大小
(void*)cu, // 参数(倒计时秒数)
configMAX_PRIORITIES - 3, // 优先级
&cu->task_handle); // 任务句柄(可选)
if (ret != pdPASS) {
printf("创建倒计时任务失败!\r\n");
cu->task_handle = NULL;
}
}
void cancel_countup(countup_t *cu)
{
if (cu == NULL) {
return; // 未运行,无需取消
}
cu->active = false;// 标记为停止
// 如果任务句柄有效,删除任务
if (cu->task_handle != NULL) {
xTaskNotifyGive(cu->task_handle); // 唤醒任务(可选:用于快速退出阻塞)
vTaskDelete(cu->task_handle); // 删除任务
cu->task_handle = NULL;
}
stop_jump_detection(); // 停止检测
if(ble_connected_flag){
ble_send_data((const uint8_t *)"cancel", 6 );
}
printf("计时已取消!\r\n");
}
typedef struct {
uint32_t start_time;
int total_seconds;
bool running;
TaskHandle_t task_handle; // 新增:保存任务句柄
} countdown_t;
void countdown_init(countdown_t *cd)
{
cd->start_time = 0;
cd->total_seconds = 0;
cd->running = false;
cd->task_handle = NULL;
}
int countdown_get_remaining(const countdown_t *cd)//获取剩余时间
{
if (!cd || !cd->running || cd->total_seconds <= 0) {
return 0;
}
uint32_t elapsed_ms = bflb_mtimer_get_time_ms() - cd->start_time;
int elapsed_seconds = elapsed_ms / 1000;
int remaining = cd->total_seconds - elapsed_seconds;
return (remaining > 0) ? remaining : 0;
}
// 倒计时任务函数
void countdown_task(void *arg)
{
countdown_t *cd_t = (countdown_t *)arg; // 传入倒计时秒数
printf("countdown start:%d seconds\r\n", cd_t->total_seconds);
while (countdown_get_remaining(cd_t) > 0) {
int remain = countdown_get_remaining(cd_t);
printf("remain:%d seconds\r\n", remain);
//测试代码
// if(ble_connected_flag){
// char buf[16];
// snprintf(buf, sizeof(buf), "J:%d", remain);
// ble_send_data((uint8_t *)buf, strlen(buf));
// }
// 使用 RTOS 延时(不阻塞其他任务)
vTaskDelay(pdMS_TO_TICKS(1000)); // 等待 1 秒
}
cd_t->running = false;
stop_jump_detection();
if(ble_connected_flag){
ble_send_data((const uint8_t *)"end", 3 );
}
buzzer_short_on_times(4,100,50);
printf("countdown end!\r\n");
vTaskDelete(NULL); // 自删除任务
}
// 在 main 或其他任务中启动倒计时
void start_countdown(countdown_t *cd, int seconds)
{
if (cd == NULL || seconds <= 0) {
return;
}
if(cd->running){
return; // 已经在运行中
}
cd->start_time = bflb_mtimer_get_time_ms();
cd->total_seconds = seconds;
cd->running = true;
cd->task_handle = NULL; // 初始化为 NULL
BaseType_t ret = xTaskCreate(countdown_task,
"countdown",
512, // 栈大小
(void*)cd, // 参数(倒计时秒数)
configMAX_PRIORITIES - 3, // 优先级
&cd->task_handle); // 任务句柄(可选)
if (ret != pdPASS) {
printf("创建倒计时任务失败!\r\n");
cd->running = false; // 回滚状态
cd->task_handle = NULL;
}
}
void cancel_countdown(countdown_t *cd)
{
if (cd == NULL || !cd->running) {
return; // 未运行,无需取消
}
// 标记为停止
cd->running = false;
// 如果任务句柄有效,删除任务
if (cd->task_handle != NULL) {
xTaskNotifyGive(cd->task_handle); // 唤醒任务(可选:用于快速退出阻塞)
vTaskDelete(cd->task_handle); // 删除任务
cd->task_handle = NULL;
}
stop_jump_detection(); // 停止检测
if(ble_connected_flag){
ble_send_data((const uint8_t *)"cancel", 6 );
}
printf("倒计时已取消!\r\n");
}
// 在主循环中调用
countdown_t my_countdown;
void i2c_transfer(uint8_t addr, size_t len, uint8_t *buffer)
{
struct bflb_i2c_msg_s msgs;
msgs.addr = addr;
msgs.flags = 0;
msgs.buffer = buffer;
msgs.length = len;
bflb_i2c_transfer(i2c0, &msgs, 1);
}
uint8_t u8x8_gpio_and_delay_template(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
switch (msg)
{
case U8X8_MSG_GPIO_AND_DELAY_INIT: // called once during init phase of u8g2/u8x8
break; // can be used to setup pins
case U8X8_MSG_DELAY_NANO: // delay arg_int * 1 nano second
break;
case U8X8_MSG_DELAY_100NANO: // delay arg_int * 100 nano seconds
break;
case U8X8_MSG_DELAY_10MICRO: // delay arg_int * 10 micro seconds
bflb_mtimer_delay_us(10 * arg_int);
break;
case U8X8_MSG_DELAY_MILLI: // delay arg_int * 1 milli second
bflb_mtimer_delay_ms(arg_int);
break;
case U8X8_MSG_DELAY_I2C: // arg_int is the I2C speed in 100KHz, e.g. 4 = 400 KHz
break; // arg_int=1: delay by 5us, arg_int = 4: delay by 1.25us
case U8X8_MSG_GPIO_D0: // D0 or SPI clock pin: Output level in arg_int
// case U8X8_MSG_GPIO_SPI_CLOCK:
break;
case U8X8_MSG_GPIO_D1: // D1 or SPI data pin: Output level in arg_int
// case U8X8_MSG_GPIO_SPI_DATA:
break;
case U8X8_MSG_GPIO_D2: // D2 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D3: // D3 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D4: // D4 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D5: // D5 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D6: // D6 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_D7: // D7 pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_E: // E/WR pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_CS: // CS (chip select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_DC: // DC (data/cmd, A0, register select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_RESET: // Reset pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_CS1: // CS1 (chip select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_CS2: // CS2 (chip select) pin: Output level in arg_int
break;
case U8X8_MSG_GPIO_I2C_CLOCK: // arg_int=0: Output low at I2C clock pin
break; // arg_int=1: Input dir with pullup high for I2C clock pin
case U8X8_MSG_GPIO_I2C_DATA: // arg_int=0: Output low at I2C data pin
break; // arg_int=1: Input dir with pullup high for I2C data pin
case U8X8_MSG_GPIO_MENU_SELECT:
u8x8_SetGPIOResult(u8x8, /* get menu select pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_NEXT:
u8x8_SetGPIOResult(u8x8, /* get menu next pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_PREV:
u8x8_SetGPIOResult(u8x8, /* get menu prev pin state */ 0);
break;
case U8X8_MSG_GPIO_MENU_HOME:
u8x8_SetGPIOResult(u8x8, /* get menu home pin state */ 0);
break;
default:
u8x8_SetGPIOResult(u8x8, 1); // default return value
break;
}
return 1;
}
uint8_t u8x8_byte_i2c(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void *arg_ptr)
{
static uint8_t buffer[32]; /* u8g2/u8x8 will never send more than 32 bytes between START_TRANSFER and END_TRANSFER */
static uint8_t buf_idx;
uint8_t *data;
switch (msg)
{
case U8X8_MSG_BYTE_SEND:
data = (uint8_t *)arg_ptr;
while (arg_int > 0)
{
buffer[buf_idx++] = *data;
data++;
arg_int--;
}
break;
case U8X8_MSG_BYTE_INIT:
/* add your custom code to init i2c subsystem */
board_i2c0_gpio_init();
i2c0 = bflb_device_get_by_name("i2c0");
bflb_i2c_init(i2c0, 400000);
break;
case U8X8_MSG_BYTE_SET_DC:
/* ignored for i2c */
break;
case U8X8_MSG_BYTE_START_TRANSFER:
buf_idx = 0;
break;
case U8X8_MSG_BYTE_END_TRANSFER:
i2c_transfer(u8x8_GetI2CAddress(u8x8) >> 1, buf_idx, buffer);
break;
default:
return 0;
}
return 1;
}
// LED控制任务
static void led_control_task(void *pvParameters)
{
static uint32_t last_toggle = 0;
bool led_on = false;
while (1) {
uint32_t now = bflb_mtimer_get_time_ms();
switch (g_key_manager.led_state) {
case LED_OFF:
bflb_gpio_reset(g_key_manager.gpio, LED_PIN);
break;
case LED_ON:
bflb_gpio_set(g_key_manager.gpio, LED_PIN);
break;
case LED_BLINKING:
if (now - last_toggle >= 500) {
led_on = !led_on;
if (led_on) {
bflb_gpio_set(g_key_manager.gpio, LED_PIN);
} else {
bflb_gpio_reset(g_key_manager.gpio, LED_PIN);
}
last_toggle = now;
}
break;
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// 事件处理任务
static void event_handler_task(void *pvParameters)
{
KeyEvent event;
while (1) {
if (xQueueReceive(key_event_queue, &event, portMAX_DELAY)) {
switch (event.button_id) {
case 1:
switch (event.action) {
case KEY_ACTION_CLICK:
LOG_I("Button 1: Single Click\r\n");
g_key_manager.led_state = LED_ON;
buzzer_short_on(20);
if(!my_countdown.running){
jump_time += 10;//增加时长
}
break;
case KEY_ACTION_DOUBLE_CLICK:
LOG_I("Button 1: Double Click\r\n");
buzzer_short_on(20);
if(!my_countdown.running){
jump_time -= 10;//减少时长
}
if(jump_time<10) jump_time=10;
g_key_manager.led_state = LED_BLINKING;
break;
case KEY_ACTION_LONG_PRESS:
buzzer_short_on(20);
if(!my_countdown.running){
jump_time = 60;//恢复默认时长
}
LOG_I("Button 1: Long Press\r\n");
break;
default:
break;
}
break;
case 2:
switch (event.action) {
case KEY_ACTION_CLICK:
LOG_I("Button 2: Single Click\r\n");
if(!auto_mode){
buzzer_short_on_times(1,100,50);
start_countdown(&my_countdown, jump_time);//启动倒计时
start_jump_detection();//启动跳绳检测
if(ble_connected_flag){
ble_send_data((const uint8_t *)"start", 5 );//通知手机开始
}
}
g_key_manager.led_state = LED_ON;
break;
case KEY_ACTION_DOUBLE_CLICK:
buzzer_short_on_times(2,100,50);
cancel_countdown(&my_countdown);//取消倒计时
LOG_I("Button 2: Double Click\r\n");
g_key_manager.led_state = LED_BLINKING;
break;
case KEY_ACTION_LONG_PRESS:
buzzer_short_on_times(3,100,50);
auto_mode = !auto_mode;
if(auto_mode){
if(my_countdown.running){
cancel_countdown(&my_countdown);//取消倒计时
}
start_jump_detection();//启动跳绳检测
start_countup(&my_countup);//启动正向计时
}else{
cancel_countup(&my_countup);//取消正向计时
}
LOG_I("Button 2: Long Press\r\n");
break;
default:
break;
}
break;
}
}
vTaskDelay(pdMS_TO_TICKS(10));
}
}
const uint8_t miao[] = {
0x88,0x00,0x87,0x00,0x84,0x02,0xA4,0x04,0xAF,0x04,0xA4,0x04,0x96,0x00,0x8E,0x04,
0x05,0x02,0x04,0x01,0xC4,0x00,0x34,0x00,/*"秒",0*/
};
const uint8_t ci[] = {
0x20,0x00,0x21,0x00,0xE2,0x07,0x10,0x04,0x50,0x02,0x48,0x00,0x40,0x00,0xA4,0x00,
0xA2,0x00,0x11,0x01,0x08,0x02,0x04,0x04,/*"次",1*/
};
static void u8g2_update_task(void *pvParameters)
{
while (1) {
u8g2_ClearBuffer(&u8g2);
u8g2_SetFont(&u8g2,u8g2_font_siji_t_6x10);
if(ble_connected_flag){
u8g2_SetDrawColor(&u8g2,1);//背景黑色
}else{
u8g2_SetDrawColor(&u8g2,0); //背景白色
}
u8g2_DrawGlyph(&u8g2,5, 11, 57355); //蓝牙标志
u8g2_SetDrawColor(&u8g2,1);//背景黑色
u8g2_DrawGlyph(&u8g2,35, 11, adc_value); //电池电量标志从22到31
if(auto_mode){
u8g2_DrawStr(&u8g2,22, 11, "A"); //自动触发模式标志
}else{
u8g2_DrawStr(&u8g2,22, 11, "M"); //手动触发模式标志
}
u8g2_DrawHVLine(&u8g2, 0, 13, 50, 0);//横线
u8g2_DrawHVLine(&u8g2, 50, 0, 64, 1);//竖线
u8g2_DrawHVLine(&u8g2, 0, 38, 50, 0);//短横线
// u8g2_SetFont(&u8g2,u8g2_font_wqy12_t_chinese3);
char buffer[32];
// u8g2_DrawUTF8(&u8g2,0, 30, buffer);
// u8g2_DrawUTF8(&u8g2,115, 62, "次");
u8g2_DrawXBMP(&u8g2, 37, 20, 12, 12, miao); //秒 图形字体
u8g2_DrawXBMP(&u8g2, 115, 50, 12, 12, ci); //次 图形字体
snprintf(buffer, sizeof(buffer), "%d", jump_count);//跳绳次数jump_count
u8g2_SetFont(&u8g2,u8g2_font_logisoso30_tr);
u8g2_DrawStr(&u8g2,65, 45, buffer);
//蓝牙模式发送数据
if(ble_connected_flag && jump_event){
jump_event = false;
char buf[16];
snprintf(buf, sizeof(buf), "J:%d", jump_count);
ble_send_data((uint8_t *)buf, strlen(buf));
}
if(auto_mode){
int duration = countup_get_duration(&my_countup);
snprintf(buffer, sizeof(buffer), "%d", duration);//正向计时
}else{
int remain = countdown_get_remaining(&my_countdown);
snprintf(buffer, sizeof(buffer), "%d", remain);
}
u8g2_SetFont(&u8g2,u8g2_font_logisoso16_tr);
u8g2_DrawStr(&u8g2,15, 60, buffer); //倒数计时
snprintf(buffer, sizeof(buffer), "%d", jump_time);//设定跳绳时长
u8g2_DrawStr(&u8g2,0, 33, buffer);
u8g2_SendBuffer(&u8g2);
vTaskDelay(pdMS_TO_TICKS(10));
}
}
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, BUZZER_PIN, GPIO_OUTPUT | GPIO_PULLDOWN | GPIO_SMT_EN | GPIO_DRV_3);
bflb_gpio_init(gpio, BUTTON_PIN, GPIO_INPUT | GPIO_PULLDOWN | GPIO_SMT_EN | GPIO_DRV_0);
//bflb_gpio_init(gpio, INTERRUPT_PIN, GPIO_INPUT | GPIO_PULLUP | GPIO_SMT_EN);
bflb_gpio_int_init(gpio, INTERRUPT_PIN, GPIO_INT_TRIG_MODE_SYNC_FALLING_EDGE);
bflb_gpio_int_mask(gpio, INTERRUPT_PIN, false);
bflb_irq_attach(gpio->irq_num, gpio_isr, gpio);
bflb_irq_disable(gpio->irq_num);
printf("gpio interrupt\r\n");
}
void buzzer_short_on(int duration_ms)
{
bflb_gpio_set(gpio, BUZZER_PIN);
vTaskDelay(pdMS_TO_TICKS(duration_ms));
bflb_gpio_reset(gpio, BUZZER_PIN);
}
void buzzer_short_on_times(int times,int delay_ms,int duration_ms)
{
for(int i=0;i<times;i++){
buzzer_short_on(duration_ms);
vTaskDelay(pdMS_TO_TICKS(delay_ms));
}
}
void ADC_setup(void)
{
board_adc_gpio_init();
adc = bflb_device_get_by_name("adc");
/* adc clock = XCLK / 2 / 32 */
struct bflb_adc_config_s cfg;
cfg.clk_div = ADC_CLK_DIV_32;
cfg.scan_conv_mode = true;
cfg.continuous_conv_mode = false;
cfg.differential_mode = false;
cfg.resolution = ADC_RESOLUTION_16B;
cfg.vref = ADC_VREF_3P2V;
bflb_adc_init(adc, &cfg);
static struct bflb_adc_channel_s chan[] = {
{ .pos_chan = ADC_CHANNEL_11,
.neg_chan = ADC_CHANNEL_GND },
};
bflb_adc_channel_config(adc, chan, 1);
}
static void adc_sampling_task(void *pvParameters){
while (1)
{ bflb_adc_start_conversion(adc);
while (bflb_adc_get_count(adc) < 1) {
vTaskDelay(pdMS_TO_TICKS(1));
}
struct bflb_adc_result_s result;
uint32_t raw_data = bflb_adc_read_raw(adc);
bflb_adc_parse_result(adc, &raw_data, &result, 1);
printf("pos chan %d,%d mv \r\n", result.pos_chan, result.millivolt);
display_content_based_on_value(result.millivolt);
bflb_adc_stop_conversion(adc);
vTaskDelay(pdMS_TO_TICKS(30000));
}
}
void display_content_based_on_value(int value) {
if (value >= 2029) {
adc_value = 57931;
printf("区间 0: 最高值\n");
} else if (value >= 1990) {
adc_value = 57930;
printf("区间 1: 很高\n");
} else if (value >= 1951) {
adc_value = 57929;
printf("区间 2: 高\n");
} else if (value >= 1912) {
adc_value = 57928;
printf("区间 3: 较高\n");
} else if (value >= 1873) {
adc_value = 57927;
printf("区间 4: 中上\n");
} else if (value >= 1834) {
adc_value = 57926;
printf("区间 5: 中\n");
} else if (value >= 1795) {
adc_value = 57925;
printf("区间 6: 中下\n");
} else if (value >= 1756) {
adc_value = 57924;
printf("区间 7: 较低\n");
} else if (value >= 1717) {
adc_value = 57923;
printf("区间 8: 低\n");
} else { // value <= 1716
adc_value = 57922;//没电
printf("区间 9: 最低值\n");
}
}
//蓝牙设置
#define TARGET_NAME "jump_rope"
// 定义 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);
// 定义 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 %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!"; // 你想返回的数据
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: %.*s\n", 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_reset(gpio, RED_LED_PIN); //熄灭红色LED
bflb_gpio_set(gpio, BLUE_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, BLUE_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. \r\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));
// 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) \r\n", err);
}
printf("Start advertising success.\r\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) \r\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(TARGET_NAME);
bt_gatt_service_register(&custom_service); // 注册自定义服务
//ble_tp_init();
// start advertising
ble_start_adv();
}
}
void init_ble(void)
{
/* set ble controller EM Size */
btblecontroller_em_config();
/* Init rf */
if (0 != rfparam_init(0, NULL, 0)) {
printf("PHY RF init failed!\r\n");
return;
}
// Initialize BLE controller
btble_controller_init(configMAX_PRIORITIES - 1);
// Initialize BLE Host stack
hci_driver_init();
bt_enable(bt_enable_cb);
}
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;
}
int main(void)
{
board_init();
configASSERT((configMAX_PRIORITIES > 4));
init_LED_GPIO();
init_ble();
ADC_setup();
xTaskCreate(adc_sampling_task, "adc_sampling_task", 256, NULL, configMAX_PRIORITIES - 4, NULL);
LOG_I("key test\r\n");
key_manager_init(); //初始化按键管理器
key_manager_start();//启动按键管理器任务
// 创建其他任务
//xTaskCreate(led_control_task, "led_ctrl", 256, NULL, configMAX_PRIORITIES - 3, NULL);
xTaskCreate(event_handler_task, "evt_handler", 1024, NULL, configMAX_PRIORITIES - 3, NULL);
countdown_init(&my_countdown); // 设置默认值
u8g2_Setup_ssd1306_i2c_128x64_noname_f(&u8g2, U8G2_R0, u8x8_byte_i2c, u8x8_gpio_and_delay_template);
u8g2_InitDisplay(&u8g2);
u8g2_SetPowerSave(&u8g2, 0);
u8g2_SetContrast(&u8g2,50);//亮度设置
xTaskCreate(u8g2_update_task, "u8g2_upd", 2048, NULL, configMAX_PRIORITIES - 2, NULL);
vTaskStartScheduler();
while(1){
bflb_mtimer_delay_ms(1000);
}
}
上面提到的蓝牙是我使用之前制作的BW21-CBV-KIT相机充当一个数据收集设备,在该设备启动后会自动搜索周边的跳绳设备,并在启动后默认展示之前的跳绳数据并生成图表,如果跳绳启动则会进入跳绳动画显示状态,如果是手动模式则还会将数据汇总并存储进SD卡中。
整体做完以后进行了测试,功能方面都没有问题,就是这个排针和排母的组合导致设备较厚且我做的壳子尺寸较短,导致使用过程中手把握持不舒适,需要学习以下如何做外壳。