I had an AI-M61-32S development board on hand. The board integrates LEDs, serial interfaces, Wi-Fi, and Bluetooth. With this board available, what was still missing for an automatic jump rope counter were: a display, buttons, a buzzer, a charging/discharging circuit, and a counting sensor.

For the display, I planned to use a 0.96-inch OLED, which is sufficient to show jump rope data and remains clearly visible at all times. Two tactile buttons are used: one for setting the time, and the other for starting or canceling the counting process. Charging is handled using a GX4056 charging IC and a low-dropout LDO regulator from existing stock, making the circuit simple to design. For counting, an ITR9606 infrared sensor and LM393 comparator are used to implement an infrared break-beam counting method.

After finalizing the hardware, I began a simple circuit design. The overall design follows a modular approach to make assembly and disassembly easier. The PCB mainly includes the counting circuit, charging circuit, buttons, buzzer, and pin header connectors for the development board and the OLED. Due to the use of pin headers and sockets, the overall thickness of the device is relatively large.

For the mechanical design, the main challenge was fixing the rotating shaft of the jump rope. Since I don’t know how to use 3D modeling software, my approach was to purchase ready-made parts from Taobao and assemble them manually. After searching, I ended up with a rather unusual combination: a 5 mm diamond-shaped bearing block with mounting holes and PCB solder terminals, which together allowed the shaft and optical encoder disk to be fixed directly onto the PCB.

After receiving the PCB, I first soldered the small components and tested the battery charging circuit and OLED display.

After testing, I mounted the board onto a bracket and measured the dimensions to determine the enclosure size.

Fully assembled without enclosure

For the enclosure, I directly used the 3D enclosure design feature in EasyEDA to create a simple case. Although I carefully measured many dimensions before designing the enclosure, after printing it I found that the power switch was too long, preventing the PCB from fitting inside. In the end, I drilled holes in the frame and bent the switch slightly, which allowed the board to fit.

Assembled with enclosure:

Then I attached a jump rope and assembled everything together.

Software Design

For the software, I referenced many tutorials written by experienced forum members, including U8G2 porting guides, as well as official SDK examples. The main features implemented include:

After startup, the system enters manual mode. The OLED displays Bluetooth status, battery voltage, manual countdown time, and jump count.

In manual mode:

Button 1

Button 2

Firmware Source Code

#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");" *="" clock="XCLK" 2="" 32="" struct="" bflb_adc_c cfg;="" cfg.clk_div="ADC_CLK_DIV_32;" cfg.scan_c cfg.c cfg.differential_mode="false;" cfg.resolution="ADC_RESOLUTION_16B;" cfg.vref="ADC_VREF_3P2V;" bflb_adc_init(adc,="" &cfg);="" static="" bflb_adc_channel_s="" chan[]="{" .pos_chan="ADC_CHANNEL_11," .neg_chan="ADC_CHANNEL_GND" },="" };="" bflb_adc_channel_config(adc,="" chan,="" 1);="" adc_sampling_task(void="" *pvparameters){="" while="" (1)="" bflb_adc_start_conversion(adc);="" (bflb_adc_get_count(adc)="" <="" 1)="" vtaskdelay(pdms_to_ticks(1));="" bflb_adc_result_s="" result;="" uint32_t="" raw_data="bflb_adc_read_raw(adc);" bflb_adc_parse_result(adc,="" &raw_data,="" &result,="" 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));="" 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);
  
    }
}</times;i++){></freertos.h>

The Bluetooth mentioned above uses a previously built BW21-CBV-KIT camera module as a data collection device. After startup, the device automatically searches for nearby jump rope counters.

Final Notes

After completing the project, I performed functional testing. Everything worked correctly. However, the combination of pin headers and sockets makes the device relatively thick, and the enclosure I designed is a bit short. As a result, the grip is uncomfortable during use.

Next step: learn how to design better enclosures and improve ergonomics.